Files
audi-app/custom_components/audi_dashboard/einstellungen.py
T
tobias 70253e83a5 Fahrterkennung: FMM003-Funklöcher reißen keine Fahrt mehr auseinander
Die Zündungs-Entität stammt vom FMM003-Tracker; verliert er unterwegs
kurz die Verbindung, meldet Home Assistant "unavailable" statt eines
echten Zündungszustands. Die Live-Erkennung wertete das bisher wie
"aus" - eine Funklücke über der Pausenzeit teilte eine durchgehende
Fahrt in zwei. zuendung_geaendert() ignoriert "unavailable"/"unknown"
jetzt vollständig und leitet "fährt gerade" aus dem eigenen
Zwischenstand (fahrt_start_ts) statt aus dem letzten Rohwert ab.

historienimport.py bekommt zusätzlich eine Plausibilitätsprüfung: eine
errechnete Durchschnittsgeschwindigkeit über 300 km/h deutet auf einen
Kilometerstand-Ausreißer an der Fahrtgrenze hin, nicht auf eine echte
Fahrt - die Strecke wird dann verworfen statt eine unmögliche Fahrt
anzuzeigen.

Tankerkennung: optionaler zweiter Sensor für das Tankvolumen in Litern
(TANK_LITER_SENSOR, direkt vom CAN) neben dem bisherigen
Prozent-Füllstand. Ist er zugeordnet, übernimmt er die automatische
Tankerkennung vollständig - genauer als der Umweg über das im
Fahrzeugprofil hinterlegte Tankvolumen, und der erkannte Anstieg
liefert gleich eine grobe Vorbelegung für die getankte Menge statt
eines leeren Feldes. Ohne den Sensor bleibt alles beim Alten. Der
historische Import zieht mit derselben Präferenz nach.

Manifest auf 2026.8.25.2, beide Änderungsrunden live im Testcontainer
verifiziert (py_compile, Neustart, sauberes Setup-Log).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 22:04:26 +02:00

237 lines
14 KiB
Python

