diff --git a/AGENTS.md b/AGENTS.md index 0601113..34baea9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,8 @@ # AGENTS.md — Project state, review findings, open items, and working rules -**Last updated: 2026-09-01** (Fahrtende ohne den 900-s-Nachlauf des Geräts, Manifest `2026.9.1.4`, +**Last updated: 2026-09-01** („Geparkt seit" kommt von der Zündung, Manifest `2026.9.1.6`, +Abschnitt BB. Davor: GNSS-Streckenrollen vorbereitet, `2026.9.1.5`, Abschnitt BA. +Davor: Fahrtende ohne den 900-s-Nachlauf des Geräts, Manifest `2026.9.1.4`, siehe Abschnitt AZ. Davor am selben Tag: Fahrtbeginn und -ende kommen vom Gerät (`MELDEZEIT_SENSOR`), Manifest `2026.9.1.3`, siehe Abschnitt AY: dazu die Trennung von Trip- und Zündungsrolle und die Gerätezeit für Tankvorgänge. Davor am selben Tag: Phantomfahrten beim Neustart abgestellt, Tankstand auf `wert_bei()` @@ -6567,6 +6569,87 @@ Verifiziert in `audi_ha_test`: Testfahrt anschließend über `fahrt_loeschen` entfernt; Bestand unverändert 11 Fahrten. +## BA. Zwei Rollen für die GNSS-Strecke, vorbereitet ohne Verbraucher (2026.9.1.5) + +`11806` steht seit dem 01.09.2026 auf **GNSS**, und der Zähler liefert sofort, was er soll: +`total_calculated_mileage = 0.034` kurz nach dem Zurücksetzen — **Meterauflösung**, gegenüber +147 Schritten von exakt 1,0 km beim CAN-Wert über die gesamte Aufzeichnung. + +Zwei neue Sensorrollen, **beide noch ohne Verbraucher**: + +- `GNSS_KM_SENSOR` → `total_calculated_mileage`. Der robustere der beiden für eine + Fahrtstrecke: ein verlorener Datensatz verfälscht die Differenz nicht, der Zähler läuft weiter. +- `GNSS_TEILSTRECKE_SENSOR` → `segment_mileage` (Strecke seit dem letzten Datensatz, + `11802 = Between records`). Taugt für eine Summe nur, wenn kein Satz fehlt — zeigt dafür, **wo** + Strecke verloren ging, und ist damit die bessere Gegenprobe. + +**Warum leer und ohne Wirkung:** die Rechnung soll gegen echte GNSS-Daten entstehen, nicht gegen +erfundene Reihen. Die Rollen sind angelegt und im Testcontainer bereits zugeordnet, damit nach der +ersten echten Fahrt sofort gemessen werden kann. Solange kein Code sie liest, ändert die Zuordnung +nichts. + +**Die Absicht dahinter, damit sie nicht verlorengeht:** `KM_SENSOR` bleibt der Anker — er ist der +Tacho des Fahrzeugs und driftet nicht. Der GNSS-Zähler liefert die Nachkommastelle dazwischen und +verliert Strecke, wo kein Empfang ist. Beides zusammen ergibt eine Strecke auf 100 m genau, die +trotzdem am Fahrzeugwert hängt. Der alte Warnhinweis bei `KM_SENSOR` („niemals +`total_calculated_mileage` zuordnen") gilt unverändert — er zeigt jetzt auf die neue Rolle. + +**Vor dem Zuordnen prüfen:** steht `11802` auf `Continuous` statt `Between records`, zählt +`segment_mileage` die laufende Fahrt statt der Teilstrecke und die zweite Rolle ist unbrauchbar. + +Geprüft: 27 FELDER-Einträge, 27 Dataclass-Felder, keine Abweichung in beide Richtungen; Zuordnung +über den echten Dienst geschrieben, alle 27 Einträge stehen. + +## BB. „Geparkt seit" kommt von der Zündung, nicht aus der Fahrtenliste (2026.9.1.6) + +Die Anzeige rechnete `jetzt − ts_end` der jüngsten Fahrt. Das ist keine Aussage über das Parken, +sondern über die **Fahrterkennung** — und die kann daneben liegen: am 01.09.2026 stand dort +„Geparkt seit 2 Tg. 16 Std.", während am Vorabend nachweislich 7 km gefahren wurden. Die Fahrt +fehlte im Bestand, weil `trip_status` seit 18:39 festhing. + +Neu: der Koordinator merkt sich den Zeitpunkt, zu dem die Zündung zuletzt auf „aus" ging — **in +der Zeit des Geräts** (`verlauf.geraetezeit()`), nicht in der Ankunftszeit. Er liegt im +Laufzeit-Store neben `fahrt_start_ts` und überlebt einen Neustart; `veroeffentlichung` gibt ihn +als `geparkt_seit` heraus. + +**Kein Rückfall auf die Fahrtenliste.** Ist der Wert `null`, zeigen beide Oberflächen „Fahrzeug +steht, Parkdauer unbekannt". Ausdrückliche Ansage des Eigentümers: eine falsche Zahl ist schlechter +als keine. + +### Derselbe Neustart-Fehler, zum zweiten Mal + +Beim Ausliefern fiel im Protokoll auf: `Zündung aus - geparkt seit …` **bei jedem Neustart**. Der +Zuhörer las die Registrierung der Entität als Wechsel — exakt die Phantomfahrt aus Abschnitt AX, nur +an anderer Stelle. `if alt is None: return` behebt es; der gespeicherte Wert überlebt den Neustart +ohnehin. **Merkposten:** jeder neue Zustandszuhörer in diesem Projekt braucht diese Prüfung. + +Live nachgewiesen: Neustart lässt `09:34:15` unverändert · Zündung an → `null` · Zündung aus mit +einer Gerätezeit von vor 40 Minuten → `09:04:57` statt `jetzt`. + +Companion-App gleichgezogen (Paritätsregel): `profilAdapter` um `geparktSeit` erweitert, +`standortZustand()` braucht die Fahrtenliste nicht mehr. `tsc --noEmit` sauber, 165/165 Tests grün. + +### Genauigkeit hängt am Operanden des Geräts + +Der Zeitpunkt ist nur so genau, wie das Gerät ihn meldet. Stand der Operand des +Zündungselements auf **Monitoring** (3), erzeugte ein Wechsel keinen eigenen Datensatz und der +Zeitstempel wäre der des nächsten — beim Abstellen also bis zu 900 s zu spät. Der Eigentümer hat +Ignition und Movement am 01.09.2026 auf **On Change** gestellt; damit ist der Zeitpunkt exakt. +Priorität bleibt Low, es kostet also keine zusätzliche Funkverbindung, nur einen Datensatz mehr in +der Warteschlange. + +### Wichtig für die noch offene Streckenrechnung: der GNSS-Zähler driftet im Stand + +Die 0,034 km, die `total_calculated_mileage` kurz nach dem Zurücksetzen zeigte, waren **kein +Beleg für Auflösung, sondern GPS-Drift** — der Dongle lag unbewegt auf dem Schreibtisch (Angabe des +Eigentümers, 01.09.2026). Static Navigation ist eingeschaltet (`106 = 1`, Quelle `112 = Movement`) +und friert die Position ein, offenbar aber nicht den Kilometerzähler. + +**Folge für Abschnitt BA:** die GNSS-Zuwächse dürfen nicht ungefiltert summiert werden. Sie brauchen +ein Tor — nur zählen, wenn Bewegung gemeldet ist. Dass `Movement` jetzt auf On Change steht, macht +genau dieses Tor erst möglich. Vor der ersten echten Fahrt ist die Größenordnung der Drift unbekannt; +sie ist als Erstes zu messen. + ## Working conventions (observed — keep them) - German is the project language: identifiers, comments, commits, UI texts. Exceptions: diff --git a/companion-app/src/daten/profilAdapter.ts b/companion-app/src/daten/profilAdapter.ts index 624cf0c..3f94bdd 100644 --- a/companion-app/src/daten/profilAdapter.ts +++ b/companion-app/src/daten/profilAdapter.ts @@ -68,6 +68,7 @@ export interface Fahrzeug { an nur, dass die Zündung an ist. `null` heißt „kein Zündungssensor zugeordnet", also unbekannt — nicht „steht". */ zuendung: boolean | null + geparktSeit: string | null standortLat: number | null standortLon: number | null standortZeit: string | null @@ -171,6 +172,7 @@ export function profilZuFahrzeug(profil: Profil, status: Fahrzeugstatus): Fahrze gesichert: status.gesichert ?? null, sicherheitscheck: status.sicherheitscheck ?? [], zuendung: status.zuendung ?? null, + geparktSeit: (status.geparkt_seit as string | null) ?? null, standortLat: status.standort_lat ?? null, standortLon: status.standort_lon ?? null, standortZeit: status.standort_zeit ?? null, diff --git a/companion-app/src/daten/service.test.ts b/companion-app/src/daten/service.test.ts index b779ec7..194d209 100644 --- a/companion-app/src/daten/service.test.ts +++ b/companion-app/src/daten/service.test.ts @@ -27,6 +27,7 @@ function fahrzeug(odo: number, modus = "hersteller"): Fahrzeug { gesichert: null, sicherheitscheck: [], zuendung: null, + geparktSeit: null, standortLat: null, standortLon: null, standortZeit: null, diff --git a/companion-app/src/screens/Standort.tsx b/companion-app/src/screens/Standort.tsx index 6dffac7..2c20b39 100644 --- a/companion-app/src/screens/Standort.tsx +++ b/companion-app/src/screens/Standort.tsx @@ -112,15 +112,22 @@ function dauerSeitText(ts: number | null): string { damit aber nichts zu tun und bleibt als eigene Zahl stehen. */ const PARKPLATZ_AB_MIN = 15 +/* Seit wann das Fahrzeug steht: aus dem Backend, hergeleitet aus dem + Zeitpunkt, zu dem die Zuendung zuletzt ausging (in der Zeit des Geraets). + + Bis 2026-09-01 stand hier das Ende der juengsten Fahrt aus der Liste. Das + war keine Aussage ueber das Parken, sondern ueber die Fahrterkennung: eine + verpasste Fahrt laesst das Fahrzeug seit Tagen stehen, obwohl es gestern + unterwegs war. Real vorgekommen - am 31.08.2026 zeigte die Anzeige + "geparkt seit 2 Tg. 16 Std.", waehrend am Vorabend 7 km gefahren wurden. + + Kein Rueckfall auf die Fahrtenliste: null heisst unbekannt, und das wird + auch so angezeigt. Eine falsche Zahl ist schlechter als keine. */ function standortZustand( fahrzeug: Fahrzeug, - fahrten: readonly { ts_end: string }[], ): { faehrt: boolean; unbekannt: boolean; seitTs: number | null; geparkt: boolean } { - let neuesteTs: number | null = null - for (const t of fahrten) { - const ts = new Date(t.ts_end).getTime() - if (!Number.isNaN(ts) && (neuesteTs === null || ts > neuesteTs)) neuesteTs = ts - } + const roh = fahrzeug.geparktSeit ? new Date(fahrzeug.geparktSeit).getTime() : NaN + const neuesteTs: number | null = Number.isNaN(roh) ? null : roh if (fahrzeug.zuendung === true) return { faehrt: true, unbekannt: false, seitTs: null, geparkt: false } const minuten = neuesteTs != null ? (Date.now() - neuesteTs) / 60000 : null return { @@ -241,7 +248,7 @@ export function StandortVorschau({ geheZu }: { geheZu: () => void }) { /** Vollbild-Standortansicht — Vorlage: vStandort() im Panel. */ export function Standort() { - const { fahrzeug, einstellungen, fahrten } = useDaten() + const { fahrzeug, einstellungen } = useDaten() const behaelter = useRef(null) const kartenRef = useRef(null) const fahrzeugMarkerRef = useRef(null) @@ -442,7 +449,7 @@ export function Standort() { : (positionsFehler ?? "Abstand wird ermittelt …") const koordinaten = fahrzeugPos ? `${de(fahrzeugPos.lat, 4)}, ${de(fahrzeugPos.lon, 4)}` : "" const adresseAnzeige = !bekannt ? "Kein GPS-Signal vom Fahrzeug" : (adresse ?? koordinaten) - const zustand = standortZustand(fahrzeug, fahrten) + const zustand = standortZustand(fahrzeug) const kartenlink = fahrzeugPos ? `https://www.google.com/maps/dir/?api=1&destination=${fahrzeugPos.lat},${fahrzeugPos.lon}` : null @@ -584,7 +591,7 @@ export function Standort() { ? "Fahrzustand unbekannt" : zustand.seitTs != null ? `Geparkt seit ${dauerSeitText(zustand.seitTs)}` - : "Fahrzeug steht"} + : "Fahrzeug steht, Parkdauer unbekannt"} )} diff --git a/custom_components/audi_dashboard/einstellungen.py b/custom_components/audi_dashboard/einstellungen.py index 3dc94a5..f848b66 100644 --- a/custom_components/audi_dashboard/einstellungen.py +++ b/custom_components/audi_dashboard/einstellungen.py @@ -52,7 +52,7 @@ class Sensorzuordnung: # Ölwechsel-Prognose. # # Beim FMM003 hier NICHT den selbst berechneten Gesamtkilometerstand - # (*_total_calculated_mileage) zuordnen: der beruht auf GPS-Strecken- + # (*_total_calculated_mileage) zuordnen - dafür gibt es GNSS_KM_SENSOR # rechnung statt auf dem Tacho und damit auf einer anderen Zählbasis als # der echte Fahrzeug-Kilometerstand. Wer ihn einträgt, verfälscht alle # drei genannten Auswertungen mit einem inkonsistenten Basiswert. Der vom @@ -60,6 +60,37 @@ class Sensorzuordnung: # Kilometerstand der EU-Data-Act-Integration ist der richtige. KM_SENSOR: str = "" + # Der geräteeigene Kilometerzähler, wenn er auf GNSS rechnet + # (Trip \ Odometer -> Calculation Source = GNSS). Er löst am 01.09.2026 + # nachgemessen auf **einen Meter** auf (0,034 km direkt nach dem + # Zurücksetzen), während der vom CAN gelesene Fahrzeug-Kilometerstand nur + # ganze Kilometer hergibt - über die gesamte Aufzeichnung 147 Schritte von + # exakt 1,0 km, kein einziger feinerer. + # + # Gedacht als FEINAUFLÖSUNG, nicht als Ersatz: KM_SENSOR bleibt der Anker, + # weil er der Tacho des Fahrzeugs ist und nicht driftet. Dieser hier liefert + # die Nachkommastelle dazwischen und verliert Strecke, wo kein GNSS-Empfang + # ist. Beides zusammen ergibt eine Strecke auf 100 m genau, die trotzdem am + # Fahrzeugwert hängt. + # + # NOCH OHNE VERBRAUCHER: die Rolle ist angelegt, damit nach der ersten + # echten GNSS-Fahrt nur noch gemessen und nicht mehr zugeordnet werden muss. + # Solange sie niemand liest, ändert eine Zuordnung nichts. + GNSS_KM_SENSOR: str = "" + + # Die Strecke seit dem letzten Datensatz, ebenfalls vom Gerät gerechnet + # (Trip \ Odometer -> Mode = "Between records"; steht die Einstellung auf + # "Continuous", zählt das Feld stattdessen die laufende Fahrt und die Rolle + # ist unbrauchbar - vor dem Zuordnen also nachsehen). + # + # Wozu zusätzlich zu GNSS_KM_SENSOR: der Gesamtzähler ist die robustere + # Quelle für eine Fahrtstrecke, weil ein verlorener Datensatz die Differenz + # nicht verfälscht - er zählt weiter. Die Teilstrecken taugen dafür nur, + # wenn keiner fehlt. Umgekehrt zeigen sie, WO Strecke verloren ging, und + # sind damit die bessere Gegenprobe. Auch diese Rolle hat noch keinen + # Verbraucher. + GNSS_TEILSTRECKE_SENSOR: str = "" + # Das Trip-Signal des Geräts: es beginnt erst, wenn Zündung UND Bewegung # UND "Start Speed" zusammenkommen, und endet erst nach dem # "Ignition OFF Timeout" des Geräts. Damit bringt es die Pausentoleranz @@ -185,6 +216,16 @@ SCHLUESSEL: set[str] = set(STANDARDWERTE) # "Nur passende Sensoren anzeigen") - eine fehlende oder leere Liste bedeutet # "keine Einschränkung" bzw. "diese Rolle hat üblicherweise keine Einheit". FELDER: list[dict] = [ + {"key": "GNSS_KM_SENSOR", "label": "Kilometerzähler (GNSS)", "gruppe": "fahrterkennung", + "hinweis": "Der geräteeigene Zähler, auf einen Meter genau. Verfeinert die Fahrtstrecke; der Kilometerstand oben bleibt der maßgebliche Wert.", + "domains": ["sensor"], "device_classes": ["distance"], "units": ["km"], "liste": False, "pflicht": False, + "beispiel": "total_calculated_mileage", + "stichworte": ["gnss", "gps", "calculated", "mileage", "kilometer"]}, + {"key": "GNSS_TEILSTRECKE_SENSOR", "label": "Teilstrecke je Datensatz (GNSS)", "gruppe": "fahrterkennung", + "hinweis": "Strecke seit dem letzten Datensatz - Gegenprobe zum Kilometerzähler und Hinweis darauf, wo Strecke fehlt.", + "domains": ["sensor"], "device_classes": ["distance"], "units": ["km"], "liste": False, "pflicht": False, + "beispiel": "segment_mileage", + "stichworte": ["segment", "teilstrecke", "abschnitt", "mileage"]}, {"key": "TRIP_SENSOR", "label": "Trip-Status", "gruppe": "fahrterkennung", "hinweis": "on = Fahrt läuft. Das gefilterte Fahrtsignal des Geräts - erkennt Fahrtbeginn und -ende. Ohne Zuordnung übernimmt die Zündung diese Aufgabe.", "domains": ["binary_sensor"], "device_classes": [], "units": [], "liste": False, "pflicht": False, diff --git a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js index 6ef6f9f..ba29334 100644 --- a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js +++ b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js @@ -194,6 +194,7 @@ function profilZuCar(p, status) { gesichert: status.gesichert, sicherheitscheck: status.sicherheitscheck || [], zuendung: status.zuendung ?? null, + geparktSeit: status.geparkt_seit ?? null, standortLat: status.standort_lat, standortLon: status.standort_lon, standortGenauigkeitM: status.standort_genauigkeit_m, @@ -785,9 +786,20 @@ function letzteFahrt() { } return neueste; } +/* Seit wann das Fahrzeug steht: aus dem Backend, hergeleitet aus dem + Zeitpunkt, zu dem die Zuendung zuletzt ausging (in der Zeit des Geraets). + + Bis 2026-09-01 stand hier das Ende der juengsten Fahrt aus der Liste. Das + war keine Aussage ueber das Parken, sondern ueber die Fahrterkennung: eine + verpasste Fahrt laesst das Fahrzeug seit Tagen stehen, obwohl es gestern + unterwegs war. Real vorgekommen - am 31.08.2026 zeigte die Anzeige + "geparkt seit 2 Tg. 16 Std.", waehrend am Vorabend 7 km gefahren wurden. + + Kein Rueckfall auf die Fahrtenliste: null heisst unbekannt, und das wird + auch so angezeigt. Eine falsche Zahl ist schlechter als keine. */ function standortZustand() { - const t = letzteFahrt(); - const seitTs = t && t.ts_end ? new Date(t.ts_end).getTime() : null; + const roh = CAR.geparktSeit ? new Date(CAR.geparktSeit).getTime() : NaN; + const seitTs = Number.isNaN(roh) ? null : roh; if (CAR.zuendung === true) return { faehrt: true, seitTs: null, parkplatz: false }; const minuten = seitTs != null ? (Date.now() - seitTs) / 60000 : null; return { @@ -1351,7 +1363,7 @@ function vStandort() { ? `Zündung an` : zustand.unbekannt ? `${ciSVG(CI.parking)}Fahrzustand unbekannt` - : `${ciSVG(CI.parking)}${zustand.seitTs != null ? "Geparkt seit " + dauerSeitText(zustand.seitTs) : "Fahrzeug steht"}`} + : `${ciSVG(CI.parking)}${zustand.seitTs != null ? "Geparkt seit " + dauerSeitText(zustand.seitTs) : "Fahrzeug steht, Parkdauer unbekannt"}`}
diff --git a/custom_components/audi_dashboard/koordinator.py b/custom_components/audi_dashboard/koordinator.py index a7c2edf..87a8148 100644 --- a/custom_components/audi_dashboard/koordinator.py +++ b/custom_components/audi_dashboard/koordinator.py @@ -109,6 +109,8 @@ class Koordinator: self.fahrt_start_ts: datetime.datetime | None = None self.tiefststand_pct: float | None = None self.tiefststand_liter: float | None = None + # Seit wann das Fahrzeug steht (Zeit des Geraets). None = unbekannt. + self.geparkt_seit_ts: datetime.datetime | None = None # Ergebnis des letzten "Auf Update prüfen"/"Update installieren" - # nur auf Tastendruck gesetzt (siehe dienste.py), nie im normalen # Veröffentlichungs-Takt neu berechnet, damit keine automatischen @@ -242,7 +244,42 @@ class Koordinator: neu = ereignis.data["new_state"] return (neu.state if neu else None, alt.state if alt else None) - async def _zuendung_angezeigt(self, _ereignis: Event[EventStateChangedData]) -> None: + async def _zuendung_angezeigt(self, ereignis: Event[EventStateChangedData]) -> None: + """Die Zündung dient nur der Anzeige - und hält den Parkbeginn fest. + + "Geparkt seit" hing bis 2026-09-01 am Ende der jüngsten Fahrt in der + Liste. Das ist keine Aussage über das Parken, sondern über die + Fahrterkennung: eine verpasste Fahrt lässt das Fahrzeug seit Tagen + stehen, obwohl es gestern unterwegs war (real vorgekommen am + 31.08.2026). Der Zeitpunkt, zu dem die Zündung zuletzt ausging, ist + die direkte Antwort. + + Mit der Gerätezeit, nicht mit der Ankunft: der Datensatz kommt bei + niedriger Priorität erst mit der nächsten Übertragung an, trägt aber + seinen eigenen Zeitstempel (siehe verlauf.geraetezeit()).""" + neu, alt = self._zustaende(ereignis) + # Kein Vorzustand heisst: die Entitaet taucht gerade erst auf (Start von + # Home Assistant, Neuladen der Integration). Das ist kein Wechsel der + # Zuendung. Ohne diese Pruefung stempelte JEDER Neustart den Parkbeginn + # neu - live beobachtet beim Ausliefern von 2026.9.1.6 - und die + # Parkdauer begann wieder bei null. Der gespeicherte Wert ueberlebt den + # Neustart ohnehin (_laufzeit_laden). Dieselbe Pruefung wie in + # fahrterkennung.zuendung_geaendert(). + if alt is None: + await self.fahrzeugstatus_veroeffentlichen() + return + if neu == "off": + zustand = ereignis.data["new_state"] + seit = await verlauf.geraetezeit( + self.hass, + self.zuordnung.werte.MELDEZEIT_SENSOR, + zustand.last_updated if zustand else None, + datetime.datetime.now(datetime.UTC), + ) + await self.geparkt_seit_setzen(seit) + _LOGGER.info("Zündung aus - geparkt seit %s", seit.isoformat()) + elif neu == "on" and self.geparkt_seit_ts is not None: + await self.geparkt_seit_setzen(None) await self.fahrzeugstatus_veroeffentlichen() async def _fahrtsignal_geaendert(self, ereignis: Event[EventStateChangedData]) -> None: @@ -291,18 +328,31 @@ class Koordinator: self.fahrt_start_ts = None self.tiefststand_pct = gespeichert.get("tiefststand_pct") self.tiefststand_liter = gespeichert.get("tiefststand_liter") + roh = gespeichert.get("geparkt_seit_ts") + if roh: + try: + self.geparkt_seit_ts = datetime.datetime.fromisoformat(roh) + except ValueError: + self.geparkt_seit_ts = None async def _laufzeit_sichern(self) -> None: await self._store.async_save({ "fahrt_start_ts": self.fahrt_start_ts.isoformat() if self.fahrt_start_ts else None, "tiefststand_pct": self.tiefststand_pct, "tiefststand_liter": self.tiefststand_liter, + "geparkt_seit_ts": ( + self.geparkt_seit_ts.isoformat() if self.geparkt_seit_ts else None + ), }) async def fahrt_start_setzen(self, ts: datetime.datetime | None) -> None: self.fahrt_start_ts = ts await self._laufzeit_sichern() + async def geparkt_seit_setzen(self, ts: datetime.datetime | None) -> None: + self.geparkt_seit_ts = ts + await self._laufzeit_sichern() + async def tiefststand_setzen(self, wert: float | None) -> None: self.tiefststand_pct = wert await self._laufzeit_sichern() @@ -359,7 +409,7 @@ class Koordinator: self.setzen(E_ZUORDNUNG, "aktuell", self.zuordnung.aktueller_stand()) async def fahrzeugstatus_veroeffentlichen(self) -> None: - self.setzen(E_FAHRZEUGSTATUS, "aktuell", fahrzeugstatus(self.hass, self.zuordnung.werte)) + self.setzen(E_FAHRZEUGSTATUS, "aktuell", fahrzeugstatus(self.hass, self.zuordnung.werte, self.geparkt_seit_ts)) async def beleg_ergebnis_veroeffentlichen(self, daten: dict) -> None: self.setzen(E_BELEG_ERGEBNIS, "aktuell", daten) diff --git a/custom_components/audi_dashboard/manifest.json b/custom_components/audi_dashboard/manifest.json index ea1afd4..b5a6b1c 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.1.4", + "version": "2026.9.1.6", "documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md", "issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues", "codeowners": ["@paul"], diff --git a/custom_components/audi_dashboard/veroeffentlichung.py b/custom_components/audi_dashboard/veroeffentlichung.py index 21c0cbf..b042a08 100644 --- a/custom_components/audi_dashboard/veroeffentlichung.py +++ b/custom_components/audi_dashboard/veroeffentlichung.py @@ -21,6 +21,8 @@ Oberfläche, landet aber nie in der Datenbank. from __future__ import annotations +import datetime + from homeassistant.core import HomeAssistant from .einstellungen import POSITIONEN, Sensorzuordnung @@ -188,7 +190,11 @@ def _sicherheitscheck(hass: HomeAssistant, werte: Sensorzuordnung) -> list[dict] return eintraege -def fahrzeugstatus(hass: HomeAssistant, werte: Sensorzuordnung) -> dict: +def fahrzeugstatus( + hass: HomeAssistant, + werte: Sensorzuordnung, + geparkt_seit: datetime.datetime | None = None, +) -> dict: """Bündelt die live aus Home Assistant gelesenen Fahrzeugwerte frontend-freundlich, damit die Oberfläche keine Entity-IDs kennen muss.""" sicherheitscheck = _sicherheitscheck(hass, werte) @@ -240,6 +246,13 @@ def fahrzeugstatus(hass: HomeAssistant, werte: Sensorzuordnung) -> dict: # Kilometerstand blieb dauerhaft "offen" und das Fahrzeug damit # dauerhaft "fahrend". None bedeutet: kein Zündungssensor zugeordnet. "zuendung": _zu_bool(zustand_oder_none(hass, werte.ZUENDUNG_SENSOR)), + # Seit wann das Fahrzeug steht - der Zeitpunkt, zu dem die Zündung + # zuletzt ausging, in der Zeit des Geräts. None heißt ausdrücklich + # "unbekannt" und ist von den Oberflächen auch so anzuzeigen: die + # frühere Herleitung aus der jüngsten Fahrt der Liste sah zwar immer + # nach einer Antwort aus, war aber eine über die Fahrterkennung und + # nicht über das Parken (siehe koordinator._zuendung_angezeigt). + "geparkt_seit": geparkt_seit.isoformat() if geparkt_seit else None, "standort_lat": standort["lat"], "standort_lon": standort["lon"], "standort_genauigkeit_m": standort["genauigkeit_m"],