Fahrtzeiten vom Geraet, Trip und Zuendung getrennt (2026.9.1.3)

Enthaelt 2026.9.1.2, das nie einzeln ausgeliefert wurde.

FAHRT- UND TANKZEITEN KOMMEN VOM GERAET

Fahrtbeginn war datetime.now() - die Uhrzeit, zu der Home Assistant den
Wechsel verarbeitet. Hat das Geraet gepuffert (Funkloch, Tiefgarage,
Tiefschlaf), liegt die Wahrheit beliebig weit davor. Gemessen: ein Datensatz
mit Fahrtende trug die Geraetezeit 07:29:04 und kam um 07:35:23 an.

Neue Sensorrolle MELDEZEIT_SENSOR (message_timestamp, Unix-Sekunden). Der
Wert ist UTC, keine Ortszeit - nachgemessen lag der Versatz zur UTC-Uhr von
HA bei Sekunden, nicht bei zwei Stunden; eine Zeitzonenumrechnung baute einen
Zwei-Stunden-Fehler ein.

geraetezeit() liegt in verlauf.py, weil Fahrt- und Tankerkennung denselben
Zeitstempel bestimmen muessen. tankerkennung._automatisch_anlegen() stempelte
bisher ebenfalls mit now(); ein aus einem gepufferten Datensatz erkannter
Tankvorgang trug damit die Uhrzeit des Auftauchens.

Die flespi-Integration setzt die Entitaeten eines Datensatzes nacheinander -
gemessen Zuendung 07:35:23.633, zugehoerige Meldezeit 2 ms spaeter. Wer
sofort liest, bekommt den vorigen Datensatz: beim Fahren zehn Sekunden
daneben, im Stand Stunden (On Stop Min Period = 43200 s). geraetezeit()
wartet deshalb bis zu 2 s auf den passenden Wert. Zwei Notbremsen, beide live
ausgeloest: Zeitstempel in der Zukunft und Ende vor dem Beginn.

TRIP UND ZUENDUNG SIND ZWEI ROLLEN

ZUENDUNG_SENSOR zeigte auf das Trip-Signal; im Setup stand "Zuendung/ACC" ueber
einer Trip-Entitaet und die echte Zuendung war nirgends zugeordnet. Neu:
TRIP_SENSOR loest Fahrten aus, ZUENDUNG_SENSOR liefert nur noch die Anzeige
"faehrt/steht". verlauf.fahrtsignal() ist die eine Stelle, die entscheidet -
Trip-Status wenn zugeordnet, sonst Zuendung, damit bestehende Installationen
unveraendert weiterlaufen und Livepfad und Rueckblick nicht auseinanderlaufen.

VERIFIZIERT in audi_ha_test, jeder Weg einzeln:
- Fahrtbeginn 08:30:03 (Geraetezeit) statt 08:38:27 (Ankunft)
- Fahrtende 08:04:40 -> 08:09:22 statt 08:11:04 -> 08:11:24
- Tankvorgang 08:18:45 statt 08:38:51 - 20 Minuten frueher
- Meldezeit in der Zukunft und Ende vor Beginn: Warnung, Ankunft genommen
- Zuendung schalten legt keine Fahrt mehr an, Trip-Signal schon

