Files
audi-app/custom_components/audi_dashboard/geraet.py
T
tobias 4ab6b9fca5 Debug-Mode, eine Version-Kachel, Knoepfe nach HIG, Bilder ohne Verzoegerung (2026.9.4.11)
Debug-Mode: Zugaenge und Dongle stehen nicht mehr verstreut oben in den
Einstellungen, sondern zusammen hinter einem Schalter am Seitenende - dazu
neu der letzte Datensatz des Dongles (geraet.py). "Letzter Datensatz" heisst
dabei: alle Entitaeten des Geraets, deren Aktualisierung hoechstens zwei
Sekunden nach der juengsten liegt - die flespi-Integration setzt einen
Datensatz in Millisekunden. Angezeigt mit Ankunft, Geraetezeit und dem
Rueckstand zwischen beiden.

Beim Verifizieren gefunden: app_version_veroeffentlichen() lief nur beim
Start und nach einer Aktion, und beim Start haben die Entitaeten des Geraets
noch keinen Zustand - die Kachel waere dauerhaft leer geblieben. Laeuft
jetzt im Minutentakt mit, ohne Netzaufruf.

Ausserdem:
- "Version" und "Integration-Update" sind eine Kachel
- Speichern/Verwerfen als zwei gleich breite Knoepfe, der bestaetigende
  gefuellt und rechts; "Verwerfen" war in der App blosser Text
- Bilder werden nach dem ersten Zeichnen in der Leerlaufzeit vorgeladen UND
  dekodiert. Die Verzoegerung beim Seitenwechsel kam nicht vom Netz (31 Tage
  Cache), sondern vom asynchronen Dekodieren eines frisch erzeugten <img>.

214/214 Tests, Panel als Modul geparst, live gegen audi_ha_test geprueft.
2026-09-04 12:15:33 +02:00

141 lines
5.5 KiB
Python