"""Welche Home-Assistant-Entität welche Rolle im Fahrzeug spielt.
Das ist die einzige technische Konfiguration der App. Alles andere (Pausen-
zeit, Reifendaten, Versicherung, ...) gehört ins Fahrzeugprofil und ist über
die Oberfläche änderbar.
**Im Auslieferstand ist hier alles leer.** Das ist Absicht, kein unfertiger
Zustand: welche Entity-IDs richtig sind, hängt an der jeweiligen Instanz und
ihren Integrationen. Zugeordnet wird nach der Installation im Setup-Menü der
App (Einstellungen -> Fahrzeug einrichten -> Setup), das die Zuordnung nach
audi_dashboard/entitaeten.json schreibt (siehe zuordnung.py).
Ein leeres Feld ist der sichere Zustand: die betroffene Kachel zeigt
"unbekannt" statt eines falschen Werts (siehe zustand_oder_none() in
veroeffentlichung.py), und die trigger-gebundenen Felder registrieren gar
keinen Zustandsbeobachter, statt einen gegen eine nicht existierende Entität
zu registrieren. Eine gesetzte, aber falsche Entity-ID ist deshalb schlechter
als eine leere - genau das ist 2026-08-23 einmal passiert, als hier noch die
Entity-IDs einer längst abgeräumten Testinstanz standen.
Zwei typische Quellen auf dieser Instanz: der Teltonika FMM003 (GPS-Tracker
mit CAN-Anbindung, über flespi angebunden) und eine EU-Data-Act-Integration
des Herstellers. Welche davon welche Rolle bedient, entscheidet das
Setup-Menü - nicht diese Datei.
UNTERSCHIED ZUR PYSCRIPT-FASSUNG: dort waren das Modul-Variablen, die
entitaeten.py zur Laufzeit per setattr() auf dem importierten Modul-Objekt
überschrieb. Das funktionierte, war aber ein globaler, veränderlicher Zustand
quer durch alle Dateien. Hier ist es ein ganz normales Objekt, das der
Koordinator hält und weiterreicht.
"""
from __future__ import annotations
from dataclasses import dataclass, field, fields
# Vier gleich aufgebaute Positionen für Türen und Fenster.
POSITIONEN = ["vorne links", "vorne rechts", "hinten links", "hinten rechts"]
@dataclass
class Sensorzuordnung:
"""Die wirksame Zuordnung. Feldnamen = Schlüssel in entitaeten.json."""
# Fahrterkennung: Start/Ende einer Fahrt über den Zündungs-/ACC-Status
# (on = Fahrt läuft). Dieselbe Entität speist auch die Anzeige
# "fährt/steht" - damit können Erfassung und Anzeige nicht auseinander-
# laufen.
ZUENDUNG_SENSOR: str = ""
# Kilometerstand - für Fahrtabschluss-Screening, Reifenzähler und
# Ölwechsel-Prognose.
#
# Beim FMM003 hier NICHT den selbst berechneten Gesamtkilometerstand
# (*_total_calculated_mileage) zuordnen: der beruht auf GPS-Strecken-
# rechnung statt auf dem Tacho und damit auf einer anderen Zählbasis als
# der echte Fahrzeug-Kilometerstand. Wer ihn einträgt, verfälscht alle
# drei genannten Auswertungen mit einem inkonsistenten Basiswert. Der vom
# CAN gelesene Wert (*_total_vehicle_mileage_read_from_can) bzw. der
# Kilometerstand der EU-Data-Act-Integration ist der richtige.
KM_SENSOR: str = ""
# Tankfüllstand in Prozent - für die automatische Tankerkennung und die
# Anzeige in der Übersicht.
TANK_SENSOR: str = ""
# Tankvolumen in Litern, direkt vom CAN (nicht aus dem Prozentwert
# umgerechnet). Optional - ist er gesetzt, übernimmt er die automatische
# Tankerkennung von TANK_SENSOR: ein Anstieg misst sich dann direkt in
# Litern statt über den Umweg Prozentpunkte -> Tankvolumen aus dem
# Fahrzeugprofil, und der erkannte Anstieg selbst dient als grobe
# Vorbelegung für die getankte Menge (siehe tankerkennung.py).
TANK_LITER_SENSOR: str = ""
# Reichweite (Übersicht).
RANGE_SENSOR: str = ""
# 12V-Batteriespannung. Beim FMM003 ist das external_power_voltage - 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: str = ""
# Knopf für eine sofortige Neuabfrage beim Fahrzeug.
REFRESH_BUTTON: str = ""
# Türen/Fenster (je vier Positionen, siehe POSITIONEN).
TUER_SENSOREN: list[str] = field(default_factory=list)
FENSTER_SENSOREN: list[str] = field(default_factory=list)
HECKKLAPPE_SENSOR: str = ""
HAUBE_SENSOR: str = ""
# Türschloss (getrennt von den Öffnungssensoren oben - zu heißt nicht
# verriegelt). Nur die Fahrertür: die Zentralverriegelung schließt immer
# alle Türen gemeinsam, ein einzelnes Türschloss lässt sich nicht separat
# bedienen - ihr Zustand gilt fürs ganze Fahrzeug (Nutzer-Entscheidung).
TUERSCHLOSS_SENSOR: str = ""
# Schiebedach (offen/zu) und Standlicht (an/aus) - ergänzen die
# Fenster-Gruppe bzw. stehen für sich, siehe _sicherheitscheck() in
# veroeffentlichung.py.
DACH_SENSOR: str = ""
LICHT_SENSOR: str = ""
# Vom Fahrzeug selbst gemeldete Service-Fälligkeit (ergänzt die
# App-eigene, aus dem Servicebuch berechnete Prognose).
NAECHSTER_OELWECHSEL_SENSOR: str = ""
OELWECHSEL_STRECKE_SENSOR: str = ""
NAECHSTE_INSPEKTION_SENSOR: str = ""
INSPEKTION_STRECKE_SENSOR: str = ""
# Live-GPS-Position (Übersicht -> Standort-Kachel). Breiten-/Längengrad
# als zwei eigene sensor-Entities: flespi liefert Koordinaten so, nicht
# als Attribute einer device_tracker-Entity.
STANDORT_LAT_SENSOR: str = ""
STANDORT_LON_SENSOR: str = ""
def _standardwerte() -> dict[str, object]:
"""Die eingebauten Werte, einmal eingefroren.
Gebraucht für "Zurücksetzen" im Setup-Menü: die Zuordnung wird bei jedem
Anwenden vollständig neu gesetzt, ein weggelassener Override fällt damit
auf genau diesen Wert zurück."""
leer = Sensorzuordnung()
werte: dict[str, object] = {}
for f in fields(leer):
wert = getattr(leer, f.name)
werte[f.name] = list(wert) if isinstance(wert, list) else wert
return werte
STANDARDWERTE: dict[str, object] = _standardwerte()
SCHLUESSEL: set[str] = set(STANDARDWERTE)
# Katalog aller Rollen fürs Setup-Menü im Frontend. Reihenfolge =
# Anzeige-Reihenfolge innerhalb jeder Gruppe. domains/device_classes/units
# sind Erwartungen fürs Frontend-seitige Filtern und Vorschlagen (Schalter
# "Nur passende Sensoren anzeigen") - eine fehlende oder leere Liste bedeutet
# "keine Einschränkung" bzw. "diese Rolle hat üblicherweise keine Einheit".
FELDER: list[dict] = [
{"key": "ZUENDUNG_SENSOR", "label": "Zündung/ACC-Status", "gruppe": "fahrterkennung",
"hinweis": "on = Fahrt läuft, erkennt Fahrtbeginn/-ende.",
"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": "TANK_LITER_SENSOR", "label": "Tankvolumen (Liter)", "gruppe": "fahrterkennung",
"hinweis": "Optional, vom CAN statt aus dem Prozentwert umgerechnet - macht die automatische "
"Tankerkennung genauer und liefert eine Vorbelegung für die getankte Menge.",
"domains": ["sensor"], "device_classes": ["volume"], "units": ["L", "l"], "liste": False, "pflicht": False,
"stichworte": ["tankvolumen", "liter", "litres", "fuel volume", "kraftstoffvolumen"]},
{"key": "RANGE_SENSOR", "label": "Reichweite", "gruppe": "uebersicht",
"hinweis": "Für die Übersicht - bleibt sie 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": "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": "TUERSCHLOSS_SENSOR", "label": "Türschloss (Fahrertür)", "gruppe": "sicherheit",
"hinweis": "\"aus\"/off = verriegelt. Die Zentralverriegelung schließt alle Türen gemeinsam - "
"die Fahrertür genügt für \"Fahrzeug verriegelt\".",
"domains": ["binary_sensor"], "device_classes": ["lock"], "units": [], "liste": False,
"pflicht": False, "stichworte": ["türschloss", "schloss", "lock", "verriegelt", "fahrertür"]},
{"key": "DACH_SENSOR", "label": "Schiebedach", "gruppe": "sicherheit",
"hinweis": "\"aus\"/off = zu.",
"domains": ["binary_sensor"], "device_classes": ["window", "door", "opening"], "units": [], "liste": False, "pflicht": False,
"stichworte": ["dach", "schiebedach", "sunroof", "roof"]},
{"key": "LICHT_SENSOR", "label": "Standlicht", "gruppe": "sicherheit",
"hinweis": "\"aus\"/off = kein Licht.",
"domains": ["binary_sensor"], "device_classes": ["light"], "units": [], "liste": False, "pflicht": False,
"stichworte": ["licht", "standlicht", "light", "parking lights"]},
{"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"]},
]
# In der pyscript-Fassung stand hier eine Liste TRIGGER_GEBUNDEN: die drei
# Felder ZUENDUNG/KM/TANK_SENSOR waren beim Modul-Laden einmalig in die
# @state_trigger-Registrierung eingebacken, eine Änderung wirkte deshalb erst
# nach einem HA-Neustart, und das Setup-Menü musste genau davor warnen.
# Diese Integration bindet ihre Zustandsbeobachter bei jeder Änderung neu
# (koordinator.trigger_neu_binden) - die Einschränkung, der Warnhinweis und
# der eigens dafür gebaute Neustart-Dienst sind damit ersatzlos weg.
for _feld in FELDER:
_feld["trigger_gebunden"] = False