Fahrten bearbeitbar, Inspektionsprognose, Bootstrap-Takt reisst nicht mehr ab

Sechs gemeldete Punkte plus ein Fund beim Nachlesen des Ladevorgangs:

- HECKKLAPPENSCHLOSS_SENSOR ueberall entfernt (einstellungen.py,
  entitaeten.py-Katalog, _sicherheitscheck()). HECKKLAPPE_SENSOR bleibt.
- Bild-Platzhalter in den Einstellungen war halb abgeschnitten: die bare
  .carfix-Regel der Desktop-Container-Query traf auch den Platzhalter-Div,
  dessen inset:0 von der festen Hoehe uebersteuert wurde. Auf .bildbox
  eingegrenzt.
- Inspektion bekommt eine eigene "voraussichtlich am ..."-Zeile (nur Datum).
  oelwechselPrognose() dafuer auf den gemeinsamen Kern servicePrognose()
  zurueckgefuehrt.
- "Neue Fahrt" bietet jetzt alle Felder, die die Einzelfahrt anzeigt, und die
  Einzelfahrt laesst sich bearbeiten - gemeinsames fahrtFelder()/
  fahrtFormularWerte() nach dem Muster von tankFelder(). Backend: Service
  audi_dashboard_fahrt_aktualisieren, profil.fahrt_bearbeiten() (edited_fields
  schuetzt gegen die Automatik, nicht gegen den Nutzer). Die Art-Pille
  speichert jetzt ebenfalls statt nur lokal umzuschalten.
- Titelzeile steht auf dem Handy buendig zu den Kacheln: .back:not(.on) gibt
  seine Breite und den Gap auf allen Breiten frei, .topbar links 16px.
- Ladebildschirm: nachladeAnstossen() und datenLaden() stiegen bei fehlendem
  HASS aus, ohne eine naechste Runde zu planen - genau die Sackgasse, aus der
  bisher nur ein Menueklick half. Beide planen jetzt weiter, der Timer wird
  beim Abhaengen gestoppt.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-17 11:40:36 +02:00
