diff --git a/AGENTS.md b/AGENTS.md index 20a43ae..6d28147 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # AGENTS.md — Project state, review findings, open items, and working rules **Last updated: 2026-09-02** (Null heisst unbekannt, Momentanwerte nur fuer -"jetzt", Uebersicht-Symbol vergroessert, `2026.9.2.5`, Abschnitt BN. +"jetzt", Uebersicht-Symbol vergroessert, `2026.9.2.7`, Abschnitte BN und BO. Davor: Verbrauchsfaktor aus dem Tankbeleg, `2026.9.1.32`, Abschnitt BM. Davor: Reifenzaehler ohne Fahrzeugwechsel, „Raeder“ statt „Reifen“, `2026.9.1.29`, Abschnitt BL. Davor: Ortsnamen aus dem Backend, Verbrauchseichung an @@ -7554,6 +7554,56 @@ halb so hoch wie breit. Echte Hoehengleichheit mit den Nachbarn gibt es nur durch Neuzeichnen des Bogens mit groesserem Winkel - dem Eigentuemer angeboten, nicht unaufgefordert gemacht. +## BO. "Geparkt seit 20698 Tg." - die Meldezeit war nur nach vorn geprueft (2026.9.2.7) + +Gemeldet von der realen Instanz. Nachgerechnet: 20698 Tage und 9:39 vor dem +Ablesezeitpunkt ergeben den Sekundenwert **0** - die Unix-Epoche. + +`geraetezeit()` prueft seit Abschnitt AY, ob die Meldezeit in der ZUKUNFT +liegt (falsch gestellte Geraeteuhr). Nach hinten gab es keine Grenze. Ein +Sensor, der 0 meldet, rutschte damit ungehindert durch, und +`fromtimestamp(0)` ergibt 1970. + +### Warum "der Dongle liefert nicht" die falsche Erklaerung war + +Auf die Rueckfrage des Eigentuemers nachgesehen: im Testcontainer steht +`message_timestamp` auf 1788342429, also exakt jetzt. Beide Instanzen haengen +an derselben flespi-Quelle - am Geraet kann es also nicht liegen. + +Die wahrscheinlichere Ursache ist die Zuordnung. Das flespi-Geraet hat FUENF +aehnlich klingende Sensoren, aber nur zwei sind Zeitstempel: + +| Entitaet | Wert | tauglich | +|---|---|---| +| `…_message_timestamp` | 1788342429 | **ja - die richtige** | +| `…_timestamp_when_server_received_a_message` | 1788342433 | nein, Ankunftszeit | +| `…_id_of_channel_that_received_a_message` | 1421491 | nein, eine ID | +| `…_id_of_device_that_received_a_message` | 8767678 | nein, eine ID | +| `…_id_of_device_type_of_device…` | 1304 | nein, eine ID | + +Eine faelschlich zugeordnete ID ergibt als Unix-Zeit ebenfalls einen Zeitpunkt +kurz nach 1970 - dieselbe Anzeige, andere Ursache. Beim Eigentuemer noch +offen, welcher der beiden Faelle vorliegt. + +### Der Riegel sitzt an drei Stellen, nicht an einer + +1. `geraetezeit_plausibel()` (neu in `verlauf.py`): nicht aus der Zukunft + (`MELDEZEIT_VORLAUF`, 5 min) und nicht aelter als `MELDEZEIT_RUECKLAUF`. +2. `geparkt_seit_setzen()` prueft mit - so kommt ein solcher Wert gar nicht + erst in den Laufzeit-Store. +3. **Beim LADEN**, und das ist der Teil, der die reale Instanz betrifft: der + bereits gespeicherte Epochenwert haette jedes Update ueberlebt. Geprueft + wurde dort nur, ob sich das Datum lesen laesst, und + `_parkbeginn_nachholen()` springt nicht ein, solange etwas gespeichert ist. + +`MELDEZEIT_RUECKLAUF = 365 Tage` (Vorgabe des Eigentuemers, zuerst 30). Sehr +weit gefasst und genau deshalb ungefaehrlich: das Geraet puffert real nur +Stunden bis Tage, die Grenze kann also keine echte gepufferte Meldung +verwerfen - was sie fangen soll, liegt Jahrzehnte daneben. + +An acht Faellen geprueft: Epoche, beide ID-Verwechslungen und 366 Tage +verworfen; jetzt, 2 Tage und 364 Tage angenommen. + ## Working conventions (observed — keep them) - German is the project language: identifiers, comments, commits, UI texts. Exceptions: diff --git a/custom_components/audi_dashboard/koordinator.py b/custom_components/audi_dashboard/koordinator.py index c3493f1..f642e4d 100644 --- a/custom_components/audi_dashboard/koordinator.py +++ b/custom_components/audi_dashboard/koordinator.py @@ -353,9 +353,23 @@ class Koordinator: roh = gespeichert.get("geparkt_seit_ts") if roh: try: - self.geparkt_seit_ts = datetime.datetime.fromisoformat(roh) + geladen = datetime.datetime.fromisoformat(roh) except ValueError: - self.geparkt_seit_ts = None + geladen = None + # Auch beim LADEN pruefen, nicht nur beim Setzen: ein einmal + # falsch gespeicherter Wert (auf der Instanz des Eigentuemers am + # 02.09.2026 die Unix-Epoche, weil der Meldezeit-Sensor 0 lieferte) + # ueberlebt sonst jeden Neustart - und _parkbeginn_nachholen() + # springt nicht ein, weil ja etwas gespeichert ist. + if geladen is not None and not verlauf.geraetezeit_plausibel( + geladen, datetime.datetime.now(datetime.UTC) + ): + _LOGGER.warning( + "Gespeicherter Parkbeginn %s ist unglaubwuerdig - verworfen", + geladen.isoformat(), + ) + geladen = None + self.geparkt_seit_ts = geladen async def _laufzeit_sichern(self) -> None: await self._store.async_save({ @@ -422,7 +436,7 @@ class Koordinator: gemeldet = datetime.datetime.fromtimestamp(epoche, datetime.UTC) except (ValueError, OSError, OverflowError): gemeldet = None - if gemeldet is not None and gemeldet <= jetzt: + if verlauf.geraetezeit_plausibel(gemeldet, jetzt): seit = gemeldet await self.geparkt_seit_setzen(seit) @@ -431,6 +445,11 @@ class Koordinator: ) async def geparkt_seit_setzen(self, ts: datetime.datetime | None) -> None: + if ts is not None and not verlauf.geraetezeit_plausibel( + ts, datetime.datetime.now(datetime.UTC) + ): + _LOGGER.warning("Parkbeginn %s ist unglaubwuerdig - nicht gesetzt", ts.isoformat()) + ts = None self.geparkt_seit_ts = ts await self._laufzeit_sichern() diff --git a/custom_components/audi_dashboard/manifest.json b/custom_components/audi_dashboard/manifest.json index 9713384..eefea02 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.2.5", + "version": "2026.9.2.7", "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/verlauf.py b/custom_components/audi_dashboard/verlauf.py index 7657618..fb277f4 100644 --- a/custom_components/audi_dashboard/verlauf.py +++ b/custom_components/audi_dashboard/verlauf.py @@ -197,6 +197,39 @@ MELDEZEIT_TAKT_S = 0.05 # falsch gestellte Geräteuhr darf die Auswertung nicht mitreißen. MELDEZEIT_VORLAUF = datetime.timedelta(minutes=5) +# Und wie weit sie hoechstens ZURUECKliegen darf. +# +# Der Grund ist real: am 02.09.2026 stand auf der Instanz des Eigentuemers +# "Geparkt seit 20698 Tg. 9 Std. 39 Min." - das ist der 31.12.1969, also die +# Unix-Epoche. Der Meldezeit-Sensor hatte 0 gemeldet (dasselbe Verhalten wie +# external_power_voltage bei abgestecktem Dongle, siehe Abschnitt Z), und +# fromtimestamp(0) ergibt genau diesen Zeitpunkt. Geprueft wurde bis dahin nur +# die Zukunft, nicht die Vergangenheit. +# +# 365 Tage (Vorgabe des Eigentuemers). Das Geraet puffert real nur Stunden bis +# Tage (On-Stop-Sendeperiode 24 h), die Grenze ist also sehr weit gefasst - und +# genau deshalb ungefaehrlich: sie kann keine echte gepufferte Meldung +# verwerfen. Was sie fangen soll, liegt Jahrzehnte daneben: eine 0 ergibt 1970, +# und eine faelschlich zugeordnete ID-Entitaet (im flespi-Geraet heissen drei +# davon aehnlich wie der Zeitstempel) ebenfalls - 1304 etwa waere der +# 01.01.1970, 00:21 Uhr. +MELDEZEIT_RUECKLAUF = datetime.timedelta(days=365) + + +def geraetezeit_plausibel( + gemeldet: datetime.datetime | None, jetzt: datetime.datetime +) -> bool: + """Kann dieser Zeitstempel wirklich vom Geraet stammen? + + Weder aus der Zukunft (falsch gestellte Uhr) noch aus grauer Vorzeit - + siehe MELDEZEIT_RUECKLAUF fuer den Fall, der das ausgeloest hat.""" + if gemeldet is None: + return False + return ( + gemeldet <= jetzt + MELDEZEIT_VORLAUF + and gemeldet >= jetzt - MELDEZEIT_RUECKLAUF + ) + async def geraetezeit( hass: HomeAssistant, @@ -247,6 +280,14 @@ async def geraetezeit( gemeldet.isoformat(), standard.isoformat(), ) return standard + if gemeldet < standard - MELDEZEIT_RUECKLAUF: + _LOGGER.warning( + "Meldezeit %s liegt mehr als %s Tage zurueck (jetzt %s) - verworfen. " + "Der Sensor meldet vermutlich 0, oder im Setup ist statt des " + "Zeitstempels eine ID-Entitaet zugeordnet", + gemeldet.isoformat(), MELDEZEIT_RUECKLAUF.days, standard.isoformat(), + ) + return standard return gemeldet