From 64f457dd873fa3284251d69a455b95d86d0e59c2 Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Thu, 13 Aug 2026 15:52:05 +0200 Subject: [PATCH] Setup-Zuordnung: Zuruecksetzen wirkt, Sicherung, Absicherung Drei Befunde aus REVIEW_main_2026-08-13.md, alle an der Testinstanz geprueft. Zuruecksetzen (Befund 2 und 5): overrides_anwenden() uebersprang leere Werte und setzte nur belegte. Weil setattr das Modul-Attribut dauerhaft veraendert, blieb ein einmal gesetzter Wert danach fuer immer stehen - bei 15 von 17 Feldern hatte Zuruecksetzen keine Wirkung, und die Oberflaeche zeigte wieder den alten Wert, als sei das Speichern fehlgeschlagen. Umgekehrt wurde eine Liste aus leeren Eintraegen gesetzt statt uebersprungen, was den Sicherheitscheck mit zwoelf unbekannt-Zeilen fuellte. Jetzt wird fuer jedes bekannte Feld geschrieben: der Override, wenn belegt, sonst der eingebaute Standardwert. Damit ist der Vorgang zugleich wiederholbar. Sicherung (Befund 6): entitaeten.json war weder in backup.py noch in der Wiederherstellung noch im Browser-Export enthalten - die gesamte Zuordnung waere nach einem Rueckspielen still weg gewesen. Jetzt ueberall dabei; fehlt der Abschnitt in aelteren Sicherungen, bleibt die aktuelle Zuordnung stehen. Absicherung (Befund 7): overrides_lesen() fing kaputtes JSON nicht ab. Der Aufruf steht in beim_start() vor alles_veroeffentlichen() - eine unlesbare Zeile liess das Panel komplett leer bleiben. Geprueft: es folgt jetzt eine verstaendliche Meldung, alle sechs Entitaeten werden trotzdem veroeffentlicht. Dabei ein pyscript-Fallstrick gefunden und im Code vermerkt: Generator- ausdruecke sind nicht implementiert (not implemented ast ast_generatorexp), Mengen-, Listen- und Dict-Comprehensions dagegen schon. --- homeassistant/.gitignore | 1 + homeassistant/pyscript/backup.py | 25 +++++-- homeassistant/pyscript/modules/entitaeten.py | 74 ++++++++++++++++---- homeassistant/www/audi-dashboard-app.js | 8 ++- 4 files changed, 91 insertions(+), 17 deletions(-) diff --git a/homeassistant/.gitignore b/homeassistant/.gitignore index 356efdf..d3c7379 100644 --- a/homeassistant/.gitignore +++ b/homeassistant/.gitignore @@ -5,6 +5,7 @@ data/fahrzeugprofil.json data/fahrten.jsonl data/tankvorgaenge.jsonl +data/entitaeten.json data/tests/belege/*.pdf # Generiertes Installations-Bundle (nur eine Kopie der bereits getrackten diff --git a/homeassistant/pyscript/backup.py b/homeassistant/pyscript/backup.py index 54b658e..4ebf61a 100644 --- a/homeassistant/pyscript/backup.py +++ b/homeassistant/pyscript/backup.py @@ -25,11 +25,20 @@ import json import os import shutil +import entitaeten import frontend_veroeffentlichung import profil BACKUP_ORDNER = f"{profil.BASIS}/backups" -_DATEIEN = ["fahrzeugprofil.json", "fahrten.jsonl", "tankvorgaenge.jsonl"] +# entitaeten.json gehört dazu: Ohne sie wäre nach einer Wiederherstellung die +# komplette Sensor-Zuordnung aus dem Setup-Menü verloren, und weil sie sonst +# nirgends abgelegt ist, gäbe es nichts zurückzuholen. +_DATEIEN = [ + "fahrzeugprofil.json", + "fahrten.jsonl", + "tankvorgaenge.jsonl", + "entitaeten.json", +] def _backup_jetzt(): @@ -75,10 +84,14 @@ def _automatisches_backup(): @service -def audi_dashboard_backup_wiederherstellen(profil_json=None, fahrten_json=None, tankvorgaenge_json=None): +def audi_dashboard_backup_wiederherstellen(profil_json=None, fahrten_json=None, tankvorgaenge_json=None, entitaeten_json=None): """Stellt ein über "Backup exportieren" heruntergeladenes Backup wieder - her - überschreibt Profil, Fahrten und Tankvorgänge komplett. Aufruf als - pyscript.audi_dashboard_backup_wiederherstellen.""" + her - überschreibt Profil, Fahrten, Tankvorgänge und die Sensor-Zuordnung + komplett. Aufruf als pyscript.audi_dashboard_backup_wiederherstellen. + + entitaeten_json ist optional: ältere Sicherungen, die vor dem Setup-Menü + entstanden sind, enthalten den Abschnitt nicht - dann bleibt die aktuelle + Zuordnung unangetastet, statt sie zu leeren.""" if profil_json: p = json.loads(profil_json) if isinstance(profil_json, str) else profil_json profil.profil_schreiben(p) @@ -88,5 +101,9 @@ def audi_dashboard_backup_wiederherstellen(profil_json=None, fahrten_json=None, if tankvorgaenge_json is not None: tankvorgaenge = json.loads(tankvorgaenge_json) if isinstance(tankvorgaenge_json, str) else tankvorgaenge_json profil.tankvorgaenge_schreiben(tankvorgaenge) + if entitaeten_json: + zuordnung = json.loads(entitaeten_json) if isinstance(entitaeten_json, str) else entitaeten_json + entitaeten.overrides_schreiben(zuordnung) + entitaeten.overrides_anwenden() frontend_veroeffentlichung.alles_veroeffentlichen() log.info("audi_dashboard: Backup wiederhergestellt") diff --git a/homeassistant/pyscript/modules/entitaeten.py b/homeassistant/pyscript/modules/entitaeten.py index 0a27a2d..5de66b7 100644 --- a/homeassistant/pyscript/modules/entitaeten.py +++ b/homeassistant/pyscript/modules/entitaeten.py @@ -151,12 +151,34 @@ _STANDARDWERTE = {feld["key"]: getattr(einstellungen, feld["key"], None) for fel # atomares Schreiben über eine .tmp-Datei + os.replace. def overrides_lesen(): + """Liest die gespeicherten Zuordnungen, oder {} wenn die Datei fehlt bzw. + beschädigt ist. + + Der try/except ist nicht Vorsicht um ihrer selbst willen: overrides_ + anwenden() läuft in beim_start() VOR alles_veroeffentlichen(). Ohne die + Absicherung reißt eine einzige unlesbare Zeile in dieser Datei den + gesamten Startvorgang mit - das Panel bliebe komplett leer, ohne dass + irgendetwas auf die Ursache hindeutet.""" if not os.path.exists(OVERRIDES_PFAD): return {} f = task.executor(io.open, OVERRIDES_PFAD, "r") inhalt = f.read() f.close() - return json.loads(inhalt) if inhalt.strip() else {} + if not inhalt.strip(): + return {} + try: + gelesen = json.loads(inhalt) + except ValueError as fehler: + log.error( + f"audi_dashboard: {OVERRIDES_PFAD} ist kein gültiges JSON ({fehler}). " + "Die eingebauten Standardwerte gelten weiter; die Zuordnung lässt sich " + "im Setup-Menü neu speichern." + ) + return {} + if not isinstance(gelesen, dict): + log.error(f"audi_dashboard: {OVERRIDES_PFAD} enthält kein Objekt - wird ignoriert.") + return {} + return gelesen def overrides_schreiben(mapping): @@ -175,19 +197,47 @@ def overrides_schreiben(mapping): # ------------------------------------------------------- Anwenden/Lesen --- +def _ist_leer(wert): + """Leer heißt: keine Zuordnung. Auch eine Liste, die nur leere Einträge + enthält - das Setup-Menü schickt für unbelegte Positionen ["","",...], + und das ist inhaltlich dasselbe wie gar keine Liste. + + Die Schleife steht hier bewusst statt eines any(...)-Generatorausdrucks: + pyscript kennt Generatorausdrücke nicht und wirft zur Laufzeit + "not implemented ast ast_generatorexp". Mengen-, Listen- und + Dict-Comprehensions funktionieren dagegen (siehe _SCHLUESSEL oben).""" + if wert in (None, ""): + return True + if isinstance(wert, list): + for eintrag in wert: + if eintrag: + return False + return True + return False + + def overrides_anwenden(): - """setattr(einstellungen, key, wert) für jeden in entitaeten.json - hinterlegten, nicht-leeren Wert - siehe Kopfkommentar, warum das - ausreicht, damit alle bestehenden Verbraucher den neuen Wert sofort - sehen (Ausnahme: die drei TRIGGER_GEBUNDEN-Felder, dort erst nach - Neustart).""" + """Setzt für JEDES bekannte Feld den wirksamen Wert auf dem laufenden + einstellungen-Modul - den Override, wenn einer hinterlegt ist, sonst den + eingebauten Standardwert. + + Wichtig ist das "jedes": Eine frühere Fassung übersprang leere Werte und + setzte nur die belegten. Weil setattr() das Modul-Attribut aber dauerhaft + verändert, blieb ein einmal gesetzter Wert danach für immer stehen - + "Zurücksetzen" im Setup-Menü hatte bei allen Feldern mit leerem + Standardwert (15 von 17) schlicht keine Wirkung, und die Oberfläche zeigte + beim nächsten Zeichnen wieder den alten Wert, als sei das Speichern + fehlgeschlagen. Umgekehrt wurde eine Liste aus leeren Einträgen gesetzt + statt übersprungen, was den Sicherheitscheck mit zwölf "unbekannt"-Zeilen + füllte und "Sicher abgestellt" dauerhaft grau werden ließ. + + Jedes Feld bei jedem Durchlauf zu schreiben ist damit nicht nur korrekt, + sondern auch idempotent: der Zustand hängt allein an entitaeten.json, + nicht an der Reihenfolge vorheriger Aufrufe.""" overrides = overrides_lesen() - for key, wert in overrides.items(): - if key not in _SCHLUESSEL: - continue - if wert in (None, "", []): - continue - setattr(einstellungen, key, wert) + for key in _SCHLUESSEL: + wert = overrides.get(key) + setattr(einstellungen, key, _STANDARDWERTE[key] if _ist_leer(wert) else wert) def aktueller_stand(): diff --git a/homeassistant/www/audi-dashboard-app.js b/homeassistant/www/audi-dashboard-app.js index 3f5ecc7..3cdd77b 100644 --- a/homeassistant/www/audi-dashboard-app.js +++ b/homeassistant/www/audi-dashboard-app.js @@ -3382,7 +3382,10 @@ function ereignisseVerdrahten() { const bk = e.target.closest("[data-backup]"); if (bk && bk.dataset.backup === "jetzt") { serviceRufen("audi_dashboard_backup_jetzt", {}); return; } if (bk && bk.dataset.backup === "export") { - const paket = { erstellt: new Date().toISOString(), profil: PROFIL_ROH, fahrten: TRIPS, tankvorgaenge: FILLS }; + // Die Sensor-Zuordnung gehört mit ins Backup: sie liegt sonst nur in + // entitaeten.json und wäre nach einer Wiederherstellung verloren. + const paket = { erstellt: new Date().toISOString(), profil: PROFIL_ROH, fahrten: TRIPS, tankvorgaenge: FILLS, + entitaeten: (ENTITAETEN && ENTITAETEN.werte) || null }; const a = document.createElement("a"); a.href = "data:application/json;charset=utf-8," + encodeURIComponent(JSON.stringify(paket, null, 2)); a.download = `backup_${new Date().toISOString().slice(0, 10)}.json`; document.body.appendChild(a); a.click(); a.remove(); @@ -3522,6 +3525,9 @@ function ereignisseVerdrahten() { profil_json: JSON.stringify(paket.profil), fahrten_json: JSON.stringify(TRIPS), tankvorgaenge_json: JSON.stringify(FILLS), + // Fehlt der Abschnitt (Sicherung von vor dem Setup-Menü), bleibt die + // aktuelle Zuordnung stehen, statt geleert zu werden. + ...(paket.entitaeten ? { entitaeten_json: JSON.stringify(paket.entitaeten) } : {}), }); bild = 0; render(); } catch (err) { hinweis("Die Datei ließ sich nicht lesen", err.message); }