co-authored by Claude Opus 5
parent 91dfd4807a
commit 2d76f10c1c
17 changed files with 670 additions and 161 deletions
+192 -59
View File
@@ -1123,24 +1123,24 @@ function fahrzeugMeldungRoh(art) {
return { ts: null, km: null };
}
/* App-eigene Ölwechsel-Prognose, eigenständig statt über die allgemeine
tagesleistung()/prognose()-Mischrechnung (die für Ölwechsel zu träge ist,
weil sie den langfristigen Durchschnitt seit dem allerersten Servicebuch-
eintrag mit einfließen lässt): die Berechnung beginnt bewusst komplett neu
ab dem Datum des letzten tatsächlich durchgeführten Ölwechsels.
/* App-eigene Service-Prognose, eigenständig statt über die allgemeine
tagesleistung()/prognose()-Mischrechnung (die dafür zu träge ist, weil sie
den langfristigen Durchschnitt seit dem allerersten Servicebucheintrag mit
einfließen lässt): die Berechnung beginnt bewusst komplett neu ab dem Datum
der letzten tatsächlich durchgeführten Arbeit dieser Art.
Logik (wie vorgegeben): km und Tage seit dem letzten Ölwechsel ergeben die
aktuelle Fahrleistung pro Tag; hochgerechnet auf die restlichen km bis zum
eingestellten Intervall (Kundeneigene Einstellung, siehe CAR.oel) ergibt
das ein Datum - gedeckelt auf spätestens das Intervall-Zeitlimit (z. B. 1
Jahr) nach dem letzten Ölwechsel, falls seither wenig gefahren wurde. */
function oelwechselPrognose() {
Logik (wie vorgegeben): km und Tage seit diesem Eintrag ergeben die aktuelle
Fahrleistung pro Tag; hochgerechnet auf die restlichen km bis zum
eingestellten Intervall ergibt das ein Datum - gedeckelt auf spätestens das
Intervall-Zeitlimit (z. B. 1 Jahr) nach dem letzten Eintrag, falls seither
wenig gefahren wurde.
Gemeinsame Grundlage für Ölwechsel und Inspektion (siehe die beiden
Aufrufer darunter) - beide rechnen identisch, nur mit anderem Intervall. */
function servicePrognose(stichwort, intervallKm, intervallMonate) {
if (!CAR.odoBekannt) return null;
const letzter = letzterEintrag("öl");
const letzter = letzterEintrag(stichwort);
if (!letzter) return null;
const oel = CAR.oel;
const intervallKm = oel.modus === "hersteller" ? oel.herstellerKm : Math.min(oel.km, oel.herstellerKm);
const intervallMonate = oel.modus === "hersteller" ? oel.herstellerMonate : Math.min(oel.monate, oel.herstellerMonate);
const zielKm = letzter.km + intervallKm;
const restKm = Math.max(0, zielKm - CAR.odo);
const kappDatum = monatePlus(letzter.datum, intervallMonate);
@@ -1151,13 +1151,13 @@ function oelwechselPrognose() {
const tageSeit = (heute() - dat(letzter.datum)) / dTag;
const kmSeit = CAR.odo - letzter.km;
if (tageSeit <= 0 || kmSeit <= 0) {
// Noch keine belastbare Fahrleistung seit dem Ölwechsel bekannt (z. B.
// Noch keine belastbare Fahrleistung seit dem Eintrag bekannt (z. B.
// direkt danach) - ohne Rate keine seriöse Km-Prognose, also das
// Zeitlimit als einzig belastbare Grenze zeigen.
datum = kappDatum;
} else {
const kmProTagSeitOel = kmSeit / tageSeit;
const tageBisZiel = restKm / kmProTagSeitOel;
const kmProTagSeither = kmSeit / tageSeit;
const tageBisZiel = restKm / kmProTagSeither;
const berechnet = new Date(heute().getTime() + tageBisZiel * dTag);
datum = berechnet < kappDatum ? berechnet : kappDatum;
}
@@ -1165,6 +1165,27 @@ function oelwechselPrognose() {
return { km: zielKm, restKm, datum };
}
function oelwechselPrognose() {
const oel = CAR.oel;
return servicePrognose("öl",
oel.modus === "hersteller" ? oel.herstellerKm : Math.min(oel.km, oel.herstellerKm),
oel.modus === "hersteller" ? oel.herstellerMonate : Math.min(oel.monate, oel.herstellerMonate));
}
/* Inspektion: dieselben 30.000 km / 24 Monate wie in intervalle()/termine().
Angezeigt wird davon bewusst nur das Datum ("voraussichtlich am …") - die
Restkilometer stehen bei der Inspektion schon in der Zeile daneben. */
function inspektionPrognose() { return servicePrognose("inspektion", 30000, 24); }
/* Kleine graue Zweitzeile unter dem Termin-Titel, in Service-Kachel und
Service-Seite identisch aufgebaut. Leer, solange kein passender
Servicebucheintrag vorliegt. */
function prognoseZeile(art) {
const p = art === "Ölwechsel" ? oelwechselPrognose() : art === "Inspektion" ? inspektionPrognose() : null;
if (!p) return "";
const km = art === "Ölwechsel" ? de(p.restKm) + " km, " : "";
return `<br><span style="color:var(--fg3);font-size:11px">${km}voraussichtlich am ${dedat(p.datum)}</span>`;
}
function terminText(t) {
const restKm = t.km !== null && CAR.odoBekannt ? t.km - CAR.odo : null;
const restKmAbs = restKm !== null ? Math.abs(restKm) : null;
@@ -1318,22 +1339,25 @@ function vAudi() {
// (Sensor nicht verfügbar), fällt sie auf dieselbe App-eigene
// Servicebuch-Schätzung zurück wie Hauptuntersuchung.
const fm = t.art === "Hauptuntersuchung" ? { ts: null, km: null } : fahrzeugMeldungRoh(t.art);
// Zusätzlich zur realen Meldung klein die App-eigene Prognose
// (Servicebuch + tatsächliche Fahrleistung seit der letzten Arbeit,
// siehe servicePrognose()). Steht links unter dem Kachel-Titel statt
// rechts bei "am"/"in", die beide gleichwertig (gleiche Schriftgröße
// wie "am") untereinander in der rechten Spalte stehen. Die Inspektion
// bekommt sie auf Wunsch auch ohne Fahrzeugmeldung und ohne den
// km-Anteil - beim Ölwechsel stünde die Zahl dort doppelt, weil
// terminText() den generischen Termin schon "voraussichtlich" nennt.
const eigen = prognoseZeile(t.art);
if (fm.ts == null && fm.km == null) {
const x = terminText(t);
return `<div class="row" ${letzte}>
<dt>${t.art}</dt><dd>${x.haupt}${x.neben ? `<small style="text-align:left">${x.neben}</small>` : ""}</dd></div>`;
<dt>${t.art}${t.art === "Inspektion" ? eigen : ""}</dt>
<dd>${x.haupt}${x.neben ? `<small style="text-align:left">${x.neben}</small>` : ""}</dd></div>`;
}
const haupt = fm.ts ? dedat(new Date(fm.ts)) : "";
const neben = fm.km != null ? "in " + de(fm.km) + " km" : "";
// Zusätzlich zur realen Meldung bei Ölwechsel klein die App-eigene
// Prognose (Servicebuch + tatsächliche Fahrleistung seit dem letzten
// Wechsel, siehe oelwechselPrognose()) - bei Inspektion bewusst nicht,
// wie gefordert. Steht links unter dem Kachel-Titel statt rechts bei
// "am"/"in", die beide gleichwertig (gleiche Schriftgröße wie "am")
// untereinander in der rechten Spalte stehen.
const eigen = t.art === "Ölwechsel" ? oelwechselPrognose() : null;
return `<div class="row" ${letzte}>
<dt>${t.art}${eigen ? `<br><span style="color:var(--fg3);font-size:11px">${de(eigen.restKm)} km, voraussichtlich am ${dedat(eigen.datum)}</span>` : ""}</dt>
<dt>${t.art}${eigen}</dt>
<dd>${haupt}${neben ? `<span style="display:block">${neben}</span>` : ""}</dd></div>`;
}).join("")}
</dl></button>
@@ -1628,11 +1652,12 @@ function vService() {
const anzeige = iv.hersteller || iv.eigen;
const fm = fahrzeugMeldungRoh(t.art);
const fmDatum = fm.ts ? dedat(new Date(fm.ts)) : null;
// Forecast-Zeile nur bei Ölwechsel (Kundeneigenes Intervall aus
// "Einrichten", eigenständige Reset-nach-Service-Logik, siehe
// oelwechselPrognose()) - bei Inspektion und Hauptuntersuchung
// bewusst keine Forecast-Zeile mehr.
// Forecast-Zeile aus dem kundeneigenen Intervall in "Einrichten" mit
// eigenständiger Reset-nach-Service-Logik (servicePrognose()): beim
// Ölwechsel mit Restkilometern, bei der Inspektion auf Wunsch nur mit
// dem Datum. Hauptuntersuchung bleibt bewusst ohne.
const eigenOel = t.art === "Ölwechsel" ? oelwechselPrognose() : null;
const eigenInsp = t.art === "Inspektion" ? inspektionPrognose() : null;
return `<div class="row" ${letzte}>
<dt>${t.art}
${t.vereinbart ? `<br><span style="color:var(--red);font-size:11px">Werkstatt am ${dedat(new Date(t.vereinbart))}</span>` : ""}
@@ -1642,8 +1667,10 @@ function vService() {
${fm.km != null || fmDatum ? `
<span class="werte">${fm.km != null ? de(fm.km) + " km" : ""}${fm.km != null && fmDatum ? " / " : ""}${fmDatum || ""}</span>
${eigenOel ? `<small style="text-align:left">${de(eigenOel.restKm)} km, voraussichtlich am ${dedat(eigenOel.datum)}</small>` : ""}
${eigenInsp ? `<small style="text-align:left">voraussichtlich am ${dedat(eigenInsp.datum)}</small>` : ""}
` : `
<span class="werte">${eigenOel ? de(eigenOel.restKm) + " km · " + dedat(eigenOel.datum) : (anzeige.km !== null ? de(anzeige.km) + " km · " : "") + dedat(anzeige.datum)}</span>
${eigenInsp ? `<small style="text-align:left">voraussichtlich am ${dedat(eigenInsp.datum)}</small>` : ""}
`}
</dd>
</div>`; }).join("")}
@@ -2440,17 +2467,69 @@ function vReifen() {
${satz("winter")}`;
}
/* Gemeinsames Formular für "Neue Fahrt" und das Bearbeiten einer bestehenden
Fahrt - dieselben Felder an beiden Stellen, damit sie nicht auseinander-
laufen können (gleiches Muster wie tankFelder()/tankFormularWerte()). Die
Felder decken genau das ab, was die Einzelfahrt-Seite anzeigt; Verbrauch,
Status und km-Quelle bleiben außen vor, die meldet das Fahrzeug bzw. leitet
das Backend ab. */
const FAHRT_ARTEN = ["privat", "arbeitsweg"];
/* Datum für ein <input type="date"> in Ortszeit. Bewusst nicht über
toISOString(), das rechnet nach UTC um und liefert kurz nach Mitternacht
den Vortag. */
function datumsfeld(d) {
const zwei = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${zwei(d.getMonth() + 1)}-${zwei(d.getDate())}`;
}
function zeitfeld(d) {
const zwei = (n) => String(n).padStart(2, "0");
return `${zwei(d.getHours())}:${zwei(d.getMinutes())}`;
}
function fahrtFelder(praefix, t) {
const start = t && t.ts_start ? new Date(t.ts_start) : new Date();
const dauer = t && t.duration_s != null ? Math.round(t.duration_s / 60) : "";
const art = t && t.art ? t.art : FAHRT_ARTEN[0];
const wert = (v) => (v == null ? "" : v);
return `
<div class="feld"><label for="${praefix}Datum">Datum</label><input type="date" id="${praefix}Datum" value="${datumsfeld(start)}"></div>
<div class="feld"><label for="${praefix}Start">Startzeit</label><input type="time" id="${praefix}Start" value="${t ? zeitfeld(start) : "08:00"}"></div>
<div class="feld"><label for="${praefix}Dauer">Dauer</label>
<span class="mitEinheit"><input type="number" min="0" id="${praefix}Dauer" value="${dauer}" placeholder="0"><span>min</span></span></div>
<div class="feld"><label for="${praefix}Art">Art</label>
<select id="${praefix}Art">${FAHRT_ARTEN.map((a) => `<option ${a === art ? "selected" : ""}>${a}</option>`).join("")}</select></div>
<div class="feld"><label for="${praefix}Startort">Startort</label><input id="${praefix}Startort" value="${esc(wert(t && t.start_address))}" placeholder="unbekannt"></div>
<div class="feld"><label for="${praefix}Zielort">Zielort</label><input id="${praefix}Zielort" value="${esc(wert(t && t.end_address))}" placeholder="unbekannt"></div>
<div class="feld"><label for="${praefix}OdoStart">Startkilometer</label><input type="number" id="${praefix}OdoStart" value="${wert(t && t.odo_start)}" placeholder="wird ergänzt"></div>
<div class="feld"><label for="${praefix}OdoEnde">Endkilometer</label><input type="number" id="${praefix}OdoEnde" value="${wert(t && t.odo_end)}" placeholder="wird ergänzt"></div>
<div class="feld" style="border-bottom:0"><label for="${praefix}Distanz">Gefahrene Distanz</label>
<span class="mitEinheit"><input type="number" step="0.1" id="${praefix}Distanz" value="${wert(t && t.distance_km)}" placeholder="aus km-Stand"><span>km</span></span></div>`;
}
function fahrtFormularWerte(praefix) {
const zahl = (id) => { const v = $(`#${praefix}${id}`).value; return v === "" ? null : parseFloat(v.replace(",", ".")); };
const text = (id) => $(`#${praefix}${id}`).value.trim() || null;
const dauer = parseInt($(`#${praefix}Dauer`).value, 10) || 0;
const tsStart = new Date(`${$(`#${praefix}Datum`).value}T${$(`#${praefix}Start`).value}:00`);
return {
ts_start: tsStart.toISOString(),
ts_end: new Date(tsStart.getTime() + dauer * 60000).toISOString(),
art: $(`#${praefix}Art`).value,
start_ort: text("Startort"), ziel_ort: text("Zielort"),
odo_start: zahl("OdoStart"), odo_end: zahl("OdoEnde"), distanz: zahl("Distanz"),
};
}
function vTripsFormular() {
// eigene Funktion, damit sowohl der volle als auch der Leer-Zustand
// (§11 Abnahmekriterium 1: eine Fahrt muss erfasst werden können) dasselbe
// Formular zeigen - vorher fehlte es im Leer-Zustand komplett, der Knopf
// wechselte zu "Abbrechen", aber es erschienen keine Felder.
return `${fahrtOffen ? `<div class="tile"><span class="label">Neue Fahrt</span>
<div class="feld"><label>Datum</label><input type="date" id="neuFahrtDatum" value="${new Date().toISOString().slice(0, 10)}"></div>
<div class="feld"><label>Startzeit</label><input type="time" id="neuFahrtStart" value="08:00"></div>
<div class="feld" style="border-bottom:0"><label>Dauer in Minuten</label><input type="number" id="neuFahrtDauer" placeholder="0"></div>
<button class="aktion" data-fahrtspeichern>Speichern</button></div>` : ""}
<button class="aktion primaer" data-fahrt>${fahrtOffen ? "Abbrechen" : "Fahrt erfassen"}</button>`;
${fahrtFelder("neuFahrt", null)}
<button class="aktion primaer" data-fahrtspeichern>Speichern</button></div>` : ""}
<button class="aktion ${fahrtOffen ? "" : "primaer"}" data-fahrt>${fahrtOffen ? "Abbrechen" : "Fahrt erfassen"}</button>`;
}
function vTrips() {
const jahre = [...new Set(TRIPS.map((t) => new Date(t.ts_start).getFullYear()))].sort((a, b) => b - a);
@@ -2491,6 +2570,16 @@ function vTrips() {
function vTrip(id) {
const t = TRIPS.find((x) => x.trip_id === id);
if (!t) return `<div class="tile">Fahrt nicht gefunden</div>`;
if (fahrtBearbeitenOffen) {
return `
<div class="tile"><span class="label">Fahrt bearbeiten</span>
${fahrtFelder("bearbFahrt", t)}
<button class="aktion primaer" data-fahrtaktualisieren="${t.trip_id}">Speichern</button>
<button class="aktion" data-fahrtbearbeiten>Abbrechen</button>
</div>`;
}
return `
${mapBox()}
<div class="tile flat" style="margin:18px 0 14px">
@@ -2509,7 +2598,9 @@ function vTrip(id) {
<div class="row"><dt>Dauer</dt><dd>${Math.round(t.duration_s / 60)} min</dd></div>
<div class="row"><dt>Verbrauch</dt><dd>${fahrtVerbrauch(t) != null ? de(fahrtVerbrauch(t), 1) + " l/100 km" : "liegt nicht vor"}<small>vom Fahrzeug gemeldet</small></dd></div>
<div class="row"><dt>Status</dt><dd>${t.status}<small>Quelle: ${t.km_quelle || ""}</small></dd></div>
</dl></div>
</dl>
<button class="aktion" data-fahrtbearbeiten>Werte bearbeiten</button>
</div>
<button class="aktion loeschen" data-tripdel="${t.trip_id}">Fahrt löschen</button>`;
}
@@ -2637,6 +2728,7 @@ function vStat() {
let neuOffen = false;
let fahrtOffen = false;
let tankBearbeitenOffen = false;
let fahrtBearbeitenOffen = false;
// Ergebnis eines geparsten, noch nicht gespeicherten Belegs: { praefix, daten }
// - überlagert in tankFelder() die Beleg-Felder des jeweils offenen Formulars.
let belegEntwurf = null;
@@ -3324,6 +3416,9 @@ const go = (name, id) => {
entwurfIndex = null;
}
sheet = null;
// Ein offenes Bearbeiten-Formular gehört zu genau der Fahrt, auf der es
// geöffnet wurde - sonst begrüßt die nächste Fahrt einen mit Eingabefeldern.
fahrtBearbeitenOffen = false;
/* B3: echter Verlauf statt statischer ZURUECK-Tabelle. Ein Tabwechsel setzt
den Stapel zurück, weil Tabs Wurzeln sind, keine Ebenen. */
if (istTab(name)) routeStack = [];
@@ -3333,6 +3428,7 @@ const go = (name, id) => {
};
function zurueck() {
sheet = null;
fahrtBearbeitenOffen = false;
const ziel = routeStack.pop() || { name: ZURUECK[route.name] || "home" };
navRichtung = "raus";
route = ziel; render();
@@ -3821,12 +3917,18 @@ function ereignisseVerdrahten() {
}
if (e.target.closest("[data-fahrt]")) { fahrtOffen = !fahrtOffen; render(); return; }
if (e.target.closest("[data-fahrtspeichern]")) {
const datum = $("#neuFahrtDatum").value, start = $("#neuFahrtStart").value, dauer = parseInt($("#neuFahrtDauer").value, 10) || 0;
const tsStart = new Date(`${datum}T${start}:00`);
const tsEnd = new Date(tsStart.getTime() + dauer * 60000);
serviceRufen("audi_dashboard_fahrt_manuell_anlegen", { ts_start: tsStart.toISOString(), ts_end: tsEnd.toISOString(), art: "privat" });
serviceRufen("audi_dashboard_fahrt_manuell_anlegen", fahrtFormularWerte("neuFahrt"));
fahrtOffen = false; render(); return;
}
if (e.target.closest("[data-fahrtbearbeiten]")) {
fahrtBearbeitenOffen = !fahrtBearbeitenOffen; render(); return;
}
const fahrtAkt = e.target.closest("[data-fahrtaktualisieren]");
if (fahrtAkt) {
serviceRufen("audi_dashboard_fahrt_aktualisieren",
{ trip_id: fahrtAkt.dataset.fahrtaktualisieren, ...fahrtFormularWerte("bearbFahrt") });
fahrtBearbeitenOffen = false; render(); return;
}
if (e.target.closest("[data-tankspeichern]")) {
const w = tankFormularWerte("neuTank");
if (w.liter > 0 && w.kosten > 0) {
@@ -3987,7 +4089,14 @@ function ereignisseVerdrahten() {
const a = e.target.closest("[data-acc]");
if (a) { const b = ROOT.getElementById(a.dataset.acc); a.setAttribute("aria-expanded", b.classList.toggle("on")); return; }
const art = e.target.closest("[data-art]");
if (art) { const tr = TRIPS.find((x) => x.trip_id === art.dataset.art); tr.art = tr.art === "arbeitsweg" ? "privat" : "arbeitsweg"; render(); return; }
if (art) {
const tr = TRIPS.find((x) => x.trip_id === art.dataset.art);
tr.art = tr.art === "arbeitsweg" ? "privat" : "arbeitsweg";
// Bis 2026-08-17 blieb das Umschalten rein lokal und war nach dem
// naechsten Backend-Push wieder weg - jetzt wandert es mit.
serviceRufen("audi_dashboard_fahrt_aktualisieren", { trip_id: tr.trip_id, art: tr.art });
render(); return;
}
// Standort (Vollbild) - Karten-/Menü-Bedienung manipuliert SMAP/das Menü-
// DOM direkt statt über render() (siehe Kopfkommentar Standort-Abschnitt).
if (e.target.closest("[data-standort-umschalten]")) { standortMenuUmschalten(); return; }
@@ -4209,8 +4318,15 @@ function belegErgebnisVerarbeiten(daten) {
Deshalb jetzt ohne festes Abbruchlimit: nach der anfänglichen schnellen
Serie (siehe NACHLADE_INTERVALL_MS) läuft es mit einem größeren, aber
endlosen Intervall weiter, bis Daten da sind oder das Element abgehängt
wird (disconnectedCallback stoppt hier nichts explizit, aber ein
getimeter Aufruf ohne HASS/callWS bricht oben ohnehin ab). */
wird (disconnectedCallback stoppt den Timer, connectedCallback stößt ihn
über datenLaden() wieder an).
2026-08-17: Der Takt darf an keiner Stelle abreißen. Vorher stieg sowohl
nachladeAnstossen() (bei fehlendem HASS/callWS) als auch datenLaden() (bei
fehlendem HASS) still aus, ohne eine nächste Runde zu planen - danach half
nur noch ein zufälliger hass-Push oder ein Klick ins Menü (go() ruft
datenLaden() mit auf). Genau das war das gemeldete "Lädt bis man auf
einen Menüpunkt klickt". */
const NACHGELADEN = {};
let nachladeTimer = null;
let nachladeVersuche = 0;
@@ -4219,7 +4335,7 @@ const NACHLADE_LANGSAM_MS = 10000;
const NACHLADE_SCHNELLE_VERSUCHE = 15;
function nachladeAnstossen() {
if (nachladeTimer || !HASS || !HASS.callWS) return;
if (nachladeTimer) return;
nachladeVersuche++;
// Erster Versuch fast sofort statt erst nach vollem Intervall - der
// Wettlauf mit pyscripts beim_start() (siehe Kopfkommentar) ist meist
@@ -4232,26 +4348,40 @@ function nachladeAnstossen() {
: NACHLADE_LANGSAM_MS;
nachladeTimer = setTimeout(async () => {
nachladeTimer = null;
try {
const states = await HASS.callWS({ type: "get_states" });
const finden = (id) => states.find((s) => s.entity_id === id);
NACHGELADEN.profil = finden("pyscript.audi_dashboard_profil") || NACHGELADEN.profil;
NACHGELADEN.fahrten = finden("pyscript.audi_dashboard_fahrten") || NACHGELADEN.fahrten;
NACHGELADEN.tank = finden("pyscript.audi_dashboard_tankvorgaenge") || NACHGELADEN.tank;
NACHGELADEN.status = finden("pyscript.audi_dashboard_fahrzeugstatus") || NACHGELADEN.status;
NACHGELADEN.batt = finden("pyscript.audi_dashboard_batterieverlauf") || NACHGELADEN.batt;
// Ohne diese Zeile bleibt der Setup-Katalog nach einem HA-Neustart leer,
// bis zufaellig ein Push kommt - siehe setupBereit().
NACHGELADEN.entitaeten = finden("pyscript.audi_dashboard_entitaeten") || NACHGELADEN.entitaeten;
} catch (e) {
// wird beim naechsten Versuch erneut probiert
// HASS kann hier noch fehlen: panel_custom erzeugt das Element und setzt
// hass nicht immer in dieser Reihenfolge. Frueher brach die Schleife in so
// einem Fall oben ganz ab (Bedingung !HASS || !HASS.callWS) und wurde nie
// wieder angestossen - genau der dauerhaft haengende "Lädt …"-Bildschirm.
// Jetzt wird die Runde nur uebersprungen, der Takt laeuft weiter.
if (HASS && HASS.callWS) {
try {
const states = await HASS.callWS({ type: "get_states" });
const finden = (id) => states.find((s) => s.entity_id === id);
NACHGELADEN.profil = finden("pyscript.audi_dashboard_profil") || NACHGELADEN.profil;
NACHGELADEN.fahrten = finden("pyscript.audi_dashboard_fahrten") || NACHGELADEN.fahrten;
NACHGELADEN.tank = finden("pyscript.audi_dashboard_tankvorgaenge") || NACHGELADEN.tank;
NACHGELADEN.status = finden("pyscript.audi_dashboard_fahrzeugstatus") || NACHGELADEN.status;
NACHGELADEN.batt = finden("pyscript.audi_dashboard_batterieverlauf") || NACHGELADEN.batt;
// Ohne diese Zeile bleibt der Setup-Katalog nach einem HA-Neustart leer,
// bis zufaellig ein Push kommt - siehe setupBereit().
NACHGELADEN.entitaeten = finden("pyscript.audi_dashboard_entitaeten") || NACHGELADEN.entitaeten;
} catch (e) {
// wird beim naechsten Versuch erneut probiert
}
}
datenLaden(true);
// Rueckversicherung: datenLaden() stoesst die naechste Runde selbst an,
// aber nur, wenn es nicht vorher aussteigt. Der Takt darf an keiner
// Stelle abreissen, solange noch keine Daten da sind.
if (!DATEN_GELADEN) nachladeAnstossen();
}, verzoegerung);
}
async function datenLaden(erzwingen) {
if (!HASS) return;
if (!HASS) {
if (!DATEN_GELADEN) nachladeAnstossen();
return;
}
const profilState = HASS.states["pyscript.audi_dashboard_profil"] || NACHGELADEN.profil;
const fahrtenState = HASS.states["pyscript.audi_dashboard_fahrten"] || NACHGELADEN.fahrten;
const tankState = HASS.states["pyscript.audi_dashboard_tankvorgaenge"] || NACHGELADEN.tank;
@@ -4355,6 +4485,9 @@ class AudiDashboardPanel extends HTMLElement {
}
disconnectedCallback() {
// Nachlade-Takt anhalten, solange das Panel nicht im Dokument haengt -
// connectedCallback() stoesst ihn ueber datenLaden() wieder an.
if (nachladeTimer) { clearTimeout(nachladeTimer); nachladeTimer = null; }
// Alle drei Karten, nicht nur MAP: sonst bleibt beim Abhaengen des Panels
// (Standort- oder Uebersichtsseite offen) eine Leaflet-Instanz samt ihrem
// window-Resize-Horcher und laufenden Kachelabrufen am Leben.