diff --git a/README.md b/README.md
index ad0166f..27c2658 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,6 @@ Für Tests gibt es eine komplette Test-Home-Assistant-Umgebung mit SMB-Zugriff u
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`
diff --git a/deploy-test.ps1 b/deploy-test.ps1
index 94edf48..75e4930 100644
--- a/deploy-test.ps1
+++ b/deploy-test.ps1
@@ -5,7 +5,6 @@ $sourceDir = Join-Path $repoRoot "sternwarte-homeassistant-card\dist"
$targetDir = "\\192.168.1.71\config\www"
$files = @(
"sternwarte-card.js",
- "sternwarte-card-retro.js",
"astronomy.browser.min.js",
"moon_small.png"
)
diff --git a/sternwarte-homeassistant-card/dist/sternwarte-card-retro.js b/sternwarte-homeassistant-card/dist/sternwarte-card-retro.js
deleted file mode 100644
index 99becb8..0000000
--- a/sternwarte-homeassistant-card/dist/sternwarte-card-retro.js
+++ /dev/null
@@ -1,1580 +0,0 @@
-/* Sternwarte Card · Retro Edition
- * Vintage Maschinen-Bedienpaneel mit echten 7-Segment-Anzeigen,
- * Kippschaltern, Jewel-Lampen, illuminierten Druckknoepfen und
- * analoger Zeigerinstrument-Anzeige. Funktional identisch mit der
- * Standard-Card, aber komplett unabhaengig.
- */
-
-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 { return null; }
-})();
-
-const MOON_IMAGE_URL = (() => {
- try { return new URL("./moon_small.png", import.meta.url).toString(); }
- catch { return null; }
-})();
-
-/* ---------- 7-Segment-SVG ---------- */
-
-const SEG_POLYS = {
- a: "6,0 24,0 27,3 24,6 6,6 3,3",
- b: "27,5 30,8 30,20 27,23 24,20 24,8",
- c: "27,29 30,32 30,44 27,47 24,44 24,32",
- d: "6,44 24,44 27,47 24,50 6,50 3,47",
- e: "3,29 6,32 6,44 3,47 0,44 0,32",
- f: "3,5 6,8 6,20 3,23 0,20 0,8",
- g: "6,22 24,22 27,25 24,28 6,28 3,25",
-};
-const SEG_MAP = {
- "0": "abcdef", "1": "bc", "2": "abdeg", "3": "abcdg", "4": "bcfg",
- "5": "acdfg", "6": "acdefg", "7": "abc", "8": "abcdefg", "9": "abcdfg",
- "-": "g", " ": "", "_": "d", "?": "abeg",
-};
-
-function seg7(text, opts = {}) {
- const { color = "red", size = 1, pad = 0 } = opts;
- let str = String(text ?? "");
- if (pad > 0 && str.length < pad) str = str.padStart(pad, " ");
- const out = [];
- for (const ch of str) {
- if (ch === ":") {
- out.push(``);
- continue;
- }
- if (ch === ".") {
- out.push(``);
- continue;
- }
- const lit = SEG_MAP[ch.toUpperCase()] ?? "";
- const polys = Object.entries(SEG_POLYS).map(([name, points]) =>
- ``
- ).join("");
- out.push(``);
- }
- return `${out.join("")}`;
-}
-
-/* ---------- Card ---------- */
-
-class SternwarteCardRetro 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 9; }
- getGridOptions() { return { rows: 9, columns: 18, min_rows: 8 }; }
-
- _state(id) { return this._hass?.states?.[id]; }
-
- _ensureClockTimer() {
- if (this._clockTimer) return;
- this._clockTimer = window.setInterval(() => {
- if (this._hass) this._requestRender();
- }, 1000);
- }
-
- _astronomy() { return window.Astronomy || null; }
-
- _ensureAstronomy() {
- const a = this._astronomy();
- if (a) return Promise.resolve(a);
- 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((s) => s.src === ASTRONOMY_LIB_URL);
- if (existing) {
- existing.addEventListener("load", finish, { once: true });
- existing.addEventListener("error", finish, { once: true });
- return;
- }
- const s = document.createElement("script");
- s.src = ASTRONOMY_LIB_URL; s.async = true;
- s.addEventListener("load", finish, { once: true });
- s.addEventListener("error", finish, { once: true });
- document.head.appendChild(s);
- });
- 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 img = new Image();
- img.addEventListener("load", () => {
- this._moonTexture = img;
- this._moonTexturePromise = null;
- if (this._hass) this._requestRender();
- resolve(img);
- }, { once: true });
- img.addEventListener("error", () => { this._moonTexturePromise = null; resolve(null); }, { once: true });
- img.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((f) => f.editing); }
- _scheduleFieldState(k) { return this._scheduleInputState[k]; }
- _scheduleInputValue(k, fb) { const f = this._scheduleFieldState(k); return f.draft || this._isoForInput(fb); }
-
- _resetScheduleDrafts() {
- for (const f of Object.values(this._scheduleInputState)) {
- f.draft = ""; f.editing = false; f.pending = false;
- }
- }
-
- _clearScheduleField(key, domain, service) {
- const f = this._scheduleFieldState(key);
- f.draft = ""; f.editing = false; f.pending = false;
- this._call(domain, service);
- this._requestRender();
- }
-
- _beginScheduleEdit(k, fb) {
- const f = this._scheduleFieldState(k);
- f.editing = true; f.pending = false;
- if (!f.draft) f.draft = this._isoForInput(fb);
- }
- _updateScheduleDraft(k, v) { const f = this._scheduleFieldState(k); f.draft = v; f.pending = false; }
- _endScheduleEdit(k) {
- const f = this._scheduleFieldState(k); f.editing = false;
- if (!this._isScheduleEditing() && this._pendingRender) this._requestRender();
- }
- _bindScheduleInput(k, id, fb) {
- const i = this.shadowRoot.getElementById(id); if (!i) return;
- i.addEventListener("focus", () => this._beginScheduleEdit(k, fb));
- i.addEventListener("input", (e) => this._updateScheduleDraft(k, e.target.value));
- i.addEventListener("change", (e) => this._updateScheduleDraft(k, e.target.value));
- i.addEventListener("blur", () => this._endScheduleEdit(k));
- }
-
- _firstExistingEntityId(ids) {
- for (const id of ids) if (id && this._state(id)) return id;
- return ids.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 c = this.config?.power;
- if (c && this._state(c)) return c;
- const states = this._hass?.states || {};
- const fb = Object.keys(states).find((id) => {
- if (!id.startsWith("switch.")) return false;
- const n = id.toLowerCase();
- const fn = String(states[id]?.attributes?.friendly_name || "").toLowerCase();
- return n.includes("dachsteuer") || (n.includes("sternwarte") && n.includes("power")) || fn.includes("dachsteuerung");
- });
- return fb || c || "switch.sternwarte_dachsteuerung";
- }
-
- _call(d, s) { try { Promise.resolve(this._hass.callService(d, s, {})).catch(() => {}); } catch {} }
- _callEntity(d, s, id) {
- if (!id) return;
- try { Promise.resolve(this._hass.callService(d, s, { entity_id: id })).catch(() => {}); } catch {}
- }
-
- _bindClick(id, fn) {
- this.shadowRoot.getElementById(id)?.addEventListener("click", (e) => {
- e.preventDefault(); e.stopPropagation(); fn();
- });
- }
-
- _showMoreInfo(id) {
- if (!id) return;
- const ev = new Event("hass-more-info", { bubbles: true, composed: true });
- ev.detail = { entityId: id };
- this.dispatchEvent(ev);
- }
- _bindMoreInfo(id, entityId) {
- const el = this.shadowRoot.getElementById(id);
- if (!el || !entityId) return;
- const open = (e) => { e.preventDefault(); e.stopPropagation(); this._showMoreInfo(entityId); };
- el.addEventListener("click", open);
- el.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") open(e); });
- }
-
- _setDateTime(eid, inputId, key) {
- const i = this.shadowRoot.getElementById(inputId);
- const f = this._scheduleFieldState(key);
- const v = f?.draft || i?.value || "";
- if (!v || !eid) return;
- f.pending = true; f.editing = false; f.draft = v;
- this._hass.callService("datetime", "set_value", { entity_id: eid, datetime: v });
- if (this._pendingRender) this._requestRender();
- }
-
- _isoForInput(state) {
- if (!state || !state.state || state.state === "unknown" || state.state === "unavailable") return "";
- const v = new Date(state.state); if (Number.isNaN(v.getTime())) return "";
- const p = (n) => String(n).padStart(2, "0");
- return `${v.getFullYear()}-${p(v.getMonth() + 1)}-${p(v.getDate())}T${p(v.getHours())}:${p(v.getMinutes())}`;
- }
-
- _seg7DateTime(state) {
- if (!state || !state.state || state.state === "unknown" || state.state === "unavailable") return "--:--";
- const v = new Date(state.state); if (Number.isNaN(v.getTime())) return "--:--";
- const p = (n) => String(n).padStart(2, "0");
- return `${p(v.getDate())}.${p(v.getMonth() + 1)} ${p(v.getHours())}:${p(v.getMinutes())}`;
- }
-
- _roofPositionValue() {
- const v = Number(this._state(this.config.roofPosition)?.state);
- if (Number.isNaN(v)) return null;
- return Math.max(0, Math.min(100, Math.round(v)));
- }
-
- _windValue() {
- const w = this._state(this.config.wind);
- const v = Number(w?.state);
- if (Number.isNaN(v)) return { num: null, unit: "km/h", level: 0 };
- let level = 0;
- if (v > 15) level = 2; else if (v > 10) level = 1;
- return { num: Math.round(v), unit: w?.attributes?.unit_of_measurement ?? "km/h", level };
- }
-
- /* --- astronomy / moon (unchanged from original) --- */
-
- _isMESZ(d) {
- const y = d.getUTCFullYear();
- const lastSunday = (m, h) => {
- const e = new Date(Date.UTC(y, m + 1, 0, h));
- while (e.getUTCDay() !== 0) e.setUTCDate(e.getUTCDate() - 1);
- return e;
- };
- return d >= lastSunday(2, 1) && d < lastSunday(9, 1);
- }
- _astroObserver() {
- const a = this._astronomy(); if (!a) return null;
- const lat = Number(this.config.astroLat), lon = Number(this.config.astroLon);
- if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
- return new a.Observer(lat, lon, 0);
- }
- _astroTimeToUtcHour(t) {
- if (!t) return null;
- const d = t.date instanceof Date ? t.date : null;
- if (!d || Number.isNaN(d.getTime())) return null;
- return d.getUTCHours() + d.getUTCMinutes() / 60 + d.getUTCSeconds() / 3600;
- }
- _calcTwilight(date) {
- const a = this._astronomy(), o = this._astroObserver();
- const empty = { sun: { dawn: null, dusk: null }, civil: { dawn: null, dusk: null }, naut: { dawn: null, dusk: null }, astro: { dawn: null, dusk: null } };
- if (!a || !o) return empty;
- try {
- const body = a.Body?.Sun ?? "Sun";
- const start = new Date(date); start.setHours(0, 0, 0, 0);
- const limit = 2;
- const ev = (alt) => ({
- dawn: this._astroTimeToUtcHour(a.SearchAltitude(body, o, +1, start, limit, alt)),
- dusk: this._astroTimeToUtcHour(a.SearchAltitude(body, o, -1, start, limit, alt)),
- });
- return {
- sun: { dawn: this._astroTimeToUtcHour(a.SearchRiseSet(body, o, +1, start, limit, 0)), dusk: this._astroTimeToUtcHour(a.SearchRiseSet(body, o, -1, start, limit, 0)) },
- civil: ev(-6), naut: ev(-12), astro: ev(-18),
- };
- } catch { return empty; }
- }
- _moonStateAt(d) {
- const a = this._astronomy(), o = this._astroObserver();
- if (!a || !o) return { altitude: -Infinity, illumination: 0, phaseAngle: 0 };
- try {
- const body = a.Body?.Moon ?? "Moon";
- const eq = a.Equator(body, d, o, true, true);
- const hz = a.Horizon(d, o, eq.ra, eq.dec, "normal");
- const ill = a.Illumination(body, d);
- const ph = Number(a.MoonPhase(d)) || 0;
- return {
- altitude: (Number(hz?.altitude) || 0) * Math.PI / 180,
- illumination: Math.max(0, Math.min(1, Number(ill?.phase_fraction) || 0)),
- phaseAngle: ((ph % 360) + 360) % 360,
- };
- } catch { return { altitude: -Infinity, illumination: 0, phaseAngle: 0 }; }
- }
- _moonVisualState() {
- const m = this._moonStateAt(new Date());
- const ph = m.phaseAngle ?? 0;
- const waxing = ph <= 180;
- const r = ph * Math.PI / 180;
- return {
- illumination: Math.max(0, Math.min(1, m.illumination || 0)),
- illuminationPercent: Math.round((m.illumination || 0) * 100),
- waxing, phaseAngle: ph, cosPhase: Math.cos(r),
- };
- }
- _renderMoonPhaseDataUrl(mv, size = 64) {
- const tx = this._moonTexture; if (!tx) return "";
- const c = document.createElement("canvas"); c.width = size; c.height = size;
- const ctx = c.getContext("2d"); if (!ctx) return "";
- const r = size / 2 - 1, x = size / 2, y = size / 2;
- const cp = Number(mv.cosPhase) || 0;
- ctx.save(); ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.clip();
- ctx.fillStyle = "rgba(8,10,16,0.96)"; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
- ctx.globalAlpha = 0.98; ctx.drawImage(tx, x - r, y - r, r * 2, r * 2); ctx.globalAlpha = 1;
- ctx.fillStyle = "rgba(8,10,16,0.96)"; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
- ctx.save(); ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.clip();
- if (mv.illumination >= 0.999) {
- ctx.drawImage(tx, x - r, y - r, r * 2, r * 2);
- } else if (mv.illumination > 0.001) {
- ctx.beginPath();
- for (let i = 0; i <= 72; i++) {
- const t = -r + (2 * r * i) / 72;
- const xr = Math.sqrt(Math.max(0, r * r - t * t));
- const xt = (mv.waxing ? cp : -cp) * xr;
- if (i === 0) ctx.moveTo(x + xt, y + t); else ctx.lineTo(x + xt, y + t);
- }
- for (let i = 72; i >= 0; i--) {
- const t = -r + (2 * r * i) / 72;
- const xr = Math.sqrt(Math.max(0, r * r - t * t));
- ctx.lineTo(x + (mv.waxing ? xr : -xr), y + t);
- }
- ctx.closePath(); ctx.clip();
- ctx.drawImage(tx, x - r, y - r, r * 2, r * 2);
- }
- ctx.restore(); ctx.restore();
- ctx.strokeStyle = "rgba(255,200,80,0.35)"; ctx.lineWidth = 1;
- ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.stroke();
- return c.toDataURL("image/png");
- }
-
- _astroState() {
- const now = new Date();
- const tw = this._calcTwilight(now);
- const off = this._isMESZ(now) ? 2 : 1;
- const shift = (h) => ((h - 12 + 24) % 24);
- 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 = [];
- const colors = { night: "#0a1320", astro: "#23426d", naut: "#3d6aaf", civil: "#b87a3a", day: "#e8b94c" };
- const add = (s, e, k) => {
- if (s === null || e === null) return;
- const ss = shift(s), ee = shift(e);
- if (ee < ss) { segs.push({ start: ss, end: 24, color: colors[k] }); segs.push({ start: 0, end: ee, color: colors[k] }); return; }
- if (ee <= ss) return;
- segs.push({ start: ss, end: ee, color: colors[k] });
- };
- 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) => {
- const a = `${(s.start / 24) * 100}%`, b = `${(s.end / 24) * 100}%`;
- return [`${s.color} ${a}`, `${s.color} ${b}`];
- });
- const grad = stops.length ? `linear-gradient(90deg, ${stops.join(", ")})` : colors.night;
- const toLocal = (st) => {
- if (!st?.state || st.state === "unknown" || st.state === "unavailable") return null;
- const v = new Date(st.state); if (Number.isNaN(v.getTime())) return null;
- return shift(v.getHours() + v.getMinutes() / 60);
- };
- 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())),
- };
- }
-
- /* ---------- render ---------- */
-
- render() {
- if (!this.shadowRoot || !this._hass) return;
-
- const powerEid = this._powerEntityId();
- const powerState = this._state(powerEid)?.state;
- const powerOn = powerState === "on";
- const powerOff = powerState === "off";
-
- const rainOn = this._state(this.config.rain)?.state === "on";
- 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 openSchedEid = this._scheduleOpenEntityId();
- const closeSchedEid = this._scheduleCloseEntityId();
- const openSched = this._state(openSchedEid);
- const closeSched = this._state(closeSchedEid);
- const openInputV = this._scheduleInputValue("open", openSched);
- const closeInputV = this._scheduleInputValue("close", closeSched);
- if (openInputV === this._isoForInput(openSched)) {
- this._scheduleFieldState("open").draft = "";
- this._scheduleFieldState("open").pending = false;
- }
- if (closeInputV === this._isoForInput(closeSched)) {
- this._scheduleFieldState("close").draft = "";
- this._scheduleFieldState("close").pending = false;
- }
-
- const pos = this._roofPositionValue();
- const wind = this._windValue();
- const moonV = this._moonVisualState();
- const moonImg = this._renderMoonPhaseDataUrl(moonV, 96);
- const astro = this._astroState();
-
- let openDis, closeDis, stopDis;
- if (!powerOn) { openDis = closeDis = stopDis = true; }
- else if (movingOn) { openDis = closeDis = true; stopDis = false; }
- else if (openOn && !closedOn) { openDis = true; closeDis = false; stopDis = true; }
- else if (closedOn && !openOn) { openDis = false; closeDis = true; stopDis = true; }
- else if (closedOn && openOn) { openDis = closeDis = true; stopDis = false; }
- else { openDis = closeDis = stopDis = false; }
-
- const now = new Date();
- const pad = (n) => String(n).padStart(2, "0");
- const timeStr = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
- const dateStr = `${pad(now.getDate())}.${pad(now.getMonth() + 1)}.${String(now.getFullYear()).slice(-2)}`;
-
- /* analog gauge for roof position */
- const gaugeAngle = pos === null ? -135 : -135 + (pos / 100) * 270;
- const positionStr = pos === null ? " --" : String(pos).padStart(3, " ");
- const windStr = wind.num === null ? " --" : String(wind.num).padStart(3, " ");
- const moonStr = String(moonV.illuminationPercent).padStart(3, " ");
-
- /* status string for main display */
- let statusStr = "READY";
- if (!powerOn && powerOff) statusStr = " STBY";
- else if (movingOn) statusStr = " RUN ";
- else if (openOn && !closedOn) statusStr = "OPEN ";
- else if (closedOn && !openOn) statusStr = "SHUT ";
- else if (rainOn) statusStr = "RAIN ";
-
- const css = this._renderStyles();
-
- this.shadowRoot.innerHTML = `
-
-
-
-
-
-
-
-
- ${rainOn ? `
-
-
- ⚠ REGEN ERKANNT · DACH GESPERRT ⚠
-
-
- ` : ""}
-
-
-
-
-
PRIMARY READOUT
-
-
- UHR
- ${seg7(timeStr, { color: "red", size: 0.75 })}
-
-
- DAT
- ${seg7(dateStr, { color: "amber", size: 0.55 })}
-
-
- POS
- ${seg7(positionStr, { color: "red", size: 0.75 })}
- %
-
-
- WND
- ${seg7(windStr, { color: wind.level === 2 ? "red" : wind.level === 1 ? "amber" : "green", size: 0.75 })}
- ${wind.unit}
-
-
- MND
- ${seg7(moonStr, { color: "amber", size: 0.55 })}
- %
-
-
- STA
- ${seg7(statusStr, { color: powerOn ? "green" : "amber", size: 0.55 })}
-
-
-
-
-
-
-
-
DOME STATUS
-
- ${this._jewel("DACH ZU", "green", closedOn)}
- ${this._jewel("OFFEN", "green", openOn)}
- ${this._jewel("BEWEGT", "amber", movingOn, true)}
- ${this._jewel("REGEN", "red", rainOn, true)}
- ${this._jewel("POWER", "white", powerOn)}
- ${this._jewel("FEHLER", "red", powerState === undefined || powerState === "unavailable")}
-
-
-
-
-
POSITION
-
-
-
-
-
-
-
MOON SCOPE
-
-
- ${moonImg ? `

` : `
`}
-
-
-
-
- ${seg7(moonStr, { color: "amber", size: 0.5 })}
- % ILL
-
-
-
-
-
-
- DIURNAL CHART · 12 → 12
-
-
-
- ${astro.openMarkerLeft === null ? "" : `
`}
- ${astro.closeMarkerLeft === null ? "" : `
`}
-
-
- 12
- 18
- 00
- 06
- 12
-
-
-
-
-
-
-
-
MASTER POWER
-
-
-
-
- ${seg7(powerOn ? " ON" : powerOff ? "OFF" : "---", { color: powerOn ? "green" : "red", size: 0.5 })}
-
-
-
-
-
-
-
-
-
- SCHEDULE PROGRAMMER
-
-
-
ÖFFNEN
-
${seg7(this._seg7DateTime(openSched), { color: "amber", size: 0.45 })}
-
-
-
-
-
-
SCHLIESSEN
-
${seg7(this._seg7DateTime(closeSched), { color: "amber", size: 0.45 })}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- 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("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(openSchedEid, "open-schedule-input", "open"));
- this._bindClick("close-schedule-btn", () => this._setDateTime(closeSchedEid, "close-schedule-input", "close"));
- this._bindClick("open-schedule-clear-btn", () => this._clearScheduleField("open", "sternwarte", "clear_open_schedule"));
- this._bindClick("close-schedule-clear-btn", () => this._clearScheduleField("close", "sternwarte", "clear_close_schedule"));
- this._bindMoreInfo("wind-btn", this.config.wind);
- this._bindMoreInfo("pos-gauge", this.config.roofPosition);
- this._bindMoreInfo("moon-scope", this.config.roofStatus);
- this._bindScheduleInput("open", "open-schedule-input", openSched);
- this._bindScheduleInput("close", "close-schedule-input", closeSched);
-
- /* power toggle */
- const tog = this.shadowRoot.getElementById("power-toggle");
- if (tog) {
- const flip = () => {
- const svc = powerOn ? "turn_off" : "turn_on";
- this._callEntity("switch", svc, powerEid);
- };
- tog.addEventListener("click", (e) => { e.preventDefault(); flip(); });
- tog.addEventListener("keydown", (e) => {
- if (e.key === "Enter" || e.key === " ") { e.preventDefault(); flip(); }
- });
- }
- }
-
- _jewel(label, color, on, blink = false) {
- return `
-
- `;
- }
-
- _gaugeTicks() {
- let s = "";
- for (let i = 0; i <= 10; i++) {
- const ang = -135 + (i / 10) * 270;
- const major = i % 5 === 0;
- const r1 = 75, r2 = major ? 64 : 69;
- const rad = ang * Math.PI / 180;
- const x1 = 100 + r1 * Math.sin(rad), y1 = 110 - r1 * Math.cos(rad);
- const x2 = 100 + r2 * Math.sin(rad), y2 = 110 - r2 * Math.cos(rad);
- s += ``;
- }
- return s;
- }
-
- _renderStyles() {
- return `
- @import url('https://fonts.googleapis.com/css2?family=Special+Elite&family=Share+Tech+Mono&display=swap');
-
- :host {
- display: block;
- font-family: 'Share Tech Mono', 'Courier New', monospace;
- }
- ha-card {
- background: transparent;
- border: none;
- box-shadow: none;
- overflow: hidden;
- }
-
- /* === overall console === */
- .console {
- position: relative;
- padding: 22px 22px 18px;
- border-radius: 14px;
- color: #f1e4c5;
- background:
- radial-gradient(circle at 30% 20%, rgba(255,235,180,0.06), transparent 60%),
- radial-gradient(circle at 80% 90%, rgba(0,0,0,0.35), transparent 70%),
- repeating-linear-gradient(90deg, rgba(255,255,255,0.025) 0 1px, transparent 1px 3px),
- linear-gradient(160deg, #4d5a3e 0%, #3b4731 35%, #2c361f 100%);
- box-shadow:
- inset 0 0 0 2px #1c2210,
- inset 0 0 0 4px #6b7a55,
- inset 0 0 60px rgba(0,0,0,0.5),
- 0 8px 24px rgba(0,0,0,0.45);
- text-shadow: 0 1px 0 rgba(0,0,0,0.4);
- }
- .rivets {
- position: absolute;
- width: 14px; height: 14px;
- border-radius: 50%;
- background:
- radial-gradient(circle at 30% 30%, #f1ebd2 0%, #8a8064 35%, #2a2515 75%, #000 100%);
- box-shadow:
- inset -1px -1px 2px rgba(0,0,0,0.7),
- 0 1px 2px rgba(0,0,0,0.6);
- }
- .r-tl { top: 8px; left: 8px; }
- .r-tr { top: 8px; right: 8px; }
- .r-bl { bottom: 8px; left: 8px; }
- .r-br { bottom: 8px; right: 8px; }
-
- /* === header / dymo === */
- .header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- margin-bottom: 14px;
- }
- .dymo {
- font-family: 'Special Elite', 'Courier New', serif;
- font-size: 14px;
- letter-spacing: 0.18em;
- color: #2a200f;
- background:
- repeating-linear-gradient(90deg, rgba(255,255,255,0.06) 0 1px, transparent 1px 2px),
- linear-gradient(180deg, #d8c98a 0%, #bba85f 100%);
- padding: 8px 18px;
- border-radius: 4px;
- border: 1px solid #6b5a25;
- box-shadow:
- inset 0 1px 0 rgba(255,255,255,0.4),
- inset 0 -2px 0 rgba(0,0,0,0.2),
- 0 2px 4px rgba(0,0,0,0.45);
- text-shadow: none;
- }
- .header-right {
- display: flex; align-items: center; gap: 12px;
- }
- .serial {
- font-size: 10px;
- letter-spacing: 0.25em;
- opacity: 0.6;
- font-family: 'Special Elite', serif;
- }
- .pwr-led {
- width: 14px; height: 14px;
- border-radius: 50%;
- background: #2a0a0a;
- box-shadow:
- inset 0 0 0 2px #1a1a1a,
- inset 0 0 4px rgba(0,0,0,0.8),
- 0 0 0 3px #4a3f1f;
- }
- .pwr-led.on {
- background: radial-gradient(circle at 30% 30%, #fff 0%, #6cf06c 40%, #1f8a1f 100%);
- box-shadow:
- inset 0 0 0 2px #1a1a1a,
- inset 0 0 4px rgba(0,0,0,0.4),
- 0 0 12px rgba(108,240,108,0.7),
- 0 0 0 3px #4a3f1f;
- }
- .pwr-led.off {
- background: radial-gradient(circle at 30% 30%, #ffd9d9 0%, #c43030 50%, #4a0000 100%);
- box-shadow:
- inset 0 0 0 2px #1a1a1a,
- inset 0 0 4px rgba(0,0,0,0.4),
- 0 0 8px rgba(196,48,48,0.5),
- 0 0 0 3px #4a3f1f;
- }
-
- /* === alarm strip === */
- .alarm-strip {
- display: flex; align-items: center; justify-content: center;
- gap: 14px;
- margin-bottom: 14px;
- padding: 8px 14px;
- background:
- repeating-linear-gradient(45deg, #1a1305 0 12px, #5a2c0c 12px 24px);
- border: 2px solid #1a1305;
- border-radius: 4px;
- animation: alarm-flash 1s linear infinite;
- }
- .alarm-text {
- font-weight: 700;
- font-size: 14px;
- letter-spacing: 0.25em;
- color: #ffeacc;
- }
- .alarm-lamp {
- width: 14px; height: 14px;
- border-radius: 50%;
- background: radial-gradient(circle at 30% 30%, #fff 0%, #ff5a30 50%, #6a1500 100%);
- box-shadow: 0 0 12px rgba(255,90,48,0.9);
- animation: alarm-flash 0.7s linear infinite alternate;
- }
- @keyframes alarm-flash {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.55; }
- }
-
- /* === bezels (inset metal panels) === */
- .bezel {
- position: relative;
- padding: 22px 14px 14px;
- border-radius: 6px;
- background:
- repeating-linear-gradient(90deg, rgba(255,255,255,0.03) 0 1px, transparent 1px 2px),
- linear-gradient(160deg, #2a2418 0%, #1b170e 100%);
- box-shadow:
- inset 0 2px 4px rgba(0,0,0,0.7),
- inset 0 -1px 0 rgba(255,255,255,0.05),
- 0 1px 0 rgba(255,255,255,0.06);
- border: 1px solid #0a0805;
- }
- .bezel-label {
- position: absolute;
- top: -8px; left: 12px;
- padding: 2px 8px;
- font-family: 'Special Elite', serif;
- font-size: 10px;
- letter-spacing: 0.22em;
- color: #f1e4c5;
- background: linear-gradient(180deg, #6b7a55 0%, #4d5a3e 100%);
- border: 1px solid #1c2210;
- border-radius: 2px;
- box-shadow: 0 1px 2px rgba(0,0,0,0.5);
- text-shadow: 0 1px 0 rgba(0,0,0,0.4);
- }
-
- /* === rows === */
- .row { display: grid; gap: 14px; margin-bottom: 14px; }
- .row-displays {
- grid-template-columns: minmax(0, 2.2fr) minmax(0, 1.4fr) minmax(0, 1fr);
- }
- .row-controls {
- grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
- }
- .col-stack { display: grid; grid-template-rows: auto auto; gap: 14px; }
-
- /* === 7-seg === */
- .seg7 {
- display: inline-flex;
- align-items: flex-end;
- gap: 2px;
- padding: 4px 6px;
- --d7-base-w: 30px;
- --d7-base-h: 50px;
- }
- .d7, .d7-colon, .d7-dot {
- width: calc(var(--d7-base-w) * var(--d7-size, 1));
- height: calc(var(--d7-base-h) * var(--d7-size, 1));
- display: inline-block;
- }
- .seg {
- fill: rgba(0,0,0,0.5);
- opacity: 0.10;
- transition: opacity 80ms linear;
- }
- .seg.on { opacity: 1; }
- .col-red .seg.on {
- fill: #ff2a18;
- filter: drop-shadow(0 0 2px #ff5538) drop-shadow(0 0 4px rgba(255,40,20,0.6));
- }
- .col-amber .seg.on {
- fill: #ffaa1a;
- filter: drop-shadow(0 0 2px #ffc04a) drop-shadow(0 0 4px rgba(255,170,30,0.6));
- }
- .col-green .seg.on {
- fill: #4cff60;
- filter: drop-shadow(0 0 2px #80ff90) drop-shadow(0 0 4px rgba(76,255,96,0.5));
- }
- .col-red .dot { fill: #ff2a18; filter: drop-shadow(0 0 2px #ff5538); }
- .col-amber .dot { fill: #ffaa1a; filter: drop-shadow(0 0 2px #ffc04a); }
- .col-green .dot { fill: #4cff60; filter: drop-shadow(0 0 2px #80ff90); }
-
- /* === display panel === */
- .display-bezel {
- background:
- repeating-linear-gradient(0deg, rgba(255,255,255,0.02) 0 1px, transparent 1px 3px),
- linear-gradient(160deg, #100a04 0%, #1c130a 100%);
- }
- .display-grid {
- display: grid;
- gap: 6px;
- }
- .seg-row {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 6px 10px;
- background:
- radial-gradient(ellipse at center, rgba(255,255,255,0.03) 0%, transparent 70%),
- #050200;
- border-radius: 3px;
- box-shadow:
- inset 0 2px 6px rgba(0,0,0,0.9),
- inset 0 0 0 1px #2a1f0a;
- }
- .seg-cap {
- font-family: 'Special Elite', serif;
- font-size: 10px;
- letter-spacing: 0.2em;
- color: #c5a960;
- min-width: 36px;
- }
- .seg-unit {
- font-family: 'Special Elite', serif;
- font-size: 11px;
- letter-spacing: 0.15em;
- color: #c5a960;
- margin-left: auto;
- }
-
- /* === lamps === */
- .lamp-bezel {
- background:
- repeating-linear-gradient(45deg, rgba(255,255,255,0.02) 0 2px, transparent 2px 4px),
- linear-gradient(160deg, #2c2618 0%, #1b170e 100%);
- }
- .lamps {
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 10px 6px;
- }
- .jewel {
- display: flex; flex-direction: column; align-items: center; gap: 4px;
- }
- .jewel-bezel {
- width: 38px; height: 38px;
- border-radius: 50%;
- background:
- radial-gradient(circle at 30% 30%, #fff 0%, #c0b89a 25%, #5a4f30 70%, #1a1408 100%);
- padding: 4px;
- box-shadow:
- inset 0 1px 2px rgba(255,255,255,0.4),
- inset 0 -2px 4px rgba(0,0,0,0.6),
- 0 2px 4px rgba(0,0,0,0.6);
- }
- .jewel-lens {
- width: 100%; height: 100%;
- border-radius: 50%;
- position: relative;
- }
- .jewel-lens::after {
- content: "";
- position: absolute; top: 4px; left: 6px;
- width: 8px; height: 6px;
- border-radius: 50%;
- background: rgba(255,255,255,0.45);
- filter: blur(1px);
- }
- .lens-green.off { background: radial-gradient(circle at 35% 35%, #2d4a30 0%, #0c1a0e 70%); }
- .lens-green.on { background: radial-gradient(circle at 35% 35%, #c4ffc8 0%, #4cff60 35%, #1a8a28 100%); box-shadow: 0 0 16px rgba(76,255,96,0.7); }
- .lens-amber.off { background: radial-gradient(circle at 35% 35%, #4a3a14 0%, #1a1408 70%); }
- .lens-amber.on { background: radial-gradient(circle at 35% 35%, #fff5c4 0%, #ffc14a 35%, #b07b10 100%); box-shadow: 0 0 16px rgba(255,193,74,0.7); }
- .lens-red.off { background: radial-gradient(circle at 35% 35%, #4a1414 0%, #1a0808 70%); }
- .lens-red.on { background: radial-gradient(circle at 35% 35%, #ffd0d0 0%, #ff4030 35%, #8a0a0a 100%); box-shadow: 0 0 16px rgba(255,64,48,0.75); }
- .lens-white.off { background: radial-gradient(circle at 35% 35%, #3a3528 0%, #14110a 70%); }
- .lens-white.on { background: radial-gradient(circle at 35% 35%, #fff 0%, #fce8b4 50%, #b08020 100%); box-shadow: 0 0 16px rgba(255,232,180,0.7); }
- .blink { animation: lamp-blink 0.9s linear infinite; }
- @keyframes lamp-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
-
- .jewel-label {
- font-family: 'Special Elite', serif;
- font-size: 9px;
- letter-spacing: 0.18em;
- color: #d8c98a;
- text-align: center;
- }
-
- /* === gauge === */
- .gauge-bezel {
- background:
- radial-gradient(ellipse at 50% 0%, rgba(255,255,255,0.04), transparent 60%),
- linear-gradient(160deg, #1f1a0e 0%, #100c06 100%);
- cursor: pointer;
- outline: none;
- }
- .gauge-bezel:focus-visible { box-shadow: inset 0 2px 4px rgba(0,0,0,0.7), 0 0 0 2px #c5a960; }
- .gauge { width: 100%; height: auto; display: block; }
-
- /* === moon scope === */
- .moon-bezel {
- display: flex; flex-direction: column; align-items: center; gap: 8px;
- cursor: pointer;
- }
- .scope {
- width: 96px; height: 96px;
- border-radius: 50%;
- padding: 6px;
- background:
- radial-gradient(circle at 30% 30%, #f1ebd2 0%, #8a8064 30%, #2a2515 80%, #000 100%);
- box-shadow:
- inset 0 2px 4px rgba(255,255,255,0.4),
- inset 0 -3px 6px rgba(0,0,0,0.7),
- 0 3px 6px rgba(0,0,0,0.6);
- }
- .scope-glass {
- width: 100%; height: 100%;
- border-radius: 50%;
- position: relative;
- overflow: hidden;
- background: #050810;
- box-shadow: inset 0 0 0 2px #1a1408;
- }
- .moon-img {
- width: 100%; height: 100%;
- display: block;
- border-radius: 50%;
- }
- .moon-img.placeholder {
- background: radial-gradient(circle at 30% 30%, #aaa 0%, #444 60%, #111 100%);
- }
- .scope-cross {
- position: absolute; inset: 0;
- background:
- linear-gradient(90deg, transparent 49.5%, rgba(255,200,80,0.18) 49.5% 50.5%, transparent 50.5%),
- linear-gradient(0deg, transparent 49.5%, rgba(255,200,80,0.18) 49.5% 50.5%, transparent 50.5%);
- pointer-events: none;
- }
- .scope-readout {
- display: flex; align-items: center; gap: 4px;
- }
-
- /* === twilight strip === */
- .twilight-bezel {
- background:
- linear-gradient(160deg, #1f1a0e 0%, #100c06 100%);
- }
- .strip {
- position: relative;
- height: 38px;
- border-radius: 3px;
- overflow: hidden;
- box-shadow:
- inset 0 2px 4px rgba(0,0,0,0.7),
- inset 0 0 0 1px #2a1f0a;
- }
- .strip-paper {
- position: absolute; inset: 0;
- opacity: 0.92;
- }
- .strip-grid {
- position: absolute; inset: 0;
- background:
- repeating-linear-gradient(90deg, transparent 0 calc(100%/24 - 1px), rgba(0,0,0,0.35) calc(100%/24 - 1px) calc(100%/24)),
- repeating-linear-gradient(0deg, transparent 0 8px, rgba(255,255,255,0.04) 8px 9px);
- pointer-events: none;
- }
- .strip-mark {
- position: absolute;
- top: -2px; bottom: -2px;
- width: 3px;
- margin-left: -1px;
- z-index: 2;
- box-shadow: 0 0 6px currentColor;
- }
- .strip-mark.open { background: #4cff60; color: rgba(76,255,96,0.8); }
- .strip-mark.close { background: #ffaa1a; color: rgba(255,170,26,0.8); }
- .strip-now {
- position: absolute;
- top: -3px; bottom: -3px;
- width: 4px;
- margin-left: -2px;
- background: #ff2a18;
- box-shadow: 0 0 8px rgba(255,42,24,0.85);
- z-index: 3;
- }
- .strip-ticks {
- position: absolute; inset: 0;
- pointer-events: none;
- }
- .strip-ticks span {
- position: absolute;
- top: 50%; transform: translate(-50%, -50%);
- font-family: 'Special Elite', serif;
- font-size: 10px;
- color: rgba(255,255,255,0.85);
- text-shadow: 0 0 3px rgba(0,0,0,0.8);
- }
-
- /* === toggle switch (bat-handle) === */
- .toggle-bezel {
- display: flex; flex-direction: column; align-items: center;
- }
- .toggle-area {
- position: relative;
- display: flex; flex-direction: column; align-items: center; gap: 8px;
- padding-top: 6px;
- }
- .toggle-frame {
- position: relative;
- width: 70px; height: 130px;
- display: flex; align-items: center; justify-content: center;
- }
- .toggle-base {
- position: absolute;
- top: 50%; left: 50%;
- transform: translate(-50%, -50%);
- width: 46px; height: 46px;
- border-radius: 50%;
- background:
- radial-gradient(circle at 30% 30%, #f1ebd2 0%, #8a8064 35%, #1a1408 100%);
- box-shadow:
- inset 0 1px 2px rgba(255,255,255,0.5),
- inset 0 -3px 6px rgba(0,0,0,0.6),
- 0 3px 6px rgba(0,0,0,0.6);
- }
- .toggle-base::after {
- content: "";
- position: absolute;
- top: 50%; left: 50%;
- transform: translate(-50%, -50%);
- width: 16px; height: 16px;
- border-radius: 50%;
- background: radial-gradient(circle, #1a1408 0%, #000 100%);
- box-shadow: inset 0 2px 3px rgba(0,0,0,0.9);
- }
- .toggle-handle {
- position: absolute;
- top: 50%; left: 50%;
- width: 14px; height: 70px;
- margin-left: -7px;
- margin-top: -35px;
- transform-origin: 50% 50%;
- transition: transform 220ms cubic-bezier(0.5, 1.6, 0.4, 1);
- }
- .toggle.up .toggle-handle { transform: translateY(-22px) rotate(0deg); }
- .toggle.down .toggle-handle { transform: translateY(22px) rotate(180deg); }
- .toggle-handle::before {
- content: "";
- position: absolute;
- left: 50%; top: 30px;
- width: 10px; height: 40px;
- margin-left: -5px;
- background: linear-gradient(90deg, #4a4a4a 0%, #c8c8c8 35%, #f4f4f4 50%, #c8c8c8 65%, #4a4a4a 100%);
- border-radius: 2px;
- box-shadow: 0 0 4px rgba(0,0,0,0.5);
- }
- .toggle-tip {
- position: absolute;
- left: 50%; top: 0;
- width: 22px; height: 30px;
- margin-left: -11px;
- background:
- radial-gradient(ellipse at 35% 30%, #fff 0%, #d8d8d8 30%, #6a6a6a 70%, #2a2a2a 100%);
- border-radius: 50% 50% 45% 45%;
- box-shadow:
- inset 0 -3px 6px rgba(0,0,0,0.5),
- 0 2px 4px rgba(0,0,0,0.6);
- }
- .toggle-marks {
- position: absolute; inset: 0;
- pointer-events: none;
- }
- .toggle-marks .mk {
- position: absolute;
- left: 50%; transform: translateX(-50%);
- font-family: 'Special Elite', serif;
- font-size: 11px;
- letter-spacing: 0.2em;
- color: #d8c98a;
- }
- .toggle-marks .mk.top { top: 4px; color: #4cff60; }
- .toggle-marks .mk.bot { bottom: 4px; color: #ff6040; }
- .toggle { cursor: pointer; outline: none; }
- .toggle:focus-visible .toggle-base { box-shadow: 0 0 0 3px #c5a960, inset 0 1px 2px rgba(255,255,255,0.5), inset 0 -3px 6px rgba(0,0,0,0.6); }
-
- /* guard bracket */
- .guard {
- position: absolute;
- top: -2px; left: 50%; transform: translateX(-50%);
- width: 60px; height: 110px;
- border: 3px solid #c43030;
- border-bottom: none;
- border-radius: 30px 30px 0 0;
- box-shadow:
- inset 0 0 0 1px rgba(255,255,255,0.15),
- 0 2px 4px rgba(0,0,0,0.5);
- background: transparent;
- pointer-events: none;
- transition: transform 280ms ease;
- z-index: 2;
- }
- .guard.lifted {
- transform: translateX(-50%) translateY(-12px) rotateX(-65deg);
- transform-origin: 50% 100%;
- }
- .power-stat {
- text-align: center;
- }
-
- /* === push buttons === */
- .buttons {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 14px;
- align-items: center;
- justify-items: center;
- padding: 8px 0;
- }
- .pbtn {
- position: relative;
- width: 90px; height: 90px;
- border-radius: 50%;
- border: none;
- cursor: pointer;
- font-family: 'Special Elite', serif;
- font-size: 11px;
- letter-spacing: 0.2em;
- color: #fff;
- text-shadow: 0 1px 2px rgba(0,0,0,0.7);
- background:
- radial-gradient(circle at 30% 30%, var(--btn-hi, #fff) 0%, var(--btn-mid, #888) 35%, var(--btn-lo, #222) 100%);
- box-shadow:
- inset 0 2px 4px rgba(255,255,255,0.4),
- inset 0 -4px 8px rgba(0,0,0,0.6),
- 0 0 0 6px #1c1610,
- 0 0 0 7px #6b5a25,
- 0 5px 10px rgba(0,0,0,0.6);
- transition: transform 80ms ease, box-shadow 80ms ease;
- }
- .pbtn:active:not(:disabled) {
- transform: translateY(2px);
- box-shadow:
- inset 0 4px 8px rgba(0,0,0,0.6),
- inset 0 -1px 2px rgba(255,255,255,0.2),
- 0 0 0 6px #1c1610,
- 0 0 0 7px #6b5a25,
- 0 2px 4px rgba(0,0,0,0.6);
- }
- .pbtn:disabled {
- cursor: not-allowed;
- filter: grayscale(0.7) brightness(0.55);
- }
- .pbtn-cap {
- display: block;
- margin-top: 4px;
- font-weight: 700;
- }
- .pbtn-green {
- --btn-hi: #d8ffd8; --btn-mid: #4cd060; --btn-lo: #0a4a18;
- box-shadow:
- inset 0 2px 4px rgba(255,255,255,0.5),
- inset 0 -4px 8px rgba(0,0,0,0.5),
- 0 0 0 6px #1c1610,
- 0 0 0 7px #6b5a25,
- 0 0 18px rgba(76,208,96,0.45),
- 0 5px 10px rgba(0,0,0,0.6);
- }
- .pbtn-amber {
- --btn-hi: #fff0c8; --btn-mid: #e69a2a; --btn-lo: #5a3208;
- box-shadow:
- inset 0 2px 4px rgba(255,255,255,0.5),
- inset 0 -4px 8px rgba(0,0,0,0.5),
- 0 0 0 6px #1c1610,
- 0 0 0 7px #6b5a25,
- 0 0 18px rgba(230,154,42,0.45),
- 0 5px 10px rgba(0,0,0,0.6);
- }
- .pbtn-mushroom {
- width: 110px; height: 110px;
- --btn-hi: #ffdada; --btn-mid: #d62020; --btn-lo: #4a0606;
- box-shadow:
- inset 0 3px 6px rgba(255,255,255,0.5),
- inset 0 -6px 12px rgba(0,0,0,0.6),
- 0 0 0 8px #1c1610,
- 0 0 0 9px #c43030,
- 0 0 22px rgba(214,32,32,0.55),
- 0 6px 12px rgba(0,0,0,0.7);
- }
- .pbtn-mushroom .pbtn-cap {
- font-size: 13px;
- letter-spacing: 0.3em;
- }
- .pbtn-mushroom:disabled {
- filter: grayscale(0.5) brightness(0.6);
- }
-
- /* === schedule programmer === */
- .sched-bezel {
- background:
- repeating-linear-gradient(0deg, rgba(255,255,255,0.02) 0 1px, transparent 1px 3px),
- linear-gradient(160deg, #1f1a0e 0%, #100c06 100%);
- }
- .sched-grid {
- display: grid; gap: 10px;
- }
- .sched-row {
- display: grid;
- grid-template-columns: 90px minmax(0, 1fr) minmax(150px, 200px) max-content max-content;
- gap: 10px;
- align-items: center;
- }
- .sched-cap {
- font-family: 'Special Elite', serif;
- font-size: 11px;
- letter-spacing: 0.18em;
- color: #c5a960;
- }
- .sched-readout {
- padding: 4px 8px;
- background: #050200;
- border-radius: 3px;
- box-shadow:
- inset 0 2px 4px rgba(0,0,0,0.9),
- inset 0 0 0 1px #2a1f0a;
- min-height: 28px;
- display: flex; align-items: center;
- }
- input[type="datetime-local"] {
- width: 100%;
- box-sizing: border-box;
- padding: 7px 10px;
- border-radius: 3px;
- border: 1px solid #2a1f0a;
- background:
- repeating-linear-gradient(0deg, transparent 0 4px, rgba(255,255,255,0.03) 4px 5px),
- #1a1408;
- color: #f1e4c5;
- font-family: 'Share Tech Mono', monospace;
- font-size: 13px;
- letter-spacing: 0.05em;
- box-shadow: inset 0 2px 3px rgba(0,0,0,0.6);
- }
- input[type="datetime-local"]::-webkit-calendar-picker-indicator {
- filter: invert(0.85) sepia(1) saturate(2) hue-rotate(-10deg);
- cursor: pointer;
- }
- .kbtn {
- font-family: 'Special Elite', serif;
- font-size: 11px;
- letter-spacing: 0.18em;
- padding: 9px 12px;
- border: 1px solid #1a1408;
- border-radius: 3px;
- background:
- linear-gradient(180deg, #d8c98a 0%, #a89559 50%, #76632a 100%);
- color: #1a1408;
- cursor: pointer;
- box-shadow:
- inset 0 1px 0 rgba(255,255,255,0.5),
- inset 0 -2px 0 rgba(0,0,0,0.25),
- 0 2px 3px rgba(0,0,0,0.5);
- text-shadow: 0 1px 0 rgba(255,255,255,0.3);
- transition: transform 80ms ease, box-shadow 80ms ease;
- }
- .kbtn:active {
- transform: translateY(1px);
- box-shadow:
- inset 0 2px 3px rgba(0,0,0,0.4),
- inset 0 -1px 0 rgba(255,255,255,0.15),
- 0 1px 1px rgba(0,0,0,0.5);
- }
- .kbtn-secondary {
- background: linear-gradient(180deg, #6b7a55 0%, #4d5a3e 50%, #2c361f 100%);
- color: #f1e4c5;
- text-shadow: 0 1px 0 rgba(0,0,0,0.4);
- }
- .kbtn-info {
- background: linear-gradient(180deg, #5a8ac4 0%, #3d6aaf 50%, #234783 100%);
- color: #f1e4c5;
- text-shadow: 0 1px 0 rgba(0,0,0,0.4);
- }
- .sched-actions {
- display: flex; gap: 10px; flex-wrap: wrap;
- margin-top: 4px;
- }
-
- /* === footer === */
- .footer {
- margin-top: 16px;
- padding-top: 10px;
- display: flex;
- justify-content: space-between;
- gap: 12px;
- font-family: 'Special Elite', serif;
- font-size: 10px;
- letter-spacing: 0.2em;
- color: rgba(241,228,197,0.55);
- border-top: 1px dashed rgba(241,228,197,0.2);
- flex-wrap: wrap;
- }
-
- /* === responsive === */
- @media (max-width: 880px) {
- .row-displays { grid-template-columns: 1fr; }
- .row-controls { grid-template-columns: 1fr; }
- .lamps { grid-template-columns: repeat(3, 1fr); }
- .sched-row {
- grid-template-columns: 1fr;
- }
- }
- @media (max-width: 520px) {
- .console { padding: 16px; }
- .dymo { font-size: 11px; padding: 6px 12px; letter-spacing: 0.12em; }
- .lamps { grid-template-columns: repeat(2, 1fr); }
- .pbtn { width: 72px; height: 72px; font-size: 9px; }
- .pbtn-mushroom { width: 90px; height: 90px; }
- .buttons { gap: 8px; }
- }
- `;
- }
-}
-
-if (!customElements.get("sternwarte-card-retro")) {
- customElements.define("sternwarte-card-retro", SternwarteCardRetro);
-}
-
-window.customCards = window.customCards || [];
-if (!window.customCards.some((c) => c?.type === "sternwarte-card-retro")) {
- window.customCards.push({
- type: "sternwarte-card-retro",
- name: "Sternwarte Card · Retro",
- description: "Vintage Maschinen-Bedienpaneel mit 7-Segment-Anzeigen, Kippschaltern und beleuchteten Druckknöpfen",
- });
-}
diff --git a/sternwarte-homeassistant-card/dist/sternwarte-card.js b/sternwarte-homeassistant-card/dist/sternwarte-card.js
index 8c0805a..88c2c6b 100644
--- a/sternwarte-homeassistant-card/dist/sternwarte-card.js
+++ b/sternwarte-homeassistant-card/dist/sternwarte-card.js
@@ -813,8 +813,10 @@ class SternwarteCard extends HTMLElement {
if (!state?.state || state.state === "unknown" || state.state === "unavailable") return null;
const value = this._parseEntityDate(state.state);
if (!value) return null;
- const localHour = value.getHours() + value.getMinutes() / 60 + value.getSeconds() / 3600;
- return (localHour - 12 + 24) % 24;
+ const windowEnd = new Date(windowStart);
+ windowEnd.setDate(windowEnd.getDate() + 1);
+ if (value < windowStart || value >= windowEnd) return null;
+ return (value.getTime() - windowStart.getTime()) / 3600000;
};
return {
@@ -1550,11 +1552,13 @@ class SternwarteCard extends HTMLElement {
+
+
@@ -1586,6 +1590,8 @@ class SternwarteCard extends HTMLElement {
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("open-nautical-btn", () => this._call("sternwarte", "set_nautical_open_schedule"));
+ this._bindClick("close-nautical-btn", () => this._call("sternwarte", "set_nautical_close_schedule"));
this._bindClick("clear-schedule-btn", () => {
this._resetScheduleDrafts();
this._call("sternwarte", "clear_schedule");
diff --git a/sternwarte-homeassistant-integration/custom_components/sternwarte/__init__.py b/sternwarte-homeassistant-integration/custom_components/sternwarte/__init__.py
index 602fe50..945d6d8 100644
--- a/sternwarte-homeassistant-integration/custom_components/sternwarte/__init__.py
+++ b/sternwarte-homeassistant-integration/custom_components/sternwarte/__init__.py
@@ -18,6 +18,8 @@ from .const import (
SERVICE_POWER_OFF,
SERVICE_POWER_ON,
SERVICE_SET_NAUTICAL_SCHEDULE,
+ SERVICE_SET_NAUTICAL_OPEN_SCHEDULE,
+ SERVICE_SET_NAUTICAL_CLOSE_SCHEDULE,
SERVICE_STOP_ROOF,
)
from .coordinator import SternwarteDataUpdateCoordinator
@@ -77,6 +79,14 @@ async def _async_handle_set_nautical_schedule(hass: HomeAssistant, call: Service
await _async_call_on_all(hass, "async_set_next_nautical_schedule")
+async def _async_handle_set_nautical_open_schedule(hass: HomeAssistant, call: ServiceCall) -> None:
+ await _async_call_on_all(hass, "async_set_next_nautical_open_schedule")
+
+
+async def _async_handle_set_nautical_close_schedule(hass: HomeAssistant, call: ServiceCall) -> None:
+ await _async_call_on_all(hass, "async_set_next_nautical_close_schedule")
+
+
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Sternwarte from a config entry."""
coordinator = SternwarteDataUpdateCoordinator(hass, entry)
@@ -106,6 +116,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
SERVICE_SET_NAUTICAL_SCHEDULE,
partial(_async_handle_set_nautical_schedule, hass),
)
+ hass.services.async_register(
+ DOMAIN,
+ SERVICE_SET_NAUTICAL_OPEN_SCHEDULE,
+ partial(_async_handle_set_nautical_open_schedule, hass),
+ )
+ hass.services.async_register(
+ DOMAIN,
+ SERVICE_SET_NAUTICAL_CLOSE_SCHEDULE,
+ partial(_async_handle_set_nautical_close_schedule, hass),
+ )
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@@ -127,6 +147,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
SERVICE_CLEAR_OPEN_SCHEDULE,
SERVICE_CLEAR_CLOSE_SCHEDULE,
SERVICE_SET_NAUTICAL_SCHEDULE,
+ SERVICE_SET_NAUTICAL_OPEN_SCHEDULE,
+ SERVICE_SET_NAUTICAL_CLOSE_SCHEDULE,
):
if hass.services.has_service(DOMAIN, service):
hass.services.async_remove(DOMAIN, service)
diff --git a/sternwarte-homeassistant-integration/custom_components/sternwarte/const.py b/sternwarte-homeassistant-integration/custom_components/sternwarte/const.py
index def4b4e..9e687c9 100644
--- a/sternwarte-homeassistant-integration/custom_components/sternwarte/const.py
+++ b/sternwarte-homeassistant-integration/custom_components/sternwarte/const.py
@@ -49,3 +49,5 @@ ENTITY_ROOF_POSITION: Final = "roof_position"
ENTITY_SCHEDULE_OPEN: Final = "schedule_open"
ENTITY_SCHEDULE_CLOSE: Final = "schedule_close"
ENTITY_SET_NAUTICAL_SCHEDULE: Final = "set_nautical_schedule"
+SERVICE_SET_NAUTICAL_OPEN_SCHEDULE: Final = "set_nautical_open_schedule"
+SERVICE_SET_NAUTICAL_CLOSE_SCHEDULE: Final = "set_nautical_close_schedule"
diff --git a/sternwarte-homeassistant-integration/custom_components/sternwarte/coordinator.py b/sternwarte-homeassistant-integration/custom_components/sternwarte/coordinator.py
index de1e123..aa763d9 100644
--- a/sternwarte-homeassistant-integration/custom_components/sternwarte/coordinator.py
+++ b/sternwarte-homeassistant-integration/custom_components/sternwarte/coordinator.py
@@ -249,6 +249,22 @@ class SternwarteDataUpdateCoordinator(DataUpdateCoordinator[SternwarteState]):
await self._async_save_runtime_state()
await self.async_request_refresh()
+ async def async_set_next_nautical_open_schedule(self) -> None:
+ """Set only the next nautical dusk as the opening schedule."""
+ now = dt_util.now()
+ when = self._get_next_nautical_event(now, "dusk")
+ if when is None:
+ raise HomeAssistantError("Keine kommende nautische Abenddämmerung gefunden")
+ await self.async_set_open_schedule(when)
+
+ async def async_set_next_nautical_close_schedule(self) -> None:
+ """Set only the next nautical dawn as the closing schedule."""
+ now = dt_util.now()
+ when = self._get_next_nautical_event(now, "dawn")
+ if when is None:
+ raise HomeAssistantError("Keine kommende nautische Morgendämmerung gefunden")
+ await self.async_set_close_schedule(when)
+
async def async_turn_power_on(self) -> None:
"""Turn roof power on."""
async with self._command_lock:
@@ -831,6 +847,17 @@ class SternwarteDataUpdateCoordinator(DataUpdateCoordinator[SternwarteState]):
)
return dt_util.as_local(event_utc).replace(second=0, microsecond=0)
+ def _get_next_nautical_event(self, now: datetime, event_name: str) -> datetime | None:
+ for day_offset in range(3):
+ base_date = now + timedelta(days=day_offset)
+ twilight = self._calc_twilight(base_date)
+ if twilight["naut"] is None:
+ continue
+ event = self._event_to_local_datetime(base_date, twilight["naut"][event_name])
+ if event and event > now:
+ return event
+ return None
+
def _get_next_nautical_schedule(self, now: datetime) -> dict[str, datetime | None] | None:
for day_offset in range(3):
base_date = now + timedelta(days=day_offset)
diff --git a/sternwarte-homeassistant-integration/custom_components/sternwarte/services.yaml b/sternwarte-homeassistant-integration/custom_components/sternwarte/services.yaml
index f1f88d3..6719a47 100644
--- a/sternwarte-homeassistant-integration/custom_components/sternwarte/services.yaml
+++ b/sternwarte-homeassistant-integration/custom_components/sternwarte/services.yaml
@@ -33,3 +33,11 @@ clear_close_schedule:
set_nautical_schedule:
name: Nautische Dämmerung setzen
description: Setzt Öffnen und Schließen auf die nächste nautische Abend- und Morgendämmerung.
+
+set_nautical_open_schedule:
+ name: Nautische Öffnungszeit setzen
+ description: Setzt nur Öffnen auf die nächste nautische Abenddämmerung.
+
+set_nautical_close_schedule:
+ name: Nautische Schließzeit setzen
+ description: Setzt nur Schließen auf die nächste nautische Morgendämmerung.