Parkbeginn nach einem Neustart aus der Aufzeichnung holen (2026.9.1.7)

Der Zuhoerer bekommt den Zuendungswechsel nur mit, wenn Home Assistant laeuft.
Ging die Zuendung aus, waehrend es unten war - ueber Nacht der Normalfall -,
feuerte nichts, und die Registrierung beim Hochfahren wird bewusst ignoriert
(sonst begaenne die Parkdauer bei jedem Neustart neu). Ergebnis war
"Parkdauer unbekannt", obwohl der Zeitpunkt im recorder steht.

_parkbeginn_nachholen() laeuft einmal in _nach_start und schlaegt ihn nach -
nur, wenn kein Wert gespeichert ist und die Zuendung auf "aus" steht.
Genommen wird der letzte echte Wechsel an -> aus im Fenster von sieben Tagen;
gibt es keinen, bleibt es bei "unbekannt" statt den Rand des Fensters zu
erfinden.

Wieder mit der Meldezeit desselben Datensatzes statt seiner Ankunftszeit -
dieselbe Begruendung wie in verlauf.geraetezeit(), hier rueckblickend.

VERIFIZIERT in audi_ha_test, der Test trennt genau die beiden Zeiten:
Speicher geleert, neu gestartet -> "Parkbeginn aus der Aufzeichnung
nachgeholt: 09:04:57". Das ist die Geraetezeit; angekommen war der Datensatz
um 09:44. Ein zweiter Neustart ruehrt den Wert nicht an, schreibt keine Zeile
und erzeugt keinen Fehler.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 11:56:53 +02:00
parent 553e128066
commit d1e3f7230c
3 changed files with 97 additions and 3 deletions
+20 -1
View File
@@ -1,6 +1,7 @@
# AGENTS.md — Project state, review findings, open items, and working rules # AGENTS.md — Project state, review findings, open items, and working rules
**Last updated: 2026-09-01** („Geparkt seit" kommt von der Zündung, Manifest `2026.9.1.6`, **Last updated: 2026-09-01** („Geparkt seit" kommt von der Zündung und wird nach einem Neustart
aus der Aufzeichnung nachgeholt, Manifest `2026.9.1.7`,
Abschnitt BB. Davor: GNSS-Streckenrollen vorbereitet, `2026.9.1.5`, Abschnitt BA. 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`, 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`), siehe Abschnitt AZ. Davor am selben Tag: Fahrtbeginn und -ende kommen vom Gerät (`MELDEZEIT_SENSOR`),
@@ -6629,6 +6630,24 @@ einer Gerätezeit von vor 40 Minuten → `09:04:57` statt `jetzt`.
Companion-App gleichgezogen (Paritätsregel): `profilAdapter` um `geparktSeit` erweitert, Companion-App gleichgezogen (Paritätsregel): `profilAdapter` um `geparktSeit` erweitert,
`standortZustand()` braucht die Fahrtenliste nicht mehr. `tsc --noEmit` sauber, 165/165 Tests grün. `standortZustand()` braucht die Fahrtenliste nicht mehr. `tsc --noEmit` sauber, 165/165 Tests grün.
### Nachtrag (2026.9.1.7): der Parkbeginn überlebt jetzt auch einen Neustart über Nacht
Der Zuhörer bekommt den Wechsel nur mit, wenn Home Assistant läuft. Ging die Zündung aus, während
es unten war — über Nacht der Normalfall —, feuerte nichts, und die Registrierung beim Hochfahren
wird ja bewusst ignoriert. Ergebnis: „Parkdauer unbekannt", obwohl der Zeitpunkt im recorder steht.
`_parkbeginn_nachholen()` läuft einmal in `_nach_start` und schlägt ihn dort nach — **nur**, wenn
kein Wert gespeichert ist und die Zündung gerade auf „aus" steht. Genommen wird der letzte echte
Wechsel **an → aus** im Fenster von sieben Tagen; gibt es keinen, bleibt es bei „unbekannt", statt
den Rand des Fensters zu erfinden.
Und wieder mit der **Meldezeit desselben Datensatzes**, nicht mit seiner Ankunftszeit — dieselbe
Begründung wie in `geraetezeit()`, hier nur rückblickend statt live.
Live nachgewiesen, und der Test trennt genau die beiden Zeiten: Speicher geleert, neu gestartet →
`Parkbeginn aus der Aufzeichnung nachgeholt: 09:04:57`. Das ist die Gerätezeit; angekommen war der
Datensatz um 09:44. Ein zweiter Neustart rührt den Wert nicht an und schreibt keine Zeile.
### Genauigkeit hängt am Operanden des Geräts ### Genauigkeit hängt am Operanden des Geräts
Der Zeitpunkt ist nur so genau, wie das Gerät ihn meldet. Stand der Operand des Der Zeitpunkt ist nur so genau, wie das Gerät ihn meldet. Stand der Operand des
@@ -76,11 +76,26 @@ from .const import (
E_ZUORDNUNG, E_ZUORDNUNG,
SIGNAL_AKTUALISIERT, SIGNAL_AKTUALISIERT,
) )
from .veroeffentlichung import fahrzeugstatus, spannung_cache_vorladen from .veroeffentlichung import (
fahrzeugstatus,
spannung_cache_vorladen,
zustand_oder_none,
)
from .zuordnung import Zuordnung from .zuordnung import Zuordnung
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Wie weit zurück nach dem letzten Zündungswechsel gesucht wird, wenn der
# Parkbeginn nach einem Neustart fehlt. Sieben Tage decken jede realistische
# Standzeit ab und bleiben eine Abfrage, die der recorder in einem Rutsch
# beantwortet; weiter zurück reicht seine Aufbewahrung ohnehin selten.
PARKBEGINN_FENSTER = datetime.timedelta(days=7)
# Wie weit um den Zündungswechsel herum nach der zugehörigen Meldezeit gesucht
# wird. Beide stammen aus demselben Datensatz und liegen deshalb
# Millisekunden auseinander (gemessen: 2 ms) - eine Minute ist reichlich.
MELDEZEIT_FENSTER = datetime.timedelta(minutes=1)
# Wie oft der Live-Fahrzeugstatus neu aus den Quellsensoren gelesen wird. # Wie oft der Live-Fahrzeugstatus neu aus den Quellsensoren gelesen wird.
TAKT_STATUS = datetime.timedelta(seconds=20) TAKT_STATUS = datetime.timedelta(seconds=20)
# Profil/Fahrten/Tankvorgänge ändern sich normalerweise nur durch Aktionen, # Profil/Fahrten/Tankvorgänge ändern sich normalerweise nur durch Aktionen,
@@ -160,6 +175,7 @@ class Koordinator:
async def _nach_start(self, _hass: HomeAssistant) -> None: async def _nach_start(self, _hass: HomeAssistant) -> None:
await fahrterkennung.nach_neustart_fortsetzen(self) await fahrterkennung.nach_neustart_fortsetzen(self)
await self._parkbeginn_nachholen()
# Erst hier (nicht in starten()): das Geräte-/Entity-Register der # Erst hier (nicht in starten()): das Geräte-/Entity-Register der
# zugeordneten Sensoren ist vor async_at_started oft noch nicht # zugeordneten Sensoren ist vor async_at_started oft noch nicht
# vollständig - siehe die Begründung von trigger_neu_binden() oben. # vollständig - siehe die Begründung von trigger_neu_binden() oben.
@@ -349,6 +365,65 @@ class Koordinator:
self.fahrt_start_ts = ts self.fahrt_start_ts = ts
await self._laufzeit_sichern() await self._laufzeit_sichern()
async def _parkbeginn_nachholen(self) -> None:
"""Holt den Parkbeginn aus der Aufzeichnung, wenn er fehlt.
Der Zuhörer (_zuendung_angezeigt) bekommt den Wechsel nur mit, wenn Home
Assistant läuft. Ging die Zündung aus, während es unten war - über Nacht
der Normalfall -, feuert nichts; die Registrierung der Entität beim
Hochfahren wird bewusst ignoriert, sonst begänne die Parkdauer bei jedem
Neustart neu. Der Zeitpunkt steht aber im recorder, und dort wird er hier
einmalig nachgeschlagen.
Nur wenn WIRKLICH ein Wechsel an -> aus im Fenster liegt. Steht die
Zündung schon die ganze Zeit auf aus, wissen wir den Beginn nicht - dann
bleibt es bei \"unbekannt\", statt den Rand des Fensters zu erfinden. Eine
falsche Zahl ist schlechter als keine (Ansage des Eigentümers,
2026-09-01)."""
if self.geparkt_seit_ts is not None:
return
sensor = self.zuordnung.werte.ZUENDUNG_SENSOR
if not sensor or zustand_oder_none(self.hass, sensor) != "off":
return
jetzt = datetime.datetime.now(datetime.UTC)
punkte = await verlauf.verlauf_lesen(
self.hass, sensor, jetzt - PARKBEGINN_FENSTER, jetzt
)
# verlauf_lesen() filtert unknown/unavailable schon heraus - eine
# Funklücke trennt die Reihe also nicht in zwei Wechsel auf.
wechsel: datetime.datetime | None = None
vorher: str | None = None
for ts, wert in punkte:
if wert == "off" and vorher == "on":
wechsel = ts
vorher = wert
if wechsel is None:
return
# Die Ankunftszeit des Datensatzes ist nicht seine Entstehungszeit. Die
# Meldezeit desselben Datensatzes ist es - dieselbe Begründung wie in
# verlauf.geraetezeit(), hier nur rückblickend statt live.
seit = wechsel
meldesensor = self.zuordnung.werte.MELDEZEIT_SENSOR
if meldesensor:
reihe = await verlauf.verlauf_lesen(
self.hass, meldesensor, wechsel - MELDEZEIT_FENSTER, wechsel + MELDEZEIT_FENSTER
)
epoche = verlauf.naechster_wert(wechsel, reihe)
if epoche is not None:
try:
gemeldet = datetime.datetime.fromtimestamp(epoche, datetime.UTC)
except (ValueError, OSError, OverflowError):
gemeldet = None
if gemeldet is not None and gemeldet <= jetzt:
seit = gemeldet
await self.geparkt_seit_setzen(seit)
_LOGGER.info(
"Parkbeginn aus der Aufzeichnung nachgeholt: %s", seit.isoformat()
)
async def geparkt_seit_setzen(self, ts: datetime.datetime | None) -> None: async def geparkt_seit_setzen(self, ts: datetime.datetime | None) -> None:
self.geparkt_seit_ts = ts self.geparkt_seit_ts = ts
await self._laufzeit_sichern() await self._laufzeit_sichern()
@@ -1,7 +1,7 @@
{ {
"domain": "audi_dashboard", "domain": "audi_dashboard",
"name": "Audi Dashboard", "name": "Audi Dashboard",
"version": "2026.9.1.6", "version": "2026.9.1.7",
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md", "documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues", "issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
"codeowners": ["@paul"], "codeowners": ["@paul"],