const DEFAULT_ENTITIES = { roof: "cover.sternwarte_dach", power: "switch.sternwarte_dachsteuerung", roofClosed: "binary_sensor.sternwarte_dach_zu", roofOpen: "binary_sensor.sternwarte_dach_offen", roofMoving: "binary_sensor.sternwarte_dach_bewegt", roofStatus: "sensor.sternwarte_dachstatus", roofPosition: "sensor.sternwarte_dachposition", rain: "binary_sensor.sternwarte_regen", wind: "sensor.sternwarte_windgeschwindigkeit", scheduleOpen: "datetime.sternwarte_offnen_geplant", scheduleClose: "datetime.sternwarte_schliessen_geplant", astroLat: 50.2984230, astroLon: 11.0481766, }; const ASTRONOMY_LIB_URL = (() => { try { return new URL("./astronomy.browser.min.js", import.meta.url).toString(); } catch (error) { return null; } })(); const MOON_IMAGE_URL = (() => { try { return new URL("./moon_small.png", import.meta.url).toString(); } catch (error) { return null; } })(); class SternwarteCard extends HTMLElement { constructor() { super(); this._scheduleInputState = { open: { draft: "", editing: false, pending: false }, close: { draft: "", editing: false, pending: false }, }; this._pendingRender = false; this._astronomyLoadPromise = null; this._moonTexture = null; this._moonTexturePromise = null; } connectedCallback() { this._ensureAstronomy(); this._ensureMoonTexture(); this._ensureClockTimer(); } disconnectedCallback() { if (this._clockTimer) { window.clearInterval(this._clockTimer); this._clockTimer = null; } } setConfig(config) { this.config = { ...DEFAULT_ENTITIES, ...config }; if (!this.shadowRoot) { this.attachShadow({ mode: "open" }); } } set hass(hass) { this._hass = hass; this._ensureClockTimer(); this._requestRender(); } getCardSize() { return 10; } getGridOptions() { return { rows: 10, columns: 18, min_rows: 9, }; } _state(entityId) { return this._hass?.states?.[entityId]; } _ensureClockTimer() { if (this._clockTimer) return; this._clockTimer = window.setInterval(() => { if (this._hass) this._requestRender(); }, 30000); } _astronomy() { return window.Astronomy || null; } _ensureAstronomy() { const astronomy = this._astronomy(); if (astronomy) return Promise.resolve(astronomy); if (this._astronomyLoadPromise) return this._astronomyLoadPromise; if (!ASTRONOMY_LIB_URL) return Promise.resolve(null); this._astronomyLoadPromise = new Promise((resolve) => { const finish = () => { const loaded = this._astronomy(); if (loaded && this._hass) { this._requestRender(); } this._astronomyLoadPromise = null; resolve(loaded || null); }; const existing = Array.from(document.querySelectorAll("script")) .find((script) => script.src === ASTRONOMY_LIB_URL); if (existing) { existing.addEventListener("load", finish, { once: true }); existing.addEventListener("error", finish, { once: true }); return; } const script = document.createElement("script"); script.src = ASTRONOMY_LIB_URL; script.async = true; script.addEventListener("load", finish, { once: true }); script.addEventListener("error", finish, { once: true }); document.head.appendChild(script); }); return this._astronomyLoadPromise; } _ensureMoonTexture() { if (this._moonTexture) return Promise.resolve(this._moonTexture); if (this._moonTexturePromise) return this._moonTexturePromise; if (!MOON_IMAGE_URL) return Promise.resolve(null); this._moonTexturePromise = new Promise((resolve) => { const image = new Image(); image.addEventListener("load", () => { this._moonTexture = image; this._moonTexturePromise = null; if (this._hass) this._requestRender(); resolve(image); }, { once: true }); image.addEventListener("error", () => { this._moonTexturePromise = null; resolve(null); }, { once: true }); image.src = MOON_IMAGE_URL; }); return this._moonTexturePromise; } _requestRender() { if (this._isScheduleEditing()) { this._pendingRender = true; return; } this._pendingRender = false; this.render(); } _isScheduleEditing() { return Object.values(this._scheduleInputState).some((field) => field.editing); } _scheduleFieldState(key) { return this._scheduleInputState[key]; } _scheduleInputValue(key, fallbackState) { const field = this._scheduleFieldState(key); if (field.draft) return field.draft; return this._isoForInput(fallbackState); } _resetScheduleDrafts() { for (const field of Object.values(this._scheduleInputState)) { field.draft = ""; field.editing = false; field.pending = false; } } _beginScheduleEdit(key, fallbackState) { const field = this._scheduleFieldState(key); field.editing = true; field.pending = false; if (!field.draft) { field.draft = this._isoForInput(fallbackState); } } _updateScheduleDraft(key, value) { const field = this._scheduleFieldState(key); field.draft = value; field.pending = false; } _endScheduleEdit(key, event) { const field = this._scheduleFieldState(key); field.editing = false; if (!this._isScheduleEditing() && this._pendingRender) { this._requestRender(); } } _bindScheduleInput(key, inputId, fallbackState) { const input = this.shadowRoot.getElementById(inputId); if (!input) return; input.addEventListener("focus", () => this._beginScheduleEdit(key, fallbackState)); input.addEventListener("input", (event) => this._updateScheduleDraft(key, event.target.value)); input.addEventListener("change", (event) => this._updateScheduleDraft(key, event.target.value)); input.addEventListener("blur", (event) => this._endScheduleEdit(key, event)); } _firstExistingEntityId(entityIds) { for (const entityId of entityIds) { if (entityId && this._state(entityId)) return entityId; } return entityIds.find(Boolean) || null; } _scheduleOpenEntityId() { return this._firstExistingEntityId([ this.config.scheduleOpen, "datetime.sternwarte_offnen_geplant", "datetime.sternwarte_oeffnen_geplant", ]); } _scheduleCloseEntityId() { return this._firstExistingEntityId([ this.config.scheduleClose, "datetime.sternwarte_schliessen_geplant", ]); } _powerEntityId() { const configured = this.config?.power; if (configured && this._state(configured)) { return configured; } const states = this._hass?.states || {}; const fallback = Object.keys(states).find((entityId) => { if (!entityId.startsWith("switch.")) return false; const normalized = entityId.toLowerCase(); const friendlyName = String(states[entityId]?.attributes?.friendly_name || "").toLowerCase(); return ( normalized.includes("dachsteuer") || (normalized.includes("sternwarte") && normalized.includes("power")) || friendlyName.includes("dachsteuerung") ); }); return fallback || configured || "switch.sternwarte_dachsteuerung"; } async _call(domain, service) { try { const result = this._hass.callService(domain, service, {}); Promise.resolve(result).catch(() => {}); } catch (error) {} } async _callEntity(domain, service, entityId) { if (!entityId) return; try { const result = this._hass.callService(domain, service, { entity_id: entityId }); Promise.resolve(result).catch(() => {}); } catch (error) {} } _bindClick(id, handler) { this.shadowRoot.getElementById(id)?.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); handler(); }); } _showMoreInfo(entityId) { if (!entityId) return; const event = new Event("hass-more-info", { bubbles: true, composed: true, }); event.detail = { entityId }; this.dispatchEvent(event); } _bindMoreInfo(id, entityId) { const element = this.shadowRoot.getElementById(id); if (!element || !entityId) return; const openMoreInfo = (event) => { event.preventDefault(); event.stopPropagation(); this._showMoreInfo(entityId); }; element.addEventListener("click", openMoreInfo); element.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { openMoreInfo(event); } }); } _setDateTime(entityId, inputId, key) { const input = this.shadowRoot.getElementById(inputId); const field = this._scheduleFieldState(key); const value = field?.draft || input?.value || ""; if (!value) return; if (!entityId) return; field.pending = true; field.editing = false; field.draft = value; this._hass.callService("datetime", "set_value", { entity_id: entityId, datetime: value, }); if (this._pendingRender) { this._requestRender(); } } _isoForInput(state) { if (!state || !state.state || state.state === "unknown" || state.state === "unavailable") { return ""; } const value = new Date(state.state); if (Number.isNaN(value.getTime())) return ""; const pad = (n) => String(n).padStart(2, "0"); return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}T${pad(value.getHours())}:${pad(value.getMinutes())}`; } _formatDateTime(state) { if (!state || !state.state || state.state === "unknown" || state.state === "unavailable") { return "\u2014"; } const value = new Date(state.state); if (Number.isNaN(value.getTime())) return "\u2014"; const pad = (n) => String(n).padStart(2, "0"); return `${pad(value.getDate())}.${pad(value.getMonth() + 1)}. ${pad(value.getHours())}:${pad(value.getMinutes())}`; } _roofStatusText() { const status = this._state(this.config.roofStatus)?.state; switch (status) { case "offen": return "Offen"; case "geschlossen": return "Geschlossen"; case "in_bewegung": return "In Bewegung"; case "sensorfehler": return "Sensorfehler"; case "unbekannt": return "Unbekannt"; default: return this._labelForRoof(); } } _roofPositionValue() { const value = Number(this._state(this.config.roofPosition)?.state); if (Number.isNaN(value)) return null; return Math.max(0, Math.min(100, Math.round(value))); } _labelForRoof() { const roof = this._state(this.config.roof); const moving = this._state(this.config.roofMoving); const closed = this._state(this.config.roofClosed); const open = this._state(this.config.roofOpen); if (moving?.state === "on") return "In Bewegung"; if (open?.state === "on" && closed?.state !== "on") return "Offen"; if (closed?.state === "on" && open?.state !== "on") return "Geschlossen"; if (roof?.state === "opening") return "\u00d6ffnet"; if (roof?.state === "closing") return "Schlie\u00dft"; return "Unbekannt"; } _boolBadge(label, entityId, onText = "Aktiv", offText = "Aus", extraClass = "", options = {}) { const state = this._state(entityId); const on = state?.state === "on"; const animStyle = (on && extraClass) ? ` style="animation-delay: -${Date.now() % 1000}ms"` : ""; const idAttr = options.id ? ` id="${options.id}"` : ""; const clickableClass = options.clickable ? " clickable" : ""; const roleAttr = options.clickable ? ` role="button" tabindex="0" aria-label="${label} öffnen"` : ""; return `
${label} ${on ? onText : offText}
`; } _openBlockedReason() { const rain = this._state(this.config.rain)?.state === "on"; const moving = this._state(this.config.roofMoving)?.state === "on"; const open = this._state(this.config.roofOpen)?.state === "on"; if (rain) return "\u00d6ffnen gesperrt: Regen erkannt"; if (moving) return "\u00d6ffnen gesperrt: Dach ist in Bewegung"; if (open) return "Dach ist bereits offen"; return ""; } _powerIndicatorClassName() { const state = this._state(this._powerEntityId())?.state; if (state === "on") return "on"; if (state === "off") return "off"; return "unknown"; } _inputPanelStates() { const roofStatus = this._state(this.config.roofStatus); const rawInputs = Array.isArray(roofStatus?.attributes?.inputs) ? roofStatus.attributes.inputs : []; const knownInputs = { 7: { label: "Regen", entityId: this.config.rain, accent: "warn" }, 9: { label: "Dach bewegt", entityId: this.config.roofMoving, accent: "motion" }, 10: { label: "Dach zu", entityId: this.config.roofClosed, accent: "ok" }, 11: { label: "Dach offen", entityId: this.config.roofOpen, accent: "ok" }, }; return Array.from({ length: 12 }, (_, index) => { const known = knownInputs[index] || {}; const entityState = known.entityId ? this._state(known.entityId) : null; const rawValue = typeof rawInputs[index] === "boolean" ? rawInputs[index] : null; const entityValue = entityState?.state === "on" ? true : entityState?.state === "off" ? false : null; const on = rawValue ?? entityValue; return { key: `input-${index}`, label: known.label || "", shortLabel: `IN ${index}`, accent: known.accent || "neutral", on, }; }); } _windIndicatorState() { const wind = this._state(this.config.wind); const value = Number(wind?.state); const display = wind?.state && wind.state !== "unknown" && wind.state !== "unavailable" ? `${wind.state} ${wind.attributes?.unit_of_measurement ?? "km/h"}` : "\u2014"; if (Number.isNaN(value)) { return { display, className: "", }; } if (value <= 10) { return { display, className: "wind-good", }; } if (value <= 15) { return { display, className: "wind-warn", }; } return { display, className: "wind-danger", }; } _isMESZ(date) { const year = date.getUTCFullYear(); const lastSunday = (month, hour) => { const event = new Date(Date.UTC(year, month + 1, 0, hour)); while (event.getUTCDay() !== 0) event.setUTCDate(event.getUTCDate() - 1); return event; }; return date >= lastSunday(2, 1) && date < lastSunday(9, 1); } _astroObserver() { const astronomy = this._astronomy(); if (!astronomy) return null; const lat = Number(this.config.astroLat); const lon = Number(this.config.astroLon); if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null; return new astronomy.Observer(lat, lon, 0); } _astroTimeToUtcHour(eventTime) { if (!eventTime) return null; const date = eventTime.date instanceof Date ? eventTime.date : null; if (!date || Number.isNaN(date.getTime())) return null; return date.getUTCHours() + date.getUTCMinutes() / 60 + date.getUTCSeconds() / 3600; } _calcTwilight(date) { const astronomy = this._astronomy(); const observer = this._astroObserver(); if (!astronomy || !observer) { return { sun: { dawn: null, dusk: null }, civil: { dawn: null, dusk: null }, naut: { dawn: null, dusk: null }, astro: { dawn: null, dusk: null }, }; } try { const bodySun = astronomy.Body?.Sun ?? "Sun"; const start = new Date(date); start.setHours(0, 0, 0, 0); const limitDays = 2; const altitudeEvent = (altitude) => ({ dawn: this._astroTimeToUtcHour(astronomy.SearchAltitude(bodySun, observer, +1, start, limitDays, altitude)), dusk: this._astroTimeToUtcHour(astronomy.SearchAltitude(bodySun, observer, -1, start, limitDays, altitude)), }); return { sun: { dawn: this._astroTimeToUtcHour(astronomy.SearchRiseSet(bodySun, observer, +1, start, limitDays, 0)), dusk: this._astroTimeToUtcHour(astronomy.SearchRiseSet(bodySun, observer, -1, start, limitDays, 0)), }, civil: altitudeEvent(-6), naut: altitudeEvent(-12), astro: altitudeEvent(-18), }; } catch (error) { return { sun: { dawn: null, dusk: null }, civil: { dawn: null, dusk: null }, naut: { dawn: null, dusk: null }, astro: { dawn: null, dusk: null }, }; } } _moonStateAt(date) { const astronomy = this._astronomy(); const observer = this._astroObserver(); if (!astronomy || !observer) { return { altitude: Number.NEGATIVE_INFINITY, illumination: 0, phaseAngle: 0 }; } try { const bodyMoon = astronomy.Body?.Moon ?? "Moon"; const equator = astronomy.Equator(bodyMoon, date, observer, true, true); const horizon = astronomy.Horizon(date, observer, equator.ra, equator.dec, "normal"); const illumination = astronomy.Illumination(bodyMoon, date); const phaseAngle = Number(astronomy.MoonPhase(date)) || 0; return { altitude: (Number(horizon?.altitude) || 0) * Math.PI / 180, illumination: Math.max(0, Math.min(1, Number(illumination?.phase_fraction) || 0)), phaseAngle: ((phaseAngle % 360) + 360) % 360, }; } catch (error) { return { altitude: Number.NEGATIVE_INFINITY, illumination: 0, phaseAngle: 0 }; } } _sunAltitudeDegAt(date) { const astronomy = this._astronomy(); const observer = this._astroObserver(); if (!astronomy || !observer) return Number.NEGATIVE_INFINITY; try { const bodySun = astronomy.Body?.Sun ?? "Sun"; const equator = astronomy.Equator(bodySun, date, observer, true, true); const horizon = astronomy.Horizon(date, observer, equator.ra, equator.dec, "normal"); return Number(horizon?.altitude); } catch (error) { return Number.NEGATIVE_INFINITY; } } _astroPhaseAt(date) { const altitude = this._sunAltitudeDegAt(date); if (!Number.isFinite(altitude)) return "night"; if (altitude >= -0.833) return "day"; if (altitude >= -6) return "civil"; if (altitude >= -12) return "naut"; if (altitude >= -18) return "astro"; return "night"; } _moonVisualState() { const moon = this._moonStateAt(new Date()); const phaseAngle = moon.phaseAngle ?? 0; const waxing = phaseAngle <= 180; const radians = phaseAngle * Math.PI / 180; return { illumination: Math.max(0, Math.min(1, moon.illumination || 0)), illuminationPercent: Math.round((moon.illumination || 0) * 100), waxing, phaseAngle, cosPhase: Math.cos(radians), }; } _renderMoonPhaseDataUrl(moonVisual, size = 64) { const texture = this._moonTexture; if (!texture) return ""; const canvas = document.createElement("canvas"); canvas.width = size; canvas.height = size; const ctx = canvas.getContext("2d"); if (!ctx) return ""; const radius = size / 2 - 1; const x = size / 2; const y = size / 2; const cosPhase = Number(moonVisual.cosPhase) || 0; ctx.save(); ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); ctx.clip(); ctx.fillStyle = "rgba(8,10,16,0.96)"; ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 0.98; ctx.drawImage(texture, x - radius, y - radius, radius * 2, radius * 2); ctx.globalAlpha = 1; ctx.fillStyle = "rgba(8,10,16,0.96)"; ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); ctx.fill(); ctx.save(); ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); ctx.clip(); if (moonVisual.illumination >= 0.999) { ctx.drawImage(texture, x - radius, y - radius, radius * 2, radius * 2); } else if (moonVisual.illumination > 0.001) { ctx.beginPath(); for (let i = 0; i <= 72; i += 1) { const t = -radius + (2 * radius * i) / 72; const xr = Math.sqrt(Math.max(0, radius * radius - t * t)); const xt = (moonVisual.waxing ? cosPhase : -cosPhase) * xr; if (i === 0) { ctx.moveTo(x + xt, y + t); } else { ctx.lineTo(x + xt, y + t); } } for (let i = 72; i >= 0; i -= 1) { const t = -radius + (2 * radius * i) / 72; const xr = Math.sqrt(Math.max(0, radius * radius - t * t)); ctx.lineTo(x + (moonVisual.waxing ? xr : -xr), y + t); } ctx.closePath(); ctx.clip(); ctx.drawImage(texture, x - radius, y - radius, radius * 2, radius * 2); } ctx.restore(); ctx.restore(); ctx.strokeStyle = "rgba(255,255,255,0.18)"; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); ctx.stroke(); return canvas.toDataURL("image/png"); } _moonTrackState() { const now = new Date(); const cycleStart = new Date(now); cycleStart.setHours(12, 0, 0, 0); if (now.getHours() < 12) { cycleStart.setDate(cycleStart.getDate() - 1); } const stepMinutes = 10; const totalSteps = Math.round((24 * 60) / stepMinutes); const segments = []; let segmentStart = null; let currentStep = 0; const currentMoon = this._moonStateAt(now); for (; currentStep <= totalSteps; currentStep += 1) { const sample = new Date(cycleStart.getTime() + currentStep * stepMinutes * 60000); const visible = this._moonStateAt(sample).altitude > 0; if (visible && segmentStart === null) { segmentStart = currentStep; } if ((!visible || currentStep === totalSteps) && segmentStart !== null) { const endStep = visible && currentStep === totalSteps ? currentStep : currentStep - 1; const startHour = (segmentStart * stepMinutes) / 60; const endHour = ((endStep + 1) * stepMinutes) / 60; segments.push({ left: `${(startHour / 24) * 100}%`, width: `${Math.max(((endHour - startHour) / 24) * 100, 0.5)}%`, }); segmentStart = null; } } return { opacity: "0.38", segments, }; } _formatHourValue(hourValue) { if (hourValue === null || hourValue === undefined) return "\u2014"; const hours = ((hourValue % 24) + 24) % 24; const h = Math.floor(hours); const m = Math.floor((hours - h) * 60); return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`; } _astroState() { const now = new Date(); const windowStart = new Date(now); windowStart.setHours(12, 0, 0, 0); if (now < windowStart) { windowStart.setDate(windowStart.getDate() - 1); } const nowLocal = (now.getTime() - windowStart.getTime()) / 3600000; const segments = []; const colors = { night: "#16202f", astro: "#2a4978", naut: "#4a76bf", civil: "#c68c50", day: "#eed06f", }; const sampleMinutes = 5; const totalSamples = Math.floor((24 * 60) / sampleMinutes); let activePhase = null; let segmentStart = 0; const pushSegment = (startHour, endHour, phaseName) => { if (!phaseName || endHour <= startHour) return; segments.push({ start: startHour, end: endHour, color: colors[phaseName], }); }; for (let index = 0; index <= totalSamples; index += 1) { const minutesFromStart = Math.min(index * sampleMinutes, 24 * 60); const sampleDate = new Date(windowStart.getTime() + minutesFromStart * 60000); const phase = this._astroPhaseAt(sampleDate); if (activePhase === null) { activePhase = phase; segmentStart = minutesFromStart / 60; continue; } if (phase !== activePhase) { pushSegment(segmentStart, minutesFromStart / 60, activePhase); activePhase = phase; segmentStart = minutesFromStart / 60; } } if (activePhase !== null) { pushSegment(segmentStart, 24, activePhase); } segments.sort((a, b) => a.start - b.start); const toWindowHour = (state) => { if (!state?.state || state.state === "unknown" || state.state === "unavailable") return null; const value = new Date(state.state); if (Number.isNaN(value.getTime())) return null; const hour = (value.getTime() - windowStart.getTime()) / 3600000; if (hour < 0 || hour > 24) return null; return hour; }; return { now: now.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" }), phaseSegments: segments.map((segment) => ({ left: `${(segment.start / 24) * 100}%`, width: `${Math.max(((segment.end - segment.start) / 24) * 100, 0.35)}%`, color: segment.color, })), markerLeft: `${(nowLocal / 24) * 100}%`, openMarkerLeft: toWindowHour(this._state(this._scheduleOpenEntityId())), closeMarkerLeft: toWindowHour(this._state(this._scheduleCloseEntityId())), }; } render() { if (!this.shadowRoot || !this._hass) return; const powerEntityId = this._powerEntityId(); const powerIndicatorClassName = this._powerIndicatorClassName(); const openScheduleEntityId = this._scheduleOpenEntityId(); const closeScheduleEntityId = this._scheduleCloseEntityId(); const openSchedule = this._state(openScheduleEntityId); const closeSchedule = this._state(closeScheduleEntityId); const openInputValue = this._scheduleInputValue("open", openSchedule); const closeInputValue = this._scheduleInputValue("close", closeSchedule); if (openInputValue === this._isoForInput(openSchedule)) { this._scheduleFieldState("open").draft = ""; this._scheduleFieldState("open").pending = false; } if (closeInputValue === this._isoForInput(closeSchedule)) { this._scheduleFieldState("close").draft = ""; this._scheduleFieldState("close").pending = false; } const roofPosition = this._roofPositionValue(); const windIndicator = this._windIndicatorState(); const inputPanelStates = this._inputPanelStates(); const movingOn = this._state(this.config.roofMoving)?.state === "on"; const openOn = this._state(this.config.roofOpen)?.state === "on"; const closedOn = this._state(this.config.roofClosed)?.state === "on"; const openBlockedReason = this._openBlockedReason(); const powerState = this._state(powerEntityId)?.state; const powerOn = powerState === "on"; const powerOff = powerState === "off"; const powerOnButtonDisabled = powerOn; const powerOffButtonDisabled = powerOff; let openButtonDisabled; let closeButtonDisabled; let stopButtonDisabled; if (!powerOn) { openButtonDisabled = true; closeButtonDisabled = true; stopButtonDisabled = true; } else if (movingOn) { openButtonDisabled = true; closeButtonDisabled = true; stopButtonDisabled = false; } else if (openOn && !closedOn) { openButtonDisabled = true; closeButtonDisabled = false; stopButtonDisabled = true; } else if (closedOn && !openOn) { openButtonDisabled = false; closeButtonDisabled = true; stopButtonDisabled = true; } else if (closedOn && openOn) { openButtonDisabled = true; closeButtonDisabled = true; stopButtonDisabled = false; } else { openButtonDisabled = false; closeButtonDisabled = false; stopButtonDisabled = false; } const astro = this._astroState(); const moonTrack = this._moonTrackState(); const moonVisual = this._moonVisualState(); const moonImageDataUrl = this._renderMoonPhaseDataUrl(moonVisual, 56); this.shadowRoot.innerHTML = `
\u00d6ffnen geplant ${this._formatDateTime(openSchedule)}
Schlie\u00dfen geplant ${this._formatDateTime(closeSchedule)}
Wind ${windIndicator.display}
Mond
${moonVisual.illuminationPercent} %
${this._boolBadge("Dach zu", this.config.roofClosed, "Ja", "Nein")} ${this._boolBadge("Dach offen", this.config.roofOpen, "Ja", "Nein")} ${this._boolBadge("Dach bewegt", this.config.roofMoving, "Ja", "Nein", "moving")} ${this._boolBadge("Regen", this.config.rain, "Ja", "Nein", "rain", { id: "rain-badge", clickable: true })}
Jetzt${astro.now}
12
18
0
6
12
${astro.phaseSegments.map((segment) => `
`).join("")} ${moonTrack.segments.map((segment) => `
`).join("")} ${astro.openMarkerLeft === null ? "" : `
`} ${astro.closeMarkerLeft === null ? "" : `
`}
Dachposition ${roofPosition === null ? "\u2014" : `${roofPosition} %`}
Steuerung
${openBlockedReason ? `
${openBlockedReason}
` : ""}
Zeitplanung
\u00d6ffnen geplant ${this._formatDateTime(openSchedule)}
Schlie\u00dfen geplant ${this._formatDateTime(closeSchedule)}
Eing\u00e4nge
${inputPanelStates.map((input) => `
${input.shortLabel} ${input.on === true ? "AN" : input.on === false ? "AUS" : "UNBEKANNT"}
`).join("")}
`; this._bindClick("open-btn", () => this._callEntity("cover", "open_cover", this.config.roof)); this._bindClick("stop-btn", () => this._callEntity("cover", "stop_cover", this.config.roof)); this._bindClick("close-btn", () => this._callEntity("cover", "close_cover", this.config.roof)); this._bindClick("power-on-btn", () => this._callEntity("switch", "turn_on", powerEntityId)); this._bindClick("power-off-btn", () => this._callEntity("switch", "turn_off", powerEntityId)); this._bindClick("nautical-btn", () => this._call("sternwarte", "set_nautical_schedule")); this._bindClick("clear-schedule-btn", () => { this._resetScheduleDrafts(); this._call("sternwarte", "clear_schedule"); this._requestRender(); }); this._bindClick("open-schedule-btn", () => this._setDateTime(openScheduleEntityId, "open-schedule-input", "open")); this._bindClick("close-schedule-btn", () => this._setDateTime(closeScheduleEntityId, "close-schedule-input", "close")); this._bindMoreInfo("wind-stat", this.config.wind); this._bindMoreInfo("rain-badge", this.config.rain); this._bindScheduleInput("open", "open-schedule-input", openSchedule); this._bindScheduleInput("close", "close-schedule-input", closeSchedule); } } if (!customElements.get("sternwarte-card")) { customElements.define("sternwarte-card", SternwarteCard); } window.customCards = window.customCards || []; if (!window.customCards.some((card) => card?.type === "sternwarte-card")) { window.customCards.push({ type: "sternwarte-card", name: "Sternwarte Card", description: "Eigene Card f\u00fcr die Sternwarte", }); }