Files
audi-app/homeassistant/pyscript/modules/frontend_veroeffentlichung.py
T
tobias 25d1cebd12 Fuenf gemeldete Bugs behoben: Haubenschloss/Tuerschloesser entfernt, Bestaetigungsdialog-Transparenz, geteilte FMM003-Koordinaten, Laedt-Haenger, generischer Beleg-Parser
Entfernt Haubenschloss-/Tuerschloss-Erkennung dauerhaft aus Setup-Katalog und
Sicherheitscheck (auf ausdruecklichen Wunsch, nicht nur leer/unzuordenbar wie
der Rest der abgeloesten VAG-Integration). Behebt eine durchscheinende
Bestaetigungs-Sheet ("Doppelt zugeordnet" u.a.) - derselbe --tile-2-
Transparenz-Fehler, der fuer das Setup-Popup schon behoben war, hier
nachgezogen. Ergaenzt einen zweiten GPS-Pfad (STANDORT_LAT_SENSOR/
STANDORT_LON_SENSOR) fuer Integrationen wie flespi, die Breiten-/Laengengrad
als zwei eigene Sensoren statt als device_tracker-Attribute liefern. Haertet
den bekannten "Laedt ..."-Haenger beim App-Start zusaetzlich ab: Tab-Klicks
pruefen jetzt aktiv nach, ob Daten inzwischen da sind. Ergaenzt
shell_beleg_parser.py um einen stationsunabhaengigen Fallback fuer
Tankbelege unbekannter Formate (Adresse/Gesamtbetrag/Menge/Rabatt-Herleitung
wie vom Nutzer vorgegeben) - die bestehende Shell-Erkennung bleibt
unveraendert und weiterhin durch die zehn echten Testbelege abgedeckt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 11:32:15 +02:00

222 lines
9.4 KiB
Python

"""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). Zwei
mögliche Quellen, in dieser Reihenfolge versucht: (1) STANDORT_TRACKER,
eine device_tracker-Entity mit latitude/longitude/gps_accuracy-Attributen
(HA-Konvention) - der Normalfall; (2) STANDORT_LAT_SENSOR/
STANDORT_LON_SENSOR, zwei eigene sensor-Entities für Breiten-/Längengrad -
manche Integrationen (z. B. flespi) liefern Koordinaten so, statt als
Attribute einer einzelnen Tracker-Entity. Fehlt beides oder ist (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 Attribute und der
Aktualisierungszeitpunkt gebraucht werden: 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}
tracker_id = einstellungen.STANDORT_TRACKER
if tracker_id:
try:
wert = state.get(tracker_id)
attribute = state.getattr(tracker_id)
except NameError:
attribute = None
if attribute and attribute.get("latitude") is not None:
zeit = None
try:
zeit = wert.last_updated.isoformat()
except AttributeError:
pass
return {
"lat": _zu_zahl(attribute.get("latitude")),
"lon": _zu_zahl(attribute.get("longitude")),
"genauigkeit_m": _zu_zahl(attribute.get("gps_accuracy")),
"zeit": zeit,
}
lat_id, lon_id = einstellungen.STANDORT_LAT_SENSOR, einstellungen.STANDORT_LON_SENSOR
if lat_id and lon_id:
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}
return leer
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()