4ab6b9fca5
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.
141 lines
5.5 KiB
Python
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),
|
|
}
|