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 <[email protected]>
This commit is contained in:
2026-08-12 15:55:59 +02:00
co-authored by Claude Sonnet 5
parent 7e979ecfab
commit 9794193803
13 changed files with 848 additions and 197 deletions
+9 -5
View File
@@ -13,11 +13,15 @@ import einstellungen
import fahrtabschluss_logik
@state_trigger(f"{einstellungen.KM_SENSOR}")
def kilometerstand_geaendert(value=None, old_value=None):
if value is None or old_value is None:
return
fahrtabschluss_logik.screening_durchfuehren(einstellungen.KM_SENSOR)
# Ohne konfigurierten Kilometerstand-Sensor keinen Trigger registrieren -
# eine leere Entity-ID als @state_trigger-Ausdruck ist ungetestetes
# Verhalten, siehe gleiches Muster in fahrterkennung.py.
if einstellungen.KM_SENSOR:
@state_trigger(f"{einstellungen.KM_SENSOR}")
def kilometerstand_geaendert(value=None, old_value=None):
if value is None or old_value is None:
return
fahrtabschluss_logik.screening_durchfuehren(einstellungen.KM_SENSOR)
@service
+47 -44
View File
@@ -1,17 +1,25 @@
"""Fahrterkennung über die WLAN-Verbindung zum Fahrzeug (§7.1).
"""Fahrterkennung über den Zündungs-/ACC-Status des FMM003 (§7.1).
Start: der WLAN-Sensor der Companion App meldet den in den Einstellungen
hinterlegten Fahrzeug-WLAN-Namen (Fahrzeugprofil, Feld fahrzeug.wlan_name).
Ende: die Verbindung bricht ab und kommt binnen der Pausenzeit nicht zurück.
Start: die Zündungs-Entität (einstellungen.ZUENDUNG_SENSOR) wechselt auf
"on". Ende: sie wechselt auf "off" und kommt binnen der Pausenzeit nicht
zurück.
Die Pausenregel wird über task.unique() umgesetzt: jede neue Änderung des
WLAN-Sensors killt eine noch laufende, wartende Bestätigung des vorherigen
Ereignisses. Kommt die Verbindung während der Wartezeit zurück, wird dadurch
die wartende Ende-Bestätigung automatisch abgebrochen, ohne dass die Fahrt
beendet wird - das entspricht der Pausenregel aus §7.1.
2026-08-12: ersetzt die frühere WLAN-basierte Erkennung (Vergleich des vom
iPhone gemeldeten verbundenen WLANs gegen den im Fahrzeugprofil hinterlegten
Namen) - die Zündung ist ein direkteres, von jeder Companion-App-Eigenheit
unabhängiges Signal.
Die Pausenregel wird über task.unique() umgesetzt: jede neue Änderung der
Zündungs-Entität killt eine noch laufende, wartende Bestätigung des
vorherigen Ereignisses. Kommt die Zündung während der Wartezeit zurück, wird
dadurch die wartende Ende-Bestätigung automatisch abgebrochen, ohne dass die
Fahrt beendet wird - das entspricht der Pausenregel aus §7.1.
Die Entity-IDs stehen zentral in pyscript/modules/einstellungen.py - dort vor
der Installation einmalig anpassen (siehe INSTALL.md).
der Installation einmalig anpassen (siehe INSTALL.md), oder über das
Setup-Menü in der Oberfläche zuordnen (siehe entitaeten.py; Änderungen dort
wirken für DIESEN Trigger allerdings erst nach einem Neustart, da
@state_trigger die Entity-ID einmalig beim Modul-Laden einliest).
EINSCHRÄNKUNG: der Zustand einer laufenden Fahrt (_fahrt_start_ts) lebt nur
im Arbeitsspeicher dieses Skripts. Ein HA-Neustart mitten in einer Fahrt oder
@@ -32,11 +40,6 @@ import profil
_fahrt_start_ts = None
def _profil_wlan_name():
p = profil.profil_lesen()
return p.get("fahrzeug", {}).get("wlan_name")
def _pausenzeit_sekunden():
p = profil.profil_lesen()
return p.get("einstellungen", {}).get("fahrten_pausenzeit_min", 15) * 60
@@ -47,39 +50,39 @@ def beim_start():
profil.ordner_sicherstellen()
@state_trigger(f"{einstellungen.WLAN_SENSOR}")
def wlan_verbindung_geaendert(value=None, old_value=None):
# Killt eine noch laufende Ende-Bestätigung aus einer vorherigen
# Änderung - das ist der Mechanismus hinter der Pausenregel.
task.unique("audi_dashboard_fahrt_ende_pruefung")
# Ohne konfigurierte Zündungs-Entität keinen Trigger registrieren - eine
# leere Entity-ID als @state_trigger-Ausdruck ist ungetestetes Verhalten
# (siehe zustand_oder_none()-Kommentar in frontend_veroeffentlichung.py zur
# Grundhaltung "kein geratener Wert"), hier stattdessen ganz weggelassen.
if einstellungen.ZUENDUNG_SENSOR:
@state_trigger(f"{einstellungen.ZUENDUNG_SENSOR}")
def zuendung_geaendert(value=None, old_value=None):
# Killt eine noch laufende Ende-Bestätigung aus einer vorherigen
# Änderung - das ist der Mechanismus hinter der Pausenregel.
task.unique("audi_dashboard_fahrt_ende_pruefung")
ziel = _profil_wlan_name()
if not ziel:
log.warning("audi_dashboard: kein fahrzeug.wlan_name im Fahrzeugprofil hinterlegt")
return
global _fahrt_start_ts
an_jetzt = (value == "on")
an_vorher = (old_value == "on")
global _fahrt_start_ts
verbunden_jetzt = (value == ziel)
verbunden_vorher = (old_value == ziel)
if an_jetzt and not an_vorher:
if _fahrt_start_ts is None:
_fahrt_start_ts = datetime.datetime.now(datetime.timezone.utc)
log.info(f"audi_dashboard: Fahrt gestartet um {_fahrt_start_ts.isoformat()}")
return
if verbunden_jetzt and not verbunden_vorher:
if _fahrt_start_ts is None:
_fahrt_start_ts = datetime.datetime.now(datetime.timezone.utc)
log.info(f"audi_dashboard: Fahrt gestartet um {_fahrt_start_ts.isoformat()}")
return
if an_vorher and not an_jetzt and _fahrt_start_ts is not None:
start_ts = _fahrt_start_ts
abbruch_ts = datetime.datetime.now(datetime.timezone.utc)
wartezeit_s = _pausenzeit_sekunden()
if verbunden_vorher and not verbunden_jetzt and _fahrt_start_ts is not None:
start_ts = _fahrt_start_ts
abbruch_ts = datetime.datetime.now(datetime.timezone.utc)
wartezeit_s = _pausenzeit_sekunden()
# task.unique() oben markiert diese Ausführung als die aktuell
# gültige. Kommt die Zündung vor Ablauf der Wartezeit zurück,
# ruft der nächste Trigger erneut task.unique() auf und killt
# genau diesen task.sleep() - der Code danach läuft dann nie.
task.sleep(wartezeit_s)
# task.unique() oben markiert diese Ausführung als die aktuell
# gültige. Kommt die Verbindung vor Ablauf der Wartezeit zurück,
# ruft der nächste Trigger erneut task.unique() auf und killt genau
# diesen task.sleep() - der Code danach läuft dann nie.
task.sleep(wartezeit_s)
_fahrt_beenden(start_ts, abbruch_ts)
_fahrt_beenden(start_ts, abbruch_ts)
def _fahrt_beenden(start_ts, ende_ts):
@@ -120,7 +123,7 @@ def _fahrt_beenden(start_ts, ende_ts):
@service
def audi_dashboard_fahrt_manuell_anlegen(ts_start=None, ts_end=None, art="privat"):
"""Manuelle Fahrterfassung aus der Oberfläche (§5.3), unabhängig von der
automatischen WLAN-Erkennung. Aufruf als pyscript.audi_dashboard_fahrt_manuell_anlegen."""
automatischen Zündungs-Erkennung. Aufruf als pyscript.audi_dashboard_fahrt_manuell_anlegen."""
start_ts = datetime.datetime.fromisoformat(ts_start)
ende_ts = datetime.datetime.fromisoformat(ts_end)
dauer_s = int((ende_ts - start_ts).total_seconds())
+24
View File
@@ -19,6 +19,7 @@ statt für jedes einzelne Feld einen eigenen Service zu brauchen.
import json
import einstellungen
import entitaeten
import frontend_veroeffentlichung
import profil
@@ -26,6 +27,10 @@ import profil
@time_trigger("startup")
def beim_start():
profil.ordner_sicherstellen()
# Vor der ersten Veröffentlichung anwenden, damit sie die im Setup-Menü
# gespeicherten Zuordnungen (data/entitaeten.json) schon zeigt, siehe
# entitaeten.py-Kopfkommentar.
entitaeten.overrides_anwenden()
frontend_veroeffentlichung.alles_veroeffentlichen()
@@ -73,3 +78,22 @@ def audi_dashboard_profil_schreiben(profil_json=None):
profil.profil_schreiben(p)
frontend_veroeffentlichung.profil_veroeffentlichen()
log.info("audi_dashboard: Profil aktualisiert")
@service
def audi_dashboard_entitaeten_schreiben(zuordnung_json=None):
"""Speichert die im Setup-Menü (Einstellungen -> Fahrzeug einrichten ->
Setup) gewählte Sensor-zu-Entität-Zuordnung, wendet sie sofort an und
veröffentlicht Katalog+Status neu, damit Übersicht/Mein Audi ohne
Neustart die neuen Werte zeigen (Ausnahme: die drei trigger-gebundenen
Felder, siehe entitaeten.py). Aufruf als
pyscript.audi_dashboard_entitaeten_schreiben."""
if not zuordnung_json:
log.warning("audi_dashboard: entitaeten_schreiben ohne zuordnung_json aufgerufen")
return
mapping = json.loads(zuordnung_json) if isinstance(zuordnung_json, str) else zuordnung_json
entitaeten.overrides_schreiben(mapping)
entitaeten.overrides_anwenden()
frontend_veroeffentlichung.entitaeten_veroeffentlichen()
frontend_veroeffentlichung.fahrzeugstatus_veroeffentlichen()
log.info("audi_dashboard: Entitäten-Zuordnung aktualisiert")
+62 -83
View File
@@ -7,106 +7,85 @@ des Fahrzeugprofils (data/fahrzeugprofil.json) und über die Oberfläche
Fahrzeugstatus-Abfrage brauchen sie als festen Wert, bevor überhaupt ein
Profil gelesen werden kann.
In Home Assistant unter Entwicklerwerkzeuge -> Zustände nachschlagen.
In Home Assistant unter Entwicklerwerkzeuge -> Zustände nachschlagen. Werte
lassen sich außerdem über das Setup-Menü in der Oberfläche zuordnen (siehe
entitaeten.py) - Änderungen von dort werden zur Laufzeit auf diese Variablen
angewendet (überschreiben also die hier hinterlegten Standardwerte), ohne
diese Datei anzufassen.
Die drei ersten (WLAN, Kilometerstand, Tankfüllstand) werden vom Backend
zwingend gebraucht (§7.1, §7.2, §7.4, §7.5). Alles danach speist nur die
Übersicht im Frontend (§5.1, §5.2, §9) — bleibt ein Wert leer ("") oder passt
eine Entity-ID nicht zur tatsächlichen Integration, liefert
zustand_oder_none() in frontend_veroeffentlichung.py für das jeweilige Feld
None statt abzustürzen; die Oberfläche zeigt dann „unbekannt" statt eines
falschen Werts.
Alle Entity-IDs unten sind an der echten Integration TommiG1/HA_VAG-EU-Data-Act
(Version 0.6.35, per HACS installiert) real geprüft, nicht nur vermutet: das
Gerät heißt dort "Audi RS 4 Avant" (mit Leerzeichen vor der "4") - deshalb
"rs_4" mit Unterstrich im Slug, nicht "rs4". Eine ältere Integrationsversion
hatte hier noch deutsche Feldnamen (z. B. "..._tur_vorne_links"); die aktuelle
Version benennt englisch (z. B. "..._front_left_door").
2026-08-12: Die HACS-Integration TommiG1/HA_VAG-EU-Data-Act (bisherige
Quelle für Kilometerstand, Tankfüllstand, Türen/Fenster/Schlösser,
Ölwechsel-/Inspektionsdaten) wird nicht mehr verwendet - die zugehörigen
Entity-IDs sind deshalb unten bewusst leer. Neue Datenquelle ist der
Teltonika FMM003 (GPS-Tracker mit CAN-Anbindung, siehe AGENTS.md Abschnitt
B); er liefert Standort, Zündungsstatus und Batteriespannung, aber keinen
Tankfüllstand, keine Tür-/Fenster-/Schlossdaten und keine Ölwechsel-/
Inspektionstermine - die entsprechenden Kacheln zeigen deshalb bis auf
Weiteres "unbekannt" statt eines falschen Werts (siehe zustand_oder_none()
in frontend_veroeffentlichung.py). Bleibt ein Wert leer ("") oder passt eine
Entity-ID nicht zur tatsächlichen Integration, liefert zustand_oder_none()
für das jeweilige Feld None statt abzustürzen.
"""
# Companion-App-Sensor für das aktuell verbundene WLAN (iOS-App:
# Einstellungen -> Companion App -> Sensoren -> "Verbundenes WLAN")
WLAN_SENSOR = "sensor.iphone_wifi_connection"
# Fahrterkennung (§7.1): Start/Ende einer Fahrt wird über den Zündungs-/ACC-
# Status des FMM003 erkannt (on = Fahrt läuft), nicht mehr über die WLAN-
# Verbindung des iPhones zum Fahrzeug (siehe fahrterkennung.py).
ZUENDUNG_SENSOR = "binary_sensor.testzone_fmm003_engine_ignition_or_acc_status"
# Kilometerstand aus der HACS-Integration TommiG1/HA_VAG-EU-Data-Act
KM_SENSOR = "sensor.audi_rs_4_avant_mileage"
# Kilometerstand - bisher aus der TommiG1-Integration, aktuell keine Quelle
# vorhanden. Der FMM003 liefert unter sensor.testzone_fmm003_total_calculated_mileage
# einen selbst berechneten Wert, der aber auf einer anderen Zählbasis beruht
# als der echte Fahrzeug-Kilometerstand (GPS-Streckenberechnung statt
# Tacho) - bewusst NICHT automatisch übernommen, um Reifenzähler,
# Ölwechsel-Prognose und Fahrtabschluss-Screening nicht mit einem
# inkonsistenten Basiswert zu verfälschen. Bei Bedarf über das Setup-Menü
# gezielt zuordnen.
KM_SENSOR = ""
# Tankfüllstand aus derselben Integration (Prozent)
TANK_SENSOR = "sensor.audi_rs_4_avant_fuel_level"
# Tankfüllstand (Prozent) - keine Quelle mehr vorhanden (der FMM003 ist kein
# Tankgeber).
TANK_SENSOR = ""
# Reichweite (§5.1 Übersicht)
RANGE_SENSOR = "sensor.audi_rs_4_avant_range_primary"
# Reichweite (§5.1 Übersicht) - keine Quelle mehr vorhanden.
RANGE_SENSOR = ""
# 12V-Batteriespannung (Mein Audi -> Zustand). Die aktuell genutzte
# HACS-Integration (TommiG1/HA_VAG-EU-Data-Act 0.6.35) liefert dafür KEINEN
# Sensor (real geprüft: kein "battery"/"voltage"-Entity unter diesem Gerät) -
# deshalb hier bewusst leer gelassen. Liefert später eine andere Quelle (z. B.
# ein OBD-Adapter) eine Spannung, einfach die Entity-ID eintragen; bis dahin
# zeigt die Oberfläche "unbekannt" statt eines falschen Werts (siehe
# zustand_oder_none() in frontend_veroeffentlichung.py).
BATTERIE_SENSOR = ""
# 12V-Batteriespannung (Mein Audi -> Zustand). Vom FMM003 geliefert -
# external_power_voltage ist die vom Gerät gemessene Bordnetzspannung des
# Fahrzeugs, NICHT battery_voltage (das ist die interne Pufferbatterie des
# Trackers selbst und hat mit der Fahrzeugbatterie nichts zu tun).
BATTERIE_SENSOR = "sensor.testzone_fmm003_external_power_voltage"
# Der von der Integration selbst bereitgestellte Knopf für eine sofortige
# Neuabfrage beim Fahrzeug (Entity-Domäne "button", real geprüft) - genau der
# Weg, den "Ziehen zum Aktualisieren" im Frontend auslöst (siehe
# audi_dashboard_jetzt_aktualisieren() in frontend_api.py), statt eines
# generischen homeassistant.update_entity auf einzelne Sensoren.
REFRESH_BUTTON = "button.audi_rs_4_avant_refresh_now"
# Knopf für eine sofortige Neuabfrage beim Fahrzeug - kam aus der
# TommiG1-Integration, keine Entsprechung beim FMM003 vorhanden.
REFRESH_BUTTON = ""
# Türen (§4.1) - die Integration liefert vier Einzelsensoren statt eines
# Summensensors. binary_sensor, device_class "door": "off" = zu (real
# geprüft).
TUER_SENSOREN = [
"binary_sensor.audi_rs_4_avant_front_left_door",
"binary_sensor.audi_rs_4_avant_front_right_door",
"binary_sensor.audi_rs_4_avant_rear_left_door",
"binary_sensor.audi_rs_4_avant_rear_right_door",
]
# Türen (§4.1) - keine Quelle mehr vorhanden.
TUER_SENSOREN = []
# Fenster (§4.1) - device_class "window": "off" = zu (real geprüft)
FENSTER_SENSOREN = [
"binary_sensor.audi_rs_4_avant_front_left_window",
"binary_sensor.audi_rs_4_avant_front_right_window",
"binary_sensor.audi_rs_4_avant_rear_left_window",
"binary_sensor.audi_rs_4_avant_rear_right_window",
]
# Fenster (§4.1) - keine Quelle mehr vorhanden.
FENSTER_SENSOREN = []
# Verriegelung - kein lock.*-Entity vorhanden, sondern vier
# Türschloss-Einzelsensoren. binary_sensor mit device_class "lock" (real
# geprüft): "on" = entriegelt, "off" = verriegelt (Home-Assistant-Konvention
# für diese device_class).
TUERSCHLOSS_SENSOREN = [
"binary_sensor.audi_rs_4_avant_front_left_door_lock",
"binary_sensor.audi_rs_4_avant_front_right_door_lock",
"binary_sensor.audi_rs_4_avant_rear_left_door_lock",
"binary_sensor.audi_rs_4_avant_rear_right_door_lock",
]
# Verriegelung - keine Quelle mehr vorhanden.
TUERSCHLOSS_SENSOREN = []
# Heckklappe und Motorhaube - von der Integration separat von den vier Türen
# geführt, für "Sicher abgestellt" (§9) aber ebenso relevant wie diese
HECKKLAPPE_SENSOR = "binary_sensor.audi_rs_4_avant_tailgate"
HECKKLAPPENSCHLOSS_SENSOR = "binary_sensor.audi_rs_4_avant_tailgate_lock"
HAUBE_SENSOR = "binary_sensor.audi_rs_4_avant_hood"
HAUBENSCHLOSS_SENSOR = "binary_sensor.audi_rs_4_avant_hood_lock"
# Heckklappe und Motorhaube - keine Quelle mehr vorhanden.
HECKKLAPPE_SENSOR = ""
HECKKLAPPENSCHLOSS_SENSOR = ""
HAUBE_SENSOR = ""
HAUBENSCHLOSS_SENSOR = ""
# Vom Fahrzeug selbst gemeldete Service-Fälligkeit (ergänzt die App-eigene,
# aus dem Servicebuch berechnete Prognose - siehe fahrzeugMeldung() im
# Frontend). Je ein Datum (ISO mit Zeitzone) und eine Streckenangabe in km;
# die Streckensensoren liefern negative Werte (Restkilometer als Countdown),
# im Frontend wird davon nur der Betrag angezeigt.
NAECHSTER_OELWECHSEL_SENSOR = "sensor.audi_rs_4_avant_oil_change_due"
OELWECHSEL_STRECKE_SENSOR = "sensor.audi_rs_4_avant_oil_change_distance"
NAECHSTE_INSPEKTION_SENSOR = "sensor.audi_rs_4_avant_inspection_due"
INSPEKTION_STRECKE_SENSOR = "sensor.audi_rs_4_avant_inspection_distance"
# aus dem Servicebuch berechnete Prognose) - keine Quelle mehr vorhanden.
NAECHSTER_OELWECHSEL_SENSOR = ""
OELWECHSEL_STRECKE_SENSOR = ""
NAECHSTE_INSPEKTION_SENSOR = ""
INSPEKTION_STRECKE_SENSOR = ""
# Live-GPS-Position des Fahrzeugs (Übersicht -> Standort-Kachel). Erwartet
# eine device_tracker.*-Entity mit "latitude"/"longitude"-Attributen (HA-
# Konvention, siehe _standort() in frontend_veroeffentlichung.py) - bleibt
# leer, solange keine Quelle angebunden ist (siehe AGENTS.md Abschnitt B,
# FMM003/flespi offen); die Oberfläche zeigt dann "kein GPS-Signal" statt
# eines falschen Werts, genau wie bei BATTERIE_SENSOR oben.
STANDORT_TRACKER = ""
# Konvention, siehe _standort() in frontend_veroeffentlichung.py) - jetzt
# vom FMM003 geliefert.
STANDORT_TRACKER = "device_tracker.testzone_fmm003"
# Update-Funktion (Einstellungen -> "Update suchen", siehe
# updateverwaltung.py): Git-Repository, in das dieses Projekt gepflegt wird -
@@ -0,0 +1,188 @@
"""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}
@@ -20,6 +20,7 @@ tankvorgaenge_veroeffentlichen() auf einen Zeitraum eingeschränkt werden
"""
import einstellungen
import entitaeten
import profil
@@ -39,6 +40,12 @@ def batterieverlauf_veroeffentlichen():
state.set("pyscript.audi_dashboard_batterieverlauf", "aktuell", new_attributes={"daten": profil.batterieverlauf_lesen()})
def entitaeten_veroeffentlichen():
"""Katalog + aktuell wirksame Zuordnung fürs Setup-Menü (Einstellungen ->
Fahrzeug einrichten -> Setup) - siehe entitaeten.py."""
state.set("pyscript.audi_dashboard_entitaeten", "aktuell", new_attributes={"daten": entitaeten.aktueller_stand()})
def beleg_ergebnis_veroeffentlichen(daten):
"""Ergebnis eines Beleg-Uploads (geparste Werte fürs Ausfüllen eines
Entwurfs, oder ein Fehler wie 'Falsches Belegdatum') fürs Frontend, nach
@@ -190,3 +197,4 @@ def alles_veroeffentlichen():
tankvorgaenge_veroeffentlichen()
fahrzeugstatus_veroeffentlichen()
batterieverlauf_veroeffentlichen()
entitaeten_veroeffentlichen()
+7 -3
View File
@@ -85,9 +85,13 @@ def beim_start():
_fortschreiben_und_veroeffentlichen(_aktueller_km())
@state_trigger(f"{einstellungen.KM_SENSOR}")
def kilometerstand_geaendert(value=None):
_fortschreiben_und_veroeffentlichen(_aktueller_km())
# Ohne konfigurierten Kilometerstand-Sensor keinen Trigger registrieren -
# eine leere Entity-ID als @state_trigger-Ausdruck ist ungetestetes
# Verhalten, siehe gleiches Muster in fahrterkennung.py.
if einstellungen.KM_SENSOR:
@state_trigger(f"{einstellungen.KM_SENSOR}")
def kilometerstand_geaendert(value=None):
_fortschreiben_und_veroeffentlichen(_aktueller_km())
@service
+20 -16
View File
@@ -67,26 +67,30 @@ def beim_start():
profil.ordner_sicherstellen()
@state_trigger(f"{einstellungen.TANK_SENSOR}")
def tankfuellstand_geaendert(value=None, old_value=None):
global _tiefststand_pct
# Ohne konfigurierten Tankfüllstand-Sensor keinen Trigger registrieren -
# eine leere Entity-ID als @state_trigger-Ausdruck ist ungetestetes
# Verhalten, siehe gleiches Muster in fahrterkennung.py.
if einstellungen.TANK_SENSOR:
@state_trigger(f"{einstellungen.TANK_SENSOR}")
def tankfuellstand_geaendert(value=None, old_value=None):
global _tiefststand_pct
aktuell = _als_zahl(value)
if aktuell is None:
return
aktuell = _als_zahl(value)
if aktuell is None:
return
if _tiefststand_pct is None:
_tiefststand_pct = aktuell
return
if _tiefststand_pct is None:
_tiefststand_pct = aktuell
return
if aktuell <= _tiefststand_pct:
_tiefststand_pct = aktuell
return
if aktuell <= _tiefststand_pct:
_tiefststand_pct = aktuell
return
anstieg = aktuell - _tiefststand_pct
if anstieg >= _schwelle_prozent():
_tankvorgang_automatisch_anlegen(anstieg)
_tiefststand_pct = aktuell
anstieg = aktuell - _tiefststand_pct
if anstieg >= _schwelle_prozent():
_tankvorgang_automatisch_anlegen(anstieg)
_tiefststand_pct = aktuell
def _tankvorgang_automatisch_anlegen(anstieg_pct):