Dämmerungsgrafik gefixt

This commit is contained in:
2026-05-19 08:38:34 +02:00
parent e57d53b61a
commit 826b386296
3 changed files with 152 additions and 75 deletions
+21
View File
@@ -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.
+44 -10
View File
@@ -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())),
+87 -65
View File
@@ -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 {
<div class="astro-tick-label" style="left:75%">6</div>
<div class="astro-tick" style="left:100%"></div>
<div class="astro-tick-label" style="left:100%">12</div>
<div class="astro-bar" style="background:${astro.gradient}">
<div class="astro-bar">
${astro.phaseSegments.map((segment) => `<div class="astro-phase" style="left:${segment.left};width:${segment.width};background:${segment.color}"></div>`).join("")}
${moonTrack.segments.map((segment) => `<div class="astro-moon-track" style="left:${segment.left};width:${segment.width};opacity:${moonTrack.opacity}"></div>`).join("")}
${astro.openMarkerLeft === null ? "" : `<div class="astro-marker open" style="left:${(astro.openMarkerLeft / 24) * 100}%"></div>`}
${astro.closeMarkerLeft === null ? "" : `<div class="astro-marker close" style="left:${(astro.closeMarkerLeft / 24) * 100}%"></div>`}