Fahrtstrecke auf 100 m: CAN als Anker, GNSS als Nachkommastelle (2026.9.1.10)

Zwei echte Fahrten am 01.09.2026 haben die Datenform geliefert, auf die
2026.9.1.5 gewartet hat.

strecke_aus_zaehler() summiert die Zuwaechse des geraeteeigenen
Kilometerzaehlers und behandelt einen Ruecksprung als Ruecksetzung. Weder der
rohe Zaehler noch die Teilstrecken taugen allein: der Zaehler verliert bei
einer Ruecksetzung alles Vorherige (Fahrt 1: 0,415 km), die Teilstrecken
verlieren einzelne Datensaetze (Fahrt 2: 4 von 50 null, 0,2 km zu wenig). An
jedem Datensatz sind beide identisch - sie gehen nur verschieden kaputt.

_gnss_verfeinern() ersetzt die grobe Strecke nur, wenn die feine innerhalb der
Rundungsunschaerfe des Ankers liegt (GNSS_TOLERANZ_KM = 1.0; beide Enden des
CAN-Werts sind auf ganze Kilometer gerundet). Sonst behaelt der Fahrzeugwert
recht.

VERIFIZIERT gegen die echten Fahrten:
- Fahrt 1 (mit Ruecksetzung): 6,958 km, Google Maps sagt 7,1
- Fahrt 2 (sauber): 6,816 km
- live: 'Strecke auf 6.896 km verfeinert (Kilometerstand sagte 7.0 km)'
- drei aeltere Fahrten korrekt abgelehnt (209178 km gegen 21 km Anker)
- sechs Randfaelle: zwei Ruecksetzungen, ein Punkt, leer, unlesbare Werte

Nebenbei bestaetigt: NACHLAUF_S = 900 (Zuendung aus 16:33:39, Trip-Signal aus
16:48:44, gespeichertes Ende 16:33:36); der Rueckwaertssprung-Schutz griff live
beim Fahrzeugwechsel (-147361 km verworfen, Fahrt blieb offen); die
RPM-Zuendung prellt nicht; das Bewegungstor meldet wieder Stillstand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 17:31:33 +02:00
parent 0124254ea9
commit af83805c34
7 changed files with 186 additions and 4 deletions
@@ -32,9 +32,11 @@ import logging
from typing import TYPE_CHECKING
from .verlauf import (
GNSS_TOLERANZ_KM,
UNPLAUSIBLE_KMH,
durchschnitt_kmh,
hoechstwert_im_fenster,
strecke_aus_zaehler,
naechster_wert,
route_aus_verlauf,
verbrauch_aus_literstaenden,
@@ -159,6 +161,14 @@ async def durchfuehren(k: Koordinator) -> None:
)
fahrt["avg_speed_kmh"] = schnitt
# Erst nach den Kilometerstaenden: die Verfeinerung braucht die grobe
# Strecke als Anker und laeuft nur auf Fahrten, die sie schon haben.
gnss_sensor = k.zuordnung.werte.GNSS_KM_SENSOR
if gnss_sensor:
for fahrt in fahrten:
if fahrt.get("km_quelle") == "odometer":
await _gnss_verfeinern(k, gnss_sensor, fahrt)
tempo_sensor = k.zuordnung.werte.GESCHWINDIGKEIT_SENSOR
if tempo_sensor:
ohne_vmax = [f for f in fahrten if f.get("vmax_kmh") is None]
@@ -223,6 +233,52 @@ async def _fahrt_screenen(k: Koordinator, km_sensor: str, fahrt: dict) -> None:
fahrt.update(aenderungen)
async def _gnss_verfeinern(k: Koordinator, gnss_sensor: str, fahrt: dict) -> None:
"""Ersetzt die auf ganze Kilometer gerundete Strecke durch die metergenaue
des Geraets - aber nur, wenn beide zueinander passen.
Der Kilometerstand des Fahrzeugs bleibt der Anker: er ist der Tacho, er
driftet nicht, und er ist die Zahl, die der Fahrer im Cockpit sieht. Nur
loest er eben ganze Kilometer auf. Der GNSS-Zaehler des Geraets loest auf
einen Meter auf, kann aber driften und Empfang verlieren.
Deshalb keine Ersetzung, sondern eine Pruefung: die feine Zahl gilt, solange
sie innerhalb der Rundungsunschaerfe des groben Ankers liegt
(GNSS_TOLERANZ_KM). Liegt sie darueber, hat der Anker recht und die feine
Zahl wird verworfen - dann fehlte Empfang, oder der Zaehler ist gedriftet.
Laeuft nur auf Fahrten mit km_quelle == \"odometer\", also genau einmal je
Fahrt: danach steht dort \"odometer+gnss\" (oder es blieb bei \"odometer\",
wenn nichts Brauchbares vorlag, und ein spaeterer Lauf versucht es erneut,
sobald mehr Verlauf da ist)."""
start = _als_zeit(fahrt.get("ts_start"))
ende = _als_zeit(fahrt.get("ts_end"))
grob = fahrt.get("distance_km")
if start is None or ende is None or grob is None:
return
punkte = await verlauf_lesen(k.hass, gnss_sensor, start, ende)
fein = strecke_aus_zaehler(punkte)
if fein is None:
return
if abs(fein - grob) > GNSS_TOLERANZ_KM:
_LOGGER.info(
"Fahrt %s: GNSS-Strecke %s km weicht um mehr als %s km vom "
"Kilometerstand (%s km) ab - der Fahrzeugwert bleibt stehen",
fahrt.get("trip_id"), fein, GNSS_TOLERANZ_KM, grob,
)
return
aenderungen = {"distance_km": fein, "km_quelle": "odometer+gnss"}
await k.ablage.fahrt_aktualisieren(fahrt["trip_id"], aenderungen)
fahrt.update(aenderungen)
_LOGGER.info(
"Fahrt %s: Strecke auf %s km verfeinert (Kilometerstand sagte %s km)",
fahrt.get("trip_id"), fein, grob,
)
async def _position_screenen(
k: Koordinator, lat_sensor: str, lon_sensor: str, fahrt: dict
) -> None: