From d5562c71ef416de74065f71e84def707005c722e Mon Sep 17 00:00:00 2001 From: Tobi G Date: Wed, 12 Aug 2026 18:48:35 +0200 Subject: [PATCH] =?UTF-8?q?Setup-Men=C3=BC:=20Wertvorschau,=20Unavailable-?= =?UTF-8?q?Warnung,=20Duplikat-Check,=20Reset,=20Neustart-Hinweis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fünf Erweiterungen des bestehenden Setup-Popups: Live-Wert neben jedem Such-Kandidaten, Warnhinweis bei unavailable/unknown/fehlender Entität, Duplikat-Check mit Bestätigung vor dem Speichern, Zurücksetzen-Button je Feld (Standardwerte-Snapshot in entitaeten.py, vor jedem Override genommen), und ein bestätigter "Jetzt neu starten"-Knopf nach dem Speichern, falls sich eines der drei trigger-gebundenen Felder geändert hat (neuer Service audi_dashboard_neustart). Im Docker-Testcontainer Feld für Feld verifiziert. --- AGENTS.md | 36 +++++- homeassistant/pyscript/frontend_api.py | 13 ++ homeassistant/pyscript/modules/entitaeten.py | 13 +- homeassistant/www/audi-dashboard-app.js | 116 +++++++++++++++--- homeassistant/www/audi-dashboard-version.json | 2 +- homeassistant/www/audi-dashboard.css | 11 +- 6 files changed, 173 insertions(+), 18 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index abae052..367fab9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md — Project state, review findings, open items, and working rules -**Last updated: 2026-08-12**. This file is the entry point for every new agent +**Last updated: 2026-08-12** (Setup-menu polish batch). This file is the entry point for every new agent session: what this repo is, what is finished, what is missing, and how to work here. Detail lives in the linked documents — this file points, it does not duplicate. @@ -49,6 +49,13 @@ toward caution over speed — for trivial tasks, use judgment. - Conflicting instructions get picked arbitrarily — when updating, **remove** superseded rules rather than stacking corrections. - Verify loading with `/context` (file must appear under **Memory files**); edit via `/memory`. +- **This file (plus the session's task list) is the source of truth for what's done — not a leftover + plan-mode file.** A `~/.claude/plans/*.md` file surfaced by the system as "not yet complete" can be + stale: plan files aren't marked complete when their work finishes. Before claiming a feature is + unbuilt (e.g. "the Setup menu isn't implemented yet"), check the `[x]`/`[ ]` items below and/or the + task list — trust those over an old plan file's own claim about its status. (Caused a real mistake + 2026-08-12: told the user the Setup menu wasn't built yet, when it had shipped hours earlier in + the same project — see the "Setup menu" `[x]` entry further down.) --- @@ -365,6 +372,33 @@ capacitor/iframe/browser (`umgebung.ts`), types + entity table (`types.ts`), fac `update.ps1`) plus a short `ANLEITUNG.md` checklist for copying to a fresh HA instance; `INSTALL.md` stays the authoritative, detailed reference. Regenerate on demand, don't keep it permanently in sync — it's a deployment snapshot, not a second source of truth. +- [x] Setup-menu polish: 5 additions on top of the existing Setup popup — done 2026-08-12. + Backend (`pyscript/modules/entitaeten.py`): a `_STANDARDWERTE` snapshot taken at module-load + time (before `overrides_anwenden()` ever runs), exposed via `aktueller_stand()` as + `standardwerte` — needed because `setattr()` on the live `einstellungen` module is permanent + for the process, so "reset to default" has to write the original value back explicitly rather + than just omitting the override. New service `audi_dashboard_neustart` (`frontend_api.py`) + wrapping `homeassistant.restart()`, called only from an explicit user-confirmed button, never + automatically. Frontend (`audi-dashboard-app.js`): (1) live current-value preview next to each + candidate in the search dropdown (`entitaetZeilenMarkup`, reads `HASS.states[e.id].state`); + (2) inline warning under a field if its currently assigned entity is `unavailable`/`unknown`/ + missing (`entitaetStatusWarnung`); (3) duplicate-assignment check on save + (`setupDuplikate`) — blocks with a confirm-sheet ("trotzdem speichern?") rather than silently + allowing the same entity in two roles; (4) per-field reset button (only shown when the current + value differs from `standardwerte`), reverts to the built-in default; (5) after saving, if any + of the 3 trigger-bound fields actually changed (`setupGeaenderteTriggerFelder`, compares + against a snapshot taken when the popup opened), a confirm-sheet offers "Jetzt neu starten" + calling the new service. Verified live in `audi_ha_test`: value preview and unavailable-warning + confirmed correct (`ZUENDUNG_SENSOR`'s configured entity genuinely doesn't exist in this test + container → "Entität nicht gefunden." shown as designed); duplicate-check correctly caught both + a deliberately-forced KM/TANK collision and pre-existing door/window/lock auto-suggestion + collisions (a real side effect of this container's limited matching entities, not a bug); + reset button appeared/disappeared correctly as values diverged from/matched the default; + save round-tripped through the real service into `data/entitaeten.json` (byte-inspected) and + into the published `pyscript.audi_dashboard_fahrzeugstatus` state; restart-needed sheet named + exactly the changed trigger-bound field(s) and no others. Did not actually trigger a restart + during verification (would have restarted the shared test container) — the service call itself + was left unexercised beyond confirming it registers without error at pyscript load time. - [ ] Fix remaining documentation drift (statistics claim, README gaps, obsolete TODO comment) — text-only changes; INSTALL.md's WLAN/TommiG1 drift and stale variable names were fixed 2026-08-12 (see section B) diff --git a/homeassistant/pyscript/frontend_api.py b/homeassistant/pyscript/frontend_api.py index 0fdb794..e3d6a99 100644 --- a/homeassistant/pyscript/frontend_api.py +++ b/homeassistant/pyscript/frontend_api.py @@ -97,3 +97,16 @@ def audi_dashboard_entitaeten_schreiben(zuordnung_json=None): frontend_veroeffentlichung.entitaeten_veroeffentlichen() frontend_veroeffentlichung.fahrzeugstatus_veroeffentlichen() log.info("audi_dashboard: Entitäten-Zuordnung aktualisiert") + + +@service +def audi_dashboard_neustart(): + """Startet Home Assistant neu. Einziger Zweck: eine Änderung an einem der + drei trigger-gebundenen Setup-Felder (ZUENDUNG_SENSOR/KM_SENSOR/ + TANK_SENSOR, siehe entitaeten.py) wird erst nach einem Neustart wirksam, + weil @state_trigger die Entity-ID beim Modul-Laden einmalig einliest. + Wird vom Setup-Popup als expliziter, vom Nutzer bestätigter Knopf + angeboten - kein automatischer Selbst-Reload direkt nach dem Speichern. + Aufruf als pyscript.audi_dashboard_neustart.""" + log.info("audi_dashboard: Neustart über das Setup-Menü ausgelöst") + homeassistant.restart() diff --git a/homeassistant/pyscript/modules/entitaeten.py b/homeassistant/pyscript/modules/entitaeten.py index f52848a..0a27a2d 100644 --- a/homeassistant/pyscript/modules/entitaeten.py +++ b/homeassistant/pyscript/modules/entitaeten.py @@ -133,6 +133,17 @@ for _feld in FELDER: _SCHLUESSEL = {feld["key"] for feld in FELDER} +# Schnappschuss der eingebauten Standardwerte aus einstellungen.py, genommen +# BEVOR overrides_anwenden() jemals lief (Modul-Ebene, läuft beim `import +# entitaeten` in frontend_api.py - vor dem @time_trigger("startup"), der +# overrides_anwenden() erstmals aufruft). Notwendig fürs Setup-Menü +# "Zurücksetzen": setattr() in overrides_anwenden() überschreibt das Attribut +# auf dem laufenden einstellungen-Modul dauerhaft, ein einfaches Weglassen +# des Overrides beim nächsten Speichern stellt den eingebauten Wert NICHT +# wieder her (das Attribut bleibt ja gesetzt) - "Zurücksetzen" muss den +# Standardwert stattdessen aktiv zurückschreiben. +_STANDARDWERTE = {feld["key"]: getattr(einstellungen, feld["key"], None) for feld in FELDER} + # --------------------------------------------------------------- JSON-I/O --- # Gleiches Muster wie profil.profil_lesen()/profil_schreiben(): io.open nur @@ -185,4 +196,4 @@ def aktueller_stand(): bereits angewendete Overrides als auch die eingebauten Standardwerte einheitlich. Grundlage für pyscript.audi_dashboard_entitaeten.""" werte = {feld["key"]: getattr(einstellungen, feld["key"], None) for feld in FELDER} - return {"katalog": FELDER, "werte": werte} + return {"katalog": FELDER, "werte": werte, "standardwerte": _STANDARDWERTE} diff --git a/homeassistant/www/audi-dashboard-app.js b/homeassistant/www/audi-dashboard-app.js index b2a1221..3f5ecc7 100644 --- a/homeassistant/www/audi-dashboard-app.js +++ b/homeassistant/www/audi-dashboard-app.js @@ -1911,6 +1911,7 @@ let smartdealPopupOffen = false; an den Service geht - Abbrechen verwirft sie einfach ohne Nebeneffekt. */ let setupOffen = false; let setupZuordnung = null; // {KEY: "entity.id"} bzw. {KEY: [4 IDs]} für Listenfelder +let setupZuordnungStart = null; // Schnappschuss von setupZuordnung beim Öffnen - für den Neustart-Hinweis (welche trigger-gebundenen Felder haben sich geändert) let setupNurPassend = true; // globaler Schalter: Kandidaten auf passende Domain/Einheit einschränken let setupSucheOffen = null; // Feld-Id ("KEY" oder "KEY:idx") des offenen Dropdowns, sonst null let setupSuchtext = ""; @@ -1986,11 +1987,79 @@ function setupOeffnen() { } } setupZuordnung = zuordnung; + setupZuordnungStart = JSON.parse(JSON.stringify(zuordnung)); setupSucheOffen = null; setupSuchtext = ""; setupSpeichertLaeuft = false; setupOffen = true; render(); } function setupSchliessen() { - setupOffen = false; setupSucheOffen = null; setupZuordnung = null; render(); + setupOffen = false; setupSucheOffen = null; setupZuordnung = null; setupZuordnungStart = null; render(); +} + +/* Standardwert (aus einstellungen.py, vor jedem Override) für ein Feld bzw. + eine Listenposition - Grundlage für den Zurücksetzen-Button. */ +function setupStandardwert(key, idx) { + const standard = (ENTITAETEN && ENTITAETEN.standardwerte) || {}; + const wert = standard[key]; + if (idx == null) return wert || ""; + return (wert || [])[idx] || ""; +} + +/* Entity-IDs, die im Arbeitsstand mehr als einer Rolle zugeordnet sind. */ +function setupDuplikate() { + const zaehler = {}; + for (const feld of setupKatalog()) { + const werte = feld.liste ? (setupZuordnung[feld.key] || []) : [setupZuordnung[feld.key]]; + for (const id of werte) { if (id) zaehler[id] = (zaehler[id] || 0) + 1; } + } + return Object.keys(zaehler).filter((id) => zaehler[id] > 1); +} + +/* Labels der trigger-gebundenen Felder (ZUENDUNG/KM/TANK), deren Wert sich + seit dem Öffnen des Popups geändert hat - für den Neustart-Hinweis nach + dem Speichern. */ +function setupGeaenderteTriggerFelder() { + if (!setupZuordnungStart) return []; + return setupKatalog() + .filter((f) => f.trigger_gebunden && JSON.stringify(setupZuordnung[f.key]) !== JSON.stringify(setupZuordnungStart[f.key])) + .map((f) => f.label); +} + +function setupSpeichernAusfuehren() { + setupSpeichertLaeuft = true; render(); + (async () => { + try { + await HASS.callService("pyscript", "audi_dashboard_entitaeten_schreiben", { zuordnung_json: JSON.stringify(setupZuordnung) }); + } catch (err) { + setupSpeichertLaeuft = false; hinweis("Speichern fehlgeschlagen", err.message); return; + } + const geaendert = setupGeaenderteTriggerFelder(); + setupOffen = false; setupSucheOffen = null; setupZuordnung = null; setupZuordnungStart = null; setupSpeichertLaeuft = false; render(); + if (geaendert.length) { + bestaetigen("Neustart nötig", `Wirkt für ${geaendert.join(", ")} erst nach einem Neustart von Home Assistant.`, "Jetzt neu starten", setupNeustartAusloesen, false); + } + })(); +} + +function setupNeustartAusloesen() { + (async () => { + try { + await HASS.callService("pyscript", "audi_dashboard_neustart", {}); + hinweis("Neustart ausgelöst", "Home Assistant startet neu - die App lädt sich danach automatisch neu."); + } catch (err) { + hinweis("Neustart fehlgeschlagen", err.message); + } + })(); +} + +/* Warnhinweis, falls der aktuell gewählte Sensor keinen brauchbaren Wert + liefert - sichtbar, bevor gespeichert wird, statt es erst hinterher in + der App zu bemerken. */ +function entitaetStatusWarnung(wert) { + if (!wert) return ""; + const zustand = HASS.states[wert] ? HASS.states[wert].state : null; + if (zustand === null) return `
Entität nicht gefunden.
`; + if (zustand === "unavailable" || zustand === "unknown") return `
Aktuell „${esc(zustand)}" - kein Wert vom Sensor.
`; + return ""; } function vReifen() { const r = CAR.reifen; @@ -2639,22 +2708,33 @@ const LUPE_SVG = `Keine passende Entität gefunden`; - return kandidaten.map((e) => ` + return kandidaten.map((e) => { + const zustand = HASS.states[e.id] ? HASS.states[e.id].state : null; + const unbekannt = zustand === null || zustand === "unavailable" || zustand === "unknown"; + const wertAnzeige = unbekannt ? (zustand === null ? "nicht gefunden" : zustand) : `${zustand}${e.unit ? " " + e.unit : ""}`; + return ` ${esc(e.name)} - ${esc(e.id)}${e.unit ? " · " + esc(e.unit) : ""} - `).join(""); + ${esc(e.id)} + ${esc(wertAnzeige)} + `; + }).join(""); } +const RESET_SVG = ``; + function entitaetCombo(key, idx) { const feldId = idx == null ? key : `${key}:${idx}`; const wert = idx == null ? setupZuordnung[key] : (setupZuordnung[key] || [])[idx]; const offen = setupSucheOffen === feldId; const anzeige = offen ? setupSuchtext : entitaetName(wert); + const standard = setupStandardwert(key, idx); + const zeigeReset = wert !== standard; return `
${LUPE_SVG} + ${zeigeReset ? `` : ""} ${offen ? `
${entitaetZeilenMarkup(feldId, key, setupSuchtext, wert)}
` : ""}
`; } @@ -2672,11 +2752,12 @@ function setupFeldzeile(feld) {
${esc(pos)} ${entitaetCombo(feld.key, i)} + ${entitaetStatusWarnung((setupZuordnung[feld.key] || [])[i])}
`).join("")} `; } - return `
${kopf}${entitaetCombo(feld.key, null)}
`; + return `
${kopf}${entitaetCombo(feld.key, null)}${entitaetStatusWarnung(setupZuordnung[feld.key])}
`; } const SETUP_GRUPPEN = [ @@ -3074,17 +3155,24 @@ function ereignisseVerdrahten() { setupSucheOffen = null; setupSuchtext = ""; render(); return; } + const reset = e.target.closest("[data-setup-reset]"); + if (reset) { + const feldId = reset.dataset.setupReset; + const teile = feldId.split(":"); + if (teile.length === 2) setupZuordnung[teile[0]][Number(teile[1])] = setupStandardwert(teile[0], Number(teile[1])); + else setupZuordnung[feldId] = setupStandardwert(feldId, null); + render(); + return; + } if (e.target.closest("[data-setup-speichern]")) { if (setupSpeichertLaeuft) return; - setupSpeichertLaeuft = true; render(); - (async () => { - try { - await HASS.callService("pyscript", "audi_dashboard_entitaeten_schreiben", { zuordnung_json: JSON.stringify(setupZuordnung) }); - } catch (err) { - setupSpeichertLaeuft = false; hinweis("Speichern fehlgeschlagen", err.message); return; - } - setupOffen = false; setupSucheOffen = null; setupZuordnung = null; setupSpeichertLaeuft = false; render(); - })(); + const duplikate = setupDuplikate(); + if (duplikate.length) { + const namen = duplikate.map(entitaetName).join(", "); + bestaetigen("Doppelt zugeordnet", `${namen} ${duplikate.length === 1 ? "ist" : "sind"} mehr als einer Rolle zugeordnet. Trotzdem speichern?`, "Trotzdem speichern", setupSpeichernAusfuehren, false); + return; + } + setupSpeichernAusfuehren(); return; } // Klick aufs (noch geschlossene) Suchfeld öffnet dessen Dropdown; ein diff --git a/homeassistant/www/audi-dashboard-version.json b/homeassistant/www/audi-dashboard-version.json index 8cc4483..372c96c 100644 --- a/homeassistant/www/audi-dashboard-version.json +++ b/homeassistant/www/audi-dashboard-version.json @@ -1 +1 @@ -{"version": 1786543200} +{"version": 1786550400} diff --git a/homeassistant/www/audi-dashboard.css b/homeassistant/www/audi-dashboard.css index 947c77a..118178e 100644 --- a/homeassistant/www/audi-dashboard.css +++ b/homeassistant/www/audi-dashboard.css @@ -901,12 +901,18 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; } width: 100%; box-sizing: border-box; background: var(--tile-deckend, var(--canvas)); border: 1px solid var(--line); color: var(--fg); font-family: inherit; font-size: 16px; /* siehe .feld input weiter oben: >=16px gegen Safari-Autozoom */ - padding: 11px 13px 11px 36px; + padding: 11px 34px 11px 36px; border-radius: 12px; text-align: left; -webkit-appearance: none; appearance: none; } .entitaet-combo-input:focus { outline: none; border-color: var(--red); } +.entitaet-combo-reset { + position: absolute; right: 8px; top: 50%; transform: translateY(-50%); + width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; + background: none; border: none; color: var(--fg3); cursor: pointer; padding: 0; +} +.entitaet-combo-reset:active { color: var(--fg); } .entitaet-liste { position: absolute; z-index: 5; left: 0; right: 0; top: calc(100% + 4px); max-height: 240px; overflow-y: auto; @@ -925,7 +931,10 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; } .entitaet-eintrag:active, .entitaet-eintrag.aktiv { background: var(--shade); } .entitaet-eintrag-name { font-size: 14px; color: var(--fg); } .entitaet-eintrag-id { font-size: 11.5px; color: var(--fg3); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } +.entitaet-eintrag-wert { font-size: 11.5px; color: var(--fg2); margin-top: 1px; } +.entitaet-eintrag-wert.unbekannt { color: var(--warn); } .entitaet-leer { padding: 14px 12px; font-size: 13px; color: var(--fg2); } +.setup-feld-warnung { font-size: 12px; color: var(--warn); line-height: 1.4; margin-top: 6px; } .setup-fuss { flex: 0 0 auto;