diff --git a/AGENTS.md b/AGENTS.md index d254634..d061458 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1828,16 +1828,49 @@ about what is *running*. Today those two are wired completely differently: So the iOS app can be weeks behind the panel with nothing anywhere making that visible. That is a real gap, not a hypothetical one, and it gets worse once the app is genuinely in daily use. -Proposed shape (discussed 2026-08-23, **not yet built** — needs the owner's call on the delivery -layer): one shared version stamp fed into both builds instead of today's unrelated -`audi-dashboard-version.json` integer and `0.1.0`; the backend publishing the version it shipped -with, so both frontends can compare their own build stamp against it and show a visible hint when -behind (drift becomes loud instead of silent); and for delivery, OTA web-bundle updates so the iOS -shell can follow without an Xcode rebuild. Pointing Capacitor's `server.url` at HA was considered -and rejected: it would make the app as current as the panel, but the shell then cannot boot without -reaching HA, which guts the deliberately built offline queue (`api/warteschlange.ts`). Note the -whole mechanism survives the later integration conversion unchanged — that end state serves panel -assets and companion bundle from the same component, so one HACS update moves both. +**Built 2026-08-23 — the version identity and the drift warning.** See `VERSIONIERUNG.md` for the +owner-facing version. + +The first finding was that the number everyone assumed was "the version" is not one: +`audi-dashboard-version.json`'s integer is a **cache-buster**, rewritten with `UtcNow` by +`install.ps1`/`update.ps1` on every deploy regardless of whether any code changed. Two builds of +identical source get different numbers; the same build deployed twice gets two more. It can never +answer "are these the same state?". So the two jobs were separated instead of conflated: + +- **`VERSION`** (new, repo root, e.g. `2026.08.23.1`) — the identity, bumped by hand on change +- the existing integer — unchanged, still only breaks the browser cache + +`VERSION` now flows to both sides: into `audi-dashboard-version.json` as a second field `app` +(carried over by all three deploy scripts — they previously overwrote the whole file and would have +silently destroyed it), and into the companion build via vite `define` → `__APP_VERSION__`. The +backend reads the file and publishes `pyscript.audi_dashboard_app_version`; the companion compares +that against its own compiled-in stamp and, on mismatch, says so in the existing `Hinweisleiste` — +whose stated principle is already "nie eine stille Veraltung", which is exactly this case with the +staleness moved from the data to the app itself. + +Deliberate choices worth keeping: the comparison is **equality only**, never greater/less — the +version is an identifier, not a number, and ordering it would be false precision that breaks +silently on a format change. Either side missing yields `"unbekannt"` and **no** warning, so an +older backend or an offline start cannot produce a false alarm. And the vite build **fails hard** +if `VERSION` is absent rather than emitting an app that cannot detect its own staleness. The panel +needs none of this: it re-fetches on every page load and cannot go stale. + +**OTA delivery: verified viable, not yet built.** `@capgo/capacitor-updater` 8.51.14 checked +against the real package: MPL-2.0, peer `@capacitor/core: ^8.0.0` against our `^8.5.0`, and +self-hosting is first-class (`updateUrl`, or manual mode entirely). Manual mode is the good fit — +`download({version, url})` + `set()` against a plain zip under `/local/`, needing **no** custom +endpoint at all, with automatic rollback to the last good bundle via `notifyAppReady()`. It pairs +exactly with the version entity above: that entity is already the signal that a newer bundle exists. +Owner approved going this route, and a paid Apple developer account is available through Paul (Paul +Nothaft, the Gitea repo owner) — which is what makes it worthwhile, since the free account's 7-day +signature expiry would otherwise force Xcode weekly anyway. + +Capacitor's `server.url` pointed at HA was considered and **rejected**: it would make the app as +current as the panel, but the shell then cannot boot without reaching HA, gutting the deliberately +built offline queue (`api/warteschlange.ts`). + +The whole mechanism survives the later integration conversion unchanged — that end state serves +panel assets and companion bundle from the same component, so one HACS update moves both. ### G) Fresh-install audit + installer hardening (2026-08-23, before the first real deployment) @@ -1893,6 +1926,35 @@ user to check Einstellungen → System → Speicher first, and recommend `purge_ is tight (enough for the "catch up on the past" purpose, since the import moves data into the app's own permanent `.jsonl` files anyway). +**Two user-reported bugs fixed 2026-08-23, both worse than they looked:** + +*"Fahrzeug fährt" never changed.* `standortZustand()` inferred the driving state from +`TRIPS[0].status === "offen"` — two independent errors stacked. First, `"offen"` does not mean +"driving"; it means **data incomplete** (distance still missing, see `fahrtabschluss_logik.py`) — a +trip created by `_fahrt_beenden()` *when it ends*. A trip that never got its odometer, e.g. because +no `KM_SENSOR` was mapped at the time, stays `"offen"` forever. Second, `TRIPS[0]` is the **oldest** +trip, not the newest: `fahrten_veroeffentlichen()` passes `profil.fahrten_lesen()` through in file +order, which is ascending. Together: the oldest ever-incomplete trip pinned the display to "fährt" +permanently. The test instance had one open for 18 days. + +Fixed at the source rather than by patching the index: the backend now publishes `zuendung` in +`fahrzeugstatus` from the already-mapped `ZUENDUNG_SENSOR` — the same signal `fahrterkennung.py` +treats as authoritative, so display and detection cannot disagree by construction. `null` (no sensor +mapped) renders "Fahrzustand unbekannt" rather than claiming "steht". + +The companion had the mirror-image bug: `liveZustandLesen()` read `status["zuendung"]` (never +published) and `status["lat"]`/`status["lon"]` (backend publishes `standort_lat`/`standort_lon`), and +`Fahrzeug` declared none of those fields — so `LiveFahrt` showed "Das Fahrzeug steht" permanently and +never a position. Fields added to `Fahrzeugstatus`/`Fahrzeug` and the adapter, so the type now +catches this class of bug — which its own header comment already warned about from a previous +occurrence. + +*"Standortzugriff verweigert" was misleading.* It sits directly under the vehicle name where the +distance-to-car goes, but describes **this device's** browser geolocation — it read as if the car's +location were unavailable. Now "GPS offline", as the owner asked. Permission denial keeps its own +string ("GPS-Freigabe fehlt"): it is the one case with a different remedy, and "GPS offline" would +send the user looking at signal instead of at a permission toggle. + **Also fixed this round (panel-only):** the "Teilen" pill on the location sheet was invisible in day mode — `.standort-pille` used `background: var(--tile)` while the sheet under it uses `var(--tile-deckend)`, and the iOS overlay sets *both* to `#FFFFFF` in day mode. Measured contrast diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..69e24fd --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +2026.08.23.1 diff --git a/VERSIONIERUNG.md b/VERSIONIERUNG.md new file mode 100644 index 0000000..2dd5d6a --- /dev/null +++ b/VERSIONIERUNG.md @@ -0,0 +1,86 @@ +# Versionierung — warum es zwei Zahlen gibt + +Kurzfassung für den Alltag: **Wenn du etwas an der Oberfläche änderst, erhöhe +`VERSION`.** Alles Weitere erledigen die Bau- und Installationsskripte. + +--- + +## Die zwei Zahlen tun verschiedene Dinge + +Sie sahen sich lange zum Verwechseln ähnlich, deshalb hier ausdrücklich +getrennt: + +| | `VERSION` (Repo-Wurzel) | `version` in `audi-dashboard-version.json` | +|---|---|---| +| Zweck | **Identität**: welcher Stand ist das? | **Cache-Bruch**: hol die Dateien neu | +| Format | `2026.08.23.1` (Datum + laufende Nummer) | Unix-Sekunden, z. B. `1787016000` | +| Wer setzt sie | **du, von Hand**, wenn sich etwas ändert | `install.ps1` / `update.ps1` bei jedem Deploy | +| Vergleichbar zwischen Panel und iOS-App | **ja, darum geht es** | nein, jede Installation hat eine andere | + +Die Unix-Zahl taugt bewusst **nicht** als Versionsvergleich: sie wird bei jeder +Installation neu gesetzt, ohne dass sich am Code etwas geändert hätte. Sie sagt +dem Browser nur „lade neu", nicht „das ist Stand X". + +## Warum das überhaupt nötig wurde + +Das Panel und die iOS-App werden völlig unterschiedlich ausgeliefert: + +- **Panel:** holt bei jedem Seitenaufruf `audi-dashboard-version.json` mit + `cache: "no-store"` und lädt seine Dateien neu, sobald die Zahl sich geändert + hat. Es ist damit nach einem Deploy sofort aktuell, ohne Zutun. +- **iOS-App:** eine Capacitor-Hülle mit **fest gebündelten** Dateien. Sie bleibt + auf dem Stand, der beim Signieren in Xcode eingebaut wurde — unbegrenzt. + +Die Paritätsregel in `AGENTS.md` sichert, dass beide Codebasen in derselben +Sitzung geändert werden. Über das, was *läuft*, sagte bisher nichts etwas: die +iOS-App konnte wochenlang hinterherhinken, ohne dass es irgendwo sichtbar wurde. + +`VERSION` schließt genau diese Lücke — nicht, indem es die Abweichung +verhindert (das kann keine Zahl), sondern indem es sie **sichtbar** macht. + +## Wie die Zahl durchs System läuft + +``` +VERSION (2026.08.23.1) + │ + ├─► homeassistant/www/audi-dashboard-version.json { "app": "2026.08.23.1" } + │ │ + │ └─► Backend liest die Datei und veröffentlicht sie als + │ pyscript.audi_dashboard_app_version + │ │ + │ └─► die iOS-App vergleicht sie mit ihrer eigenen, + │ einkompilierten Zahl und zeigt bei Abweichung + │ einen Hinweis in der Hinweisleiste + │ + └─► companion-app: von vite beim Bauen als __APP_VERSION__ eingesetzt +``` + +Der Vergleich läuft also immer in eine Richtung: **das Backend sagt, welcher +Stand ausgeliefert wurde; die App sagt, welchen sie hat.** Stimmen sie nicht +überein, ist die App zu alt (oder, seltener, das Backend). + +## Was du tun musst + +**Bei einer Änderung an der Oberfläche oder am Backend:** + +1. `VERSION` erhöhen — bei mehreren Änderungen am selben Tag die laufende + Nummer: `2026.08.23.1` → `2026.08.23.2`, am nächsten Tag `2026.08.24.1`. +2. Panel deployen wie bisher (`update.ps1`). Das Skript trägt die neue `VERSION` + in `audi-dashboard-version.json` ein und setzt die Cache-Zahl frisch. +3. iOS-App neu bauen (`npm run build`), damit sie dieselbe Zahl einkompiliert + bekommt. + +Vergisst du Schritt 3, ist das kein stiller Fehler mehr: die App meldet selbst, +dass sie älter ist als der Server. + +**Bei einer reinen Neuinstallation** ohne Codeänderung: nichts tun. `VERSION` +bleibt, wie sie ist; nur die Cache-Zahl wird neu gesetzt. + +## Ausblick: OTA-Updates + +Sobald `@capgo/capacitor-updater` eingebaut ist (geprüft: MPL-2.0, passt zu +Capacitor 8, Selbst-Hosting ohne fremde Cloud möglich), holt sich die iOS-App +den neuen Stand selbst — dann entfällt Schritt 3 für alles, was nur +JavaScript/CSS betrifft. Xcode wird dann nur noch für echte native Änderungen +gebraucht. Der Vergleich aus dieser Datei bleibt dabei unverändert nützlich: er +ist genau das Signal, an dem die App erkennt, dass ein neues Bündel bereitliegt. diff --git a/companion-app/src/App.tsx b/companion-app/src/App.tsx index 5efa43b..43325e0 100644 --- a/companion-app/src/App.tsx +++ b/companion-app/src/App.tsx @@ -71,7 +71,11 @@ function AngemeldeteApp({ beiAbmeldung }: { beiAbmeldung: () => void }) { geheZu={geheZu} tabBeschriftung={tabBeschriftung} hinweis={ - + } > { + try { + const zustand = await this.rest.zustandLesen<{ daten?: { app?: string | null } }>( + ENTITAETEN.appVersion, + ); + return zustand.attributes?.daten?.app ?? null; + } catch { + return null; + } + } + /* ------------------------------------------- Import aus dem HA-Verlauf Bewusst NICHT über die Warteschlange, anders als die übrigen schreibenden Vorgänge: der Import ist keine Eingabe, die man im Funkloch diff --git a/companion-app/src/api/types.ts b/companion-app/src/api/types.ts index fd20929..85edb59 100644 --- a/companion-app/src/api/types.ts +++ b/companion-app/src/api/types.ts @@ -106,6 +106,13 @@ export interface Fahrzeugstatus { batteriespannung?: number | null; gesichert?: boolean | null; sicherheitscheck?: SicherheitsPunkt[]; + /** Zündung an = fährt gerade. `null`/fehlend heißt „kein Zündungssensor + zugeordnet", also unbekannt — ausdrücklich nicht „steht". */ + zuendung?: boolean | null; + standort_lat?: number | null; + standort_lon?: number | null; + standort_genauigkeit_m?: number | null; + standort_zeit?: string | null; oelwechsel_faellig_ts?: string | null; oelwechsel_faellig_km?: number | null; inspektion_faellig_ts?: string | null; @@ -163,6 +170,7 @@ export const ENTITAETEN = { belegErgebnis: "pyscript.audi_dashboard_beleg_ergebnis", updateStatus: "pyscript.audi_dashboard_update_status", importStatus: "pyscript.audi_dashboard_import_status", + appVersion: "pyscript.audi_dashboard_app_version", } as const; export type EntitaetsSchluessel = keyof typeof ENTITAETEN; diff --git a/companion-app/src/daten/DatenKontext.tsx b/companion-app/src/daten/DatenKontext.tsx index 1c890e1..100b2ee 100644 --- a/companion-app/src/daten/DatenKontext.tsx +++ b/companion-app/src/daten/DatenKontext.tsx @@ -29,6 +29,8 @@ import { type Verbindungszustand, type WartenderAuftrag, } from "../api" +import type { Versionsstand } from "./appVersion" +import { eigeneVersion, versionVergleichen } from "./appVersion" import type { Einstellungen, Fahrzeug } from "./profilAdapter" import { profilZuEinstellungen, profilZuFahrzeug, zusammenfuehren } from "./profilAdapter" @@ -40,6 +42,9 @@ export interface DatenWert { warteschlange: readonly WartenderAuftrag[] /** Zeitpunkt der letzten Statusmeldung — treibt die Veraltet-Anzeige. */ statusStand: string | null + /** Ob diese App noch dem Stand entspricht, den das Backend ausliefert. + Siehe appVersion.ts und VERSIONIERUNG.md. */ + versionsstand: Versionsstand einstellungen: Einstellungen | null fahrzeug: Fahrzeug | null @@ -81,6 +86,7 @@ export function DatenAnbieter({ const [ladefehler, setzeLadefehler] = useState(null) const [verbindung, setzeVerbindung] = useState("getrennt") const [warteschlange, setzeWarteschlange] = useState([]) + const [serverVersion, setzeServerVersion] = useState(null) // Damit profilSpeichern immer gegen den neuesten Rohstand arbeitet, auch // wenn zwischendurch ein Push hereinkam. @@ -102,6 +108,11 @@ export function DatenAnbieter({ setzeStatusStand(s.last_updated) setzeLadefehler(null) setzeBereit(true) + // Bewusst außerhalb des Promise.all oben und ohne eigenes catch hier: + // appVersionLesen() schluckt seine Fehler selbst und liefert dann null. + // Ein fehlender Versionsvergleich darf die Erstladung nie aufhalten - + // er ist ein Hinweis, keine Betriebsvoraussetzung. + setzeServerVersion(await api.appVersionLesen()) } catch (fehler) { // Kein Netz ist kein Ladefehler, solange schon Daten da sind — dann // zeigt die App den zwischengespeicherten Stand mit Offline-Hinweis. @@ -198,6 +209,7 @@ export function DatenAnbieter({ verbindung, warteschlange, statusStand, + versionsstand: versionVergleichen(eigeneVersion(), serverVersion).stand, einstellungen, fahrzeug, fahrten, diff --git a/companion-app/src/daten/appVersion.test.ts b/companion-app/src/daten/appVersion.test.ts new file mode 100644 index 0000000..9b58085 --- /dev/null +++ b/companion-app/src/daten/appVersion.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest" + +import { versionVergleichen } from "./appVersion" + +/* Der Vergleich entscheidet, ob die App dem Nutzer sagt „ich bin älter als der + Server". Beide Fehlrichtungen sind teuer: ein Fehlalarm schickt ihn grundlos + an Xcode, ein verschluckter Hinweis lässt genau die stille Veraltung zu, die + die ganze Mechanik verhindern soll. */ +describe("versionVergleichen", () => { + it("meldet Gleichstand bei identischer Version", () => { + const v = versionVergleichen("2026.08.23.1", "2026.08.23.1") + expect(v.stand).toBe("gleich") + }) + + it("meldet eine Abweichung, sobald die Zeichenketten sich unterscheiden", () => { + expect(versionVergleichen("2026.08.23.1", "2026.08.24.1").stand).toBe("abweichend") + expect(versionVergleichen("2026.08.23.2", "2026.08.23.1").stand).toBe("abweichend") + }) + + it("vergleicht nicht, wenn das Backend nichts meldet", () => { + // Altes Backend ohne die Versions-Entität, oder offline. Ein Hinweis waere + // hier geraten, nicht gewusst. + expect(versionVergleichen("2026.08.23.1", null).stand).toBe("unbekannt") + }) + + it("vergleicht nicht, wenn die App ihre eigene Version nicht kennt", () => { + expect(versionVergleichen(null, "2026.08.23.1").stand).toBe("unbekannt") + }) + + it("behandelt eine leere Zeichenkette wie fehlend, nicht wie einen Wert", () => { + expect(versionVergleichen("", "2026.08.23.1").stand).toBe("unbekannt") + expect(versionVergleichen("2026.08.23.1", "").stand).toBe("unbekannt") + }) + + it("gibt beide Staende zur Anzeige zurueck", () => { + const v = versionVergleichen("2026.08.23.1", "2026.08.24.1") + expect(v.app).toBe("2026.08.23.1") + expect(v.server).toBe("2026.08.24.1") + }) +}) diff --git a/companion-app/src/daten/appVersion.ts b/companion-app/src/daten/appVersion.ts new file mode 100644 index 0000000..44528ff --- /dev/null +++ b/companion-app/src/daten/appVersion.ts @@ -0,0 +1,45 @@ +/** + * Vergleich zwischen dem Stand, den diese App mitbringt, und dem, den das + * Backend ausliefert. Hintergrund: ../../VERSIONIERUNG.md + * + * Warum es das braucht: Das Panel lädt seine Dateien bei jedem Seitenaufruf + * frisch und kann gar nicht veralten. Die iOS-App trägt ihre Dateien fest + * gebündelt und bleibt auf dem Stand vom Signieren — unbegrenzt und bisher + * unbemerkt. Diese Datei macht daraus ein sichtbares Signal. + * + * Bewusst kein „größer/kleiner"-Vergleich: die Version ist eine Kennung + * (`2026.08.23.1`), keine Zahl. Ein Sortierversuch wäre nur scheingenau und + * würde bei einem Formatwechsel still falsche Antworten geben. Verglichen wird + * auf Gleichheit — alles andere heißt „stimmt nicht überein", und was davon + * älter ist, entscheidet der Mensch. + */ + +export type Versionsstand = + /** App und Backend melden denselben Stand. */ + | "gleich" + /** Sie weichen ab — die App ist mit hoher Wahrscheinlichkeit älter. */ + | "abweichend" + /** Kein Vergleich möglich (Backend meldet nichts, offline, altes Backend). */ + | "unbekannt" + +export interface Versionsvergleich { + stand: Versionsstand + app: string | null + server: string | null +} + +/** Die in diese App einkompilierte Version (vite, siehe vite.config.ts). */ +export function eigeneVersion(): string | null { + // Der Zugriff ist abgesichert, weil die Konstante in Testläufen und im + // Dev-Server nicht zwingend gesetzt ist. Fehlt sie, wird nicht verglichen, + // statt eine Abweichung zu behaupten. + return typeof __APP_VERSION__ === "string" && __APP_VERSION__ ? __APP_VERSION__ : null +} + +export function versionVergleichen( + app: string | null, + server: string | null, +): Versionsvergleich { + if (!app || !server) return { stand: "unbekannt", app, server } + return { stand: app === server ? "gleich" : "abweichend", app, server } +} diff --git a/companion-app/src/daten/profilAdapter.ts b/companion-app/src/daten/profilAdapter.ts index 0f7326e..2f41692 100644 --- a/companion-app/src/daten/profilAdapter.ts +++ b/companion-app/src/daten/profilAdapter.ts @@ -48,6 +48,12 @@ export interface Fahrzeug { batteriespannung: number | null gesichert: boolean | null sicherheitscheck: SicherheitsPunkt[] + /** Zündung an = das Fahrzeug fährt gerade. `null` heißt „kein + Zündungssensor zugeordnet", also unbekannt — nicht „steht". */ + zuendung: boolean | null + standortLat: number | null + standortLon: number | null + standortZeit: string | null oelwechselFaelligTs: string | null oelwechselFaelligKm: number | null inspektionFaelligTs: string | null @@ -144,6 +150,10 @@ export function profilZuFahrzeug(profil: Profil, status: Fahrzeugstatus): Fahrze batteriespannung: status.batteriespannung ?? null, gesichert: status.gesichert ?? null, sicherheitscheck: status.sicherheitscheck ?? [], + zuendung: status.zuendung ?? null, + standortLat: status.standort_lat ?? null, + standortLon: status.standort_lon ?? null, + standortZeit: status.standort_zeit ?? null, oelwechselFaelligTs: status.oelwechsel_faellig_ts ?? null, oelwechselFaelligKm: status.oelwechsel_faellig_km ?? null, inspektionFaelligTs: status.inspektion_faellig_ts ?? null, diff --git a/companion-app/src/daten/service.test.ts b/companion-app/src/daten/service.test.ts index 95fca7c..8d562a4 100644 --- a/companion-app/src/daten/service.test.ts +++ b/companion-app/src/daten/service.test.ts @@ -16,6 +16,10 @@ function fahrzeug(odo: number, modus = "hersteller"): Fahrzeug { batteriespannung: null, gesichert: null, sicherheitscheck: [], + zuendung: null, + standortLat: null, + standortLon: null, + standortZeit: null, oelwechselFaelligTs: null, oelwechselFaelligKm: null, inspektionFaelligTs: null, diff --git a/companion-app/src/screens/Hinweisleiste.tsx b/companion-app/src/screens/Hinweisleiste.tsx index b3712ea..c81268d 100644 --- a/companion-app/src/screens/Hinweisleiste.tsx +++ b/companion-app/src/screens/Hinweisleiste.tsx @@ -7,22 +7,31 @@ */ import type { Verbindungszustand, WartenderAuftrag } from "../api" +import type { Versionsstand } from "../daten/appVersion" import { SymbolWarnung } from "../symbole" export function Hinweisleiste({ verbindung, warteschlange, + versionsstand = "unbekannt", }: { verbindung: Verbindungszustand warteschlange: readonly WartenderAuftrag[] + /** Ob diese App noch dem Stand entspricht, den das Backend ausliefert. + Siehe daten/appVersion.ts und VERSIONIERUNG.md. */ + versionsstand?: Versionsstand }) { const offline = verbindung !== "verbunden" const wartend = warteschlange.length + // Derselbe Grundsatz wie beim Offline-Hinweis oben: keine stille Veraltung. + // Nur hier veraltet nicht die Anzeige, sondern die App selbst — sie trägt + // ihre Dateien fest gebündelt und merkt eine neuere Fassung sonst nie. + const veraltet = versionsstand === "abweichend" // "verbindet" gleich beim Start nicht als Offline melden — das würde bei // jedem App-Start kurz aufblitzen. - if (!offline && wartend === 0) return null - if (verbindung === "verbindet" && wartend === 0) return null + if (!offline && wartend === 0 && !veraltet) return null + if (verbindung === "verbindet" && wartend === 0 && !veraltet) return null const gescheitert = warteschlange.filter((a) => a.fehlversuche > 0).length @@ -42,6 +51,8 @@ export function Hinweisleiste({ : `${wartend} Änderungen warten auf Übertragung.` : null} {gescheitert > 0 ? ` ${gescheitert} davon mit Fehlversuchen.` : null} + {veraltet && (offline || wartend > 0) ? " " : null} + {veraltet ? "Diese App ist älter als der Server — bitte aktualisieren." : null} ) diff --git a/companion-app/src/screens/LiveFahrt.tsx b/companion-app/src/screens/LiveFahrt.tsx index a228a46..57f576c 100644 --- a/companion-app/src/screens/LiveFahrt.tsx +++ b/companion-app/src/screens/LiveFahrt.tsx @@ -24,13 +24,19 @@ export interface LiveZustand { dauerS?: number | null } -/** Liest die Live-Werte aus dem Fahrzeugstatus. Solange der FMM003 fehlt, - liefert das Backend diese Felder nicht — dann ist alles null. */ +/** Liest die Live-Werte aus dem aufbereiteten Fahrzeug-Objekt. + + Bis 2026-08-23 las diese Funktion `lat`/`lon`/`zuendung` roh aus dem + Status-Blob — Feldnamen, die es dort nie gab: das Backend veröffentlicht + `standort_lat`/`standort_lon`, und `zuendung` gab es überhaupt noch nicht. + Beides kam nie an, die Live-Ansicht meldete deshalb dauerhaft „Das + Fahrzeug steht" und zeigte nie eine Position. Jetzt über die benannten + Felder aus profilAdapter, die der Typ `Fahrzeugstatus` absichert. */ export function liveZustandLesen(status: Record): LiveZustand { - const lat = typeof status["lat"] === "number" ? status["lat"] : null - const lon = typeof status["lon"] === "number" ? status["lon"] : null + const lat = typeof status["standortLat"] === "number" ? status["standortLat"] : null + const lon = typeof status["standortLon"] === "number" ? status["standortLon"] : null return { - faehrt: status["zuendung"] === true || status["zuendung"] === 1, + faehrt: status["zuendung"] === true, geschwindigkeit: typeof status["geschwindigkeit"] === "number" ? status["geschwindigkeit"] : null, position: lat != null && lon != null ? { lat, lon } : null, streckeKm: typeof status["fahrt_strecke_km"] === "number" ? status["fahrt_strecke_km"] : null, diff --git a/companion-app/src/umgebung.d.ts b/companion-app/src/umgebung.d.ts new file mode 100644 index 0000000..4a686a3 --- /dev/null +++ b/companion-app/src/umgebung.d.ts @@ -0,0 +1,9 @@ +/** + * Von vite beim Bauen eingesetzte Konstanten (siehe vite.config.ts). + * + * `__APP_VERSION__` ist der Inhalt der Datei VERSION im Projektstamm — dieselbe + * Zahl, die auch das Panel trägt. Damit kann die App vergleichen, ob sie noch + * dem Stand entspricht, den das Backend ausliefert. Hintergrund und Ablauf: + * ../../VERSIONIERUNG.md + */ +declare const __APP_VERSION__: string diff --git a/companion-app/vite.config.ts b/companion-app/vite.config.ts index eef373b..0240bbf 100644 --- a/companion-app/vite.config.ts +++ b/companion-app/vite.config.ts @@ -1,12 +1,27 @@ +import { readFileSync } from "node:fs" +import { fileURLToPath } from "node:url" + import { defineConfig } from "vite" import react from "@vitejs/plugin-react" +// Die App-Version kommt aus der Datei VERSION im Projektstamm — derselben, aus +// der auch das Panel seine bekommt (siehe VERSIONIERUNG.md). Sie wird hier fest +// einkompiliert, damit die fertige App weiß, aus welchem Stand sie gebaut +// wurde, und das gegen den vom Backend gemeldeten Stand halten kann. +// +// Absichtlich hart: fehlt die Datei, soll der Build abbrechen statt still eine +// App ohne Versionsangabe zu erzeugen — die könnte ihre eigene Veraltung nicht +// mehr erkennen, und genau das ist der Zweck der ganzen Mechanik. +const versionsDatei = fileURLToPath(new URL("../VERSION", import.meta.url)) +const appVersion = readFileSync(versionsDatei, "utf8").trim() + // Basispfad relativ: die App wird sowohl unter einer eigenen Domain als auch // aus einem Unterordner heraus ausgeliefert (Home Assistant: /local/dm360/), // und in der Capacitor-Hülle direkt vom Dateisystem. export default defineConfig({ base: "./", plugins: [react()], + define: { __APP_VERSION__: JSON.stringify(appVersion) }, server: { port: 5173 }, build: { outDir: "dist", diff --git a/homeassistant/installationspaket/install.ps1 b/homeassistant/installationspaket/install.ps1 index c78b689..9a7840b 100644 --- a/homeassistant/installationspaket/install.ps1 +++ b/homeassistant/installationspaket/install.ps1 @@ -202,9 +202,22 @@ if ($Pruefen) { # Wie in update.ps1: die Versionsdatei bestimmt, ob Browser den neuen Code # ziehen. Immer frisch schreiben, sonst zeigt ein Browser, der die App # schon einmal geladen hatte, weiter den alten Stand. + # Zwei Zahlen mit verschiedenen Aufgaben, siehe VERSIONIERUNG.md: + # version = Unix-Sekunden, bricht den Browser-Cache, bei jedem Deploy neu + # app = der Stand aus der Datei VERSION, identifiziert die Fassung + # Die App-Version kommt aus der mitgelieferten Datei und wird ÜBERNOMMEN, + # nicht neu erfunden - sonst verlöre die iOS-App ihren Vergleichswert und + # könnte ihre eigene Veraltung nicht mehr erkennen. $version = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + $appVersion = "" + $quellVersion = Join-Path $hier "www\audi-dashboard-version.json" + if (Test-Path $quellVersion) { + try { $appVersion = (Get-Content $quellVersion -Raw | ConvertFrom-Json).app } catch { $appVersion = "" } + } $versionsDatei = Join-Path $Ziel "www\audi-dashboard-version.json" - [System.IO.File]::WriteAllText($versionsDatei, "{ ""version"": $version }", (New-Object System.Text.UTF8Encoding($false))) + $inhaltVersion = if ($appVersion) { "{ ""version"": $version, ""app"": ""$appVersion"" }" } + else { "{ ""version"": $version }" } + [System.IO.File]::WriteAllText($versionsDatei, $inhaltVersion, (New-Object System.Text.UTF8Encoding($false))) Gut "www\ kopiert (Version $version)" } diff --git a/homeassistant/installationspaket/pyscript/modules/frontend_veroeffentlichung.py b/homeassistant/installationspaket/pyscript/modules/frontend_veroeffentlichung.py index 7e6ceb2..d5640cd 100644 --- a/homeassistant/installationspaket/pyscript/modules/frontend_veroeffentlichung.py +++ b/homeassistant/installationspaket/pyscript/modules/frontend_veroeffentlichung.py @@ -19,6 +19,10 @@ tankvorgaenge_veroeffentlichen() auf einen Zeitraum eingeschränkt werden (z. B. nur das laufende Jahr), statt hier vorschnell zu vereinfachen. """ +import io +import json +import os + import einstellungen import entitaeten import profil @@ -87,6 +91,17 @@ def _abs_zahl(wert): return None if zahl is None else abs(zahl) +def _zu_bool(wert): + """binary_sensor-Zustand als echtes True/False, None bei fehlender Meldung. + + None ist hier ausdrücklich kein "nein": ohne zugeordneten Sensor weiß die + App schlicht nicht, ob gefahren wird - und muss das anzeigen dürfen (§9), + statt "steht" zu behaupten.""" + if wert is None: + return None + return str(wert).lower() in ("on", "true", "1", "open", "yes") + + def _standort(): """Live-GPS-Position des Fahrzeugs (Übersicht -> Standort-Kachel) - liest STANDORT_LAT_SENSOR/STANDORT_LON_SENSOR, zwei eigene sensor-Entities für @@ -172,6 +187,18 @@ def fahrzeugstatus_veroeffentlichen(): "batteriespannung": _zu_zahl(batteriespannung), "gesichert": gesichert, "sicherheitscheck": sicherheitscheck, + # Fährt das Fahrzeug gerade? Kommt aus derselben Zündungs-Entität, die + # auch fahrterkennung.py als maßgebliches Signal für Fahrtbeginn/-ende + # nimmt - damit sagen Anzeige und Erfassung zwangsläufig dasselbe. + # + # Vorher wurde dieser Zustand im Frontend aus dem Status der Fahrten + # abgeleitet ("offen" = fährt). Das war falsch: "offen" heißt + # unvollständige Daten (Strecke fehlt noch, siehe + # fahrtabschluss_logik.py), nicht "unterwegs" - eine Fahrt ohne + # Kilometerstand blieb dauerhaft "offen" und das Fahrzeug damit + # dauerhaft "fahrend". None bedeutet: kein Zündungssensor zugeordnet, + # Zustand unbekannt. + "zuendung": _zu_bool(zustand_oder_none(einstellungen.ZUENDUNG_SENSOR)), "standort_lat": standort["lat"], "standort_lon": standort["lon"], "standort_genauigkeit_m": standort["genauigkeit_m"], @@ -188,7 +215,41 @@ def fahrzeugstatus_veroeffentlichen(): state.set("pyscript.audi_dashboard_fahrzeugstatus", "aktuell", new_attributes={"daten": daten}) +APP_VERSION_PFAD = "/config/www/audi-dashboard-version.json" + + +def app_version_veroeffentlichen(): + """Welchen Oberflächen-Stand diese Installation ausliefert. + + Gedacht für die Companion-App: die trägt ihre Version fest einkompiliert + (vite setzt __APP_VERSION__ aus der Datei VERSION im Projektstamm) und + kann sie gegen diesen Wert halten. Weicht sie ab, ist die App älter als + der Server und sagt das selbst - siehe VERSIONIERUNG.md. + + Das Panel braucht den Vergleich nicht: es lädt seine Dateien bei jedem + Seitenaufruf frisch, sobald sich die Cache-Zahl in derselben Datei + geändert hat, und kann deshalb gar nicht veralten. + + Bewusst eine eigene Entität statt eines Felds im Fahrzeugstatus: das hier + ist eine Eigenschaft der Installation, keine des Fahrzeugs.""" + version = None + if os.path.exists(APP_VERSION_PFAD): + try: + f = task.executor(io.open, APP_VERSION_PFAD, "r") + inhalt = f.read() + f.close() + version = json.loads(inhalt).get("app") + except (ValueError, OSError) as fehler: + log.warning(f"audi_dashboard: App-Version nicht lesbar ({fehler})") + state.set( + "pyscript.audi_dashboard_app_version", + version or "unbekannt", + new_attributes={"daten": {"app": version}}, + ) + + def alles_veroeffentlichen(): + app_version_veroeffentlichen() profil_veroeffentlichen() fahrten_veroeffentlichen() tankvorgaenge_veroeffentlichen() diff --git a/homeassistant/installationspaket/update.ps1 b/homeassistant/installationspaket/update.ps1 index d50518e..2ce27f5 100644 --- a/homeassistant/installationspaket/update.ps1 +++ b/homeassistant/installationspaket/update.ps1 @@ -80,11 +80,23 @@ if (Test-Path "$hier\www\badges") { Copy-Item "$hier\www\badges\*" "$Ziel\www\badges\" -Force -Recurse } +# Zwei Zahlen mit verschiedenen Aufgaben, siehe VERSIONIERUNG.md: +# version = Unix-Sekunden, bricht den Browser-Cache, bei jedem Deploy neu +# app = identifiziert die Fassung; wird aus der mitgelieferten Datei +# übernommen, nicht neu erfunden +# An der App-Version erkennt die iOS-App, ob sie noch dem ausgelieferten Stand +# entspricht. Ginge sie hier verloren, wäre der Vergleich still abgeschaltet. $version = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() -"{ ""version"": $version }" | Out-File -FilePath "$Ziel\www\audi-dashboard-version.json" -Encoding utf8 -NoNewline +$appVersion = "" +if (Test-Path "$hier\www\audi-dashboard-version.json") { + try { $appVersion = (Get-Content "$hier\www\audi-dashboard-version.json" -Raw | ConvertFrom-Json).app } catch { $appVersion = "" } +} +$inhaltVersion = if ($appVersion) { "{ ""version"": $version, ""app"": ""$appVersion"" }" } + else { "{ ""version"": $version }" } +$inhaltVersion | Out-File -FilePath "$Ziel\www\audi-dashboard-version.json" -Encoding utf8 -NoNewline Write-Host "" -Write-Host "Fertig. Version $version." -ForegroundColor Green +Write-Host "Fertig. Version $version$(if ($appVersion) { " (App $appVersion)" })." -ForegroundColor Green Write-Host "pyscript-Änderungen sind bereits aktiv (Auto-Reload, kein Neustart nötig)." Write-Host "Frontend-Änderungen: im Browser einmal ganz normal neu laden (F5) reicht -" Write-Host "kein Hard-Refresh, kein HA-Neustart." diff --git a/homeassistant/installationspaket/www/audi-dashboard-app.js b/homeassistant/installationspaket/www/audi-dashboard-app.js index f37475c..4f16168 100644 --- a/homeassistant/installationspaket/www/audi-dashboard-app.js +++ b/homeassistant/installationspaket/www/audi-dashboard-app.js @@ -111,6 +111,7 @@ function profilZuCar(p, status) { batteriespannung: status.batteriespannung, gesichert: status.gesichert, sicherheitscheck: status.sicherheitscheck || [], + zuendung: status.zuendung ?? null, standortLat: status.standort_lat, standortLon: status.standort_lon, standortGenauigkeitM: status.standort_genauigkeit_m, @@ -588,21 +589,49 @@ function tankstellenMarkerSVG(px = 32) { return ``; } -/* Fahrzustand fürs Menü: "fährt" solange die aktuelle Fahrt noch offen ist - (siehe fahrterkennung.py), sonst "steht". Ohne echtes Bewegungssignal vom - Fahrzeug (siehe AGENTS.md, FMM003/flespi offen) ist das die beste Näherung - aus den bereits vorhandenen Fahrtdaten. +/* Fahrzustand fürs Menü: kommt aus der Zündung (CAR.zuendung, vom Backend aus + demselben Sensor veröffentlicht, den auch fahrterkennung.py als maßgebliches + Signal nimmt). Ist kein Zündungssensor zugeordnet, bleibt der Zustand + ehrlich unbekannt, statt "steht" zu behaupten. + + Vorher stand hier `TRIPS[0].status === "offen"` - gleich zwei Fehler + übereinander, die sich gegenseitig verstärkt haben: + + 1. "offen" heißt NICHT "unterwegs", sondern "Daten noch unvollständig" + (die Strecke fehlt, siehe fahrtabschluss_logik.py). Eine beendete + Fahrt ohne Kilometerstand - etwa weil zu dem Zeitpunkt kein + KM_SENSOR zugeordnet war - bleibt für immer "offen". + 2. TRIPS[0] ist die ÄLTESTE Fahrt, nicht die neueste: + fahrten_veroeffentlichen() gibt profil.fahrten_lesen() unverändert in + Dateireihenfolge weiter, und die ist aufsteigend. + + Zusammen hieß das: die älteste jemals unvollständig gebliebene Fahrt hat + das Fahrzeug dauerhaft als "fahrend" angezeigt. An der Testinstanz war das + eine Fahrt, die seit 18 Tagen beendet war. parkplatz unterscheidet einen endgültigen Stillstand (laenger als die in "Fahrten zusammenfassen" hinterlegte Pausenzeit) von einem blossen Zwischenstopp. Das Feld ist vorbereitet, wird aber derzeit von keiner Ansicht gelesen - eine "Letzter Parkplatz"-Anzeige gibt es noch nicht. */ +function letzteFahrt() { + let neueste = null; + for (const t of TRIPS) { + if (!t || !t.ts_end) continue; + if (!neueste || t.ts_end > neueste.ts_end) neueste = t; + } + return neueste; +} function standortZustand() { - const t = TRIPS[0]; - if (t && t.status === "offen") return { faehrt: true, seitTs: null, parkplatz: false }; + const t = letzteFahrt(); const seitTs = t && t.ts_end ? new Date(t.ts_end).getTime() : null; + if (CAR.zuendung === true) return { faehrt: true, seitTs: null, parkplatz: false }; const minuten = seitTs != null ? (Date.now() - seitTs) / 60000 : null; - return { faehrt: false, seitTs, parkplatz: minuten != null && minuten >= (CONFIG.pauseMin ?? 15) }; + return { + faehrt: false, + unbekannt: CAR.zuendung == null, + seitTs, + parkplatz: minuten != null && minuten >= (CONFIG.pauseMin ?? 15), + }; } function dauerSeitText(ts) { if (ts == null) return "unbekannt"; @@ -633,7 +662,7 @@ function standortAbstandText() { return USER_POS_FEHLER || "Abstand wird ermittelt …"; } function standortErfassen() { - if (!navigator.geolocation) { USER_POS_FEHLER = "Kein Zugriff auf den Standort dieses Geräts"; standortAnsichtAktualisieren(); return; } + if (!navigator.geolocation) { USER_POS_FEHLER = "GPS offline"; standortAnsichtAktualisieren(); return; } navigator.geolocation.getCurrentPosition( (pos) => { USER_POS = { lat: pos.coords.latitude, lon: pos.coords.longitude, genauigkeit: pos.coords.accuracy }; @@ -641,7 +670,16 @@ function standortErfassen() { standortAnsichtAktualisieren(); }, (err) => { - USER_POS_FEHLER = err.code === err.PERMISSION_DENIED ? "Standortzugriff verweigert" : "Standort nicht verfügbar"; + // Diese Zeile steht direkt unter dem Fahrzeugnamen, an der Stelle, wo + // sonst der Abstand zum Auto steht - sie handelt aber vom Standort + // DIESES Geräts, nicht vom Fahrzeug. "Standortzugriff verweigert" las + // sich dort so, als sei der Standort des Autos nicht abrufbar. + // + // Die verweigerte Freigabe bleibt trotzdem eigens benannt: sie ist der + // einzige Fall mit einer anderen Abhilfe (Freigabe erteilen statt auf + // GPS-Empfang warten), und "GPS offline" würde dort in die falsche + // Richtung schicken. + USER_POS_FEHLER = err.code === err.PERMISSION_DENIED ? "GPS-Freigabe fehlt" : "GPS offline"; // Auch der Fehlschlag zaehlt als Versuch. Ohne diese Zeile greift die // Drossel in standortErfassenFallsNoetig() nie, solange der Nutzer die // Freigabe verweigert - und jedes Backend-Update loeste eine neue @@ -1032,8 +1070,11 @@ function vStandort() {
${esc(adresseText)}
- ${zustand.faehrt ? `Fahrzeug fährt` - : `${ciSVG(CI.parking)}${zustand.seitTs != null ? "Geparkt seit " + dauerSeitText(zustand.seitTs) : "Fahrzeug steht"}`} + ${zustand.faehrt + ? `Fahrzeug fährt` + : zustand.unbekannt + ? `${ciSVG(CI.parking)}Fahrzustand unbekannt` + : `${ciSVG(CI.parking)}${zustand.seitTs != null ? "Geparkt seit " + dauerSeitText(zustand.seitTs) : "Fahrzeug steht"}`}
${ciSVG(CI.fuelRange)} diff --git a/homeassistant/installationspaket/www/audi-dashboard-version.json b/homeassistant/installationspaket/www/audi-dashboard-version.json index 5bdaffe..f85117c 100644 --- a/homeassistant/installationspaket/www/audi-dashboard-version.json +++ b/homeassistant/installationspaket/www/audi-dashboard-version.json @@ -1 +1 @@ -{"version": 1787015002} +{"version": 1787016000, "app": "2026.08.23.1"} diff --git a/homeassistant/pyscript/modules/frontend_veroeffentlichung.py b/homeassistant/pyscript/modules/frontend_veroeffentlichung.py index 7e6ceb2..d5640cd 100644 --- a/homeassistant/pyscript/modules/frontend_veroeffentlichung.py +++ b/homeassistant/pyscript/modules/frontend_veroeffentlichung.py @@ -19,6 +19,10 @@ tankvorgaenge_veroeffentlichen() auf einen Zeitraum eingeschränkt werden (z. B. nur das laufende Jahr), statt hier vorschnell zu vereinfachen. """ +import io +import json +import os + import einstellungen import entitaeten import profil @@ -87,6 +91,17 @@ def _abs_zahl(wert): return None if zahl is None else abs(zahl) +def _zu_bool(wert): + """binary_sensor-Zustand als echtes True/False, None bei fehlender Meldung. + + None ist hier ausdrücklich kein "nein": ohne zugeordneten Sensor weiß die + App schlicht nicht, ob gefahren wird - und muss das anzeigen dürfen (§9), + statt "steht" zu behaupten.""" + if wert is None: + return None + return str(wert).lower() in ("on", "true", "1", "open", "yes") + + def _standort(): """Live-GPS-Position des Fahrzeugs (Übersicht -> Standort-Kachel) - liest STANDORT_LAT_SENSOR/STANDORT_LON_SENSOR, zwei eigene sensor-Entities für @@ -172,6 +187,18 @@ def fahrzeugstatus_veroeffentlichen(): "batteriespannung": _zu_zahl(batteriespannung), "gesichert": gesichert, "sicherheitscheck": sicherheitscheck, + # Fährt das Fahrzeug gerade? Kommt aus derselben Zündungs-Entität, die + # auch fahrterkennung.py als maßgebliches Signal für Fahrtbeginn/-ende + # nimmt - damit sagen Anzeige und Erfassung zwangsläufig dasselbe. + # + # Vorher wurde dieser Zustand im Frontend aus dem Status der Fahrten + # abgeleitet ("offen" = fährt). Das war falsch: "offen" heißt + # unvollständige Daten (Strecke fehlt noch, siehe + # fahrtabschluss_logik.py), nicht "unterwegs" - eine Fahrt ohne + # Kilometerstand blieb dauerhaft "offen" und das Fahrzeug damit + # dauerhaft "fahrend". None bedeutet: kein Zündungssensor zugeordnet, + # Zustand unbekannt. + "zuendung": _zu_bool(zustand_oder_none(einstellungen.ZUENDUNG_SENSOR)), "standort_lat": standort["lat"], "standort_lon": standort["lon"], "standort_genauigkeit_m": standort["genauigkeit_m"], @@ -188,7 +215,41 @@ def fahrzeugstatus_veroeffentlichen(): state.set("pyscript.audi_dashboard_fahrzeugstatus", "aktuell", new_attributes={"daten": daten}) +APP_VERSION_PFAD = "/config/www/audi-dashboard-version.json" + + +def app_version_veroeffentlichen(): + """Welchen Oberflächen-Stand diese Installation ausliefert. + + Gedacht für die Companion-App: die trägt ihre Version fest einkompiliert + (vite setzt __APP_VERSION__ aus der Datei VERSION im Projektstamm) und + kann sie gegen diesen Wert halten. Weicht sie ab, ist die App älter als + der Server und sagt das selbst - siehe VERSIONIERUNG.md. + + Das Panel braucht den Vergleich nicht: es lädt seine Dateien bei jedem + Seitenaufruf frisch, sobald sich die Cache-Zahl in derselben Datei + geändert hat, und kann deshalb gar nicht veralten. + + Bewusst eine eigene Entität statt eines Felds im Fahrzeugstatus: das hier + ist eine Eigenschaft der Installation, keine des Fahrzeugs.""" + version = None + if os.path.exists(APP_VERSION_PFAD): + try: + f = task.executor(io.open, APP_VERSION_PFAD, "r") + inhalt = f.read() + f.close() + version = json.loads(inhalt).get("app") + except (ValueError, OSError) as fehler: + log.warning(f"audi_dashboard: App-Version nicht lesbar ({fehler})") + state.set( + "pyscript.audi_dashboard_app_version", + version or "unbekannt", + new_attributes={"daten": {"app": version}}, + ) + + def alles_veroeffentlichen(): + app_version_veroeffentlichen() profil_veroeffentlichen() fahrten_veroeffentlichen() tankvorgaenge_veroeffentlichen() diff --git a/homeassistant/update.ps1 b/homeassistant/update.ps1 index d50518e..57ff36a 100644 --- a/homeassistant/update.ps1 +++ b/homeassistant/update.ps1 @@ -80,11 +80,21 @@ if (Test-Path "$hier\www\badges") { Copy-Item "$hier\www\badges\*" "$Ziel\www\badges\" -Force -Recurse } +# Zwei Zahlen mit verschiedenen Aufgaben, siehe VERSIONIERUNG.md: +# version = Unix-Sekunden, bricht den Browser-Cache, bei jedem Deploy neu +# app = Inhalt der Datei VERSION im Projektstamm, identifiziert die Fassung +# Die App-Version muss mitgeschrieben werden: an ihr erkennt die iOS-App, ob sie +# noch dem ausgelieferten Stand entspricht. Ginge sie hier verloren, wäre der +# Vergleich still abgeschaltet - genau die Veraltung, die er verhindern soll. $version = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() -"{ ""version"": $version }" | Out-File -FilePath "$Ziel\www\audi-dashboard-version.json" -Encoding utf8 -NoNewline +$appVersion = "" +if (Test-Path "$hier\..\VERSION") { $appVersion = (Get-Content "$hier\..\VERSION" -Raw).Trim() } +$inhaltVersion = if ($appVersion) { "{ ""version"": $version, ""app"": ""$appVersion"" }" } + else { "{ ""version"": $version }" } +$inhaltVersion | Out-File -FilePath "$Ziel\www\audi-dashboard-version.json" -Encoding utf8 -NoNewline Write-Host "" -Write-Host "Fertig. Version $version." -ForegroundColor Green +Write-Host "Fertig. Version $version$(if ($appVersion) { " (App $appVersion)" })." -ForegroundColor Green Write-Host "pyscript-Änderungen sind bereits aktiv (Auto-Reload, kein Neustart nötig)." Write-Host "Frontend-Änderungen: im Browser einmal ganz normal neu laden (F5) reicht -" Write-Host "kein Hard-Refresh, kein HA-Neustart." diff --git a/homeassistant/www/audi-dashboard-app.js b/homeassistant/www/audi-dashboard-app.js index f37475c..4f16168 100644 --- a/homeassistant/www/audi-dashboard-app.js +++ b/homeassistant/www/audi-dashboard-app.js @@ -111,6 +111,7 @@ function profilZuCar(p, status) { batteriespannung: status.batteriespannung, gesichert: status.gesichert, sicherheitscheck: status.sicherheitscheck || [], + zuendung: status.zuendung ?? null, standortLat: status.standort_lat, standortLon: status.standort_lon, standortGenauigkeitM: status.standort_genauigkeit_m, @@ -588,21 +589,49 @@ function tankstellenMarkerSVG(px = 32) { return ``; } -/* Fahrzustand fürs Menü: "fährt" solange die aktuelle Fahrt noch offen ist - (siehe fahrterkennung.py), sonst "steht". Ohne echtes Bewegungssignal vom - Fahrzeug (siehe AGENTS.md, FMM003/flespi offen) ist das die beste Näherung - aus den bereits vorhandenen Fahrtdaten. +/* Fahrzustand fürs Menü: kommt aus der Zündung (CAR.zuendung, vom Backend aus + demselben Sensor veröffentlicht, den auch fahrterkennung.py als maßgebliches + Signal nimmt). Ist kein Zündungssensor zugeordnet, bleibt der Zustand + ehrlich unbekannt, statt "steht" zu behaupten. + + Vorher stand hier `TRIPS[0].status === "offen"` - gleich zwei Fehler + übereinander, die sich gegenseitig verstärkt haben: + + 1. "offen" heißt NICHT "unterwegs", sondern "Daten noch unvollständig" + (die Strecke fehlt, siehe fahrtabschluss_logik.py). Eine beendete + Fahrt ohne Kilometerstand - etwa weil zu dem Zeitpunkt kein + KM_SENSOR zugeordnet war - bleibt für immer "offen". + 2. TRIPS[0] ist die ÄLTESTE Fahrt, nicht die neueste: + fahrten_veroeffentlichen() gibt profil.fahrten_lesen() unverändert in + Dateireihenfolge weiter, und die ist aufsteigend. + + Zusammen hieß das: die älteste jemals unvollständig gebliebene Fahrt hat + das Fahrzeug dauerhaft als "fahrend" angezeigt. An der Testinstanz war das + eine Fahrt, die seit 18 Tagen beendet war. parkplatz unterscheidet einen endgültigen Stillstand (laenger als die in "Fahrten zusammenfassen" hinterlegte Pausenzeit) von einem blossen Zwischenstopp. Das Feld ist vorbereitet, wird aber derzeit von keiner Ansicht gelesen - eine "Letzter Parkplatz"-Anzeige gibt es noch nicht. */ +function letzteFahrt() { + let neueste = null; + for (const t of TRIPS) { + if (!t || !t.ts_end) continue; + if (!neueste || t.ts_end > neueste.ts_end) neueste = t; + } + return neueste; +} function standortZustand() { - const t = TRIPS[0]; - if (t && t.status === "offen") return { faehrt: true, seitTs: null, parkplatz: false }; + const t = letzteFahrt(); const seitTs = t && t.ts_end ? new Date(t.ts_end).getTime() : null; + if (CAR.zuendung === true) return { faehrt: true, seitTs: null, parkplatz: false }; const minuten = seitTs != null ? (Date.now() - seitTs) / 60000 : null; - return { faehrt: false, seitTs, parkplatz: minuten != null && minuten >= (CONFIG.pauseMin ?? 15) }; + return { + faehrt: false, + unbekannt: CAR.zuendung == null, + seitTs, + parkplatz: minuten != null && minuten >= (CONFIG.pauseMin ?? 15), + }; } function dauerSeitText(ts) { if (ts == null) return "unbekannt"; @@ -633,7 +662,7 @@ function standortAbstandText() { return USER_POS_FEHLER || "Abstand wird ermittelt …"; } function standortErfassen() { - if (!navigator.geolocation) { USER_POS_FEHLER = "Kein Zugriff auf den Standort dieses Geräts"; standortAnsichtAktualisieren(); return; } + if (!navigator.geolocation) { USER_POS_FEHLER = "GPS offline"; standortAnsichtAktualisieren(); return; } navigator.geolocation.getCurrentPosition( (pos) => { USER_POS = { lat: pos.coords.latitude, lon: pos.coords.longitude, genauigkeit: pos.coords.accuracy }; @@ -641,7 +670,16 @@ function standortErfassen() { standortAnsichtAktualisieren(); }, (err) => { - USER_POS_FEHLER = err.code === err.PERMISSION_DENIED ? "Standortzugriff verweigert" : "Standort nicht verfügbar"; + // Diese Zeile steht direkt unter dem Fahrzeugnamen, an der Stelle, wo + // sonst der Abstand zum Auto steht - sie handelt aber vom Standort + // DIESES Geräts, nicht vom Fahrzeug. "Standortzugriff verweigert" las + // sich dort so, als sei der Standort des Autos nicht abrufbar. + // + // Die verweigerte Freigabe bleibt trotzdem eigens benannt: sie ist der + // einzige Fall mit einer anderen Abhilfe (Freigabe erteilen statt auf + // GPS-Empfang warten), und "GPS offline" würde dort in die falsche + // Richtung schicken. + USER_POS_FEHLER = err.code === err.PERMISSION_DENIED ? "GPS-Freigabe fehlt" : "GPS offline"; // Auch der Fehlschlag zaehlt als Versuch. Ohne diese Zeile greift die // Drossel in standortErfassenFallsNoetig() nie, solange der Nutzer die // Freigabe verweigert - und jedes Backend-Update loeste eine neue @@ -1032,8 +1070,11 @@ function vStandort() {
${esc(adresseText)}
- ${zustand.faehrt ? `Fahrzeug fährt` - : `${ciSVG(CI.parking)}${zustand.seitTs != null ? "Geparkt seit " + dauerSeitText(zustand.seitTs) : "Fahrzeug steht"}`} + ${zustand.faehrt + ? `Fahrzeug fährt` + : zustand.unbekannt + ? `${ciSVG(CI.parking)}Fahrzustand unbekannt` + : `${ciSVG(CI.parking)}${zustand.seitTs != null ? "Geparkt seit " + dauerSeitText(zustand.seitTs) : "Fahrzeug steht"}`}
${ciSVG(CI.fuelRange)} diff --git a/homeassistant/www/audi-dashboard-version.json b/homeassistant/www/audi-dashboard-version.json index 5bdaffe..f85117c 100644 --- a/homeassistant/www/audi-dashboard-version.json +++ b/homeassistant/www/audi-dashboard-version.json @@ -1 +1 @@ -{"version": 1787015002} +{"version": 1787016000, "app": "2026.08.23.1"}