64f457dd87
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.
250 lines
14 KiB
Python
250 lines
14 KiB
Python
"""Sensor-zu-Entität-Zuordnung fürs Setup-Menü (Einstellungen -> Fahrzeug
|
|
einrichten -> Setup).
|
|
|
|
Ersetzt NICHT das manuelle Eintragen in einstellungen.py (dessen Kommentare
|
|
sind die eigentliche Dokumentation der Integration und bleiben unangetastet),
|
|
sondern legt eine zusätzliche, zur Laufzeit angewendete Override-Ebene
|
|
darüber: data/entitaeten.json enthält nur die vom Nutzer über das Setup-Menü
|
|
geänderten Zuordnungen, keyed nach den einstellungen.py-Variablennamen.
|
|
|
|
Warum das funktioniert, ohne jede Verbraucher-Datei anzufassen: Alle
|
|
bestehenden Leser (frontend_veroeffentlichung.py, fahrterkennung.py, ...)
|
|
machen `import einstellungen` und lesen dann `einstellungen.KM_SENSOR` -
|
|
ein lebendiger Attributzugriff auf das Modul-Objekt, kein `from einstellungen
|
|
import KM_SENSOR` (das würde den Wert beim Import einfrieren). overrides_
|
|
anwenden() unten nutzt genau das: setattr(einstellungen, key, wert) ändert
|
|
das Attribut auf dem bereits importierten Modul-Objekt, jeder spätere
|
|
Zugriff `einstellungen.KM_SENSOR` liefert sofort den neuen Wert.
|
|
|
|
Eine Ausnahme (siehe TRIGGER_GEBUNDEN unten): mehrere @state_trigger(f"...")-
|
|
Dekoratoren backen ZUENDUNG_SENSOR/KM_SENSOR/TANK_SENSOR einmalig beim
|
|
Modul-Laden in die Trigger-Registrierung ein. Eine Änderung dieser drei
|
|
Felder wirkt für die Fahrterkennung selbst erst nach einem Neustart von
|
|
Home Assistant (bzw. pyscript.reload) - das Setup-Menü weist deshalb bei
|
|
diesen drei Feldern ausdrücklich darauf hin, statt automatisch und riskant
|
|
einen Selbst-Reload aus einer laufenden pyscript-Funktion heraus zu
|
|
versuchen.
|
|
"""
|
|
|
|
import io
|
|
import json
|
|
import os
|
|
|
|
import einstellungen
|
|
|
|
BASIS = "/config/audi_dashboard"
|
|
OVERRIDES_PFAD = f"{BASIS}/entitaeten.json"
|
|
|
|
# Vier gleich aufgebaute Positionen, wie schon in frontend_veroeffentlichung.py
|
|
# (dort POSITIONEN genannt) - hier dupliziert statt importiert, damit
|
|
# entitaeten.py nicht von frontend_veroeffentlichung.py abhängt (wäre die
|
|
# falsche Richtung: die Veröffentlichung importiert diese Datei, nicht
|
|
# umgekehrt).
|
|
POSITIONEN = ["vorne links", "vorne rechts", "hinten links", "hinten rechts"]
|
|
|
|
# Katalog aller von der App genutzten Entity-Rollen, fürs Setup-Menü im
|
|
# Frontend. Reihenfolge = Anzeige-Reihenfolge innerhalb jeder Gruppe.
|
|
# domains/device_classes/units sind Erwartungen fürs Frontend-seitige
|
|
# Filtern+Vorschlagen (Schalter "Nur passende Sensoren anzeigen") - eine
|
|
# fehlende oder leere Liste bedeutet "keine Einschränkung" bzw. "diese Rolle
|
|
# hat üblicherweise keine Einheit" (binary_sensor/button/device_tracker).
|
|
FELDER = [
|
|
{"key": "ZUENDUNG_SENSOR", "label": "Zündung/ACC-Status", "gruppe": "fahrterkennung",
|
|
"hinweis": "on = Fahrt läuft, erkennt Fahrtbeginn/-ende (ersetzt die frühere WLAN-Erkennung).",
|
|
"domains": ["binary_sensor"], "device_classes": [], "units": [], "liste": False, "pflicht": True,
|
|
"stichworte": ["zündung", "ignition", "acc", "motor", "engine"]},
|
|
{"key": "KM_SENSOR", "label": "Kilometerstand", "gruppe": "fahrterkennung",
|
|
"hinweis": "Gesamt-Kilometerstand des Fahrzeugs - für Fahrtabschluss-Screening, Reifenzähler und Ölwechsel-Prognose.",
|
|
"domains": ["sensor"], "device_classes": ["distance"], "units": ["km", "mi"], "liste": False, "pflicht": False,
|
|
"stichworte": ["kilometer", "km", "mileage", "odometer", "tacho", "laufleistung"]},
|
|
{"key": "TANK_SENSOR", "label": "Tankfüllstand", "gruppe": "fahrterkennung",
|
|
"hinweis": "Füllstand in Prozent - für die automatische Tankerkennung.",
|
|
"domains": ["sensor"], "device_classes": [], "units": ["%"], "liste": False, "pflicht": False,
|
|
"stichworte": ["tank", "fuel", "kraftstoff", "füllstand", "level"]},
|
|
{"key": "RANGE_SENSOR", "label": "Reichweite", "gruppe": "uebersicht",
|
|
"hinweis": "Für die Übersicht - bleibt leer, zeigt die Oberfläche \"unbekannt\".",
|
|
"domains": ["sensor"], "device_classes": ["distance"], "units": ["km", "mi"], "liste": False, "pflicht": False,
|
|
"stichworte": ["reichweite", "range"]},
|
|
{"key": "BATTERIE_SENSOR", "label": "12V-Batteriespannung", "gruppe": "uebersicht",
|
|
"hinweis": "Nicht jede Integration liefert diesen Wert - bleibt er leer, zeigt \"Mein Audi\" \"unbekannt\".",
|
|
"domains": ["sensor"], "device_classes": ["voltage"], "units": ["V"], "liste": False, "pflicht": False,
|
|
"stichworte": ["batterie", "battery", "spannung", "volt", "12v"]},
|
|
{"key": "REFRESH_BUTTON", "label": "Sofort-Aktualisierung (Knopf)", "gruppe": "uebersicht",
|
|
"hinweis": "Der von der Integration bereitgestellte Knopf für \"Ziehen zum Aktualisieren\".",
|
|
"domains": ["button"], "device_classes": [], "units": [], "liste": False, "pflicht": False,
|
|
"stichworte": ["refresh", "aktualisieren", "update", "abrufen", "poll"]},
|
|
{"key": "TUER_SENSOREN", "label": "Türen", "gruppe": "sicherheit",
|
|
"hinweis": "\"aus\"/off = zu. Für \"Sicher abgestellt\" wichtig.",
|
|
"domains": ["binary_sensor"], "device_classes": ["door"], "units": [], "liste": True, "positionen": POSITIONEN,
|
|
"pflicht": False, "stichworte": ["tür", "door"]},
|
|
{"key": "FENSTER_SENSOREN", "label": "Fenster", "gruppe": "sicherheit",
|
|
"hinweis": "\"aus\"/off = zu.",
|
|
"domains": ["binary_sensor"], "device_classes": ["window"], "units": [], "liste": True, "positionen": POSITIONEN,
|
|
"pflicht": False, "stichworte": ["fenster", "window"]},
|
|
{"key": "TUERSCHLOSS_SENSOREN", "label": "Türschlösser", "gruppe": "sicherheit",
|
|
"hinweis": "\"on\" = entriegelt, \"aus\"/off = verriegelt (device_class \"lock\"-Konvention).",
|
|
"domains": ["binary_sensor"], "device_classes": ["lock"], "units": [], "liste": True, "positionen": POSITIONEN,
|
|
"pflicht": False, "stichworte": ["schloss", "lock", "verriegel", "entriegel"]},
|
|
{"key": "HECKKLAPPE_SENSOR", "label": "Heckklappe", "gruppe": "sicherheit",
|
|
"hinweis": "\"aus\"/off = zu.",
|
|
"domains": ["binary_sensor"], "device_classes": ["door", "opening"], "units": [], "liste": False, "pflicht": False,
|
|
"stichworte": ["heckklappe", "tailgate", "kofferraum", "trunk"]},
|
|
{"key": "HECKKLAPPENSCHLOSS_SENSOR", "label": "Heckklappenschloss", "gruppe": "sicherheit",
|
|
"hinweis": "\"on\" = entriegelt, \"aus\"/off = verriegelt.",
|
|
"domains": ["binary_sensor"], "device_classes": ["lock"], "units": [], "liste": False, "pflicht": False,
|
|
"stichworte": ["heckklappe", "tailgate", "schloss", "lock"]},
|
|
{"key": "HAUBE_SENSOR", "label": "Motorhaube", "gruppe": "sicherheit",
|
|
"hinweis": "\"aus\"/off = zu.",
|
|
"domains": ["binary_sensor"], "device_classes": ["door", "opening"], "units": [], "liste": False, "pflicht": False,
|
|
"stichworte": ["haube", "hood", "bonnet", "motorhaube"]},
|
|
{"key": "HAUBENSCHLOSS_SENSOR", "label": "Haubenschloss", "gruppe": "sicherheit",
|
|
"hinweis": "\"on\" = entriegelt, \"aus\"/off = verriegelt.",
|
|
"domains": ["binary_sensor"], "device_classes": ["lock"], "units": [], "liste": False, "pflicht": False,
|
|
"stichworte": ["haube", "hood", "schloss", "lock"]},
|
|
{"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,
|
|
"stichworte": ["ölwechsel", "oil", "service", "change"]},
|
|
{"key": "OELWECHSEL_STRECKE_SENSOR", "label": "Nächster Ölwechsel (Restkilometer)", "gruppe": "uebersicht",
|
|
"hinweis": "Meldet die Integration oft als negativen Countdown - die App zeigt nur den Betrag.",
|
|
"domains": ["sensor"], "device_classes": ["distance"], "units": ["km", "mi"], "liste": False, "pflicht": False,
|
|
"stichworte": ["ölwechsel", "oil", "distance", "strecke"]},
|
|
{"key": "NAECHSTE_INSPEKTION_SENSOR", "label": "Nächste Inspektion (Datum)", "gruppe": "uebersicht",
|
|
"hinweis": "Vom Fahrzeug selbst gemeldete Fälligkeit.",
|
|
"domains": ["sensor"], "device_classes": ["date", "timestamp"], "units": [], "liste": False, "pflicht": False,
|
|
"stichworte": ["inspektion", "inspection", "service"]},
|
|
{"key": "INSPEKTION_STRECKE_SENSOR", "label": "Nächste Inspektion (Restkilometer)", "gruppe": "uebersicht",
|
|
"hinweis": "Meldet die Integration oft als negativen Countdown - die App zeigt nur den Betrag.",
|
|
"domains": ["sensor"], "device_classes": ["distance"], "units": ["km", "mi"], "liste": False, "pflicht": False,
|
|
"stichworte": ["inspektion", "inspection", "distance", "strecke"]},
|
|
{"key": "STANDORT_TRACKER", "label": "GPS-Standort", "gruppe": "standort",
|
|
"hinweis": "Braucht latitude/longitude-Attribute (HA-Konvention für device_tracker).",
|
|
"domains": ["device_tracker"], "device_classes": [], "units": [], "liste": False, "pflicht": False,
|
|
"stichworte": ["standort", "gps", "tracker", "position", "ortung"]},
|
|
]
|
|
|
|
# Mehrere @state_trigger(f"...")-Dekoratoren (fahrterkennung.py,
|
|
# fahrtabschluss.py, tankerkennung.py, reifenzaehler.py) backen diese drei
|
|
# Felder beim Modul-Laden ein (siehe Kopfkommentar) - im Setup-Menü als
|
|
# "wirkt erst nach Neustart"-Hinweis markiert.
|
|
_TRIGGER_GEBUNDEN = {"ZUENDUNG_SENSOR", "KM_SENSOR", "TANK_SENSOR"}
|
|
for _feld in FELDER:
|
|
_feld["trigger_gebunden"] = _feld["key"] in _TRIGGER_GEBUNDEN
|
|
|
|
_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
|
|
# über task.executor (pyscript-Einschränkung, siehe Kopfkommentar dort),
|
|
# 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()
|
|
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):
|
|
"""Schreibt nur bekannte Schlüssel (siehe FELDER); unbekannte werden
|
|
stillschweigend verworfen, damit ein späterer Katalog-Umbau keine
|
|
Datenleichen in entitaeten.json hinterlässt."""
|
|
bereinigt = {k: v for k, v in mapping.items() if k in _SCHLUESSEL}
|
|
os.makedirs(BASIS, exist_ok=True)
|
|
tmp = OVERRIDES_PFAD + ".tmp"
|
|
text = json.dumps(bereinigt, ensure_ascii=False, indent=2)
|
|
f = task.executor(io.open, tmp, "w")
|
|
f.write(text)
|
|
f.close()
|
|
os.replace(tmp, OVERRIDES_PFAD)
|
|
|
|
|
|
# ------------------------------------------------------- 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():
|
|
"""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 in _SCHLUESSEL:
|
|
wert = overrides.get(key)
|
|
setattr(einstellungen, key, _STANDARDWERTE[key] if _ist_leer(wert) else wert)
|
|
|
|
|
|
def aktueller_stand():
|
|
"""Katalog + aktuell wirksamer Wert je Feld (aus dem einstellungen-Modul
|
|
selbst gelesen, nicht direkt aus der JSON-Datei) - zeigt damit sowohl
|
|
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, "standardwerte": _STANDARDWERTE}
|