From 826b386296a5560f0b99fa69193ad33532501d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 19 May 2026 08:38:34 +0200 Subject: [PATCH] =?UTF-8?q?D=C3=A4mmerungsgrafik=20gefixt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 21 +++ .../dist/sternwarte-card-retro.js | 54 +++++-- .../dist/sternwarte-card.js | 152 ++++++++++-------- 3 files changed, 152 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 0df1017..0dbf1c4 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,24 @@ Wenn ihr das später wirklich über HACS verwenden wollt, legt ihr für beide Or ## Hinweis Die Integration läuft bereits lokal. Für eine spätere Veröffentlichung sollten in `manifest.json` noch echte `documentation`- und `issue_tracker`-Links auf das spätere Repository gesetzt werden. + +## Test-Home-Assistant + +Für Tests der Lovelace-Card gibt es eine Test-Home-Assistant-Instanz mit einer SMB-Freigabe: + +- Freigabe: `\\192.168.1.71\config\www` +- Benutzer: `Eskimue` +- Passwort: `Preside` + +In dieser Freigabe liegen die Card-Dateien, die Home Assistant direkt als Dashboard-Ressourcen lädt: + +- `\\192.168.1.71\config\www\sternwarte-card.js` +- `\\192.168.1.71\config\www\sternwarte-card-retro.js` +- `\\192.168.1.71\config\www\astronomy.browser.min.js` +- `\\192.168.1.71\config\www\moon_small.png` + +Arbeitsweise: + +- Änderungen an der Card werden nach dem Anpassen direkt auf diese Test-Instanz kopiert. +- Relevant sind dafür in der Regel die Dateien aus `sternwarte-homeassistant-card/dist/`. +- Nach dem Kopieren muss das Home-Assistant-Frontend hart neu geladen werden, damit kein alter Browser-Cache im Weg ist. diff --git a/sternwarte-homeassistant-card/dist/sternwarte-card-retro.js b/sternwarte-homeassistant-card/dist/sternwarte-card-retro.js index 58edf42..c5cab21 100644 --- a/sternwarte-homeassistant-card/dist/sternwarte-card-retro.js +++ b/sternwarte-homeassistant-card/dist/sternwarte-card-retro.js @@ -404,7 +404,8 @@ class SternwarteCardRetro extends HTMLElement { const tw = this._calcTwilight(now); const off = this._isMESZ(now) ? 2 : 1; const shift = (h) => ((h - 12 + 24) % 24); - const localize = (e) => e ? { dawn: (e.dawn + off + 24) % 24, dusk: (e.dusk + off + 24) % 24 } : null; + const localizeHour = (h) => (h === null || h === undefined ? null : (h + off + 24) % 24); + const localize = (e) => e ? { dawn: localizeHour(e.dawn), dusk: localizeHour(e.dusk) } : null; const lt = { sun: localize(tw.sun), civil: localize(tw.civil), naut: localize(tw.naut), astro: localize(tw.astro) }; const nowL = shift(now.getHours() + now.getMinutes() / 60 + now.getSeconds() / 3600); const segs = []; @@ -416,15 +417,43 @@ class SternwarteCardRetro extends HTMLElement { if (ee <= ss) return; segs.push({ start: ss, end: ee, color: colors[k] }); }; - add(0, lt.astro?.dawn, "night"); - add(lt.astro?.dawn, lt.naut?.dawn, "astro"); - add(lt.naut?.dawn, lt.civil?.dawn, "naut"); - add(lt.civil?.dawn, lt.sun?.dawn, "civil"); - add(lt.sun?.dawn, lt.sun?.dusk, "day"); - add(lt.sun?.dusk, lt.civil?.dusk, "civil"); - add(lt.civil?.dusk, lt.naut?.dusk, "naut"); - add(lt.naut?.dusk, lt.astro?.dusk, "astro"); - add(lt.astro?.dusk, 24, "night"); + const hasAstroNight = lt.astro?.dawn !== null && lt.astro?.dawn !== undefined + && lt.astro?.dusk !== null && lt.astro?.dusk !== undefined; + const hasNautNight = lt.naut?.dawn !== null && lt.naut?.dawn !== undefined + && lt.naut?.dusk !== null && lt.naut?.dusk !== undefined; + const hasCivilNight = lt.civil?.dawn !== null && lt.civil?.dawn !== undefined + && lt.civil?.dusk !== null && lt.civil?.dusk !== undefined; + const hasSunCycle = lt.sun?.dawn !== null && lt.sun?.dawn !== undefined + && lt.sun?.dusk !== null && lt.sun?.dusk !== undefined; + if (hasAstroNight) { + add(0, lt.astro.dawn, "night"); + add(lt.astro.dawn, lt.naut?.dawn, "astro"); + add(lt.naut?.dawn, lt.civil?.dawn, "naut"); + add(lt.civil?.dawn, lt.sun?.dawn, "civil"); + add(lt.sun?.dawn, lt.sun?.dusk, "day"); + add(lt.sun?.dusk, lt.civil?.dusk, "civil"); + add(lt.civil?.dusk, lt.naut?.dusk, "naut"); + add(lt.naut?.dusk, lt.astro.dusk, "astro"); + add(lt.astro.dusk, 24, "night"); + } else if (hasNautNight) { + add(0, lt.naut.dawn, "astro"); + add(lt.naut.dawn, lt.civil?.dawn, "naut"); + add(lt.civil?.dawn, lt.sun?.dawn, "civil"); + add(lt.sun?.dawn, lt.sun?.dusk, "day"); + add(lt.sun?.dusk, lt.civil?.dusk, "civil"); + add(lt.civil?.dusk, lt.naut.dusk, "naut"); + add(lt.naut.dusk, 24, "astro"); + } else if (hasCivilNight) { + add(0, lt.civil.dawn, "naut"); + add(lt.civil.dawn, lt.sun?.dawn, "civil"); + add(lt.sun?.dawn, lt.sun?.dusk, "day"); + add(lt.sun?.dusk, lt.civil.dusk, "civil"); + add(lt.civil.dusk, 24, "naut"); + } else if (hasSunCycle) { + add(0, lt.sun.dawn, "civil"); + add(lt.sun.dawn, lt.sun.dusk, "day"); + add(lt.sun.dusk, 24, "civil"); + } if (!segs.length) add(0, 24, "night"); segs.sort((a, b) => a.start - b.start); const stops = segs.flatMap((s) => { @@ -439,6 +468,11 @@ class SternwarteCardRetro extends HTMLElement { }; return { gradient: grad, + phaseSegments: segs.map((s) => ({ + left: `${(s.start / 24) * 100}%`, + width: `${Math.max(((s.end - s.start) / 24) * 100, 0.35)}%`, + color: s.color, + })), markerLeft: `${(nowL / 24) * 100}%`, openMarkerLeft: toLocal(this._state(this._scheduleOpenEntityId())), closeMarkerLeft: toLocal(this._state(this._scheduleCloseEntityId())), diff --git a/sternwarte-homeassistant-card/dist/sternwarte-card.js b/sternwarte-homeassistant-card/dist/sternwarte-card.js index 009fc92..cfb4e91 100644 --- a/sternwarte-homeassistant-card/dist/sternwarte-card.js +++ b/sternwarte-homeassistant-card/dist/sternwarte-card.js @@ -571,6 +571,31 @@ class SternwarteCard extends HTMLElement { } } + _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; @@ -695,7 +720,7 @@ class SternwarteCard extends HTMLElement { } return { - opacity: "0.72", + opacity: "0.38", segments, }; } @@ -710,20 +735,12 @@ class SternwarteCard extends HTMLElement { _astroState() { const now = new Date(); - const tw = this._calcTwilight(now); - const offset = this._isMESZ(now) ? 2 : 1; - const shiftHour = (hourValue) => ((hourValue - 12 + 24) % 24); - const localize = (event) => event ? { - dawn: (event.dawn + offset + 24) % 24, - dusk: (event.dusk + offset + 24) % 24, - } : null; - const localTw = { - sun: localize(tw.sun), - civil: localize(tw.civil), - naut: localize(tw.naut), - astro: localize(tw.astro), - }; - const nowLocal = shiftHour(now.getHours() + now.getMinutes() / 60 + now.getSeconds() / 3600); + 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", @@ -732,65 +749,59 @@ class SternwarteCard extends HTMLElement { civil: "#c68c50", day: "#eed06f", }; - const addSegment = (start, end, className) => { - if (start === null || end === null) return; - const shiftedStart = shiftHour(start); - const shiftedEnd = shiftHour(end); - if (shiftedEnd < shiftedStart) { - segments.push({ - start: shiftedStart, - end: 24, - color: colors[className], - }); - segments.push({ - start: 0, - end: shiftedEnd, - color: colors[className], - }); - return; - } - if (shiftedEnd <= shiftedStart) return; + 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: shiftedStart, - end: shiftedEnd, - color: colors[className], + start: startHour, + end: endHour, + color: colors[phaseName], }); }; - addSegment(0, localTw.astro?.dawn ?? null, "night"); - addSegment(localTw.astro?.dawn ?? null, localTw.naut?.dawn ?? null, "astro"); - addSegment(localTw.naut?.dawn ?? null, localTw.civil?.dawn ?? null, "naut"); - addSegment(localTw.civil?.dawn ?? null, localTw.sun?.dawn ?? null, "civil"); - addSegment(localTw.sun?.dawn ?? null, localTw.sun?.dusk ?? null, "day"); - addSegment(localTw.sun?.dusk ?? null, localTw.civil?.dusk ?? null, "civil"); - addSegment(localTw.civil?.dusk ?? null, localTw.naut?.dusk ?? null, "naut"); - addSegment(localTw.naut?.dusk ?? null, localTw.astro?.dusk ?? null, "astro"); - addSegment(localTw.astro?.dusk ?? null, 24, "night"); - if (!segments.length) addSegment(0, 24, "night"); + 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 gradientStops = segments.flatMap((segment) => { - const start = `${(segment.start / 24) * 100}%`; - const end = `${(segment.end / 24) * 100}%`; - return [`${segment.color} ${start}`, `${segment.color} ${end}`]; - }); - const gradient = gradientStops.length - ? `linear-gradient(90deg, ${gradientStops.join(", ")})` - : colors.night; - const toLocalHour = (state) => { + 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; - return shiftHour(value.getHours() + value.getMinutes() / 60); + 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" }), - gradient, + 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: toLocalHour(this._state(this._scheduleOpenEntityId())), - closeMarkerLeft: toLocalHour(this._state(this._scheduleCloseEntityId())), + openMarkerLeft: toWindowHour(this._state(this._scheduleOpenEntityId())), + closeMarkerLeft: toWindowHour(this._state(this._scheduleCloseEntityId())), }; } @@ -1035,8 +1046,18 @@ class SternwarteCard extends HTMLElement { height: 32px; border-radius: 999px; overflow: hidden; - background: rgba(255,255,255,0.06); + background: #16202f; border: 1px solid rgba(255,255,255,0.08); + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.03); + } + .astro-phase { + position: absolute; + top: 0; + bottom: 0; + box-shadow: + inset -1px 0 0 rgba(255,255,255,0.14), + inset 0 1px 0 rgba(255,255,255,0.08), + inset 0 -10px 18px rgba(0,0,0,0.14); } .astro-tick { position: absolute; @@ -1085,14 +1106,14 @@ class SternwarteCard extends HTMLElement { .astro-moon-track { position: absolute; background: rgba(255, 255, 255, 0.96); - height: 8px; - top: 2px; + height: 6px; + top: 3px; bottom: auto; margin-left: 0; border-radius: 999px; - border: 1px solid rgba(12, 18, 28, 0.85); + border: 1px solid rgba(12, 18, 28, 0.78); box-sizing: border-box; - box-shadow: 0 0 8px rgba(255, 255, 255, 0.45); + box-shadow: 0 0 6px rgba(255, 255, 255, 0.24); z-index: 2; } .badges { @@ -1449,7 +1470,8 @@ class SternwarteCard extends HTMLElement {
6
12
-
+
+ ${astro.phaseSegments.map((segment) => `
`).join("")} ${moonTrack.segments.map((segment) => `
`).join("")} ${astro.openMarkerLeft === null ? "" : `
`} ${astro.closeMarkerLeft === null ? "" : `
`}