Fahrten: Verbrauch-Näherung, 0V-Zustandsanzeige gefiltert, Plausibilitätsgrenze für die Live-Vervollständigung

Verbrauch (l/100km) war bislang totes Schema - kein Codepfad füllte
verbrauch_l_100km je. Jetzt aus der Literstand-Differenz (TANK_LITER_SENSOR)
über die Distanz genähert, live und beim Import, in beiden Frontends.

Die "Zustand"-Kachel zeigte die Batteriespannung ungefiltert direkt vom
Sensor, unabhängig von der Plausibilitätsgrenze der Verlaufsaufzeichnung -
ein Sensorausreißer (0V) zeigte sich dort weiterhin, obwohl die Messwertliste
ihn längst verwarf. Dieselbe Grenze gilt jetzt auch für diesen Anzeigepfad.

Reale Fahrtendaten zeigten eine Fahrt mit 22km in 67s (~1180 km/h) - die
Live-Vervollständigung (screening.py) hatte anders als der Import keine
Plausibilitätsprüfung der Durchschnittsgeschwindigkeit. Jetzt gemeinsam in
verlauf.py (UNPLAUSIBLE_KMH/durchschnitt_kmh) für beide Pfade. Dabei einen
zweiten echten Bug gefunden: _fahrt_screenen() zog sein Ergebnis nie ins
In-Memory-Objekt nach (nur in die Ablage) - eine im selben Durchlauf gerade
erst ermittelte Distanz blieb für spätere Schritte (z. B. Verbrauch) bis zum
nächsten Screening unsichtbar. Beide Stellen jetzt behoben.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-08-27 21:01:57 +02:00
co-authored by Claude Sonnet 5
parent cf57c5f1a5
commit 80d73e8afd
12 changed files with 344 additions and 30 deletions
+72 -4
View File
@@ -22,7 +22,14 @@ import datetime
import logging
from typing import TYPE_CHECKING
from .verlauf import naechster_wert, route_aus_verlauf, verlauf_lesen
from .verlauf import (
UNPLAUSIBLE_KMH,
durchschnitt_kmh,
naechster_wert,
route_aus_verlauf,
verbrauch_aus_literstaenden,
verlauf_lesen,
)
if TYPE_CHECKING:
from .koordinator import Koordinator
@@ -47,9 +54,30 @@ def _als_zeit(wert: str | None) -> datetime.datetime | None:
def _vollstaendig(aenderungen: dict, fahrt: dict) -> dict:
"""Ergänzt Strecke, Quelle und Status, sobald beide Kilometerstände da
sind. Eine Fahrt ist genau dann "vollständig", wenn ihre Strecke feststeht
- "offen" heißt nicht "unterwegs", sondern "Daten fehlen noch"."""
- "offen" heißt nicht "unterwegs", sondern "Daten fehlen noch".
Verwirft eine physikalisch unplausible Durchschnittsgeschwindigkeit
(UNPLAUSIBLE_KMH) statt sie anzuzeigen, statt der bislang unbehandelten
Lücke gegenüber historienimport.py: eine falsch abgegrenzte Fahrt (z. B.
durch ein chatterndes Zündungssignal) kann sonst einen Kilometerstand-
Sprung wie "22 km in 67 s" als echte Strecke zeigen. Sowohl odo_start als
auch odo_end werden dann verworfen (nicht nur die Strecke) - die Fahrt
bleibt "offen" und wird beim nächsten Screening erneut versucht, statt
einen unmöglichen Wert dauerhaft zu speichern."""
if fahrt.get("odo_start") is not None and fahrt.get("odo_end") is not None:
aenderungen["distance_km"] = round(fahrt["odo_end"] - fahrt["odo_start"], 1)
distanz = round(fahrt["odo_end"] - fahrt["odo_start"], 1)
geschwindigkeit = durchschnitt_kmh(distanz, fahrt.get("duration_s"))
if geschwindigkeit is not None and geschwindigkeit > UNPLAUSIBLE_KMH:
_LOGGER.warning(
"Fahrt %s: Kilometerstand verworfen - unmögliche %s km/h im "
"Schnitt (%s km in %s s), vermutlich ein Ausreißer im "
"Verlauf oder eine falsch abgegrenzte Fahrt.",
fahrt.get("trip_id"), geschwindigkeit, distanz, fahrt.get("duration_s"),
)
aenderungen["odo_start"] = None
aenderungen["odo_end"] = None
return aenderungen
aenderungen["distance_km"] = distanz
aenderungen["km_quelle"] = "odometer"
aenderungen["status"] = "vollständig"
return aenderungen
@@ -81,6 +109,15 @@ async def durchfuehren(k: Koordinator) -> None:
for fahrt in ohne_position:
await _position_screenen(k, lat_sensor, lon_sensor, fahrt)
tank_liter_sensor = k.zuordnung.werte.TANK_LITER_SENSOR
if tank_liter_sensor:
ohne_verbrauch = [
f for f in fahrten
if f.get("verbrauch_l_100km") is None and f.get("distance_km")
]
for fahrt in ohne_verbrauch:
await _verbrauch_screenen(k, tank_liter_sensor, fahrt)
await _lueckenlos_verketten(k)
await k.fahrten_veroeffentlichen()
@@ -115,7 +152,15 @@ async def _fahrt_screenen(k: Koordinator, km_sensor: str, fahrt: dict) -> None:
if geaendert:
aenderungen = {"odo_start": fahrt.get("odo_start"), "odo_end": fahrt.get("odo_end")}
await k.ablage.fahrt_aktualisieren(fahrt["trip_id"], _vollstaendig(aenderungen, fahrt))
aenderungen = _vollstaendig(aenderungen, fahrt)
await k.ablage.fahrt_aktualisieren(fahrt["trip_id"], aenderungen)
# Das In-Memory-Objekt sofort mitziehen (nicht nur die Ablage) - sonst
# sehen spätere Schritte innerhalb desselben durchfuehren()-Laufs
# (z. B. _verbrauch_screenen(), das eine bekannte distance_km
# braucht) noch den alten Stand, obwohl die Strecke gerade erst in
# diesem Durchlauf ermittelt wurde. _position_screenen() macht das
# bereits genauso.
fahrt.update(aenderungen)
async def _position_screenen(
@@ -161,6 +206,29 @@ async def _position_screenen(
fahrt.update(aenderungen)
async def _verbrauch_screenen(
k: Koordinator, tank_liter_sensor: str, fahrt: dict
) -> None:
"""Trägt den Verbrauch (l/100 km) nach, sobald die Distanz feststeht und
der Tankfüllstand (Liter) das hergibt - siehe verbrauch_aus_literstaenden()
für die Näherung selbst und ihre Begrenzung."""
start = _als_zeit(fahrt.get("ts_start"))
ende = _als_zeit(fahrt.get("ts_end"))
if start is None or ende is None:
return
verlauf = await verlauf_lesen(k.hass, tank_liter_sensor, start - SUCHFENSTER, ende + SUCHFENSTER)
if not verlauf:
return
verbrauch = verbrauch_aus_literstaenden(
naechster_wert(start, verlauf), naechster_wert(ende, verlauf), fahrt.get("distance_km")
)
if verbrauch is not None:
await k.ablage.fahrt_aktualisieren(fahrt["trip_id"], {"verbrauch_l_100km": verbrauch})
fahrt["verbrauch_l_100km"] = verbrauch
async def _lueckenlos_verketten(k: Koordinator) -> None:
"""Fahrt-Ende(N) == Fahrt-Start(N+1), solange dazwischen nicht gefahren
wurde.