"""Der letzte Datensatz des Dongles, so wie er in Home Assistant angekommen ist.
WOFUER
------
Fuer die Fehlersuche am Fahrzeug, nicht fuer den taeglichen Gebrauch: Steht das
Fahrzeug wirklich, oder ist nur die Meldung alt? Liefert das Geraet gerade
ueberhaupt CAN-Werte? Die Antwort steht in Home Assistants Zustaenden, aber
verteilt auf siebzig Entitaeten - hier zusammengefasst an einer Stelle, die
beide Oberflaechen ohne eigene Registerabfrage lesen koennen.
WAS "EIN DATENSATZ" IST
-----------------------
Die flespi-Integration setzt die Entitaeten EINES Geraetedatensatzes
nacheinander, aber innerhalb weniger Millisekunden (am 01.09.2026 gemessen:
Zuendung 07:35:23.633, zugehoerige Meldezeit 07:35:23.635 - siehe Abschnitt AY
der AGENTS.md). Der letzte Datensatz ist deshalb: alle Entitaeten des Geraets,
deren letzte Aktualisierung hoechstens FENSTER_S nach der juengsten liegt.
Das ist bewusst eine Naeherung ueber die ANKUNFTSzeit. Die Zeit des Geraets
steht daneben (aus MELDEZEIT_SENSOR), und genau der Abstand zwischen beiden ist
die Angabe, die bei gepufferten Datensaetzen interessiert - er war die Wurzel
des 3-km-Fahrt-Fehlers (Abschnitt CB).
WELCHES GERAET
--------------
Nicht geraten: genommen wird das Geraet der Entitaet, die MELDEZEIT_SENSOR
zugeordnet ist, sonst das des Fahrtsignals. Ist keines von beiden zugeordnet,
gibt es nichts zu zeigen - dann bleibt der Rueckgabewert None, statt
irgendein Geraet der Instanz zu waehlen.
"""
from __future__ import annotations
import logging
from datetime import datetime, timedelta, timezone
from typing import TYPE_CHECKING, Any
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import entity_registry as er
from . import verlauf
if TYPE_CHECKING:
from .einstellungen import Sensorzuordnung
_LOGGER = logging.getLogger(__name__)
# Wie weit ein Wert hinter dem juengsten liegen darf, um noch zum selben
# Datensatz gezaehlt zu werden. Gemessen wurden Millisekunden; zwei Sekunden
# sind grosszuegig genug fuer eine langsame Instanz und immer noch weit von der
# kuerzesten Sendeperiode des Geraets entfernt (10 s beim Fahren).
FENSTER_S = 2.0
# Ein Datensatz des FMM003 traegt je nach Konfiguration einige Dutzend Werte.
# Die Obergrenze ist kein fachliches Limit, sondern verhindert, dass eine
# fehlerhafte Zuordnung die veroeffentlichten Attribute aufblaeht.
MAX_WERTE = 80
def _meldezeit(hass: HomeAssistant, werte: Sensorzuordnung) -> str | None:
"""Die Zeit, die das Geraet dem letzten Datensatz gegeben hat."""
if not werte.MELDEZEIT_SENSOR:
return None
zustand = hass.states.get(werte.MELDEZEIT_SENSOR)
if zustand is None:
return None
try:
gemeldet = datetime.fromtimestamp(float(zustand.state), tz=timezone.utc)
except (TypeError, ValueError, OSError, OverflowError):
return None
# Dieselbe Pruefung wie in der Fahrterkennung: eine Sensor-ID statt eines
# Zeitstempels ergibt sonst einen Zeitpunkt kurz nach 1970 (Abschnitt BO).
if not verlauf.geraetezeit_plausibel(gemeldet, datetime.now(timezone.utc)):
return None
return gemeldet.isoformat()
def _geraet_id(hass: HomeAssistant, werte: Sensorzuordnung) -> str | None:
register = er.async_get(hass)
for eid in (werte.MELDEZEIT_SENSOR, verlauf.fahrtsignal(werte), werte.ZUENDUNG_SENSOR):
if not eid:
continue
eintrag = register.async_get(eid)
if eintrag and eintrag.device_id:
return eintrag.device_id
return None
def letzter_datensatz(hass: HomeAssistant, werte: Sensorzuordnung) -> dict[str, Any] | None:
"""Die Werte der juengsten Meldung des Dongles - None, wenn keine da ist."""
geraet_id = _geraet_id(hass, werte)
if not geraet_id:
return None
register = er.async_get(hass)
eintraege = er.async_entries_for_device(register, geraet_id, include_disabled_entities=False)
zustaende = []
for eintrag in eintraege:
zustand = hass.states.get(eintrag.entity_id)
# unavailable/unknown tragen keinen Wert - sie stehen fuer eine
# Entitaet, die das Geraet in diesem Datensatz nicht gemeldet hat.
if zustand is None or zustand.state in ("unavailable", "unknown"):
continue
zustaende.append(zustand)
if not zustaende:
return None
juengste = max(z.last_updated for z in zustaende)
grenze = juengste - timedelta(seconds=FENSTER_S)
im_datensatz = sorted(
(z for z in zustaende if z.last_updated >= grenze),
key=lambda z: z.attributes.get("friendly_name") or z.entity_id,
)
geraete_register = dr.async_get(hass)
geraet = geraete_register.async_get(geraet_id)
name = (geraet.name if geraet else None) or geraet_id
def kurz(voll: str) -> str:
"""Der Geraetename steht schon in der Kopfzeile - hier nur das Signal."""
rest = voll[len(name):].strip() if voll.startswith(name) else voll
return rest or voll
return {
"geraet": name,
"angekommen": juengste.isoformat(),
# Die Zeit, die das GERAET dem Datensatz gegeben hat. Der Abstand zur
# Ankunft ist der Puffer-Rueckstand.
"meldezeit": _meldezeit(hass, werte),
"werte": [
{
"name": kurz(z.attributes.get("friendly_name") or z.entity_id),
"wert": z.state,
"einheit": z.attributes.get("unit_of_measurement"),
}
for z in im_datensatz[:MAX_WERTE]
],
"gesamt": len(im_datensatz),
}