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:
2026-09-04 12:15:33 +02:00
parent 63a45d4ee2
commit 4ab6b9fca5
14 changed files with 695 additions and 182 deletions
@@ -1 +1 @@
{"version":"2026.9.4.8","sha256":"d6fe041f31480ff67d9b39884e9ef8b5ac5795d8575716d4638a9f557198bc49","bytes":319995,"gebaut":"2026-09-04T09:50:03Z"}
{"version":"2026.9.4.11","sha256":"937302e85e87c62326c4f7013cb026f50243b8cc5d521d752b5a3708120d2b91","bytes":320679,"gebaut":"2026-09-04T10:13:31Z"}
@@ -503,6 +503,33 @@ function bildInfo(i) {
}
return { label: b.label, datei: b.datei, src: `/local/bilder/${b.datei}?v=${bildVersion}` };
}
/* Alle Fahrzeugfotos einmal vorab holen UND dekodieren.
Gemeldet als "Bilder laden beim Umschalten der Menues verzoegert". Am Netz
liegt es nicht: die Adresse ist innerhalb einer Sitzung stabil und Home
Assistant liefert /local/ mit 31 Tagen Cache-Dauer. Verzoegert wird das Bild
selbst - ein frisch erzeugtes <img> wird standardmaessig asynchron dekodiert
und erst danach gezeichnet, und render() erzeugt bei jedem Seitenwechsel ein
neues. decode() erledigt genau diesen Schritt vorab.
Laeuft in der Leerlaufzeit, damit es nicht mit dem ersten Datenabruf
konkurriert, und nur einmal je Bildstand. */
let bilderVorgeladen = null;
function bilderVorladen() {
if (bilderVorgeladen === bildVersion) return;
bilderVorgeladen = bildVersion;
const starten = () => {
for (const datei of [...BILDER.map((b) => b.datei), SEITE_WINTER.datei]) {
const img = new Image();
img.src = `/local/bilder/${datei}?v=${bildVersion}`;
// Ein nicht hochgeladenes Foto ist der Normalfall - still schlucken.
if (img.decode) img.decode().catch(() => {});
}
};
if (window.requestIdleCallback) window.requestIdleCallback(starten, { timeout: 4000 });
else setTimeout(starten, 2000);
}
function carImg(modus) {
if (modus === "fest") {
const info = bildInfo(startIndex());
@@ -3242,28 +3269,6 @@ function vEinst() {
` : ""}
<button class="aktion ${einrichtenOffen ? "primaer" : ""}" style="margin-top:16px" data-einrichten-auf>${einrichtenOffen ? "Fertig" : "Einrichten"}</button>
</div>
${ZUGAENGE.length ? `<div class="tile"><span class="label">Zugänge</span>
${ZUGAENGE.map((z, i) => `
<button class="acc-h" aria-expanded="false" data-acc="zug${i}"><span class="mark"></span>
<span class="k">${esc(z.titel)}</span>
<span class="v" style="color:${z.gesetzt ? "var(--ok)" : "var(--fg3)"}">${
z.gesetzt ? (z.rest ? "gesetzt · …" + esc(z.rest) : "gesetzt") : "nicht gesetzt"
}</span></button>
<div class="acc-b" id="zug${i}">
<span class="label" style="margin-top:8px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">${esc(z.zweck)}</span>
<div class="feld" style="border-bottom:0"><label for="tok${i}">Neuer Token</label>
<input id="tok${i}" type="password" autocomplete="off" data-tokenfeld="${esc(z.dienst)}"
placeholder="${z.gesetzt ? "ersetzt den hinterlegten" : "noch keiner hinterlegt"}"></div>
<button class="aktion primaer" data-tokenspeichern="${esc(z.dienst)}">Speichern</button>
${z.gesetzt ? `<button class="aktion loeschen" data-tokenloeschen="${esc(z.dienst)}">Löschen</button>` : ""}
</div>`).join("")}
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">
Ein gespeicherter Token wird nie wieder angezeigt nur ob einer hinterlegt ist.
Dieselben Felder gibt es unter Einstellungen Geräte &amp; Dienste Audi Dashboard
Konfigurieren.</span>
</div>` : ""}
${(() => {
/* Der Regler schreibt NICHT mehr beim Loslassen. Sein Wert stellt zugleich
den Schlaf-Timeout des Dongles - eine Änderung am Fahrzeug, die über zwei
@@ -3280,8 +3285,10 @@ function vEinst() {
aria-label="Minuten bis zum automatischen Fahrtende">
<output>${minuten} Min.</output>
</div>
${geaendert ? `<button class="aktion primaer" style="margin-top:14px" data-pausenzeitspeichern>Speichern</button>
<button class="aktion" data-pausenzeitverwerfen>Verwerfen</button>` : ""}
${geaendert ? `<div class="knopfpaar">
<button class="aktion" data-pausenzeitverwerfen>Verwerfen</button>
<button class="aktion primaer" data-pausenzeitspeichern>Speichern</button>
</div>` : ""}
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">
Hier können Sie einstellen, wie viele Minuten nach Ausschalten der Zündung die
@@ -3291,36 +3298,6 @@ function vEinst() {
Fortsetzen aber ein paar Sekunden fürs Aufwachen.</span>
</div>`;
})()}
${DONGLE && DONGLE.aktiv ? `<div class="tile"><span class="label">Dongle</span>
${DONGLE.stand && DONGLE.stand.fehler
? `<span class="label" style="margin-top:8px;color:var(--bad);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">${esc(DONGLE.stand.fehler)}</span>`
: ""}
${DONGLE.stand && (DONGLE.stand.werte || []).length ? `<dl class="rows">
${DONGLE.stand.werte.map((w) => `<div class="row">
<dt>${esc(w.titel)} <small style="color:var(--fg3)">${esc(w.nummer)}</small></dt>
<dd${w.abweichung ? ` style="color:var(--warn)"` : ""}>${
w.geraet === null || w.geraet === undefined ? "" : esc(String(w.geraet))
}${w.einheit ? " " + esc(w.einheit) : ""}${
w.offen !== null && w.offen !== undefined
? ` <small>→ ${esc(String(w.offen))}, unterwegs</small>`
: w.abweichung ? ` <small>(wir: ${esc(String(w.unser))})</small>` : ""
}</dd>
</div>`).join("")}
</dl>` : ""}
<button class="aktion" data-dongle ${DONGLE_LAEUFT ? "disabled" : ""}>${
DONGLE_LAEUFT ? `<span class="lade-spinner"></span>Liest …` : "Jetzt lesen"
}</button>
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">
${DONGLE.stand && DONGLE.stand.gelesen_am
// Der Punkt der Abkuerzung ist zugleich der Satzpunkt - sonst stuende
// dort "vor 1 Std..". "vor 3 Tage" hat keinen und bekommt einen.
? `Gelesen ${esc(alterText(Date.parse(DONGLE.stand.gelesen_am)).replace(/\.$/, ""))}. `
: "Noch nicht gelesen. "}Der Regler oben schreibt den Schlaf-Timeout sofort
hierher; bis das Gerät sich das nächste Mal meldet, steht die Änderung als
unterwegs" daneben.</span>
</div>` : ""}
<div class="tile"><span class="label">Darstellung</span>
<div class="seg">
<button data-mode="nacht" class="${HOST.dataset.theme === "nacht" ? "on" : ""}">Nacht</button>
@@ -3404,12 +3381,14 @@ function vEinst() {
<label class="aktion" style="display:block;text-align:center;cursor:pointer">
Backup importieren<input type="file" accept="application/json" data-backup="import" hidden></label>
</div>
<!-- Installierte Fassung und ihr Update stehen in EINER Kachel: es ist
dieselbe Sache aus zwei Blickwinkeln, und zwei Kacheln nebeneinander
zwangen zum Vergleichen zweier Zahlen ueber eine Kachelgrenze hinweg
(Vorgabe des Eigentuemers, 04.09.2026). -->
<div class="tile"><span class="label">Version</span>
<dl class="rows">
<div class="row" style="border-bottom:0"><dt>Installiert</dt><dd>${esc(APP_VERSION || "unbekannt")}<small>Home-Assistant-Integration</small></dd></div>
</dl>
</div>
<div class="tile"><span class="label">Integration-Update</span>
${(() => {
const u = INTEGRATION_UPDATE;
const laeuft = INTEGRATION_UPDATE_LAEUFT;
@@ -3460,11 +3439,112 @@ function vEinst() {
${INTEGRATION_UPDATE && INTEGRATION_UPDATE.fehler && !INTEGRATION_UPDATE_LAEUFT
? `<span class="label" style="margin-top:10px;color:var(--red)">${esc(INTEGRATION_UPDATE.fehler)}</span>`
: ""}
</div>`;
</div>
<!-- Debug-Mode: Zugaenge, Dongle und der letzte Datensatz des Geraets.
Drei Kacheln, die im Alltag nichts zu suchen haben und beim Suchen
eines Fehlers alles - deshalb zusammen, zugeklappt und ganz unten
(Vorgabe des Eigentuemers, 04.09.2026). -->
<div class="tile"><span class="label">Debug-Mode</span>
<span class="label" style="margin-top:8px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">
Zugangs-Token, die Einstellungen des Dongles und der letzte Datensatz, den
das Gerät gemeldet hat. Für die Fehlersuche, nicht für den Alltag.</span>
<button class="aktion ${debugOffen ? "primaer" : ""}" style="margin-top:16px" data-debug-auf>${debugOffen ? "Fertig" : "Öffnen"}</button>
</div>
${debugOffen ? `
${ZUGAENGE.length ? `<div class="tile"><span class="label">Zugänge</span>
${ZUGAENGE.map((z, i) => `
<button class="acc-h" aria-expanded="false" data-acc="zug${i}"><span class="mark"></span>
<span class="k">${esc(z.titel)}</span>
<span class="v" style="color:${z.gesetzt ? "var(--ok)" : "var(--fg3)"}">${
z.gesetzt ? (z.rest ? "gesetzt · …" + esc(z.rest) : "gesetzt") : "nicht gesetzt"
}</span></button>
<div class="acc-b" id="zug${i}">
<span class="label" style="margin-top:8px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">${esc(z.zweck)}</span>
<div class="feld" style="border-bottom:0"><label for="tok${i}">Neuer Token</label>
<input id="tok${i}" type="password" autocomplete="off" data-tokenfeld="${esc(z.dienst)}"
placeholder="${z.gesetzt ? "ersetzt den hinterlegten" : "noch keiner hinterlegt"}"></div>
<button class="aktion primaer" data-tokenspeichern="${esc(z.dienst)}">Speichern</button>
${z.gesetzt ? `<button class="aktion loeschen" data-tokenloeschen="${esc(z.dienst)}">Löschen</button>` : ""}
</div>`).join("")}
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">
Ein gespeicherter Token wird nie wieder angezeigt nur ob einer hinterlegt ist.
Dieselben Felder gibt es unter Einstellungen Geräte &amp; Dienste Audi Dashboard
Konfigurieren.</span>
</div>` : ""}
${DONGLE && DONGLE.aktiv ? `<div class="tile"><span class="label">Dongle</span>
${DONGLE.stand && DONGLE.stand.fehler
? `<span class="label" style="margin-top:8px;color:var(--bad);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">${esc(DONGLE.stand.fehler)}</span>`
: ""}
${DONGLE.stand && (DONGLE.stand.werte || []).length ? `<dl class="rows">
${DONGLE.stand.werte.map((w) => `<div class="row">
<dt>${esc(w.titel)} <small style="color:var(--fg3)">${esc(w.nummer)}</small></dt>
<dd${w.abweichung ? ` style="color:var(--warn)"` : ""}>${
w.geraet === null || w.geraet === undefined ? "" : esc(String(w.geraet))
}${w.einheit ? " " + esc(w.einheit) : ""}${
w.offen !== null && w.offen !== undefined
? ` <small>→ ${esc(String(w.offen))}, unterwegs</small>`
: w.abweichung ? ` <small>(wir: ${esc(String(w.unser))})</small>` : ""
}</dd>
</div>`).join("")}
</dl>` : ""}
<button class="aktion" data-dongle ${DONGLE_LAEUFT ? "disabled" : ""}>${
DONGLE_LAEUFT ? `<span class="lade-spinner"></span>Liest …` : "Jetzt lesen"
}</button>
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">
${DONGLE.stand && DONGLE.stand.gelesen_am
// Der Punkt der Abkuerzung ist zugleich der Satzpunkt - sonst stuende
// dort "vor 1 Std..". "vor 3 Tage" hat keinen und bekommt einen.
? `Gelesen ${esc(alterText(Date.parse(DONGLE.stand.gelesen_am)).replace(/\.$/, ""))}. `
: "Noch nicht gelesen. "}Der Regler oben schreibt den Schlaf-Timeout sofort
hierher; bis das Gerät sich das nächste Mal meldet, steht die Änderung als
unterwegs" daneben.</span>
</div>` : ""}
${(() => {
/* Der letzte Datensatz, wie er in Home Assistant angekommen ist - siehe
geraet.py. Der Abstand zwischen Gerätezeit und Ankunft ist die Angabe,
die bei gepufferten Datensätzen zählt: genau dieser Rückstand war die
Wurzel des 3-km-Fahrt-Fehlers. */
const d = LETZTER_DATENSATZ;
if (!d) return `<div class="tile"><span class="label">Letzter Datensatz</span>
<span class="label" style="margin-top:8px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">
Noch nichts gemeldet. Dafür braucht es die Meldezeit oder das
Fahrtsignal im Setup - und mindestens einen Datensatz des Geräts,
seit Home Assistant läuft.</span></div>`;
const rueckstand = d.meldezeit && d.angekommen
? Math.round((Date.parse(d.angekommen) - Date.parse(d.meldezeit)) / 1000)
: null;
return `<div class="tile"><span class="label">Letzter Datensatz</span>
<dl class="rows">
<div class="row"><dt>Gerät</dt><dd>${esc(d.geraet || "unbekannt")}</dd></div>
<div class="row"><dt>Angekommen</dt><dd>${dezeitpunkt(new Date(d.angekommen))}</dd></div>
<div class="row"><dt>Gerätezeit</dt><dd>${d.meldezeit ? dezeitpunkt(new Date(d.meldezeit)) : ""}${
rueckstand !== null ? `<small>Rückstand ${de(rueckstand)} s</small>` : ""
}</dd></div>
${d.werte.map((w) => `<div class="row"><dt>${esc(w.name)}</dt><dd>${esc(String(w.wert))}${
w.einheit ? " " + esc(w.einheit) : ""
}</dd></div>`).join("")}
</dl>
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">
${de(d.gesamt)} Werte aus derselben Meldung. Was das Gerät in diesem
Datensatz nicht geschickt hat, steht hier nicht - es behält in Home
Assistant seinen alten Wert. Nach einem Neustart ist Angekommen" der
Zeitpunkt des Neustarts, nicht der einer Übertragung der Rückstand
sagt dann, wie lange das Gerät schon schweigt.</span>
</div>`;
})()}
` : ""}`;
}
let nmOffen = false;
let einrichtenOffen = false;
let debugOffen = false;
/* Entwurf des Reglers "Fahrt beenden" (Minuten) - null heisst: nicht angefasst.
Er liegt bewusst NEBEN CONFIG.pausenzeit und nicht darin: erst der Knopf
"Speichern" uebernimmt ihn, weil derselbe Wert den Schlaf-Timeout des
@@ -4377,6 +4457,9 @@ let ZUGAENGE = [];
/* Was am Dongle steht, was noch aussteht - siehe flespi.py im Backend.
null, solange kein flespi-Token hinterlegt ist. */
let DONGLE = null;
/* Der letzte Datensatz, den das Geraet gemeldet hat - siehe geraet.py im
Backend. null, solange weder Meldezeit noch Fahrtsignal zugeordnet sind. */
let LETZTER_DATENSATZ = null;
// true, waehrend "Jetzt lesen" laeuft. Wie INTEGRATION_UPDATE_LAEUFT und aus
// demselben Grund: der Abruf geht ueber zwei Netze (wir -> flespi -> Geraet)
// und darf nicht wie ein toter Knopf aussehen.
@@ -5977,6 +6060,7 @@ function ereignisseVerdrahten() {
}
if (e.target.closest("[data-nmauf]")) { nmOffen = !nmOffen; render(); return; }
if (e.target.closest("[data-einrichten-auf]")) { einrichtenOffen = !einrichtenOffen; render(); return; }
if (e.target.closest("[data-debug-auf]")) { debugOffen = !debugOffen; render(); return; }
if (e.target.closest("[data-nmspeichern]")) {
const wert = parseInt($("#nm").value, 10);
if (!isNaN(wert) && wert > 0) { CAR.reifen.anzugsmoment = wert; profilSpeichern(); }
@@ -6701,6 +6785,9 @@ async function datenLaden(erzwingen) {
DONGLE = versionState.attributes.daten
? versionState.attributes.daten.dongle ?? null
: null;
LETZTER_DATENSATZ = versionState.attributes.daten
? versionState.attributes.daten.letzter_datensatz ?? null
: null;
}
if (entitaetenState && entitaetenState.attributes) ENTITAETEN = entitaetenState.attributes.daten;
@@ -6743,6 +6830,9 @@ async function datenLaden(erzwingen) {
nachladeVersuche = 0;
render();
// Erst nach dem ersten Zeichnen: die Fotos sollen den Aufbau der Seite
// nicht ausbremsen, sondern den naechsten Seitenwechsel beschleunigen.
bilderVorladen();
return;
}
}
@@ -1657,3 +1657,17 @@ img.carfix.klein { aspect-ratio: auto; height: auto; }
scroll-behavior: auto !important;
}
}
/* Ein Paar aus Bestaetigen und Abbrechen steht nebeneinander, beide gleich
breit: es ist eine Entscheidung zwischen zwei Wegen, keine Haupt- und
Nebensache. Welcher der gewollte ist, sagt die Fuellung, nicht die Groesse -
und der bestaetigende steht rechts, wie ueberall in iOS. */
.knopfpaar {
display: flex;
gap: 10px;
margin-top: var(--sp-4);
}
.knopfpaar .aktion {
flex: 1 1 0;
margin-top: 0;
}
+140
View File
@@ -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),
}
@@ -51,6 +51,7 @@ from . import (
batterie,
fahrterkennung,
flespi,
geraet,
identitaet,
reifen,
screening,
@@ -253,6 +254,12 @@ class Koordinator:
await self.profil_veroeffentlichen()
await self.fahrten_veroeffentlichen()
await self.tankvorgaenge_veroeffentlichen()
# Muss mitlaufen, seit dort der letzte Datensatz des Dongles steht:
# beim Start haben dessen Entitaeten noch gar keinen Zustand, die
# Angabe waere sonst dauerhaft leer. Kostet einen kleinen Dateilesevorgang
# (bundle.json) je Minute und KEINEN Netzaufruf - die Pruefung bei Gitea
# bleibt an ihrem Dienst, siehe Abschnitt J.
await self.app_version_veroeffentlichen()
async def _takt_batterie(self, _jetzt: datetime.datetime) -> None:
await batterie.pruefen(self)
@@ -636,6 +643,12 @@ class Koordinator:
"aktiv": flespi.zugang(self) is not None,
"stand": self.flespi_stand,
},
# Der letzte Datensatz des Dongles, wie er hier angekommen
# ist - fuer die Fehlersuche, siehe geraet.py. None, solange
# weder Meldezeit noch Fahrtsignal zugeordnet sind.
"letzter_datensatz": geraet.letzter_datensatz(
self.hass, self.zuordnung.werte
),
},
)
@@ -1,7 +1,7 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.9.4.8",
"version": "2026.9.4.11",
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
"codeowners": [