39da68cd87
Leaflet's stylesheet was appended to document.head, so it never reached the
panel's shadow root: .leaflet-tile{position:absolute} never applied, tiles laid
out as in-flow images (~1040px inside a 190px box) and the marker pane ended up
far below the visible area. That is the real cause of the long-standing
"fragmented Leaflet rendering" finding and of the invisible vehicle pin - every
tile request had actually succeeded. The stylesheet now goes into the shadow
root and is awaited before the map is built.
Also in this round:
- Map tiles are always light (Google-Maps-style), no dark variant at night.
- Vehicle marker uses the real CI poi-car icons (poi-car-l >=34px, poi-car-s
below), with a white halo so the outline stays readable on tiles.
- Removed the obsolete combined STANDORT_TRACKER field; only the split
lat/lon sensors remain.
- Receipt upload: widened the try block so base64/save failures surface, and
the frontend call site now reports a rejected service call.
- Generic receipt parser: total detection is line-based (letter-spaced
headings, no more matching the SUMME-EUR column header, tax lines excluded),
address heuristic handles 4-digit postcodes and single-line address blocks,
and "Preis/L" matches without a spelled-out "Liter".
- Design backlog: red hairline frame on list rows (delete button bled through
at fractional row heights) and select fields now use the grey background box.
Shell 10-receipt regression suite still passes; all changes verified live in
the audi_ha_test container.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
246 lines
14 KiB
Python
246 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": "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": "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_LAT_SENSOR", "label": "GPS-Standort: Breitengrad", "gruppe": "standort",
|
|
"hinweis": "Breitengrad als eigener Sensor (z. B. flespi).",
|
|
"domains": ["sensor"], "device_classes": [], "units": [], "liste": False, "pflicht": False,
|
|
"stichworte": ["breitengrad", "latitude", "lat", "standort", "gps", "position", "ortung"]},
|
|
{"key": "STANDORT_LON_SENSOR", "label": "GPS-Standort: Längengrad", "gruppe": "standort",
|
|
"hinweis": "Längengrad als eigener Sensor (z. B. flespi).",
|
|
"domains": ["sensor"], "device_classes": [], "units": [], "liste": False, "pflicht": False,
|
|
"stichworte": ["längengrad", "longitude", "lon", "lng", "standort", "gps", "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}
|