Files
audi-app/homeassistant/pyscript/modules/entitaeten.py
T
tobias 9794193803 Setup-Menü für Sensor-Zuordnung + Umstellung von WLAN/VAG-Integration auf FMM003
Setup-Menü (Einstellungen -> Fahrzeug einrichten -> Setup): ordnet alle von
der App genutzten Sensor-Rollen echten HA-Entitäten zu, statt sie in
einstellungen.py von Hand einzutragen - durchsuchbares Dropdown je Feld,
Vorauswahl aus vorhandenen Entitäten, Schalter für "nur passende Sensoren".
Neues Modul entitaeten.py (Katalog + JSON-Override, zur Laufzeit über
setattr() auf einstellungen angewendet, kein Neustart nötig außer für die
drei trigger-gebundenen Felder).

Zusätzlich: Fahrzeug-WLAN-Erkennung und alle Entity-IDs der nicht mehr
genutzten TommiG1/HA_VAG-EU-Data-Act-Integration entfernt. Fahrterkennung
läuft jetzt über den Zündungs-/ACC-Status des neu angebundenen Teltonika
FMM003 (ersetzt die WLAN-Verbindungserkennung); Standort und 12V-
Batteriespannung kommen ebenfalls vom FMM003. Kilometerstand, Tankfüllstand,
Reichweite, Türen/Fenster/Schlösser sowie Ölwechsel-/Inspektionsdaten haben
dadurch vorerst keine Quelle mehr und zeigen "unbekannt" - die Funktionen
selbst bleiben erhalten und lassen sich über das neue Setup-Menü jederzeit
neu zuordnen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 15:55:59 +02:00

189 lines
11 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}
# --------------------------------------------------------------- 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():
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 {}
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 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)."""
overrides = overrides_lesen()
for key, wert in overrides.items():
if key not in _SCHLUESSEL:
continue
if wert in (None, "", []):
continue
setattr(einstellungen, key, 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}