dc4c1ff0bf
Neue Kachel oberhalb von "Zuletzt" mit echter Leaflet-Mini-Karte, öffnet per Klick eine Vollbild-Standortansicht (Kartendarstellung wählen, auf Fahrzeug/User zentrieren, beide zeigen) mit einem ausziehbaren myAudi-Stil-Menü: Distanz zum Gerät, Adresse (Reverse- Geocoding via Nominatim), fährt/steht/Letzter-Parkplatz-Status, Tankfüllstand/Reichweite, Route- und Teilen-Aktionen. Backend: STANDORT_TRACKER-Einstellung (device_tracker-Entity) plus _standort()-Veröffentlichung, bewusst leer gelassen bis eine echte GPS-Quelle (FMM003/flespi) angebunden ist - Kachel zeigt bis dahin "kein GPS-Signal". Verifiziert in audi_ha_test mit einer manuell gesetzten Test-Position. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
193 lines
8.3 KiB
Python
193 lines
8.3 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 profil
|
|
|
|
|
|
def profil_veroeffentlichen():
|
|
state.set("pyscript.audi_dashboard_profil", "aktuell", new_attributes={"daten": profil.profil_lesen()})
|
|
|
|
|
|
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 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
|
|
latitude/longitude/gps_accuracy von der in STANDORT_TRACKER hinterlegten
|
|
device_tracker-Entity (HA-Konvention). Fehlt die Entity-ID oder ist sie
|
|
(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."""
|
|
entity_id = einstellungen.STANDORT_TRACKER
|
|
leer = {"lat": None, "lon": None, "genauigkeit_m": None, "zeit": None}
|
|
if not entity_id:
|
|
return leer
|
|
try:
|
|
wert = state.get(entity_id)
|
|
attribute = state.getattr(entity_id)
|
|
except NameError:
|
|
return leer
|
|
if not attribute or attribute.get("latitude") is None:
|
|
return leer
|
|
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,
|
|
}
|
|
|
|
|
|
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 pos, sensor in zip(POSITIONEN, einstellungen.TUERSCHLOSS_SENSOREN):
|
|
w = zustand_oder_none(sensor)
|
|
eintraege.append({"label": f"Türschloss {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),
|
|
("Haubenschloss", einstellungen.HAUBENSCHLOSS_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()
|