Selbst-Update-Sicherung, Service-Prognose ohne Servicebuch und Sicherheit neu gestaltet

- aktualisierung.py: Backup-Ordner verliert sein manifest.json beim Sichern,
  sonst laedt HA ihn nach einem Neustart als zweite audi_dashboard-Integration
  (Ursache des raetselhaften "gleicher Pfad"-Umbenennungs-Fehlers).
- service.ts/audi-dashboard-app.js: Uebersicht faellt jetzt wie Service schon
  auf die Fahrzeugmeldung zurueck, wenn kein Servicebucheintrag existiert;
  eigenes (kuerzeres) Oelwechsel-Intervall wird jetzt auch ohne Servicebuch-
  eintrag naeherungsweise aus der Herstellermeldung hochgerechnet, statt
  weiterhin unveraendert die Herstellervorgabe zu zeigen.
- Sicherheit neu gestaltet: vier Sammelzeilen (Fahrzeug verriegelt / Tueren
  und Klappen geschlossen / Fenster und Dach geschlossen / Kein Licht) statt
  einer flachen Liste, mit neuen Tuerschloss-, Dach- und Standlicht-Sensor-
  rollen, Kreis-Haekchen-Symbolen und einer Detailseite je Tuer/Motorhaube/
  Kofferraum in beiden Frontends.

