diff --git a/AGENTS.md b/AGENTS.md index e6cf69f..3ad4071 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,8 +9,9 @@ installieren - der Erweiterung fehlte die CFBundleVersion; dazu der QR-Scanner fuer den Zugangs-Token; dazu Debug-Mode, eine Version-Kachel, das Knopfpaar nach HIG und Bilder ohne Verzoegerung; dazu eine Musterseite, die die App ohne Token und ohne Netz rendert; dazu der QR-Code aus einem gespeicherten Bild, ein stiller -Debug-Knopf und der HA-Zugang bei den Zugaengen, `2026.9.4.12`, -Abschnitte CC bis CH. Davor: Geraetezeit statt Ankunftszeit - die Wurzel hinter der 3-km-Fahrt: +Debug-Knopf und der HA-Zugang bei den Zugaengen; der Cache-Brecher der Bilder haengt +jetzt am Foto statt am App-Start, `2026.9.4.14`, +Abschnitte CC bis CI. Davor: Geraetezeit statt Ankunftszeit - die Wurzel hinter der 3-km-Fahrt: das Fahrtfenster stand in Geraetezeit, der Verlauf war nach Ankunftszeit sortiert. Dazu die neun Paritaetsbefunde und die verunreinigte Batteriehistorie, `2026.9.4.1`, Abschnitt CB. Davor: Der Regler stellt den Schlaf-Timeout des Dongles, flespi lesend und schreibend, die eigene Warteschlange wieder entfallen, `2026.9.3.10`, Abschnitt BZ. Davor: Zugaenge: die Dienste-Token aus App und Panel bedienbar, der Wert kommt nie @@ -10101,3 +10102,67 @@ App meldet daraufhin voellig zu Recht „Der Server ist unter dieser Adresse nic erreichbar". Einzutragen ist die **eigene** Adresse `http://localhost:5173`; der Vite-Proxy reicht `/api` serverseitig an `:18123` weiter (Abschnitt AK). Nach der Korrektur stand die Uebersicht mit echten Daten. + +## CI. Der Cache-Brecher haengt jetzt am Foto, nicht am App-Start (2026.9.4.14) + +Entscheidung des Eigentuemers auf die drei vorgelegten Wege (Abschnitt CG): +nicht weniger vorladen, sondern den wiederholten Abruf ganz abschaffen. + +### Was falsch war + +Beide Oberflaechen haengten `?v=${Date.now()}` an jede Bildadresse - gesetzt +beim Start und nach jedem Hochladen. Das loest zwar zuverlaessig den +eigentlichen Zweck (Home Assistant liefert `/local/` mit 31 Tagen +Cache-Vorgabe, ohne Anhaengsel bliebe ein ersetztes Foto einen Monat lang das +alte), erkauft ihn aber teuer: die Adresse ist bei **jedem** Start eine andere, +der Zwischenspeicher des Browsers kann also zwischen zwei Starts nie greifen. +Mit dem Vorladen aller Fotos waeren das **5,3 MB je Start** gewesen. + +### Was jetzt gilt + +`bilder.staende()` liefert den Zeitstempel **je Datei** +(`{"seitenansicht.webp": 1788161081, …}`), veroeffentlicht in den `daten` von +`sensor.audi_dashboard_app_version` - dieselbe Entitaet, die schon Buendel, +Zugaenge, Dongle und den letzten Datensatz traegt. Elf Dateizugriffe, im +Executor, weil blockierende Aufrufe in der Ereignisschleife seit 2026.8 als +Fehler gemeldet werden. + +Damit aendert sich eine Bildadresse **genau dann, wenn das Foto ein anderes +ist** - und sonst nie. + +Drei Feinheiten, jede aus einem konkreten Fall: + +- **Ein Foto, das es nicht gibt, bekommt `v=0`.** Home Assistant liefert auch + den 404 mit Cache-Vorgabe; sobald das Foto hochgeladen ist, traegt die + Adresse seinen Zeitstempel und der gespeicherte Fehlschlag ist ueberholt. +- **Ohne Staende gilt das alte Verhalten** (`Date.now()`). Ein aelteres Backend + darf nicht dazu fuehren, dass alle Adressen auf einem festen `v=0` stehen + bleiben - das waere ein Foto, das sich nie mehr aktualisiert. +- **Die App merkt sich den letzten Stand im Browserspeicher.** Sie malt ihren + ersten Bildschirm, BEVOR die Versionsangabe eintrifft; ohne das Gedaechtnis + trug genau dieser eine Durchlauf noch die Startzeit und holte das + Uebersichtsfoto bei jedem Start erneut - live gesehen, nachdem der Rest + schon stimmte. Ein veralteter gemerkter Stand ist ungefaehrlich: sobald die + frische Angabe da ist, wechselt die Adresse und das Bild wird nachgeholt. + +`bild_hochladen`/`bild_loeschen` veroeffentlichen jetzt sofort neu - sonst +saehe die Oberflaeche das neue Foto erst beim naechsten Minutentakt. + +Das Vorladen laeuft nur noch einmal je **Bildstand** statt einmal je Start; +die Kennung ist die Liste der Staende. + +### Live nachgewiesen, beide Haelften + +Im Panel und in der App, gegen die echten Dateien des Testcontainers: + +| | | +|---|---| +| veroeffentlichte Staende | deckungsgleich mit den `mtime` der sechs vorhandenen Dateien | +| Neuladen ohne Aenderung | **5.328.771 Bytes Inhalt, 0 Bytes uebertragen**, 0 ms - alles aus dem Zwischenspeicher | +| ein Foto geaendert (`touch`) | neuer Stand veroeffentlicht, neue Adresse, **513.403 Bytes** neu geholt - die anderen unangetastet | +| App-Start, zweiter Durchlauf | **0 Bytes uebertragen insgesamt**, keine einzige Adresse mit Startzeit | + +Dazu 6 Regressionstests (`src/screens/bilder.test.ts`): Zeitstempel statt +Startzeit, gleich bleibend ohne Aenderung, wechselnd bei Aenderung, `0` fuer +ein fehlendes Foto, Rueckfall ohne Staende, und der gemerkte Stand ueber einen +Neustart. diff --git a/companion-app/src/api/types.ts b/companion-app/src/api/types.ts index e52bbff..2aacebc 100644 --- a/companion-app/src/api/types.ts +++ b/companion-app/src/api/types.ts @@ -297,6 +297,8 @@ export interface AppVersionAngabe { zugaenge?: ZugangAngabe[]; dongle?: DongleAngabe; letzter_datensatz?: LetzterDatensatz | null; + /** Zeitstempel je vorhandenem Fahrzeugfoto - siehe bilder.staende(). */ + bildstaende?: Record | null; } /* -------------------------------------------------- Entitäts-Verzeichnis */ diff --git a/companion-app/src/daten/DatenKontext.tsx b/companion-app/src/daten/DatenKontext.tsx index 360bc2b..eb8754e 100644 --- a/companion-app/src/daten/DatenKontext.tsx +++ b/companion-app/src/daten/DatenKontext.tsx @@ -34,6 +34,7 @@ import { type Verbindungszustand, type WartenderAuftrag, } from "../api" +import { bildStaendeSetzen } from "../screens/bilder" import type { Versionsstand } from "./appVersion" import { eigeneVersion, versionVergleichen } from "./appVersion" import type { Einstellungen, Fahrzeug } from "./profilAdapter" @@ -172,6 +173,10 @@ export function DatenAnbieter({ setzeZugaenge(versionsangabe?.zugaenge ?? []) setzeDongle(versionsangabe?.dongle ?? null) setzeLetzterDatensatz(versionsangabe?.letzter_datensatz ?? null) + // Kein React-Zustand: die Bildadressen werden beim Rendern gebraucht, + // teils ausserhalb von Komponenten (bilderVorladen) - dieselbe + // Ueberlegung wie bei zugangMerken(). + bildStaendeSetzen(versionsangabe?.bildstaende ?? null) return true } catch (fehler) { // Ein voruebergehender Ausfall ist kein Ladefehler, solange schon diff --git a/companion-app/src/screens/bilder.test.ts b/companion-app/src/screens/bilder.test.ts new file mode 100644 index 0000000..d3d973d --- /dev/null +++ b/companion-app/src/screens/bilder.test.ts @@ -0,0 +1,76 @@ +/** + * Der Bildstand entscheidet, wann ein Foto neu geholt wird. + * + * Genau daran hing der gemeldete Fehler: bis zum 04.09.2026 stand am Ende + * jeder Bildadresse die Startzeit der App, und damit holte jeder Start alle + * Fotos erneut (gemessen: 5,3 MB) - der Zwischenspeicher konnte zwischen zwei + * Starts gar nicht greifen. Jetzt kommt der Zeitstempel je Datei vom Backend, + * und diese Tests halten die drei Faelle fest, auf die es dabei ankommt. + */ + +import { beforeEach, describe, expect, it, vi } from "vitest" + +vi.mock("./zugang", () => ({ + zugangLesenSynchron: () => ({ basisUrl: "http://muster.local:8123", token: "x" }), + zugangVergessen: () => {}, + zugangMerken: async () => {}, +})) + +const { bildStaendeSetzen, bildStand, bildUrl } = await import("./bilder") + +describe("Bildstand", () => { + beforeEach(() => { + bildStaendeSetzen(null) + }) + + it("nimmt den Zeitstempel der Datei, nicht die Startzeit", () => { + bildStaendeSetzen({ "seitenansicht.webp": 1788500000 }) + expect(bildStand("seitenansicht.webp")).toBe(1788500000) + expect(bildUrl("seitenansicht.webp")).toBe( + "http://muster.local:8123/local/bilder/seitenansicht.webp?v=1788500000", + ) + }) + + it("bleibt gleich, solange sich das Foto nicht aendert", () => { + bildStaendeSetzen({ "seitenansicht.webp": 1788500000 }) + const erste = bildUrl("seitenansicht.webp") + // Zweiter Start, dieselbe Antwort des Backends. + bildStaendeSetzen({ "seitenansicht.webp": 1788500000 }) + expect(bildUrl("seitenansicht.webp")).toBe(erste) + }) + + it("aendert sich, sobald das Foto ein anderes ist", () => { + bildStaendeSetzen({ "seitenansicht.webp": 1788500000 }) + const vorher = bildUrl("seitenansicht.webp") + bildStaendeSetzen({ "seitenansicht.webp": 1788599999 }) + expect(bildUrl("seitenansicht.webp")).not.toBe(vorher) + }) + + it("setzt 0 fuer ein Foto, das es nicht gibt", () => { + // Wichtig, weil Home Assistant auch den 404 mit Cache-Vorgabe ausliefert: + // sobald das Foto hochgeladen ist, traegt die Adresse seinen Zeitstempel + // und der gespeicherte Fehlschlag ist damit ueberholt. + bildStaendeSetzen({ "seitenansicht.webp": 1788500000 }) + expect(bildStand("cockpit.webp")).toBe(0) + }) + + it("faellt auf die Startzeit zurueck, wenn das Backend keine Staende liefert", () => { + // Aeltere Integration: dann gilt das Verhalten von vorher, statt alle + // Adressen auf ein festes v=0 zu legen - das waere ein Foto, das sich nie + // mehr aktualisiert. + bildStaendeSetzen(null) + expect(bildStand("seitenansicht.webp")).toBeGreaterThan(1_000_000_000_000) + }) +}) + +describe("Bildstand: gemerkt ueber den Neustart", () => { + it("malt den ersten Bildschirm schon mit dem letzten bekannten Stand", async () => { + // Die Versionsangabe trifft erst NACH dem ersten Rendern ein. Ohne das + // Gedaechtnis traegt dieser eine Durchlauf die Startzeit - und holt das + // Uebersichtsfoto bei jedem Start erneut. + bildStaendeSetzen({ "seitenansicht.webp": 1788500000 }) + vi.resetModules() + const frisch = await import("./bilder") + expect(frisch.bildStand("seitenansicht.webp")).toBe(1788500000) + }) +}) diff --git a/companion-app/src/screens/bilder.ts b/companion-app/src/screens/bilder.ts index db4dd60..b3b0286 100644 --- a/companion-app/src/screens/bilder.ts +++ b/companion-app/src/screens/bilder.ts @@ -93,13 +93,67 @@ export const ERLAUBTE_DATEINAMEN: readonly string[] = [ * setzt den Wert nach jedem Hochladen oder Löschen neu. Hier dasselbe: beim * Start der App einmal, danach bei jeder Änderung. * - * Der Preis ist ein erneuter Abruf je App-Start. Der ist gewollt — ein Foto, - * das der Nutzer nicht sieht, kostet mehr als ein Megabyte über eine - * Verbindung, die ohnehin gerade das ganze Fahrzeugprofil holt. + * Bis zum 04.09.2026 war das `Date.now()` beim App-Start - eine Adresse, die + * sich bei JEDEM Start ändert. Der Zwischenspeicher griff damit zwischen + * zwei Starts nie, und mit dem Vorladen aller Fotos wären das 5,3 MB je + * Start gewesen. Jetzt kommt der Zeitstempel jeder Datei vom Backend + * (`bilder.staende()`, veröffentlicht an `sensor.audi_dashboard_app_version`): + * er ändert sich genau dann, wenn das Foto wirklich ein anderes ist. + * + * `bildVersion` bleibt als Rückfall für ein Backend, das die Stände noch + * nicht mitliefert - dann ist das Verhalten wie zuvor. */ +/* + * Gemerkt wird der letzte bekannte Stand im Browserspeicher, und zwar aus + * einem gemessenen Grund: die App malt ihren ersten Bildschirm, BEVOR die + * Versionsangabe (und damit die Stände) eintrifft. Ohne dieses Gedächtnis + * trägt genau dieser eine Durchlauf noch die Startzeit als Anhängsel - und + * holt das Übersichtsfoto damit bei jedem Start erneut, also exakt die + * Kosten, um die es hier geht (am 04.09.2026 live gesehen). + * + * Ein veralteter gemerkter Stand ist ungefährlich: sobald die frische + * Angabe da ist, wechselt die Adresse und das Bild wird nachgeholt. + */ +const SPEICHERSCHLUESSEL = "dm360.bildstaende" + +function gemerkteStaende(): Record | null { + try { + const roh = window.localStorage.getItem(SPEICHERSCHLUESSEL) + if (!roh) return null + const wert: unknown = JSON.parse(roh) + return wert && typeof wert === "object" ? (wert as Record) : null + } catch { + // Kein Speicher (privates Fenster, Testumgebung) - dann eben ohne. + return null + } +} + +let bildStaende: Record | null = + typeof window === "undefined" ? null : gemerkteStaende() let bildVersion = Date.now() -/** Nach dem Hochladen oder Löschen eines Fotos aufrufen. */ +/** Die Stände aus der veröffentlichten Entität übernehmen. */ +export function bildStaendeSetzen(staende: Record | null): void { + bildStaende = staende + try { + if (staende) window.localStorage.setItem(SPEICHERSCHLUESSEL, JSON.stringify(staende)) + else window.localStorage.removeItem(SPEICHERSCHLUESSEL) + } catch { + // Siehe oben - das Merken ist eine Beschleunigung, keine Bedingung. + } +} + +/** + * Der Stand einer Datei. 0 heißt: dieses Foto gibt es nicht - sobald es + * hochgeladen wird, ändert sich die Adresse von selbst, und auch ein + * gespeicherter 404 löst sich damit auf. + */ +export function bildStand(dateiname: string): number { + if (!bildStaende) return bildVersion + return bildStaende[dateiname] ?? 0 +} + +/** Nach dem Hochladen oder Löschen eines Fotos aufrufen (nur der Rückfall). */ export function bildVersionErneuern(): void { bildVersion = Date.now() } @@ -113,7 +167,7 @@ export function bildUrl(dateiname: string | null | undefined): string | undefine if (!dateiname) return undefined const basis = zugangLesenSynchron()?.basisUrl if (!basis) return undefined - return `${basis}/local/bilder/${dateiname}?v=${bildVersion}` + return `${basis}/local/bilder/${dateiname}?v=${bildStand(dateiname)}` } /** @@ -252,10 +306,18 @@ export async function bildVerkleinern(datei: File): Promise { * Nutzer wirklich braucht. Das Datenvolumen ist dasselbe wie beim Durchblaettern * aller Seiten, nur frueher und ohne Warten. */ +let vorgeladen: string | null = null + export function bilderVorladen(): void { if (typeof window === "undefined" || typeof Image === "undefined") return + const namen = [...BILDPLAETZE.map((b) => b.datei), SEITE_WINTER.datei] + // Kennung über alle Stände: solange sich kein Foto geändert hat, läuft das + // Vorladen kein zweites Mal - und der Browser holt die Bilder beim nächsten + // Start aus seinem eigenen Zwischenspeicher. + const kennung = namen.map(bildStand).join(",") + if (vorgeladen === kennung) return + vorgeladen = kennung const starten = () => { - const namen = [...BILDPLAETZE.map((b) => b.datei), SEITE_WINTER.datei] for (const name of namen) { const url = bildUrl(name) if (!url) continue diff --git a/custom_components/audi_dashboard/bilder.py b/custom_components/audi_dashboard/bilder.py index a29e9b2..86bc0ee 100644 --- a/custom_components/audi_dashboard/bilder.py +++ b/custom_components/audi_dashboard/bilder.py @@ -46,6 +46,43 @@ ERLAUBTE_DATEINAMEN = frozenset({ }) +def _staende(ordner: str) -> dict[str, int]: + """Zeitstempel jedes vorhandenen Fotos, als Sekunden. + + Er dient beiden Oberflaechen als Anhaengsel an der Bildadresse + (`?v=`). Home Assistant liefert /local/ mit 31 Tagen Cache-Vorgabe; + ohne wechselndes Anhaengsel zeigt eine Oberflaeche deshalb einen + Monat lang das Foto, das sie beim ersten Mal geholt hat. + + WARUM DER ZEITSTEMPEL UND NICHT DIE STARTZEIT DER APP: bis zum + 04.09.2026 haengte jede Oberflaeche `Date.now()` an - eine Adresse, + die sich bei JEDEM Start aendert. Der Zwischenspeicher des Browsers + griff damit zwischen zwei Starts nie, jeder Start holte alle Fotos + erneut (gemessen: 5,3 MB). Der Zeitstempel aendert sich dagegen + genau dann, wenn das Foto wirklich ein anderes ist - dann und nur + dann wird neu geholt. + + Ein Foto, das es nicht gibt, fehlt hier: die Oberflaechen setzen + dafuer 0 ein, und sobald es hochgeladen wird, aendert sich die + Adresse von selbst - auch ein zwischengespeicherter 404 loest sich + damit auf. + """ + ergebnis: dict[str, int] = {} + for name in ERLAUBTE_DATEINAMEN: + try: + ergebnis[name] = int(os.path.getmtime(os.path.join(ordner, name))) + except OSError: + continue + return ergebnis + + +async def staende(hass: HomeAssistant, ordner: str) -> dict[str, int]: + # Elf Dateizugriffe sind billig, gehoeren aber trotzdem nicht in die + # Ereignisschleife - Home Assistant meldet blockierende Aufrufe dort + # seit 2026.8 als Fehler. + return await hass.async_add_executor_job(_staende, ordner) + + def _pfad(ordner: str, dateiname: str | None) -> str | None: if not dateiname or dateiname not in ERLAUBTE_DATEINAMEN: return None diff --git a/custom_components/audi_dashboard/dienste.py b/custom_components/audi_dashboard/dienste.py index b922a96..318790a 100644 --- a/custom_components/audi_dashboard/dienste.py +++ b/custom_components/audi_dashboard/dienste.py @@ -407,13 +407,14 @@ def registrieren(hass: HomeAssistant, k: Koordinator) -> None: D_TANKVORGANG_LOESCHEN: _als_dienst( lambda a: belege.tankvorgang_loeschen(k, a.data["tank_id"]) ), + # Nach dem Schreiben sofort neu veroeffentlichen: der Bildstand + # haengt an der Datei, und ohne diese Zeile saehe die Oberflaeche + # das neue Foto erst beim naechsten Minutentakt. D_BILD_HOCHLADEN: _als_dienst( - lambda a: bilder.hochladen( - hass, bilder_ordner, a.data["dateiname"], a.data["daten_base64"] - ) + lambda a: _bild_hochladen(k, hass, bilder_ordner, a) ), D_BILD_LOESCHEN: _als_dienst( - lambda a: bilder.loeschen(hass, bilder_ordner, a.data["dateiname"]) + lambda a: _bild_loeschen(k, hass, bilder_ordner, a) ), D_REIFEN_WECHSELN: _als_dienst(lambda a: reifen.wechseln(k, a.data["satz"])), D_REIFEN_KM_SETZEN: _als_dienst( @@ -444,3 +445,14 @@ def registrieren(hass: HomeAssistant, k: Koordinator) -> None: def abmelden(hass: HomeAssistant) -> None: for name in SCHEMATA: hass.services.async_remove(DOMAIN, name) + +async def _bild_hochladen(k, hass, ordner, aufruf) -> None: + await bilder.hochladen( + hass, ordner, aufruf.data["dateiname"], aufruf.data["daten_base64"] + ) + await k.app_version_veroeffentlichen() + + +async def _bild_loeschen(k, hass, ordner, aufruf) -> None: + await bilder.loeschen(hass, ordner, aufruf.data["dateiname"]) + await k.app_version_veroeffentlichen() diff --git a/custom_components/audi_dashboard/frontend/app/bundle.json b/custom_components/audi_dashboard/frontend/app/bundle.json index af6f935..a147f64 100644 --- a/custom_components/audi_dashboard/frontend/app/bundle.json +++ b/custom_components/audi_dashboard/frontend/app/bundle.json @@ -1 +1 @@ -{"version":"2026.9.4.12","sha256":"bb66727726eac2e6be98dc1132bd3b532c8c6b9525f9885020a65d99d2d7fc55","bytes":321283,"gebaut":"2026-09-04T11:05:31Z"} \ No newline at end of file +{"version":"2026.9.4.14","sha256":"38298c65ce8557c805efd30f56e6978ea60857321ae008efd647cddd7b1b50d5","bytes":321436,"gebaut":"2026-09-04T11:28:44Z"} \ No newline at end of file diff --git a/custom_components/audi_dashboard/frontend/app/bundle.zip b/custom_components/audi_dashboard/frontend/app/bundle.zip index 68d4ffe..bd4c2d2 100644 Binary files a/custom_components/audi_dashboard/frontend/app/bundle.zip and b/custom_components/audi_dashboard/frontend/app/bundle.zip differ diff --git a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js index 43f85e6..180c239 100644 --- a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js +++ b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js @@ -365,11 +365,33 @@ const BILDER = [ const GALERIE = BILDER.filter((b) => !b.nurStatus); let bild = 0; const SEITE_WINTER = { label: "Auto Seite mit Winterrädern", datei: "seitenansicht-winter.webp" }; -// Bei jedem Hochladen/Löschen eines Fahrzeugfotos hochgezählt und als -// Query-Parameter an alle bilder/-URLs gehängt (siehe bildInfo() -// unten) - ohne das würde der Browser nach einem Upload weiter das alte Bild -// oder den zwischenzeitlich gecachten 404 unter derselben URL zeigen. +/* Der Stand eines Fahrzeugfotos, als Anhaengsel an seiner Adresse. + + Home Assistant liefert /local/ mit 31 Tagen Cache-Vorgabe aus; ohne + wechselndes Anhaengsel zeigt der Browser nach einem Upload weiter das alte + Bild oder den zwischenzeitlich gespeicherten 404. + + Bis zum 04.09.2026 stand hier `Date.now()` - eine Adresse, die sich bei + JEDEM Start aendert. Der Zwischenspeicher griff damit zwischen zwei Starts + nie, und mit dem Vorladen aller Fotos waeren das 5,3 MB je Start gewesen. + Jetzt kommt der Zeitstempel jeder Datei vom Backend (bilder.staende(), + veroeffentlicht an sensor.audi_dashboard_app_version): er aendert sich + genau dann, wenn das Foto wirklich ein anderes ist. + + bildVersion bleibt als Rueckfall fuer ein Backend, das die Staende noch + nicht mitliefert - dann ist das Verhalten wie zuvor. */ +let BILD_STAENDE = null; let bildVersion = Date.now(); +function bildStand(datei) { + if (!BILD_STAENDE) return bildVersion; + // 0 heisst: dieses Foto gibt es nicht. Sobald es hochgeladen wird, + // aendert sich die Adresse von selbst - auch ein gespeicherter 404 + // loest sich damit auf. + return BILD_STAENDE[datei] ?? 0; +} +function bildAdresse(datei) { + return `/local/bilder/${datei}?v=${bildStand(datei)}`; +} /* Platzhalter statt kaputtem Bild-Symbol: bei Ladefehler (Bilder aus §7a noch nicht vorhanden) wird die Bildfläche durch ein generisches Symbol @@ -499,9 +521,9 @@ async function bildVerkleinern(datei) { function bildInfo(i) { const b = BILDER[i]; if (b.name === "Seitenansicht" && CAR.reifen.aktiv === "Winter") { - return { label: SEITE_WINTER.label, datei: SEITE_WINTER.datei, src: `/local/bilder/${SEITE_WINTER.datei}?v=${bildVersion}` }; + return { label: SEITE_WINTER.label, datei: SEITE_WINTER.datei, src: bildAdresse(SEITE_WINTER.datei) }; } - return { label: b.label, datei: b.datei, src: `/local/bilder/${b.datei}?v=${bildVersion}` }; + return { label: b.label, datei: b.datei, src: bildAdresse(b.datei) }; } /* Alle Fahrzeugfotos einmal vorab holen UND dekodieren. @@ -516,12 +538,17 @@ function bildInfo(i) { konkurriert, und nur einmal je Bildstand. */ let bilderVorgeladen = null; function bilderVorladen() { - if (bilderVorgeladen === bildVersion) return; - bilderVorgeladen = bildVersion; + const dateien = [...BILDER.map((b) => b.datei), SEITE_WINTER.datei]; + // Kennung ueber alle Staende: solange sich kein Foto geaendert hat, + // laeuft das Vorladen kein zweites Mal - und der Browser holt die Bilder + // beim naechsten Start aus seinem eigenen Zwischenspeicher. + const kennung = dateien.map(bildStand).join(","); + if (bilderVorgeladen === kennung) return; + bilderVorgeladen = kennung; const starten = () => { - for (const datei of [...BILDER.map((b) => b.datei), SEITE_WINTER.datei]) { + for (const datei of dateien) { const img = new Image(); - img.src = `/local/bilder/${datei}?v=${bildVersion}`; + img.src = bildAdresse(datei); // Ein nicht hochgeladenes Foto ist der Normalfall - still schlucken. if (img.decode) img.decode().catch(() => {}); } @@ -2181,7 +2208,7 @@ function vSicherheit() { /* C6: Draufsicht oben, grüne Sammelmeldung, dann die vier Sammelzeilen - dieselbe Reihenfolge wie der Fahrzeugstatus in der myAudi-App. */ return ` -
${bildMitPlatzhalter(`/local/bilder/draufsicht.webp?v=${bildVersion}`, "draufsicht.webp", "Draufsicht", "carfix klein")}
+
${bildMitPlatzhalter(bildAdresse("draufsicht.webp"), "draufsicht.webp", "Draufsicht", "carfix klein")}
${statusKreis(g)} ${esc(sammelText)} @@ -3842,7 +3869,7 @@ function vReifen() {
- ${bildMitPlatzhalter(`/local/bilder/${radDatei}?v=${bildVersion}`, radDatei, radLabel, "radbild")} + ${bildMitPlatzhalter(bildAdresse(radDatei), radDatei, radLabel, "radbild")}
${kmOffen === k ? ` @@ -6786,6 +6813,9 @@ async function datenLaden(erzwingen) { LETZTER_DATENSATZ = versionState.attributes.daten ? versionState.attributes.daten.letzter_datensatz ?? null : null; + BILD_STAENDE = versionState.attributes.daten + ? versionState.attributes.daten.bildstaende ?? null + : null; } if (entitaetenState && entitaetenState.attributes) ENTITAETEN = entitaetenState.attributes.daten; diff --git a/custom_components/audi_dashboard/koordinator.py b/custom_components/audi_dashboard/koordinator.py index e7bc653..37ba2db 100644 --- a/custom_components/audi_dashboard/koordinator.py +++ b/custom_components/audi_dashboard/koordinator.py @@ -49,6 +49,7 @@ from homeassistant.helpers.storage import Store from . import ( batterie, + bilder, fahrterkennung, flespi, geraet, @@ -64,6 +65,7 @@ from . import ( from .ablage import Ablage from .geokodierung import Geokodierung from .const import ( + BILDER_ORDNER, BUENDEL_INFO, BUENDEL_ORDNER, BUENDEL_URL, @@ -621,6 +623,9 @@ class Koordinator: async def app_version_veroeffentlichen(self) -> None: buendel = await self.hass.async_add_executor_job(self._buendel_lesen) + bildstaende = await bilder.staende( + self.hass, self.hass.config.path(BILDER_ORDNER) + ) # Die App liest alle drei aus derselben Entität: welchen Stand diese # Installation ausliefert, ob ein nachladbares OTA-Bündel danebenliegt, # und ob eine neuere Fassung bei Gitea bereitliegt. Zusammen an einer @@ -649,6 +654,10 @@ class Koordinator: "letzter_datensatz": geraet.letzter_datensatz( self.hass, self.zuordnung.werte ), + # Der Stand jedes Fahrzeugfotos, damit beide Oberflaechen + # ein Bild nur dann neu holen, wenn es wirklich ein + # anderes ist - siehe bilder.staende(). + "bildstaende": bildstaende, }, ) diff --git a/custom_components/audi_dashboard/manifest.json b/custom_components/audi_dashboard/manifest.json index fbea92f..8813ba5 100644 --- a/custom_components/audi_dashboard/manifest.json +++ b/custom_components/audi_dashboard/manifest.json @@ -1,7 +1,7 @@ { "domain": "audi_dashboard", "name": "Audi Dashboard", - "version": "2026.9.4.12", + "version": "2026.9.4.14", "documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md", "issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues", "codeowners": [