Meldezeit auch nach hinten prüfen - "Geparkt seit 20698 Tg." war die Unix-Epoche

geraetezeit() prüfte nur, ob die Meldezeit in der Zukunft liegt. Ein Sensor,
der 0 meldet, rutschte durch, und fromtimestamp(0) ergibt 1970 - auf der realen
Instanz sichtbar als "Geparkt seit 20698 Tg. 9 Std. 39 Min.".

Neu geraetezeit_plausibel(): nicht aus der Zukunft und nicht älter als
MELDEZEIT_RUECKLAUF (365 Tage). Geprüft wird an drei Stellen - beim Auslesen
der Gerätezeit, beim Setzen des Parkbeginns und beim LADEN aus dem
Laufzeit-Store, denn ein einmal falsch gespeicherter Wert überlebt sonst jedes
Update.

Fängt auch eine im Setup falsch zugeordnete ID-Entität ab: das flespi-Gerät hat
drei ID-Sensoren, deren Namen dem Zeitstempel ähneln, und deren Werte ergeben
als Unix-Zeit ebenfalls 1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 11:58:57 +02:00
parent 960e07d7d7
commit b73a8fb334
4 changed files with 115 additions and 5 deletions
@@ -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()
@@ -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"],
@@ -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