Rueckblick misst auf Meter, Streckenwahl gemeinsam (2026.9.1.14)

Der erste Wurf (2026.9.1.12) verwarf jedes Importfenster mit distanz == 0. Zu
grob: seit der GNSS-Verfeinerung messen wir Meter, und eine Fahrt von 400
Metern steht im CAN-Wert als Null. Der Eigentuemer hat es auf den Punkt
gebracht - "0,0 moechte ich nicht, ab 0,1 schon".

Der Rueckblick liest jetzt ebenfalls den GNSS-Zaehler und entscheidet auf eine
Nachkommastelle.

Dabei stand die Toleranzlogik kurz doppelt im Code - in screening.py und in
historienimport.py. Genau die Doppelung, die dieses Projekt bei
UNPLAUSIBLE_KMH und MINDESTDAUER_S schon einmal teuer bezahlt hat. Sie liegt
jetzt gemeinsam in verlauf.py:

  strecke_waehlen(grob, fein) - die feine Zahl gilt, solange sie in der
  Rundungsunschaerfe des Ankers liegt, oder wenn es keinen Anker gibt.

  ist_gefahren(distanz) - ab 0,1 km ja. Eine unbekannte Strecke gilt als
  gefahren; nur die gemessene Null ist ein Nein.

VERIFIZIERT: 13 Faelle gegen beide Funktionen, darunter die zwei echten
Fahrten, die 400-Meter-Fahrt, die 100-Meter-Untergrenze, 40 Meter, fehlender
Anker, fehlender GNSS-Wert, gedrifteter Zaehler und beide Seiten der Grenze.
Import live: 1 Fenster uebersprungen, null 0-km-Fahrten im Bestand. Livepfad
live: vier alte Fahrten korrekt abgelehnt.

NICHT live gezeigt: ein Fenster mit 0,1-0,9 km, das der Import nun behaelt -
der Recorder enthaelt kein geschlossenes Fenster dieser Groesse.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-01 18:47:51 +02:00
co-authored by Claude Opus 5
parent be95b40d79
commit 09c7461892
7 changed files with 136 additions and 26 deletions
@@ -48,6 +48,9 @@ from .verlauf import (
MINDESTDAUER_S,
durchschnitt_kmh,
fahrtsignal,
ist_gefahren,
strecke_aus_zaehler,
strecke_waehlen,
hoechstwert_im_fenster,
route_aus_verlauf,
UNPLAUSIBLE_KMH,
@@ -163,6 +166,7 @@ async def _fahrten_importieren(k: Koordinator, verlaeufe: dict) -> dict:
bestehende = await k.ablage.fahrten_lesen()
km_verlauf = verlaeufe["km"]
gnss_verlauf = verlaeufe.get("gnss_km") or []
lat_verlauf = verlaeufe["lat"]
lon_verlauf = verlaeufe["lon"]
tank_liter_verlauf = verlaeufe["tank_liter"]
@@ -186,10 +190,22 @@ async def _fahrten_importieren(k: Koordinator, verlaeufe: dict) -> dict:
if odo_start is not None and odo_end is not None and odo_end >= odo_start:
distanz = round(odo_end - odo_start, 1)
if distanz == 0:
# Der Kilometerstand stand an beiden Enden gleich: das Fahrzeug hat
# sich nicht bewegt. Zündung an, aber nicht gefahren - Radio, Tür
# öffnen, warten. Keine Fahrt.
# Der Kilometerstand des Fahrzeugs löst nur ganze Kilometer auf. Wo der
# geräteeigene GNSS-Zähler zugeordnet ist, liefert er Meter - dieselbe
# Verfeinerung wie im Livepfad (screening._gnss_verfeinern), damit
# Rückblick und Livepfad dieselbe Fahrt nicht unterschiedlich bewerten.
#
# Sie entscheidet hier zusätzlich über Sein oder Nichtsein: eine Fahrt
# von 400 Metern steht im CAN-Wert als Null und wäre sonst verworfen
# worden, obwohl gefahren wurde.
fein = strecke_aus_zaehler(
[p for p in gnss_verlauf if f_start <= p[0] <= f_ende]
)
distanz, gnss_verwendet = strecke_waehlen(distanz, fein)
if not ist_gefahren(distanz):
# Gemessene Null: das Fahrzeug hat sich nicht bewegt. Zündung an,
# aber nicht gefahren - Radio, Tür öffnen, warten. Keine Fahrt.
#
# Die Dauer allein reicht als Prüfung nicht: der Eigentümer hatte
# nach dem Löschen aller Nullfahrten bei JEDEM Import wieder eine
@@ -197,12 +213,14 @@ async def _fahrten_importieren(k: Koordinator, verlaeufe: dict) -> dict:
# 209177 -> 209177 km, vmax 3 km/h). MINDESTDAUER_S ließ sie durch,
# weil sie mit 33 Minuten lang genug war.
#
# Nur die GEMESSENE Null wird verworfen, nicht die unbekannte:
# liegt kein Kilometerstand vor, bleibt distanz None und die Fahrt
# wird angelegt. Der Preis, bewusst getragen: eine echte Fahrt unter
# einem Kilometer, bei der der Zähler nicht umsprang, geht dabei
# verloren - sie wäre aber genau der Nulleintrag, den der
# Eigentümer nicht im Bestand haben will.
# Auf eine Nachkommastelle gerundet, nicht auf Gleichheit mit 0:
# ab 0,1 km ist es eine Fahrt (Ansage des Eigentümers, 01.09.2026),
# darunter zeigt die Oberfläche ohnehin "0,0 km" an und der Eintrag
# sähe aus wie die Nullfahrten, die er gerade ausgeräumt hat.
#
# Nur die GEMESSENE Null wird verworfen, nicht die unbekannte: liegt
# weder Kilometerstand noch GNSS-Zähler vor, bleibt distanz None und
# die Fahrt wird angelegt.
nicht_gefahren += 1
continue
@@ -221,7 +239,13 @@ async def _fahrten_importieren(k: Koordinator, verlaeufe: dict) -> dict:
fahrt = leere_fahrt(f_start, f_ende, "import")
fahrt.update({
"distance_km": distanz,
"km_quelle": "sensor" if distanz is not None else None,
"km_quelle": (
None
if distanz is None
else "odometer+gnss"
if gnss_verwendet
else "sensor"
),
"odo_start": odo_start,
"odo_end": odo_end,
"avg_speed_kmh": durchschnitt,
@@ -468,6 +492,7 @@ async def importieren(k: Koordinator, start: object, ende: object) -> None:
verlaeufe = {
"zuendung": await verlauf_lesen(k.hass, fahrtsignal(werte), von, bis),
"km": await verlauf_lesen(k.hass, werte.KM_SENSOR, von, bis),
"gnss_km": await verlauf_lesen(k.hass, werte.GNSS_KM_SENSOR, von, bis),
"tank": await verlauf_lesen(k.hass, werte.TANK_SENSOR, von, bis),
"tank_liter": await verlauf_lesen(k.hass, werte.TANK_LITER_SENSOR, von, bis),
"tank_distanz": await verlauf_lesen(k.hass, werte.TANK_DISTANZ_SENSOR, von, bis),