7276d06a18
- Türschlösser auf ein Sensor reduziert (Fahrertür genügt, Zentralverriegelung schließt alle Türen gemeinsam) - Kofferraum-/Motorhauben-Schloss entfernt. - "Sicherheit" heißt im Panel jetzt "Fahrzeugstatus", wie in der companion-app. - naechsterTermin() (Panel): sortierte null als Epoch 1970 und ließ dadurch einen datenlosen Termin (Hauptuntersuchung ohne Eintrag/Erstzulassung) immer vor einem echten, berechneten Termin (Ölwechsel) gewinnen - Ursache dafür, dass Übersicht nach dem Leeren des Servicebuchs nichts mehr zeigte. - "Licht ausgeschaltet" statt "Kein Licht" (Formulierung zurückgenommen). - Große Bildschirme: Unterseiten (Standort, Service, ...) zeigten ihren Titel in 26px statt der sonst überall genutzten 17px - wirkte neben dem durchgängig leichten Fließtext wie Fettschrift, obwohl font-weight nirgends wechselt. Jetzt dieselbe Standardgröße wie im Telefon-Layout. Details und Verifikation in AGENTS.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
180 lines
8.6 KiB
Python
180 lines
8.6 KiB
Python
"""Baut die Nutzlasten, die als Entitäts-Attribut `daten` an die Oberfläche
|
|
gehen.
|
|
|
|
Das Frontend liest die Werte über das ganz normale hass.states statt über
|
|
einen Dienst mit Rückgabewert: Zustände lesen ist ein seit Jahren stabiler,
|
|
einfacher Weg, während das Verhalten response-fähiger Dienste aus Sicht des
|
|
Browsers nicht durchgängig dokumentiert ist.
|
|
|
|
Hier stehen nur die reinen Rechenfunktionen - wer sie wann veröffentlicht,
|
|
entscheidet der Koordinator (koordinator.py).
|
|
|
|
GRÖSSENGRENZE, DIE JETZT KEINE MEHR IST: Zustands-Attribute sind in Home
|
|
Assistant nicht für beliebig große Datenmengen gedacht; ab ~16 KB warnt der
|
|
Recorder und schreibt sie trotzdem in die Datenbank. Die Fahrten- und
|
|
Tankvorgänge-Archive überschreiten das nach ein paar Jahren zwangsläufig. In
|
|
der pyscript-Fassung war das eine bekannte, offene Flanke. Diese Integration
|
|
markiert das Attribut `daten` als nicht aufzuzeichnen (siehe
|
|
_unrecorded_attributes in sensor.py) - die Nutzlast geht weiterhin an jede
|
|
Oberfläche, landet aber nie in der Datenbank.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from homeassistant.core import HomeAssistant
|
|
|
|
from .einstellungen import POSITIONEN, Sensorzuordnung
|
|
|
|
|
|
def zustand_oder_none(hass: HomeAssistant, entity_id: str | None) -> str | None:
|
|
"""Sicherer Zustandszugriff.
|
|
|
|
Eine nicht (mehr) existierende Entität ist hier der Normalfall, solange
|
|
Rollen im Setup-Menü unbelegt sind, und bleibt auch danach relevant: fällt
|
|
eine Datenquelle aus, soll die Oberfläche das zeigen, nicht an einem
|
|
Fehler hängen bleiben."""
|
|
if not entity_id:
|
|
return None
|
|
zustand = hass.states.get(entity_id)
|
|
if zustand is None or zustand.state in (None, "", "unknown", "unavailable"):
|
|
return None
|
|
return zustand.state
|
|
|
|
|
|
def _zu_zahl(wert: object) -> float | None:
|
|
try:
|
|
return float(wert) # type: ignore[arg-type]
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def _abs_zahl(wert: object) -> float | None:
|
|
zahl = _zu_zahl(wert)
|
|
return None if zahl is None else abs(zahl)
|
|
|
|
|
|
def _zu_bool(wert: object) -> bool | None:
|
|
"""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, statt
|
|
"steht" zu behaupten."""
|
|
if wert is None:
|
|
return None
|
|
return str(wert).lower() in ("on", "true", "1", "open", "yes")
|
|
|
|
|
|
def _standort(hass: HomeAssistant, werte: Sensorzuordnung) -> dict:
|
|
"""Live-GPS-Position - 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."""
|
|
leer = {"lat": None, "lon": None, "genauigkeit_m": None, "zeit": None}
|
|
|
|
lat_id, lon_id = werte.STANDORT_LAT_SENSOR, werte.STANDORT_LON_SENSOR
|
|
if not lat_id or not lon_id:
|
|
return leer
|
|
lat_zustand = hass.states.get(lat_id)
|
|
lon_zustand = hass.states.get(lon_id)
|
|
if lat_zustand is None or lon_zustand is None:
|
|
return leer
|
|
lat = _zu_zahl(zustand_oder_none(hass, lat_id))
|
|
lon = _zu_zahl(zustand_oder_none(hass, lon_id))
|
|
if lat is None or lon is None:
|
|
return leer
|
|
return {
|
|
"lat": lat,
|
|
"lon": lon,
|
|
"genauigkeit_m": None,
|
|
"zeit": lat_zustand.last_updated.isoformat(),
|
|
}
|
|
|
|
|
|
def _sicherheitscheck(hass: HomeAssistant, werte: Sensorzuordnung) -> list[dict]:
|
|
"""Die einzeln geprüften Punkte hinter "Sicher abgestellt", fürs Frontend -
|
|
gruppiert über das Feld "gruppe" in vier Sammelzeilen (Fahrzeug verriegelt
|
|
/ Türen und Klappen geschlossen / Fenster und Dach geschlossen / Licht
|
|
ausgeschaltet), die die Oberfläche durch Gruppieren dieser flachen Liste
|
|
berechnet, statt hier schon vier separate Listen zu bauen - eine flache
|
|
Liste bleibt die einfachere, additive Änderung gegenüber dem, was das
|
|
Frontend vorher schon kannte.
|
|
|
|
"ok" ist None, wenn der Sensor fehlt oder nicht verfügbar ist - genau
|
|
daraus leitet sich auch die zusammengefasste Kennzahl unten ab, damit
|
|
beide nie auseinanderlaufen können. Alle binary_sensor-device_classes
|
|
hier (door/window/opening/lock/light) folgen HAs eigener Konvention "off
|
|
= der gute Zustand" (zu/verriegelt/kein Licht) - ein einziges Prüfmuster
|
|
für alle Zeilen, kein Sonderfall je Art."""
|
|
eintraege: list[dict] = []
|
|
for pos, sensor in zip(POSITIONEN, werte.TUER_SENSOREN):
|
|
w = zustand_oder_none(hass, sensor)
|
|
eintraege.append({"label": f"Tür {pos}", "ok": None if w is None else w == "off", "gruppe": "tueren_klappen"})
|
|
for pos, sensor in zip(POSITIONEN, werte.FENSTER_SENSOREN):
|
|
w = zustand_oder_none(hass, sensor)
|
|
eintraege.append({"label": f"Fenster {pos}", "ok": None if w is None else w == "off", "gruppe": "fenster_dach"})
|
|
# "Kofferraum" statt "Heckklappe": alltagssprachlicher Name für dieselbe
|
|
# Öffnung beim Avant/Kombi - das Sensorfeld HECKKLAPPE_SENSOR selbst
|
|
# bleibt unverändert, um eine bestehende Zuordnung nicht zu brechen.
|
|
for label, sensor in (
|
|
("Kofferraum", werte.HECKKLAPPE_SENSOR),
|
|
("Motorhaube", werte.HAUBE_SENSOR),
|
|
):
|
|
w = zustand_oder_none(hass, sensor)
|
|
eintraege.append({"label": label, "ok": None if w is None else w == "off", "gruppe": "tueren_klappen"})
|
|
# Nur die Fahrertür: die Zentralverriegelung schließt alle Türen gemeinsam,
|
|
# ihr Zustand gilt fürs ganze Fahrzeug (siehe TUERSCHLOSS_SENSOR).
|
|
w = zustand_oder_none(hass, werte.TUERSCHLOSS_SENSOR)
|
|
eintraege.append({"label": "Türschloss", "ok": None if w is None else w == "off", "gruppe": "verriegelt"})
|
|
w = zustand_oder_none(hass, werte.DACH_SENSOR)
|
|
eintraege.append({"label": "Dach", "ok": None if w is None else w == "off", "gruppe": "fenster_dach"})
|
|
w = zustand_oder_none(hass, werte.LICHT_SENSOR)
|
|
eintraege.append({"label": "Standlicht", "ok": None if w is None else w == "off", "gruppe": "licht"})
|
|
return eintraege
|
|
|
|
|
|
def fahrzeugstatus(hass: HomeAssistant, werte: Sensorzuordnung) -> dict:
|
|
"""Bündelt die live aus Home Assistant gelesenen Fahrzeugwerte
|
|
frontend-freundlich, damit die Oberfläche keine Entity-IDs kennen muss."""
|
|
sicherheitscheck = _sicherheitscheck(hass, werte)
|
|
# "Sicher abgestellt": 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.
|
|
if any(e["ok"] is None for e in sicherheitscheck):
|
|
gesichert = None
|
|
else:
|
|
gesichert = all(e["ok"] for e in sicherheitscheck)
|
|
|
|
standort = _standort(hass, werte)
|
|
|
|
return {
|
|
"km": _zu_zahl(zustand_oder_none(hass, werte.KM_SENSOR)),
|
|
"tankprozent": _zu_zahl(zustand_oder_none(hass, werte.TANK_SENSOR)),
|
|
"reichweite_km": _zu_zahl(zustand_oder_none(hass, werte.RANGE_SENSOR)),
|
|
"batteriespannung": _zu_zahl(zustand_oder_none(hass, werte.BATTERIE_SENSOR)),
|
|
"gesichert": gesichert,
|
|
"sicherheitscheck": sicherheitscheck,
|
|
# Fährt das Fahrzeug gerade? Kommt aus derselben Zündungs-Entität, die
|
|
# auch die Fahrterkennung als maßgebliches Signal für Fahrtbeginn und
|
|
# -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, nicht "unterwegs" - eine Fahrt ohne
|
|
# Kilometerstand blieb dauerhaft "offen" und das Fahrzeug damit
|
|
# dauerhaft "fahrend". None bedeutet: kein Zündungssensor zugeordnet.
|
|
"zuendung": _zu_bool(zustand_oder_none(hass, werte.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) - die Streckensensoren liefern
|
|
# negative Restkilometer-Werte, hier deshalb der Betrag.
|
|
"oelwechsel_faellig_ts": zustand_oder_none(hass, werte.NAECHSTER_OELWECHSEL_SENSOR),
|
|
"oelwechsel_faellig_km": _abs_zahl(zustand_oder_none(hass, werte.OELWECHSEL_STRECKE_SENSOR)),
|
|
"inspektion_faellig_ts": zustand_oder_none(hass, werte.NAECHSTE_INSPEKTION_SENSOR),
|
|
"inspektion_faellig_km": _abs_zahl(zustand_oder_none(hass, werte.INSPEKTION_STRECKE_SENSOR)),
|
|
}
|