"""Veröffentlicht Profil, Fahrten, Tankvorgänge und den Live-Fahrzeugstatus als pyscript-Zustandsentitäten, damit das Frontend sie über das ganz normale hass.states lesen kann - kein Service-Aufruf mit Rückgabewert nötig, dessen genaues Verhalten in der Home-Assistant-Weboberfläche nicht durchgängig dokumentiert ist (siehe Kopfkommentar in frontend_api.py). Importierbar mit `import frontend_veroeffentlichung`, aufgerufen sowohl periodisch (frontend_api.py) als auch direkt nach Schreibaktionen aus fahrterkennung.py, belegverarbeitung.py und reifenzaehler.py, damit sich die Oberfläche unmittelbar aktualisiert statt bis zum nächsten Intervall zu warten. BEKANNTE GRENZE: Zustands-Attribute in Home Assistant sind nicht für beliebig große Datenmengen gedacht (~16 KB Richtwert für die Aufzeichnung in der Recorder-Datenbank). Für das Profil und den aktuellen Stand ist das unkritisch; die Fahrten- und Tankvorgänge-Archive könnten das nach einigen Jahren überschreiten. Wird das relevant, müssten fahrten_veroeffentlichen()/ tankvorgaenge_veroeffentlichen() auf einen Zeitraum eingeschränkt werden (z. B. nur das laufende Jahr), statt hier vorschnell zu vereinfachen. """ import io import json import os import einstellungen import entitaeten import profil def profil_veroeffentlichen(): daten = profil.profil_lesen() if daten is None: return state.set("pyscript.audi_dashboard_profil", "aktuell", new_attributes={"daten": daten}) def fahrten_veroeffentlichen(): state.set("pyscript.audi_dashboard_fahrten", "aktuell", new_attributes={"daten": profil.fahrten_lesen()}) def tankvorgaenge_veroeffentlichen(): state.set("pyscript.audi_dashboard_tankvorgaenge", "aktuell", new_attributes={"daten": profil.tankvorgaenge_lesen()}) 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 demselben Zustands-statt-Rückgabewert-Muster wie die übrigen veroeffentlichen()-Funktionen hier (siehe Kopfkommentar).""" state.set("pyscript.audi_dashboard_beleg_ergebnis", "aktuell", new_attributes={"daten": daten}) def zustand_oder_none(entity_id): """Sicherer Zustandszugriff: pyscripts state.get() wirft NameError, wenn die Entity (noch) nicht existiert - real an einer Testinstanz beobachtet, nicht nur vermutet. Genau das ist hier der Normalfall, solange manche der in einstellungen.py hinterlegten Entity-IDs noch Platzhalter sind, und bleibt auch danach relevant: fällt eine Datenquelle aus, soll die Oberfläche das zeigen (§9), nicht an einem Fehler hängen bleiben.""" if not entity_id: return None try: wert = state.get(entity_id) except NameError: return None if wert in (None, "unknown", "unavailable"): return None return wert def _zu_zahl(wert): try: return float(wert) except (TypeError, ValueError): return None def _abs_zahl(wert): zahl = _zu_zahl(wert) return None if zahl is None else abs(zahl) def _zu_bool(wert): """binary_sensor-Zustand als echtes True/False, None bei fehlender Meldung. None ist hier ausdrücklich kein "nein": ohne zugeordneten Sensor weiß die App schlicht nicht, ob gefahren wird - und muss das anzeigen dürfen (§9), statt "steht" zu behaupten.""" if wert is None: return None return str(wert).lower() in ("on", "true", "1", "open", "yes") def _standort(): """Live-GPS-Position des Fahrzeugs (Übersicht -> Standort-Kachel) - liest STANDORT_LAT_SENSOR/STANDORT_LON_SENSOR, zwei eigene sensor-Entities für Breiten-/Längengrad (flespi liefert Koordinaten so, nicht als Attribute einer einzelnen device_tracker-Entity). Fehlt eine der beiden Entity-IDs oder ist der Zustand (noch) nicht verfügbar, liefert diese Funktion durchgehend None statt eines geratenen Werts - genau das zustand_oder_none()-Muster von oben, hier als eigene Funktion, weil zusätzlich zum Zustand noch der Aktualisierungszeitpunkt gebraucht wird: state.get() liefert dafür ein StateVal (str-Unterklasse) mit .last_updated als datetime-Attribut - kein eigenständiges state.last_updated(), das gibt es in pyscript nicht.""" leer = {"lat": None, "lon": None, "genauigkeit_m": None, "zeit": None} lat_id, lon_id = einstellungen.STANDORT_LAT_SENSOR, einstellungen.STANDORT_LON_SENSOR if not lat_id or not lon_id: return leer try: lat_wert = state.get(lat_id) lon_wert = state.get(lon_id) except NameError: return leer lat, lon = _zu_zahl(lat_wert), _zu_zahl(lon_wert) if lat is None or lon is None: return leer zeit = None try: zeit = lat_wert.last_updated.isoformat() except AttributeError: pass return {"lat": lat, "lon": lon, "genauigkeit_m": None, "zeit": zeit} POSITIONEN = ["vorne links", "vorne rechts", "hinten links", "hinten rechts"] def _sicherheitscheck(): """Liste der einzeln geprüften Punkte hinter "Sicher abgestellt" (§9), fürs Frontend (Klick auf den Status öffnet diese Liste mit einem grünen/ roten/grauen Punkt je Zeile). "ok" ist None, wenn der Sensor fehlt oder nicht verfügbar ist - genau daraus leitet sich auch die zusammengefasste gesichert-Kennzahl unten ab, damit beide nie auseinanderlaufen können.""" eintraege = [] for pos, sensor in zip(POSITIONEN, einstellungen.TUER_SENSOREN): w = zustand_oder_none(sensor) eintraege.append({"label": f"Tür {pos}", "ok": None if w is None else w == "off"}) for pos, sensor in zip(POSITIONEN, einstellungen.FENSTER_SENSOREN): w = zustand_oder_none(sensor) eintraege.append({"label": f"Fenster {pos}", "ok": None if w is None else w == "off"}) for label, sensor in [ ("Heckklappe", einstellungen.HECKKLAPPE_SENSOR), ("Motorhaube", einstellungen.HAUBE_SENSOR), ]: w = zustand_oder_none(sensor) eintraege.append({"label": label, "ok": None if w is None else w == "off"}) return eintraege def fahrzeugstatus_veroeffentlichen(): """Bündelt die live aus Home Assistant gelesenen Fahrzeugwerte Frontend-freundlich, damit die Oberfläche keine Entity-IDs kennen muss. """ km = zustand_oder_none(einstellungen.KM_SENSOR) tankprozent = zustand_oder_none(einstellungen.TANK_SENSOR) reichweite = zustand_oder_none(einstellungen.RANGE_SENSOR) batteriespannung = zustand_oder_none(einstellungen.BATTERIE_SENSOR) # "Sicher abgestellt" (§9) - erst wenn WIRKLICH jeder einzeln geprüfte # Punkt zu/verriegelt ist, gilt das Fahrzeug als gesichert; fehlt auch # nur eine Meldung, ist der Status unbekannt statt geraten. sicherheitscheck = _sicherheitscheck() if any([e["ok"] is None for e in sicherheitscheck]): gesichert = None else: gesichert = all([e["ok"] for e in sicherheitscheck]) standort = _standort() daten = { "km": _zu_zahl(km), "tankprozent": _zu_zahl(tankprozent), "reichweite_km": _zu_zahl(reichweite), "batteriespannung": _zu_zahl(batteriespannung), "gesichert": gesichert, "sicherheitscheck": sicherheitscheck, # Fährt das Fahrzeug gerade? Kommt aus derselben Zündungs-Entität, die # auch fahrterkennung.py als maßgebliches Signal für Fahrtbeginn/-ende # nimmt - damit sagen Anzeige und Erfassung zwangsläufig dasselbe. # # Vorher wurde dieser Zustand im Frontend aus dem Status der Fahrten # abgeleitet ("offen" = fährt). Das war falsch: "offen" heißt # unvollständige Daten (Strecke fehlt noch, siehe # fahrtabschluss_logik.py), nicht "unterwegs" - eine Fahrt ohne # Kilometerstand blieb dauerhaft "offen" und das Fahrzeug damit # dauerhaft "fahrend". None bedeutet: kein Zündungssensor zugeordnet, # Zustand unbekannt. "zuendung": _zu_bool(zustand_oder_none(einstellungen.ZUENDUNG_SENSOR)), "standort_lat": standort["lat"], "standort_lon": standort["lon"], "standort_genauigkeit_m": standort["genauigkeit_m"], "standort_zeit": standort["zeit"], # Vom Fahrzeug selbst gemeldete Service-Fälligkeit (ergänzt die # App-eigene Servicebuch-Berechnung, siehe fahrzeugMeldung() im # Frontend) - die Streckensensoren liefern negative Restkilometer- # Werte, hier deshalb der Betrag. "oelwechsel_faellig_ts": zustand_oder_none(einstellungen.NAECHSTER_OELWECHSEL_SENSOR), "oelwechsel_faellig_km": _abs_zahl(zustand_oder_none(einstellungen.OELWECHSEL_STRECKE_SENSOR)), "inspektion_faellig_ts": zustand_oder_none(einstellungen.NAECHSTE_INSPEKTION_SENSOR), "inspektion_faellig_km": _abs_zahl(zustand_oder_none(einstellungen.INSPEKTION_STRECKE_SENSOR)), } state.set("pyscript.audi_dashboard_fahrzeugstatus", "aktuell", new_attributes={"daten": daten}) APP_VERSION_PFAD = "/config/www/audi-dashboard-version.json" def app_version_veroeffentlichen(): """Welchen Oberflächen-Stand diese Installation ausliefert. Gedacht für die Companion-App: die trägt ihre Version fest einkompiliert (vite setzt __APP_VERSION__ aus der Datei VERSION im Projektstamm) und kann sie gegen diesen Wert halten. Weicht sie ab, ist die App älter als der Server und sagt das selbst - siehe VERSIONIERUNG.md. Das Panel braucht den Vergleich nicht: es lädt seine Dateien bei jedem Seitenaufruf frisch, sobald sich die Cache-Zahl in derselben Datei geändert hat, und kann deshalb gar nicht veralten. Bewusst eine eigene Entität statt eines Felds im Fahrzeugstatus: das hier ist eine Eigenschaft der Installation, keine des Fahrzeugs.""" version = None if os.path.exists(APP_VERSION_PFAD): try: f = task.executor(io.open, APP_VERSION_PFAD, "r") inhalt = f.read() f.close() version = json.loads(inhalt).get("app") except (ValueError, OSError) as fehler: log.warning(f"audi_dashboard: App-Version nicht lesbar ({fehler})") state.set( "pyscript.audi_dashboard_app_version", version or "unbekannt", new_attributes={"daten": {"app": version}}, ) def alles_veroeffentlichen(): app_version_veroeffentlichen() profil_veroeffentlichen() fahrten_veroeffentlichen() tankvorgaenge_veroeffentlichen() fahrzeugstatus_veroeffentlichen() batterieverlauf_veroeffentlichen() entitaeten_veroeffentlichen()