Fahrterkennung: FMM003-Funklöcher reißen keine Fahrt mehr auseinander

Die Zündungs-Entität stammt vom FMM003-Tracker; verliert er unterwegs
kurz die Verbindung, meldet Home Assistant "unavailable" statt eines
echten Zündungszustands. Die Live-Erkennung wertete das bisher wie
"aus" - eine Funklücke über der Pausenzeit teilte eine durchgehende
Fahrt in zwei. zuendung_geaendert() ignoriert "unavailable"/"unknown"
jetzt vollständig und leitet "fährt gerade" aus dem eigenen
Zwischenstand (fahrt_start_ts) statt aus dem letzten Rohwert ab.

historienimport.py bekommt zusätzlich eine Plausibilitätsprüfung: eine
errechnete Durchschnittsgeschwindigkeit über 300 km/h deutet auf einen
Kilometerstand-Ausreißer an der Fahrtgrenze hin, nicht auf eine echte
Fahrt - die Strecke wird dann verworfen statt eine unmögliche Fahrt
anzuzeigen.

Tankerkennung: optionaler zweiter Sensor für das Tankvolumen in Litern
(TANK_LITER_SENSOR, direkt vom CAN) neben dem bisherigen
Prozent-Füllstand. Ist er zugeordnet, übernimmt er die automatische
Tankerkennung vollständig - genauer als der Umweg über das im
Fahrzeugprofil hinterlegte Tankvolumen, und der erkannte Anstieg
liefert gleich eine grobe Vorbelegung für die getankte Menge statt
eines leeren Feldes. Ohne den Sensor bleibt alles beim Alten. Der
historische Import zieht mit derselben Präferenz nach.

Manifest auf 2026.8.25.2, beide Änderungsrunden live im Testcontainer
verifiziert (py_compile, Neustart, sauberes Setup-Log).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-08-25 22:04:26 +02:00
co-authored by Claude Sonnet 5
parent f97ceab3ad
commit 70253e83a5
7 changed files with 295 additions and 37 deletions
@@ -1,23 +1,35 @@
"""Automatische Tankerkennung über den Füllstandssensor.
"""Automatische Tankerkennung über den Füllstandssensor - oder, wenn
zugeordnet, direkt über einen Tankvolumen-Sensor in Litern.
Beobachtung: der Füllstand (in Prozent) springt beim Fahren nie nach oben, er
sinkt nur - jeder Anstieg ist also ein Tankvorgang. Schwelle nach Vorgabe: ab
+5 Liter ODER +9 Prozentpunkten Anstieg gilt als nachgetankt. Die
5-Liter-Vorgabe wird über fahrzeug.tankvolumen_liter aus dem Fahrzeugprofil in
Prozentpunkte umgerechnet, damit beide Angaben auf derselben Einheit
verglichen werden können; es gilt jeweils die empfindlichere (kleinere) der
beiden Schwellen.
Beobachtung: der Füllstand springt beim Fahren nie nach oben, er sinkt nur -
jeder Anstieg ist also ein Tankvorgang. Zwei Datenquellen dafür, in
absteigender Genauigkeit:
Tiefststand-Tracking statt einfachem Vorher/Nachher-Vergleich: die Datenquelle
liefert einen Tankvorgang oft in mehreren kleinen Schritten (40% -> 45% ->
60%), von denen keiner allein die Schwelle überschreiten muss. Deshalb wird
der zuletzt bekannte Tiefststand gespeichert und der Anstieg dagegen gemessen.
Nach Anlage eines Tankvorgangs wird der Tiefststand auf den aktuellen Wert
zurückgesetzt, damit derselbe Vorgang nicht mehrfach Datensätze erzeugt.
- TANK_LITER_SENSOR (optional, z. B. vom CAN): Anstieg direkt in Litern,
Schwelle LITER_SCHWELLE. Braucht keine Umrechnung und liefert den Anstieg
selbst gleich als Vorbelegung für die getankte Menge mit.
- TANK_SENSOR (Prozent, siehe fuellstand_geaendert()): Schwelle nach Vorgabe
ab +5 Liter ODER +9 Prozentpunkten Anstieg, wobei die 5-Liter-Vorgabe über
fahrzeug.tankvolumen_liter aus dem Fahrzeugprofil in Prozentpunkte
umgerechnet wird (beide Angaben so auf derselben Einheit); es gilt jeweils
die empfindlichere (kleinere) der beiden Schwellen.
Ist der Litersensor zugeordnet, übernimmt er die Erkennung vollständig -
beide gleichzeitig auszuwerten würde denselben Tankvorgang zweimal anlegen.
Ohne ihn bleibt es beim Prozentsensor wie bisher.
Tiefststand-Tracking statt einfachem Vorher/Nachher-Vergleich, für beide
Quellen gleichermaßen: die Datenquelle liefert einen Tankvorgang oft in
mehreren kleinen Schritten (40% -> 45% -> 60%), von denen keiner allein die
Schwelle überschreiten muss. Deshalb wird der zuletzt bekannte Tiefststand
gespeichert und der Anstieg dagegen gemessen. Nach Anlage eines Tankvorgangs
wird der Tiefststand auf den aktuellen Wert zurückgesetzt, damit derselbe
Vorgang nicht mehrfach Datensätze erzeugt.
Der so angelegte Tankvorgang ist bewusst ein unvollständiger Platzhalter
(status "unvollständig", ohne Liter/Kosten/Station): erwartet wird ein
automatisch erkannter Tankvorgang mit Zeitstempel, Kilometerstand und
(status "unvollständig", ohne Kosten/Station, Liter nur als grobe Schätzung
vom Litersensor oder ganz ohne bei reiner Prozent-Erkennung): erwartet wird
ein automatisch erkannter Tankvorgang mit Zeitstempel, Kilometerstand und
gefahrener Distanz, den der Nutzer später per Beleg-Nachtrag vervollständigt.
Auch hier gilt die Härtung aus fahrterkennung.py: der Tiefststand lebt nicht
@@ -97,21 +109,58 @@ async def fuellstand_geaendert(k: Koordinator, neu: str | None) -> None:
if anstieg < schwelle_prozent(await k.ablage.profil_lesen()):
return
await _automatisch_anlegen(k, anstieg)
# Ist ein direkter Litersensor zugeordnet, übernimmt der die Erkennung
# (siehe tankvolumen_geaendert) - genauer, weil ohne den Umweg über das im
# Fahrzeugprofil hinterlegte Tankvolumen. Beide gleichzeitig auswerten
# würde denselben Tankvorgang zweimal anlegen.
if not k.zuordnung.werte.TANK_LITER_SENSOR:
await _automatisch_anlegen(k, f"Füllstandsanstieg {round(anstieg, 1)} Prozentpunkte")
await k.tiefststand_setzen(aktuell)
async def _automatisch_anlegen(k: Koordinator, anstieg_pct: float) -> None:
async def tankvolumen_geaendert(k: Koordinator, neu: str | None) -> None:
"""Wie fuellstand_geaendert(), aber direkt in Litern statt in Prozent -
braucht deshalb keine Umrechnung über das Fahrzeugprofil und liefert mit
dem gemessenen Anstieg selbst eine sinnvolle Vorbelegung für die getankte
Menge (siehe leerer_tankvorgang() - "liters" bleibt sonst None, bis der
Nutzer sie von Hand einträgt oder ein Beleg sie liefert)."""
aktuell = _als_zahl(neu)
if aktuell is None:
return
tiefststand = k.tiefststand_liter
if tiefststand is None or aktuell <= tiefststand:
await k.tiefststand_liter_setzen(aktuell)
return
anstieg = aktuell - tiefststand
if anstieg < LITER_SCHWELLE:
return
await _automatisch_anlegen(
k, f"Tankvolumenanstieg {round(anstieg, 1)} l", liter_schaetzung=anstieg
)
await k.tiefststand_liter_setzen(aktuell)
async def _automatisch_anlegen(
k: Koordinator, anlass: str, liter_schaetzung: float | None = None
) -> None:
odometer_km = _als_zahl(zustand_oder_none(k.hass, k.zuordnung.werte.KM_SENSOR))
tankvorgang = leerer_tankvorgang(
datetime.datetime.now(datetime.UTC).isoformat(), "auto"
)
tankvorgang["odometer_km"] = odometer_km
tankvorgang["distance_km"] = await k.ablage.distanz_seit_letzter_tankung(odometer_km)
if liter_schaetzung is not None:
# Grobe Vorbelegung, keine feststehende Menge - der Status bleibt
# "unvollständig" (siehe leerer_tankvorgang), bis Preis/Tankstelle
# dazukommen. Der Nutzer korrigiert sie bei Bedarf selbst, ein
# nachgetragener Beleg überschreibt sie ohnehin.
tankvorgang["liters"] = round(liter_schaetzung, 1)
await k.ablage.tankvorgang_anhaengen(tankvorgang)
await k.tankvorgaenge_veroeffentlichen()
_LOGGER.info(
"Tankvorgang %s automatisch erkannt (Füllstandsanstieg %s Prozentpunkte)",
tankvorgang["tank_id"], round(anstieg_pct, 1),
"Tankvorgang %s automatisch erkannt (%s)", tankvorgang["tank_id"], anlass
)