Details, Verifikation und Sensor-Rollen in AGENTS.md (Abschnitte J/M/N).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 15:29:37 +02:00
parent 146f809e9b
commit 5a048488ea
23 changed files with 866 additions and 97 deletions
@@ -16,7 +16,11 @@ Dienstaufruf noch läuft. Deshalb strenger als install.ps1s
bis feststeht, dass der Download vollständig und plausibel ist.
2. Erst danach ein atomarer Tausch per os.rename, nicht Löschen+Kopieren:
der alte Ordner wird zu audi_dashboard_backup umbenannt statt gelöscht -
reversibel, falls das Update Probleme macht.
reversibel, falls das Update Probleme macht. Dessen manifest.json wird
dabei zu manifest.json.bak umbenannt: HA erkennt Integrationen an jedem
manifest.json unter custom_components/, unabhängig vom Ordnernamen -
ohne diesen Schritt lädt ein Neustart mit vorhandenem Backup-Ordner
eine zweite Integration mit derselben Domain.
3. Schlägt der zweite Rename fehl, wird der erste zurückgenommen, statt
einen halb ersetzten Ordner zurückzulassen.
4. Kein automatischer Reload oder Neustart. Ein Reload aus dem eigenen,
@@ -231,11 +235,29 @@ def entpacken_pruefen_tauschen(
f"Konnte den bestehenden Ordner nicht sichern: {fehler}"
) from fehler
# HA scannt JEDEN Ordner unter custom_components/ mit manifest.json als
# eigene Integration - nach der Domain im Manifest, nicht nach dem
# Ordnernamen. Ohne diesen Schritt wäre der Backup-Ordner beim nächsten
# Neustart eine zweite Integration mit domain "audi_dashboard": HA
# importiert dann auch deren dienste.py als eigenes Modul
# (custom_components.audi_dashboard_backup.dienste), dessen __file__ im
# Backup-Ordner liegt - INTEGRATIONSORDNER und _BACKUP_ORDNER fallen für
# diese Kopie auf denselben Pfad zusammen, und ihre Dienst-Registrierung
# kollidiert mit der echten Integration. Das Manifest bleibt als .bak
# erhalten, falls die Fassung zurückgerollt werden muss.
backup_manifest = os.path.join(backup_ordner, "manifest.json")
if os.path.isfile(backup_manifest):
os.rename(backup_manifest, backup_manifest + ".bak")
try:
os.rename(staging_ordner, integrationsordner)
except OSError as fehler:
# Rollback: die alte Fassung zurückbenennen, statt HA ohne
# Integrationsordner dastehen zu lassen.
# Rollback: erst das Manifest wiederherstellen, dann die alte
# Fassung zurückbenennen - sonst stünde HA mit einem Ordner ohne
# manifest.json da.
backup_manifest_bak = backup_manifest + ".bak"
if os.path.isfile(backup_manifest_bak):
os.rename(backup_manifest_bak, backup_manifest)
os.rename(backup_ordner, integrationsordner)
raise AktualisierungsFehler(
f"Tausch fehlgeschlagen, alte Fassung wiederhergestellt: {fehler}"
@@ -81,6 +81,19 @@ class Sensorzuordnung:
HECKKLAPPE_SENSOR: str = ""
HAUBE_SENSOR: str = ""
# Schlösser (getrennt von den Öffnungssensoren oben - zu heißt nicht
# verriegelt). Je vier Türschlösser, dazu Heckklappe/Motorhaube einzeln,
# wie bei den Öffnungssensoren.
TUERSCHLOSS_SENSOREN: list[str] = field(default_factory=list)
HECKKLAPPE_SCHLOSS_SENSOR: str = ""
HAUBE_SCHLOSS_SENSOR: str = ""
# Schiebedach (offen/zu) und Standlicht (an/aus) - ergänzen die
# Fenster-Gruppe bzw. stehen für sich, siehe _sicherheitscheck() in
# veroeffentlichung.py.
DACH_SENSOR: str = ""
LICHT_SENSOR: str = ""
# Vom Fahrzeug selbst gemeldete Service-Fälligkeit (ergänzt die
# App-eigene, aus dem Servicebuch berechnete Prognose).
NAECHSTER_OELWECHSEL_SENSOR: str = ""
@@ -160,6 +173,26 @@ FELDER: list[dict] = [
"hinweis": "\"aus\"/off = zu.",
"domains": ["binary_sensor"], "device_classes": ["door", "opening"], "units": [], "liste": False, "pflicht": False,
"stichworte": ["haube", "hood", "bonnet", "motorhaube"]},
{"key": "TUERSCHLOSS_SENSOREN", "label": "Türschlösser", "gruppe": "sicherheit",
"hinweis": "\"aus\"/off = verriegelt. Für \"Fahrzeug verriegelt\" wichtig.",
"domains": ["binary_sensor"], "device_classes": ["lock"], "units": [], "liste": True, "positionen": POSITIONEN,
"pflicht": False, "stichworte": ["türschloss", "schloss", "lock", "verriegelt"]},
{"key": "HECKKLAPPE_SCHLOSS_SENSOR", "label": "Kofferraum-Schloss", "gruppe": "sicherheit",
"hinweis": "\"aus\"/off = verriegelt.",
"domains": ["binary_sensor"], "device_classes": ["lock"], "units": [], "liste": False, "pflicht": False,
"stichworte": ["kofferraum", "heckklappe", "tailgate", "schloss", "lock"]},
{"key": "HAUBE_SCHLOSS_SENSOR", "label": "Motorhauben-Schloss", "gruppe": "sicherheit",
"hinweis": "\"aus\"/off = verriegelt.",
"domains": ["binary_sensor"], "device_classes": ["lock"], "units": [], "liste": False, "pflicht": False,
"stichworte": ["haube", "hood", "schloss", "lock"]},
{"key": "DACH_SENSOR", "label": "Schiebedach", "gruppe": "sicherheit",
"hinweis": "\"aus\"/off = zu.",
"domains": ["binary_sensor"], "device_classes": ["window", "door", "opening"], "units": [], "liste": False, "pflicht": False,
"stichworte": ["dach", "schiebedach", "sunroof", "roof"]},
{"key": "LICHT_SENSOR", "label": "Standlicht", "gruppe": "sicherheit",
"hinweis": "\"aus\"/off = kein Licht.",
"domains": ["binary_sensor"], "device_classes": ["light"], "units": [], "liste": False, "pflicht": False,
"stichworte": ["licht", "standlicht", "light", "parking lights"]},
{"key": "NAECHSTER_OELWECHSEL_SENSOR", "label": "Nächster Ölwechsel (Datum)", "gruppe": "uebersicht",
"hinweis": "Vom Fahrzeug selbst gemeldete Fälligkeit, ergänzt die App-eigene Servicebuch-Prognose.",
"domains": ["sensor"], "device_classes": ["date", "timestamp"], "units": [], "liste": False, "pflicht": False,
@@ -1 +1 @@
{"version":"2026.8.24.6","sha256":"e0d8c7a6f4cc7fddc59b252e02e4ebf50002f3bd4e317ddb315670ef13299d75","bytes":232382,"gebaut":"2026-08-24T10:44:09Z"}
{"version":"2026.8.24.8","sha256":"3eddc79605bc979da46476cf000c2fb501b8bc510148da60b56b0b49b9b603c6","bytes":233244,"gebaut":"2026-08-24T12:43:13Z"}
@@ -1288,14 +1288,53 @@ function kmProTagAusFahrten() {
function meldungsPrognose(art) {
const fm = fahrzeugMeldungRoh(art);
const zeitDatum = fm.ts && gueltigesDatum(new Date(fm.ts)) ? new Date(fm.ts) : null;
if (fm.km == null) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true } : null;
if (fm.km == null) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true, restKm: null } : null;
const restKm = Math.abs(fm.km);
if (restKm <= 0) return { datum: heute(), durchZeitlimit: false };
if (restKm <= 0) return { datum: heute(), durchZeitlimit: false, restKm };
const rate = kmProTagAusFahrten();
if (!rate) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true } : null;
if (!rate) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true, restKm } : null;
const kmDatum = new Date(heute().getTime() + (restKm / rate) * dTag);
const durchZeitlimit = !!(zeitDatum && zeitDatum < kmDatum);
return { datum: durchZeitlimit ? zeitDatum : kmDatum, durchZeitlimit };
return { datum: durchZeitlimit ? zeitDatum : kmDatum, durchZeitlimit, restKm };
}
/* Eigenes (kürzeres) Intervall ganz ohne Servicebucheintrag: die
Fahrzeugmeldung kennt nur das Herstellerintervall, nicht die in
"Einrichten" gewählte kürzere Vorgabe. Ein angenommener letzter Service
wird aus der gemeldeten Fälligkeit und der bekannten Herstellerintervall-
Länge zurückgerechnet ("Fälligkeitspunkt minus Herstellerintervall"), und
das eigene Intervall ab demselben angenommenen Punkt neu hochgerechnet -
eine Näherung (der wahre letzte Service kann näher oder ferner liegen),
aber ohne sie würde die eigene Intervallwahl bis zum ersten
Servicebucheintrag komplett ignoriert (siehe oelMeldungsPrognose()). */
function meldungsPrognoseEigenesIntervall(fm, herstellerKm, herstellerMonate, eigenKm, eigenMonate) {
if (fm.km == null || !herstellerKm || eigenKm == null) return null;
const restKm = Math.max(0, Math.abs(fm.km) - herstellerKm + eigenKm);
let zeitDatum = null;
if (fm.ts && gueltigesDatum(new Date(fm.ts)) && herstellerMonate != null && eigenMonate != null) {
zeitDatum = new Date(fm.ts);
zeitDatum.setMonth(zeitDatum.getMonth() + (eigenMonate - herstellerMonate));
}
if (restKm <= 0) return { datum: heute(), durchZeitlimit: false, restKm };
const rate = kmProTagAusFahrten();
if (!rate) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true, restKm } : null;
const kmDatum = new Date(heute().getTime() + (restKm / rate) * dTag);
const durchZeitlimit = !!(zeitDatum && zeitDatum < kmDatum);
return { datum: durchZeitlimit ? zeitDatum : kmDatum, durchZeitlimit, restKm };
}
/* Fällt ganz ohne Servicebucheintrag auf die Fahrzeugmeldung zurück - bei
Herstellervorgabe unverändert (die Meldung IST die Herstellervorgabe), bei
eigenem (kürzerem) Intervall über meldungsPrognoseEigenesIntervall()
hochgerechnet, damit die Intervallwahl in "Einrichten" auch ganz ohne
Servicebucheintrag etwas bewirkt. Fehlen die Herstellerwerte selbst (kein
Sensor dafür), fällt das sanft auf die reine Meldung zurück. */
function oelMeldungsPrognose() {
const oel = CAR.oel;
if (oel.modus === "hersteller") return meldungsPrognose("Ölwechsel");
const fm = fahrzeugMeldungRoh("Ölwechsel");
const eigen = meldungsPrognoseEigenesIntervall(fm, oel.herstellerKm, oel.herstellerMonate, oel.km, oel.monate);
return eigen || meldungsPrognose("Ölwechsel");
}
function oelwechselPrognose() {
@@ -1437,29 +1476,82 @@ function vHome() {
${teaser()}`;
}
/* Kreis-Häkchen (ok) / Kreis-Kreuz (nicht ok) / Kreis-Fragezeichen (unbekannt)
statt eines einfarbigen Punkts - Zustand ist so schon am Symbol selbst
erkennbar, nicht nur an der Farbe. Nur für die Sicherheit-Ansicht (vSicherheit()/
vTuerenKlappen()) gedacht, der übrige Bestand behält den einfachen .dot. */
function statusKreis(ok) {
const klasse = ok === null ? "warn" : ok ? "" : "bad";
const glyph = ok === null
? '<path d="M5.7 6a2.3 2.3 0 1 1 2.7 2.27c-.5.09-.7.5-.7 1.03v.2M8 11.6v.01" stroke="#fff" stroke-width="1.5" stroke-linecap="round" fill="none"/>'
: ok
? '<path d="M3 8.3 6.2 11.5 13 4.5" stroke="#fff" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/>'
: '<path d="M4 4l8 8M12 4l-8 8" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/>';
return `<span class="statuskreis ${klasse}" aria-hidden="true"><svg viewBox="0 0 16 16" width="13" height="13">${glyph}</svg></span>`;
}
/* Gruppiert die flache CAR.sicherheitscheck-Liste (siehe _sicherheitscheck()
in veroeffentlichung.py) nach ihrem "gruppe"-Feld. ok ist null, sobald ein
einziger Punkt der Gruppe unbekannt ist, oder wenn die Gruppe ganz ohne
Einträge dasteht (kein Sensor dieser Rolle im Setup-Menü zugeordnet) -
nie true durch eine leere Liste erschlichen. */
function sicherheitsGruppe(gruppe) {
const eintraege = CAR.sicherheitscheck.filter((e) => e.gruppe === gruppe);
const ok = eintraege.length === 0 || eintraege.some((e) => e.ok === null)
? null
: eintraege.every((e) => e.ok);
return { eintraege, ok };
}
function vSicherheit() {
const eintraege = CAR.sicherheitscheck;
const g = CAR.gesichert;
const offen = eintraege.filter((e) => e.ok === false).length;
const unbekannt = eintraege.filter((e) => e.ok === null).length;
const sammelText = g === null
? `${unbekannt} von ${eintraege.length} Punkten liefern derzeit keinen Wert`
: g ? "Fahrzeug ist sicher abgestellt" : `${offen} Punkt${offen === 1 ? "" : "e"} prüfen`;
/* C6: Draufsicht oben, grüne Sammelmeldung, dann die Prüfliste - dieselbe
Reihenfolge wie der Fahrzeugstatus in der myAudi-App. */
const verriegelt = sicherheitsGruppe("verriegelt");
const tuerenKlappen = sicherheitsGruppe("tueren_klappen");
const fensterDach = sicherheitsGruppe("fenster_dach");
const licht = sicherheitsGruppe("licht");
const sammelText = g === null ? "Zustand nicht vollständig bekannt" : g ? "Fahrzeug ist sicher abgestellt" : "Bitte prüfen";
// Jede Zeile nennt beim Nutzer-Zustand ("ok") genau die Formulierung, die
// der Nutzer vorgegeben hat - beim Licht ist das "Kein Licht", nicht
// "Licht ausgeschaltet" (das beschreibt nur die Prüfung, nicht den Wert).
const zeile = (label, ok, labelBad, labelWarn, route) => {
const text = ok === null ? labelWarn : ok ? label : labelBad;
const klickbar = !!route;
return `<${klickbar ? "button" : "div"} class="row sicherheitszeile"${klickbar ? ` data-go="${route}:x"` : ""}>
${statusKreis(ok)}
<span class="sicherheitszeile-text">${esc(text)}</span>
${klickbar ? '<svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg>' : ""}
</${klickbar ? "button" : "div"}>`;
};
/* C6: Draufsicht oben, grüne Sammelmeldung, dann die vier Sammelzeilen -
dieselbe Reihenfolge wie der Fahrzeugstatus in der myAudi-App. */
return `
<div class="szene bleed"><div class="carbox">${bildMitPlatzhalter(`/local/bilder/draufsicht.webp?v=${bildVersion}`, "draufsicht.webp", "Draufsicht", "carfix klein")}</div></div>
<div class="tile" style="display:flex;align-items:center;gap:12px;padding:16px 20px">
<span class="dot ${g === null ? "warn" : g ? "" : "bad"}"></span>
${statusKreis(g)}
<span style="font-size:15px;color:var(--fg)">${esc(sammelText)}</span>
</div>
<div class="tile"><span class="label">Geprüfte Punkte</span>
<div class="tile sicherheitsliste">
${zeile("Fahrzeug verriegelt", verriegelt.ok, "Fahrzeug nicht verriegelt", "Verriegelung unbekannt")}
${zeile("Türen und Klappen geschlossen", tuerenKlappen.ok, "Türen oder Klappen offen", "Türen/Klappen unbekannt", "tuerenklappen")}
${zeile("Fenster und Dach geschlossen", fensterDach.ok, "Fenster oder Dach offen", "Fenster/Dach unbekannt")}
${zeile("Kein Licht", licht.ok, "Licht eingeschaltet", "Licht unbekannt")}
</div>`;
}
/* Detailansicht zu "Türen und Klappen geschlossen" - jede Tür einzeln,
Motorhaube, Kofferraum. Einzige der vier Sammelzeilen mit eigener
Detailseite (siehe vSicherheit()), im Stil der früheren flachen
"Geprüfte Punkte"-Liste. */
function vTuerenKlappen() {
const eintraege = sicherheitsGruppe("tueren_klappen").eintraege;
return `
<div class="tile"><span class="label">Türen und Klappen</span>
<dl class="rows" style="margin-top:10px">
${eintraege.length ? eintraege.map((e, i, arr) => `<div class="row" ${i === arr.length - 1 ? 'style="border-bottom:0"' : ""}>
<dt>${esc(e.label)}</dt>
<dd style="display:flex;align-items:center;justify-content:flex-end;gap:8px">
<span class="dot ${e.ok === null ? "warn" : e.ok ? "" : "bad"}"></span>
${e.ok === null ? "unbekannt" : e.ok ? "in Ordnung" : "prüfen"}
${e.ok === null ? "unbekannt" : e.ok ? "zu" : "offen"}
</dd>
</div>`).join("") : `<div class="row" style="border-bottom:0"><dt style="color:var(--fg3)">Keine Daten verfügbar</dt></div>`}
</dl>
@@ -1837,10 +1929,15 @@ function vService() {
const eigenOel = t.art === "Ölwechsel" ? oelwechselPrognose() : null;
const eigenInsp = t.art === "Inspektion" ? inspektionPrognose() : null;
// Ganz ohne Servicebucheintrag (eigenOel/eigenInsp beide null) übernimmt
// die Fahrzeugmeldung selbst die Rolle der Prognosequelle, hochgerechnet
// mit der echten Fahrleistung aus dem Fahrtenlog statt einer
// Servicebuch-Rate - siehe meldungsPrognose().
const meldungProg = hatMeldung && !eigenOel && !eigenInsp ? meldungsPrognose(t.art) : null;
// die Fahrzeugmeldung selbst die Rolle der Prognosequelle - bei
// Herstellervorgabe unverändert, beim Ölwechsel mit eigenem (kürzerem)
// Intervall über oelMeldungsPrognose() auf dieses umgerechnet, statt es
// bis zum ersten Servicebucheintrag zu ignorieren (siehe deren
// Kommentar). Hochgerechnet wird dabei mit der echten Fahrleistung aus
// dem Fahrtenlog statt einer Servicebuch-Rate.
const meldungProg = hatMeldung && !eigenOel && !eigenInsp
? (t.art === "Ölwechsel" ? oelMeldungsPrognose() : meldungsPrognose(t.art))
: null;
// Bei Herstellervorgabe rechnet die eigene Prognose ohnehin schon mit dem
// Herstellerintervall - die Restkilometerzahl daneben waere nur eine
// zweite, verwirrende km-Angabe neben dem Fahrzeug-Wert oben. Nur beim
@@ -1850,7 +1947,11 @@ function vService() {
? `voraussichtlich am ${dedat(eigenOel.datum)}`
: `${de(eigenOel.restKm)} km, voraussichtlich am ${dedat(eigenOel.datum)}`)
: eigenInsp ? `voraussichtlich am ${dedat(eigenInsp.datum)}`
: meldungProg ? `voraussichtlich am ${dedat(meldungProg.datum)}` : "";
: meldungProg
? (t.art === "Ölwechsel" && CAR.oel.modus !== "hersteller" && meldungProg.restKm != null
? `${de(meldungProg.restKm)} km, voraussichtlich am ${dedat(meldungProg.datum)}`
: `voraussichtlich am ${dedat(meldungProg.datum)}`)
: "";
const wert = hatMeldung
? `${fm.km != null ? de(fm.km) + " km" : ""}${fm.km != null && fmDatum ? " / " : ""}${fmDatum || ""}`
: (eigenOel ? de(eigenOel.restKm) + " km · " + dedat(eigenOel.datum) : (anzeige.km !== null ? de(anzeige.km) + " km · " : "") + dedat(anzeige.datum));
@@ -1961,9 +2062,26 @@ function termine() {
oelTermin.zuerst = oelTermin.datum && oelProg.datum >= oelTermin.datum ? "zeit" : "km";
oelTermin.ziel = oelTermin.zuerst === "km" ? oelProg.datum : oelTermin.datum;
}
const inspTermin = bauen("Inspektion", letzterEintrag("inspektion"), 30000, 24);
// Ganz ohne Servicebucheintrag (kein basis) übernimmt die Fahrzeugmeldung
// selbst die Rolle der Prognosequelle - genau wie vService()'s eigenes
// meldungProg das schon tut (siehe dessen Kommentar dort). Ohne diesen
// Fallback hier zeigte "Übersicht" trotz korrekt gemeldeter Fälligkeit nur
// "kein Eintrag im Servicebuch", während "Service" dieselbe Meldung
// längst richtig anzeigte - zwei Ansichten, ein Datenstand, zwei
// widersprüchliche Aussagen.
[oelTermin, inspTermin].forEach((termin) => {
if (termin.basis) return;
const m = termin.art === "Ölwechsel" ? oelMeldungsPrognose() : meldungsPrognose(termin.art);
if (!m) return;
termin.km = m.restKm != null && CAR.odoBekannt ? CAR.odo + m.restKm : null;
termin.kmDatum = m.datum;
termin.zuerst = "km";
termin.ziel = m.datum;
});
return [
oelTermin,
bauen("Inspektion", letzterEintrag("inspektion"), 30000, 24),
inspTermin,
bauen("Hauptuntersuchung", letzterEintrag("hauptuntersuchung"), null, 24,
CAR.erstzulassung && CAR.erstzulassung.includes("/") ? `01.${CAR.erstzulassung.replace("/", ".")}` : null),
];
@@ -3404,6 +3522,7 @@ const ZURUECK = {
Zurück-Pfeil. */
sicherheit: "home",
standort: "home",
tuerenklappen: "sicherheit",
};
const ICON = {
sonne: '<circle cx="10" cy="10" r="4"/><path d="M10 1v2M10 17v2M1 10h2M17 10h2M3.9 3.9l1.4 1.4M14.7 14.7l1.4 1.4M16.1 3.9l-1.4 1.4M5.3 14.7l-1.4 1.4"/>',
@@ -3748,6 +3867,7 @@ function render() {
else if (route.name === "einst") { head = ["App", "Einstellungen"]; v.innerHTML = vEinst(); }
else if (route.name === "ident") { head = ["Fahrzeug", "Identität und Technik"]; v.innerHTML = vIdent(); }
else if (route.name === "sicherheit") { head = ["Fahrzeug", "Sicherheit"]; v.innerHTML = vSicherheit(); }
else if (route.name === "tuerenklappen") { head = ["Fahrzeug", "Türen und Klappen"]; v.innerHTML = vTuerenKlappen(); }
else if (route.name === "vers") { head = ["Fahrzeug", "Versicherung/Steuer"]; v.innerHTML = vVers(); }
else if (route.name === "beitrag") { head = ["Versicherung", "Beitrag anpassen"]; v.innerHTML = vBeitrag(); }
else if (route.name === "vertragsdetails") { head = ["Versicherung", "Vertragsdetails"]; v.innerHTML = vVertragsdetails(); }
@@ -411,6 +411,26 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
:has() grenzt das Umbrechen auf genau die Zeilen ein, die eine Fussnote
fuehren - alle anderen .row bleiben unveraendert einzeilig. */
.row:has(.row-fussnote) { flex-wrap: wrap; }
/* Sammelzeilen der Sicherheit-Ansicht (vSicherheit()): Kreis-Häkchen + Text
+ optionaler Chevron statt dt/dd. Erbt Abstand/Trennlinie von .row, ersetzt
aber dessen space-between-Layout und setzt die Button-Vorgaben zurück, wenn
die Zeile klickbar ist (Türen und Klappen). */
.sicherheitszeile {
justify-content: flex-start;
width: 100%;
background: none;
border: none;
border-bottom: 1px solid var(--line);
font: inherit;
color: inherit;
text-align: left;
cursor: default;
}
button.sicherheitszeile { cursor: pointer; }
button.sicherheitszeile:active { background: var(--tile-2); }
.sicherheitsliste .sicherheitszeile:last-child { border-bottom: 0; }
.sicherheitszeile-text { flex: 1; font-size: 14.5px; color: var(--fg); }
.sicherheitszeile .chev { margin-left: auto; }
.row-fussnote {
flex: 1 0 100%;
text-align: left;
@@ -451,6 +471,12 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
.dot { width: 11px; height: 11px; border-radius: 50%; flex: 0 0 auto; background: var(--ok); display: inline-block; }
.dot.warn { background: var(--warn); }
.dot.bad { background: var(--bad); }
/* Kreis-Häkchen statt einfarbigem Punkt - für die Sicherheit-Ansicht
(vSicherheit()/vTuerenKlappen()), nach Nutzer-Vorlage: erfüllt/offen/
unbekannt sofort am Symbol erkennbar, nicht nur an der Farbe. */
.statuskreis { width: 22px; height: 22px; border-radius: 50%; flex: 0 0 auto; background: var(--ok); display: inline-flex; align-items: center; justify-content: center; }
.statuskreis.warn { background: var(--warn); }
.statuskreis.bad { background: var(--bad); }
.status .t { font-size: 14px; color: var(--fg); }
.status { transition: opacity .15s; }
.status:active { opacity: .55; }
@@ -1,7 +1,7 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.8.24.6",
"version": "2026.8.24.8",
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
"codeowners": ["@paul"],
@@ -94,26 +94,49 @@ def _standort(hass: HomeAssistant, werte: Sensorzuordnung) -> dict:
def _sicherheitscheck(hass: HomeAssistant, werte: Sensorzuordnung) -> list[dict]:
"""Die einzeln geprüften Punkte hinter "Sicher abgestellt", fürs Frontend
(Klick auf den Status öffnet diese Liste mit einem grünen/roten/grauen
Punkt je Zeile).
"""Die einzeln geprüften Punkte hinter "Sicher abgestellt", fürs Frontend -
gruppiert über das Feld "gruppe" in vier Sammelzeilen (Fahrzeug verriegelt
/ Türen und Klappen geschlossen / Fenster und Dach geschlossen / Licht
ausgeschaltet), die die Oberfläche durch Gruppieren dieser flachen Liste
berechnet, statt hier schon vier separate Listen zu bauen - eine flache
Liste bleibt die einfachere, additive Änderung gegenüber dem, was das
Frontend vorher schon kannte.
"ok" ist None, wenn der Sensor fehlt oder nicht verfügbar ist - genau
daraus leitet sich auch die zusammengefasste Kennzahl unten ab, damit
beide nie auseinanderlaufen können."""
beide nie auseinanderlaufen können. Alle binary_sensor-device_classes
hier (door/window/opening/lock/light) folgen HAs eigener Konvention "off
= der gute Zustand" (zu/verriegelt/kein Licht) - ein einziges Prüfmuster
für alle Zeilen, kein Sonderfall je Art."""
eintraege: list[dict] = []
for pos, sensor in zip(POSITIONEN, werte.TUER_SENSOREN):
w = zustand_oder_none(hass, sensor)
eintraege.append({"label": f"Tür {pos}", "ok": None if w is None else w == "off"})
eintraege.append({"label": f"Tür {pos}", "ok": None if w is None else w == "off", "gruppe": "tueren_klappen"})
for pos, sensor in zip(POSITIONEN, werte.FENSTER_SENSOREN):
w = zustand_oder_none(hass, sensor)
eintraege.append({"label": f"Fenster {pos}", "ok": None if w is None else w == "off"})
eintraege.append({"label": f"Fenster {pos}", "ok": None if w is None else w == "off", "gruppe": "fenster_dach"})
# "Kofferraum" statt "Heckklappe": alltagssprachlicher Name für dieselbe
# Öffnung beim Avant/Kombi - das Sensorfeld HECKKLAPPE_SENSOR selbst
# bleibt unverändert, um eine bestehende Zuordnung nicht zu brechen.
for label, sensor in (
("Heckklappe", werte.HECKKLAPPE_SENSOR),
("Kofferraum", werte.HECKKLAPPE_SENSOR),
("Motorhaube", werte.HAUBE_SENSOR),
):
w = zustand_oder_none(hass, sensor)
eintraege.append({"label": label, "ok": None if w is None else w == "off"})
eintraege.append({"label": label, "ok": None if w is None else w == "off", "gruppe": "tueren_klappen"})
for pos, sensor in zip(POSITIONEN, werte.TUERSCHLOSS_SENSOREN):
w = zustand_oder_none(hass, sensor)
eintraege.append({"label": f"Türschloss {pos}", "ok": None if w is None else w == "off", "gruppe": "verriegelt"})
for label, sensor in (
("Kofferraum-Schloss", werte.HECKKLAPPE_SCHLOSS_SENSOR),
("Motorhauben-Schloss", werte.HAUBE_SCHLOSS_SENSOR),
):
w = zustand_oder_none(hass, sensor)
eintraege.append({"label": label, "ok": None if w is None else w == "off", "gruppe": "verriegelt"})
w = zustand_oder_none(hass, werte.DACH_SENSOR)
eintraege.append({"label": "Dach", "ok": None if w is None else w == "off", "gruppe": "fenster_dach"})
w = zustand_oder_none(hass, werte.LICHT_SENSOR)
eintraege.append({"label": "Standlicht", "ok": None if w is None else w == "off", "gruppe": "licht"})
return eintraege