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:
@@ -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"}
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user