"""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 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 _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), ("Heckklappenschloss", einstellungen.HECKKLAPPENSCHLOSS_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, "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}) def alles_veroeffentlichen(): profil_veroeffentlichen() fahrten_veroeffentlichen() tankvorgaenge_veroeffentlichen() fahrzeugstatus_veroeffentlichen() batterieverlauf_veroeffentlichen() entitaeten_veroeffentlichen()