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:
@@ -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"}
|
||||
Binary file not shown.
@@ -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 & 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 & 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;
|
||||
}
|
||||
|
||||
@@ -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": [
|
||||
|
||||
Reference in New Issue
Block a user