Neustart-Festhaenger, OTA-Pruefsumme, Statistik-Kopfzahl (2026.8.31.4)

Die App blieb nach einem Neustart von Home Assistant auf dem alten Stand
stehen, mit einer 502-Fehlerzeile, die erst ein Beenden der App wegbekam.
Zwei Ursachen: 502/503/504 galten als harter Ladefehler statt als "Server
faehrt gerade hoch", und nach dem Wiederverbinden wurde genau einmal
nachgeladen - schlug das fehl, kam nie wieder ein Versuch. Beides behoben,
vier Regressionstests dazu.

OTA: das Buendel ist von HEAD ueber die Platte bis zu den ausgelieferten
Bytes nachweislich deckungsgleich. Die Fassung haengt jetzt trotzdem als
?v= an der Adresse (der statische Pfad kommt ohne Cache-Control, aber mit
ETag), und ein Pruefsummenfehler wird erklaert statt durchgereicht.

Statistik: der Gesamtwert steht jetzt als grosse Zahl in der Kopfzeile,
darunter bleiben vier Spalten - fuenf passen auf dem Telefon nachweislich
nicht in eine Zeile.

Fahrzeugfoto ohne Beschnitt und ohne Bodenschatten, damit auch ohne den
Schalter dafuer. Setup filtert zusaetzlich nach dem Signalnamen aus dem
Katalog.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-31 15:35:56 +02:00
co-authored by Claude Opus 5
parent 4af38d7e65
commit 90efcc5b00
19 changed files with 565 additions and 161 deletions
@@ -368,12 +368,12 @@ function markenlogo() {
<span class="ph">${esc(label)}</span>
</span>`;
}
const OHNE_SZENE = ["Cockpit", "Sitze", "Scheinwerfer"];
function szeneKlasse(i) {
if (!bodenAn) return "";
if (OHNE_SZENE.includes(BILDER[i].name)) return "";
return CAR.reifen.aktiv === "Winter" ? "winter" : "strasse";
}
/* Der angedeutete Boden unter dem Fahrzeug ist am 2026-08-31 entfallen -
der Eigentuemer wollte den Schatten weg, und damit hatte auch der
Schalter dafuer nichts mehr zu schalten. Frueher stand hier
szeneKlasse(), das je nach Bild und Reifensatz "strasse" oder "winter"
an .szene haengte; die zugehoerigen ::after-Regeln sind in
audi-dashboard.css beschrieben. */
function bildWeiter() {
bild = (bild + 1) % BILDER.length;
const img = ROOT.getElementById("carimg");
@@ -387,7 +387,7 @@ function bildWeiter() {
box.querySelector(".platzhalter-label").textContent = info.label;
}
const sz = ROOT.getElementById("szene");
if (sz) sz.className = "szene " + szeneKlasse(bild);
if (sz) sz.className = "szene";
[...ROOT.getElementById("dots").children].forEach((d, i) => d.classList.toggle("on", i === bild));
}
@@ -1654,7 +1654,7 @@ function vHome() {
zwei kurze, haeufig gelesene Textzeilen lagen bisher unter einer 190px
hohen Karte und damit auf dem Telefon unterhalb des ersten Bildschirms. */
return `
<div class="szene bleed ${szeneKlasse(startIndex())}"><div class="carbox">${carImg("fest")}</div></div>
<div class="szene bleed"><div class="carbox">${carImg("fest")}</div></div>
<div class="tile flat" style="margin-bottom:20px">
${badge()}
<span class="label" style="margin-top:10px">${esc(CONFIG.kennzeichen)}</span>
@@ -1815,7 +1815,7 @@ function vTuerenKlappen() {
function vAudi() {
const r = CAR.reifen, a = r[r.aktiv.toLowerCase()];
return `
<div class="szene ${szeneKlasse(bild)}" id="szene"><div class="carbox">${carImg("galerie")}</div></div>
<div class="szene" id="szene"><div class="carbox">${carImg("galerie")}</div></div>
<div class="tile flat" style="margin:14px 0 16px">
${badge()}
<span class="label" style="margin-top:12px">${esc(CONFIG.kennzeichen)}</span>
@@ -2793,7 +2793,6 @@ const SCHRIFT_KEY = "audi-dashboard-tableiste-schrift";
let beschriftung = (() => {
try { return localStorage.getItem(SCHRIFT_KEY) !== "aus"; } catch (e) { return true; }
})();
let bodenAn = true;
function csvDatei(name, kopf, zeilen) {
const txt = [kopf.join(";")].concat(zeilen.map((r) => r.join(";"))).join("\r\n");
const a = document.createElement("a");
@@ -2858,10 +2857,6 @@ function vEinst() {
<label for="schrift">Beschriftung in der Menüleiste</label>
<label class="switch"><input id="schrift" type="checkbox" data-schrift ${beschriftung ? "checked" : ""}><span class="track"></span></label></div>
<span class="label" style="margin-top:14px">Aus zeigt nur die Symbole und gibt etwas Höhe frei</span>
<div class="feld" style="margin-top:16px;border-bottom:0">
<label for="boden">Fahrbahn unter dem Fahrzeug</label>
<label class="switch"><input id="boden" type="checkbox" data-boden ${bodenAn ? "checked" : ""}><span class="track"></span></label></div>
<span class="label" style="margin-top:14px">Angedeutete Fahrbahn mit Schatten, nur bei Außenansichten</span>
</div>
<div class="tile"><span class="label">Bild der Übersicht</span>
<div class="feld" style="border-bottom:0"><label for="startbild">Ansicht</label>
@@ -3040,6 +3035,10 @@ function entitaetScore(feld, e, suchtext) {
if (feld.domains && feld.domains.includes(e.domain)) score += 3;
if (feld.device_classes && feld.device_classes.length && feld.device_classes.includes(e.deviceClass)) score += 3;
if (feld.units && feld.units.length && feld.units.includes(e.unit)) score += 2;
// Der Signalname aus dem Katalog ("z. B. can_fuel_volume") ist der
// treffsicherste Hinweis, den es gibt - deutlich mehr wert als ein
// einzelnes Stichwort.
if (feld.beispiel && hay.includes(feld.beispiel.toLowerCase())) score += 8;
if (suchtext && hay.includes(suchtext.trim().toLowerCase())) score += 10;
return score;
}
@@ -3048,7 +3047,7 @@ function entitaetKandidaten(feldKey, suchtext, aktuellerWert) {
const feld = setupKatalog().find((f) => f.key === feldKey);
if (!feld) return [];
const text = (suchtext || "").trim().toLowerCase();
const liste = ALLE_ENTITAETEN.filter((e) => {
let liste = ALLE_ENTITAETEN.filter((e) => {
if (e.id === aktuellerWert) return true;
if (setupNurPassend) {
if (feld.domains && feld.domains.length && !feld.domains.includes(e.domain)) return false;
@@ -3065,6 +3064,27 @@ function entitaetKandidaten(feldKey, suchtext, aktuellerWert) {
if (text && !(e.id.toLowerCase().includes(text) || e.name.toLowerCase().includes(text))) return false;
return true;
});
/* "Nur passende Sensoren" sucht zusaetzlich nach dem Namen, der neben der
Ueberschrift als "(z. B. ...)" steht - dem tatsaechlichen Signalnamen der
Integration, nicht einem ausgedachten Beispiel (siehe FELDER in
einstellungen.py). Domain und Einheit allein lassen bei einem Fahrzeug
mit siebzig Sensoren immer noch Dutzende uebrig; der Signalname trifft
genau einen.
Mit Rueckfall: findet der Name nichts, bleibt die ungefilterte Liste
stehen. Eine leere Auswahl waere schlimmer als eine lange - der Beispiel-
name stammt aus dieser einen Installation, eine andere kann ihre Signale
anders nennen. */
if (setupNurPassend && feld.beispiel) {
const beispiel = feld.beispiel.toLowerCase();
const genau = liste.filter(
(e) =>
e.id === aktuellerWert ||
e.id.toLowerCase().includes(beispiel) ||
e.name.toLowerCase().includes(beispiel.replace(/_/g, " ")),
);
if (genau.length) liste = genau;
}
liste.sort((a, b) => entitaetScore(feld, b, suchtext) - entitaetScore(feld, a, suchtext));
return liste.slice(0, 60);
}
@@ -3647,6 +3667,13 @@ function vStat() {
const fJahr = fahrtenSeit("jahr"), fMonat = fahrtenSeit("monat"), fWoche = fahrtenSeit("woche"), fTag = fahrtenSeit("tag");
const fAbs = fahrtenSeit("absolut"), tAbs = tankSeit("absolut");
const kmJahr = sum(fJahr, (t) => t.distance_km), hJahr = sum(fJahr, (t) => t.duration_s) / 3600;
// Die grosse Zahl oben in den beiden Kacheln zeigt seit dem 2026-08-31 den
// Gesamtwert, nicht mehr den des laufenden Jahres (Ansage des Eigentuemers).
// Damit steht "Absolut" dort, wo man es zuerst liest, und die Spalten
// darunter bleiben die vier Zeitraeume - fuenf passten auf dem Telefon
// nachweislich nicht in eine Zeile.
const kmAbs = sum(fAbs, (t) => t.distance_km), hAbs = sum(fAbs, (t) => t.duration_s) / 3600;
const lAbs = sum(tAbs, (f) => f.liters), eAbs = sum(tAbs, (f) => f.fuel_total_eur || 0);
const tJahr = tankSeit("jahr");
const lJahr = sum(tJahr, (f) => f.liters), eJahr = sum(tJahr, (f) => f.fuel_total_eur || 0);
const tagKm = sum(fJahr.filter((t) => !istNachtZeit(t.ts_start)), (t) => t.distance_km);
@@ -3655,12 +3682,12 @@ function vStat() {
const arbeitKm = sum(arbeitFahrten, (t) => t.distance_km), privatKm = kmJahr - arbeitKm;
const lv = langzeitverbrauch();
return `
${statTile("s1", `Distanz und Fahrzeit ${jahr}`, de(kmJahr), "km", de(hJahr) + " h",
quad("Gefahrene Distanz [km]", [[de(kmJahr), "Jahr"], [de(sum(fMonat, (t) => t.distance_km)), "Monat"], [de(sum(fWoche, (t) => t.distance_km)), "Woche"], [de(sum(fTag, (t) => t.distance_km)), "Tag"], [de(sum(fAbs, (t) => t.distance_km)), "Absolut"]]) +
quad("Fahrzeit [h]", [[de(hJahr), "Jahr"], [de(sum(fMonat, (t) => t.duration_s) / 3600), "Monat"], [de(sum(fWoche, (t) => t.duration_s) / 3600, 1), "Woche"], [de(sum(fTag, (t) => t.duration_s) / 3600, 1), "Tag"], [de(sum(fAbs, (t) => t.duration_s) / 3600), "Absolut"]]))}
${statTile("s2", `Getankte Menge und Kosten ${jahr}`, de(lJahr), "l", de(eJahr) + " €",
quad("Getankte Liter [l]", [[de(lJahr), "Jahr"], [de(sum(tankSeit("monat"), (f) => f.liters)), "Monat"], [de(sum(tankSeit("woche"), (f) => f.liters)), "Woche"], [de(sum(tankSeit("tag"), (f) => f.liters)), "Tag"], [de(sum(tAbs, (f) => f.liters)), "Absolut"]]) +
quad("Kraftstoffkosten [€]", [[de(eJahr), "Jahr"], [de(sum(tankSeit("monat"), (f) => f.fuel_total_eur || 0)), "Monat"], [de(sum(tankSeit("woche"), (f) => f.fuel_total_eur || 0)), "Woche"], [de(sum(tankSeit("tag"), (f) => f.fuel_total_eur || 0)), "Tag"], [de(sum(tAbs, (f) => f.fuel_total_eur || 0)), "Absolut"]]))}
${statTile("s1", "Distanz und Fahrzeit", de(kmAbs), "km", de(hAbs) + " h",
quad("Gefahrene Distanz [km]", [[de(kmJahr), "Jahr"], [de(sum(fMonat, (t) => t.distance_km)), "Monat"], [de(sum(fWoche, (t) => t.distance_km)), "Woche"], [de(sum(fTag, (t) => t.distance_km)), "Tag"]]) +
quad("Fahrzeit [h]", [[de(hJahr), "Jahr"], [de(sum(fMonat, (t) => t.duration_s) / 3600), "Monat"], [de(sum(fWoche, (t) => t.duration_s) / 3600, 1), "Woche"], [de(sum(fTag, (t) => t.duration_s) / 3600, 1), "Tag"]]))}
${statTile("s2", "Getankte Menge und Kosten", de(lAbs), "l", de(eAbs) + " €",
quad("Getankte Liter [l]", [[de(lJahr), "Jahr"], [de(sum(tankSeit("monat"), (f) => f.liters)), "Monat"], [de(sum(tankSeit("woche"), (f) => f.liters)), "Woche"], [de(sum(tankSeit("tag"), (f) => f.liters)), "Tag"]]) +
quad("Kraftstoffkosten [€]", [[de(eJahr), "Jahr"], [de(sum(tankSeit("monat"), (f) => f.fuel_total_eur || 0)), "Monat"], [de(sum(tankSeit("woche"), (f) => f.fuel_total_eur || 0)), "Woche"], [de(sum(tankSeit("tag"), (f) => f.fuel_total_eur || 0)), "Tag"]]))}
${statTile("s3", "Langzeitverbrauch", lv.wert, "l/100 km", lv.bis ? "bis " + dedat(lv.bis) : "",
`<dl class="rows">
<div class="row"><dt>Getankt insgesamt</dt><dd>${de(lv.liter, 1)} l</dd></div>
@@ -3668,7 +3695,7 @@ function vStat() {
<div class="row" style="border-bottom:0"><dt>Letzter Tankstop</dt><dd>${lv.bis ? dedat(lv.bis) : ""}</dd></div>
</dl>
${lv.unplausibel ? `<span class="label" style="margin-top:16px;color:var(--warn)">Aus Litern und Strecke ergibt sich derzeit kein plausibler Wert. Sobald Tankvorgänge und Kilometerstände vollständig vorliegen, erscheint er hier.</span>` : ""}
${quad("Näherung je Fahrt, je Zeitraum [l/100 km]", [[schnittFahrtVerbrauch(fJahr), "Jahr"], [schnittFahrtVerbrauch(fMonat), "Monat"], [schnittFahrtVerbrauch(fWoche), "Woche"], [schnittFahrtVerbrauch(fTag), "Tag"], [schnittFahrtVerbrauch(fAbs), "Absolut"]])}
${quad("Näherung je Fahrt, je Zeitraum [l/100 km]", [[schnittFahrtVerbrauch(fJahr), "Jahr"], [schnittFahrtVerbrauch(fMonat), "Monat"], [schnittFahrtVerbrauch(fWoche), "Woche"], [schnittFahrtVerbrauch(fTag), "Tag"]])}
<span class="label" style="margin-top:16px">Der Langzeitwert rechnet alle je getankten Liter gegen die Strecke bis zum letzten Tankstop. Die Werte je Zeitraum sind eine Näherung aus der Tankfüllstand-Differenz der einzelnen Fahrten, keine Meldung des Fahrzeugs.</span>`)}
${statTile("s4", "Tag und Nacht", kmJahr > 0 ? de(Math.round((tagKm / kmJahr) * 100)) : "", "% bei Tag", de(tagKm) + " km",
`<div class="bar"><i style="flex:${tagKm};background:var(--fg)"></i><i style="flex:${nachtKm};background:var(--fg3)"></i></div>
@@ -5773,7 +5800,6 @@ function ereignisseVerdrahten() {
}
if (e.target.dataset.oelkm !== undefined) { CAR.oel.km = parseInt(e.target.value, 10); profilSpeichern(); return; }
if (e.target.dataset.oelm !== undefined) { CAR.oel.monate = parseInt(e.target.value, 10); profilSpeichern(); return; }
if (e.target.dataset.boden !== undefined) { bodenAn = e.target.checked; render(); return; }
if (e.target.dataset.setupNurpassend !== undefined) { setupNurPassend = e.target.checked; render(); return; }
if (e.target.dataset.startbild !== undefined) { CONFIG.startbild = e.target.value; profilSpeichern(); render(); return; }
if (e.target.dataset.svsel !== undefined) { svAuswahl = parseInt(e.target.value, 10); render(); return; }