pyscript-Backend zur echten HA-Integration umgebaut (HACS-fähig)
Das Backend liegt jetzt als custom_components/audi_dashboard/ vor - eine normale Home-Assistant-Integration mit Config-Flow, einer sensor-Plattform und 18 Diensten. Damit ist die App über HACS installierbar; bis das Repo auf GitHub gespiegelt ist (HACS spricht ausschließlich mit GitHub), installiert homeassistant/installationspaket/install.ps1 denselben Ordner ohne HACS. Fünf Installationsschritte entfallen ersatzlos: der pyscript:-Block, der panel_custom:-Block, das Kopieren der Oberfläche nach www/, das langlebige Zugriffstoken (der Verlauf wird direkt über die recorder-API gelesen) und "pip install pypdf" (steht in manifest.json). Das Fahrzeugprofil legt die Integration beim ersten Start aus ihrer Vorlage an. Drei alte Schwächen sind dabei mit erledigt: - Die Nutzlast landet nicht mehr in der Recorder-Datenbank (_unrecorded_attributes - das kann nur eine echte Entität). - Eine laufende Fahrt überlebt einen Neustart (Store statt Arbeitsspeicher); fiel sie während eines Ausfalls ins Ende, schließt nach_neustart_fortsetzen() sie beim letzten aufgezeichneten Zeitpunkt. - Sensor-Zuordnungen wirken sofort - die Zustandsbeobachter werden neu gebunden, der Neustart-Hinweis und der Neustart-Dienst sind weg. Namensvertrag geändert, beide Oberflächen mitgezogen: pyscript.audi_dashboard_x -> sensor.audi_dashboard_x, pyscript.audi_dashboard_y -> audi_dashboard.y. Eine Companion-App vom alten Stand findet nach dem Umstieg nichts mehr und muss neu gebaut werden; das Panel liegt in der Integration und kann nicht driften. Der selbstgebaute Updater entfällt - HACS ist die Update-Mechanik, die Home Assistant kennt. Die Versionierung schrumpft auf eine Quelle: manifest.json. Geprüft am laufenden Testcontainer (Container byteweise identisch mit dem Repo): alle 18 Dienste, Panel, Config-Entry neu laden, Historienimport, echter Shell-Beleg in-process, Neuinstallation im Wegwerf-Container blank mit automatisch nachinstalliertem pypdf. Companion-App: tsc sauber, 112/112 Tests, beide Rauchtests gegen das laufende Backend grün. Belegparser 8/8. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
"""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.
|
||||
TANK_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 = ""
|
||||
|
||||
# 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": "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": "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
|
||||
Reference in New Issue
Block a user