Der Livetest fing dabei einen NameError, den py_compile nicht sehen konnte:
der Import von geraetezeit in tankerkennung.py fehlte. Kompiliert ist nicht
verifiziert. Alle Testdatensaetze wurden ueber die eigenen Dienste wieder
entfernt; Bestand unveraendert 11 Fahrten, 13 Tankvorgaenge, keine offene Fahrt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 10:40:21 +02:00
parent ff5a334e95
commit 4cc91c461b
8 changed files with 363 additions and 24 deletions
@@ -29,6 +29,8 @@ from __future__ import annotations
import datetime
import logging
import asyncio
from homeassistant.components.recorder import get_instance, history
from homeassistant.core import HomeAssistant
@@ -78,6 +80,19 @@ async def verlauf_lesen(
return []
def fahrtsignal(werte: object) -> str:
"""Die Entität, die Fahrtbeginn und -ende auslöst.
Der Trip-Status des Geräts, wenn er zugeordnet ist - sonst die Zündung,
wie vor der Trennung der beiden Rollen. Damit laufen bestehende
Installationen unverändert weiter, ohne dass jemand etwas zuordnen muss.
Eine gemeinsame Stelle, weil sonst Livepfad (fahrterkennung.py) und
Rückblick (historienimport.py) auseinanderlaufen könnten - dieselbe Regel
wie bei UNPLAUSIBLE_KMH und MINDESTDAUER_S."""
return getattr(werte, "TRIP_SENSOR", "") or getattr(werte, "ZUENDUNG_SENSOR", "")
def zeit_normalisiert(wert: object) -> datetime.datetime | None:
"""Akzeptiert ISO-Zeit mit oder ohne Zeitzone und normalisiert auf UTC.
@@ -139,6 +154,75 @@ def durchschnitt_kmh(distanz_km: float | None, dauer_s: float | None) -> float |
return round(distanz_km / (dauer_s / 3600.0), 1)
# Wie lange auf die Meldezeit desselben Datensatzes gewartet wird.
#
# Die flespi-Integration setzt die Entitäten eines Datensatzes nacheinander:
# am 01.09.2026 gemessen stand die Zündung um 07:35:23.633 und die zugehörige
# Meldezeit 2 ms später um 07:35:23.635. Ohne diese kurze Wartezeit läse man
# den Zeitstempel des VORIGEN Datensatzes - beim Fahren zehn Sekunden daneben,
# im Stand aber möglicherweise Stunden, weil das Gerät dort nur alle paar
# Stunden einen Satz schreibt (Data Acquisition, On Stop: 43200 s).
MELDEZEIT_FRIST_S = 2.0
MELDEZEIT_TAKT_S = 0.05
# Wie weit die Geräteuhr der von Home Assistant vorauslaufen darf, bevor der
# Zeitstempel verworfen wird. Etwas Luft für Drift, aber keine Stunden - eine
# falsch gestellte Geräteuhr darf die Auswertung nicht mitreißen.
MELDEZEIT_VORLAUF = datetime.timedelta(minutes=5)
async def geraetezeit(
hass: HomeAssistant,
sensor: str | None,
ereigniszeit: datetime.datetime | None,
standard: datetime.datetime,
) -> datetime.datetime:
"""Der Zeitstempel, den das Gerät seinem Datensatz mitgegeben hat - oder
`standard`, wenn er fehlt oder unglaubwürdig ist.
Warum das nötig ist: `standard` ist die Uhrzeit, zu der Home Assistant den
Zustandswechsel verarbeitet hat. Hat das Gerät gepuffert - Funkloch,
Tiefgarage, Tiefschlaf -, liegt die Wahrheit beliebig weit davor. Am
01.09.2026 gemessen: ein Datensatz mit Fahrtende trug die Gerätezeit
07:29:04 und kam um 07:35:23 an, sechs Minuten später.
Der Wert ist eine Unix-Sekunde in UTC, nicht Ortszeit - ebenfalls
nachgemessen: der Versatz zur UTC-Uhr von Home Assistant lag bei Sekunden,
nicht bei zwei Stunden. Eine Zeitzonenumrechnung wäre nicht nur unnötig,
sie baute einen Zwei-Stunden-Fehler ein.
Gemeinsam für Fahrterkennung und Tankerkennung: beide legen Datensätze mit
einem Zeitstempel an und dürfen ihn nie unterschiedlich bestimmen - dieselbe
Regel wie bei UNPLAUSIBLE_KMH und MINDESTDAUER_S."""
if not sensor:
return standard
if ereigniszeit is not None:
wartezeit = 0.0
while wartezeit < MELDEZEIT_FRIST_S:
zustand = hass.states.get(sensor)
if zustand is not None and zustand.last_updated >= ereigniszeit:
break
await asyncio.sleep(MELDEZEIT_TAKT_S)
wartezeit += MELDEZEIT_TAKT_S
zustand = hass.states.get(sensor)
try:
gemeldet = datetime.datetime.fromtimestamp(
float(zustand.state), datetime.UTC # type: ignore[union-attr]
)
except (AttributeError, TypeError, ValueError, OSError, OverflowError):
return standard
if gemeldet > standard + MELDEZEIT_VORLAUF:
_LOGGER.warning(
"Meldezeit %s liegt in der Zukunft (jetzt %s) - verworfen",
gemeldet.isoformat(), standard.isoformat(),
)
return standard
return gemeldet
def wert_bei(
verlauf: list[Verlaufspunkt], zeitpunkt: datetime.datetime
) -> float | None: