"""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 = "" # Das Trip-Signal des Geräts: es beginnt erst, wenn Zündung UND Bewegung # UND "Start Speed" zusammenkommen, und endet erst nach dem # "Ignition OFF Timeout" des Geräts. Damit bringt es die Pausentoleranz # selbst mit, statt dass wir einen zweiten Zeitgeber bauen. # # Bewusst getrennt von ZUENDUNG_SENSOR: die rohe Zündung prellt (am # 29.08.2026 drei Wechsel in 30 ms) und stand am 30./31.08. dreizehn # Stunden am Stück auf "an", ohne dass gefahren wurde. Als Auslöser für # Fahrten taugt sie nicht, als Anzeige "Zündung an" sehr wohl. # # Leer: dann übernimmt ZUENDUNG_SENSOR die Fahrterkennung wie früher - # bestehende Installationen laufen ohne Zutun weiter. TRIP_SENSOR: str = "" # Der Zeitstempel, den das Gerät dem Datensatz selbst mitgegeben hat # (Unix-Sekunden, UTC). Optional, aber der einzige Weg zu einem # ehrlichen Fahrtbeginn: ohne ihn ist der Beginn der Moment, in dem # Home Assistant den Wechsel verarbeitet - und der liegt hinter dem # tatsächlichen, sobald das Gerät gepuffert hat. Am 01.09.2026 # gemessen: ein Datensatz mit Fahrtende trug die Gerätezeit 07:29:04 # und kam um 07:35:23 an, sechs Minuten später. # # UTC, nicht Ortszeit - ebenfalls nachgemessen, der Versatz zur # UTC-Uhr von Home Assistant lag bei Sekunden, nicht bei zwei Stunden. # Eine Zeitzonenumrechnung wäre also nicht nur unnötig, sie baute # einen Zwei-Stunden-Fehler ein. MELDEZEIT_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 = "" # Vom Fahrzeug selbst berechnete Strecke seit der letzten Tankung # (FMM003: "calculated distance covered after the latest refueling"). # Optional - ist er gesetzt, übernimmt sein aktueller Wert das Feld # distance_km eines neu erkannten Tankvorgangs anstelle der eigenen # Kilometerstand-Subtraktion (ablage.distanz_seit_letzter_tankung()) - # direkt vom Fahrzeug gemeldet statt selbst gerechnet, und robust gegen # Lücken im eigenen Bestand. Ohne ihn bleibt die bisherige Berechnung # unverändert der Rückfall. TANK_DISTANZ_SENSOR: str = "" # Reichweite (Übersicht). GESCHWINDIGKEIT_SENSOR: str = "" 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 = "" # Außentemperatur (CAN ambient air temperature) - optional, für die # Batteriespannungs-Messwertliste: zusammen mit dem Tagesminimum # aufgezeichnet, siehe batterie.py. AUSSENTEMP_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": "TRIP_SENSOR", "label": "Trip-Status", "gruppe": "fahrterkennung", "hinweis": "on = Fahrt läuft. Das gefilterte Fahrtsignal des Geräts - erkennt Fahrtbeginn und -ende. Ohne Zuordnung übernimmt die Zündung diese Aufgabe.", "domains": ["binary_sensor"], "device_classes": [], "units": [], "liste": False, "pflicht": False, "beispiel": "trip_status_true_if_trip_started_false_if_stopped", "stichworte": ["trip", "fahrt", "trip status", "journey"]}, {"key": "ZUENDUNG_SENSOR", "label": "Zündung", "gruppe": "fahrterkennung", "hinweis": "on = Zündung an. Für die Anzeige \"fährt/steht\"; ohne zugeordneten Trip-Status erkennt sie zusätzlich Fahrtbeginn und -ende.", "domains": ["binary_sensor"], "device_classes": [], "units": [], "liste": False, "pflicht": True, "beispiel": "engine_ignition_or_acc_status", "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, "beispiel": "total_vehicle_mileage_read_from_can", "stichworte": ["kilometer", "km", "mileage", "odometer", "tacho", "laufleistung"]}, {"key": "MELDEZEIT_SENSOR", "label": "Meldezeit des Geräts", "gruppe": "fahrterkennung", "hinweis": "Zeitstempel des Datensatzes in Unix-Sekunden (UTC). Damit stammen Fahrtbeginn und -ende vom Gerät statt von der Ankunft in Home Assistant.", "domains": ["sensor"], "device_classes": [], "units": ["s", "seconds"], "liste": False, "pflicht": False, "beispiel": "message_timestamp", "stichworte": ["meldezeit", "zeitstempel", "timestamp", "message", "zeit"]}, {"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, "beispiel": "fuel_level_in_tank_read_from_can", "stichworte": ["tank", "fuel", "kraftstoff", "füllstand", "level"]}, {"key": "TANK_LITER_SENSOR", "label": "Tankvolumen", "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, "beispiel": "can_fuel_volume", "stichworte": ["tankvolumen", "liter", "litres", "fuel volume", "kraftstoffvolumen"]}, {"key": "TANK_DISTANZ_SENSOR", "label": "Strecke seit letzter Tankung", "gruppe": "fahrterkennung", "hinweis": "Optional, vom Fahrzeug selbst berechnet - ersetzt die eigene Kilometerstand-Rechnung " "für die Strecke eines Tankvorgangs.", "domains": ["sensor"], "device_classes": ["distance"], "units": ["km", "mi"], "liste": False, "pflicht": False, "beispiel": "calculated_distance_covered_after_the_latest_refueling", "stichworte": ["strecke tankung", "distance refuel", "distance since refueling", "refuelling"]}, {"key": "GESCHWINDIGKEIT_SENSOR", "label": "Geschwindigkeit", "gruppe": "fahrterkennung", "hinweis": "Optional - fuer die Hoechstgeschwindigkeit einer Fahrt. Der FMM003 liest sie laut seiner Konfiguration vom OBD/CAN, nicht aus GPS.", "domains": ["sensor"], "device_classes": ["speed"], "units": ["km/h", "mph"], "liste": False, "pflicht": False, "beispiel": "vehicle_speed_read_from_can", "stichworte": ["geschwindigkeit", "speed", "tempo", "kmh"]}, {"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, "beispiel": "vehicle_range_read_from_can", "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, "beispiel": "external_power_voltage", "stichworte": ["batterie", "battery", "spannung", "volt", "12v"]}, {"key": "AUSSENTEMP_SENSOR", "label": "Außentemperatur", "gruppe": "uebersicht", "hinweis": "CAN-Außentemperatur - optional, ergänzt die Batteriespannungs-Messwertliste um den " "Temperaturwert je Tag.", "domains": ["sensor"], "device_classes": ["temperature"], "units": ["°C", "°F"], "liste": False, "pflicht": False, "beispiel": "can_ambient_air_temperature", "stichworte": ["außentemperatur", "aussentemperatur", "ambient", "temperatur", "temperature"]}, {"key": "REFRESH_BUTTON", "label": "Sofort-Aktualisierung", "gruppe": "uebersicht", "hinweis": "Der von der Integration bereitgestellte Knopf für \"Ziehen zum Aktualisieren\".", "domains": ["button"], "device_classes": [], "units": [], "liste": False, "pflicht": False, "beispiel": "refresh_now", "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, "beispiel": "front_left_door", "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, "beispiel": "front_left_window", "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, "beispiel": "tailgate", "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, "beispiel": "hood", "stichworte": ["haube", "hood", "bonnet", "motorhaube"]}, {"key": "TUERSCHLOSS_SENSOR", "label": "Türschloss", "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, "beispiel": "front_left_door_lock", "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, "beispiel": "sunroof", "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, "beispiel": "parking_lights", "stichworte": ["licht", "standlicht", "light", "parking lights"]}, {"key": "NAECHSTER_OELWECHSEL_SENSOR", "label": "Nächster Ölwechsel", "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, "beispiel": "oil_change_due", "stichworte": ["ölwechsel", "oil", "service", "change"]}, {"key": "OELWECHSEL_STRECKE_SENSOR", "label": "Nächster Ölwechsel", "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, "beispiel": "oil_change_distance", "stichworte": ["ölwechsel", "oil", "distance", "strecke"]}, {"key": "NAECHSTE_INSPEKTION_SENSOR", "label": "Nächste Inspektion", "gruppe": "uebersicht", "hinweis": "Vom Fahrzeug selbst gemeldete Fälligkeit.", "domains": ["sensor"], "device_classes": ["date", "timestamp"], "units": [], "liste": False, "pflicht": False, "beispiel": "inspection_due", "stichworte": ["inspektion", "inspection", "service"]}, {"key": "INSPEKTION_STRECKE_SENSOR", "label": "Nächste Inspektion", "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, "beispiel": "inspection_distance", "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, "beispiel": "latitude_coordinate_value", "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, "beispiel": "longitude_coordinate_value", "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