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.
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
"""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),
|
||||
}
|
||||
Reference in New Issue
Block a user