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); }