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
@@ -1 +1 @@
{"version":"2026.9.1.9","sha256":"bdf895932e11e336474b4441a66912dd273e5f22b3bbf82e9b1cd29d6447a652","bytes":265713,"gebaut":"2026-09-01T10:24:49Z"}
{"version":"2026.9.1.10","sha256":"ea987e424774f4d2d0e7ea87f38f4b265f0605730515cacfd7193b2bb309b647","bytes":265700,"gebaut":"2026-09-01T15:30:31Z"}
@@ -1,7 +1,7 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.9.1.9",
"version": "2026.9.1.10",
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
"codeowners": ["@paul"],
@@ -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:
@@ -272,6 +272,51 @@ def route_aus_verlauf(
return punkte if len(punkte) >= 2 else None
# Wie weit die GNSS-Strecke vom Kilometerstand des Fahrzeugs abweichen darf.
#
# Beide Enden des Kilometerstands sind auf ganze Kilometer gerundet (am
# echten Fahrzeug nachgemessen: 147 Schritte von exakt 1,0 km, kein einziger
# feinerer). Die wahre Strecke liegt damit irgendwo in einem Fenster von
# +/- 1 km um die Differenz. Was darin liegt, ist mit dem Fahrzeugwert
# vereinbar; was darueber hinausgeht, ist es nicht - dann stimmt etwas nicht,
# und der Fahrzeugwert behaelt recht.
GNSS_TOLERANZ_KM = 1.0
def strecke_aus_zaehler(punkte: list[Verlaufspunkt]) -> float | None:
"""Gefahrene Strecke aus einem laufenden Kilometerzaehler des Geraets.
Summiert die Zuwaechse statt einfach Ende minus Anfang zu rechnen, weil der
Zaehler zurueckspringen kann. Ein Ruecksprung ist kein Rueckwaertsfahren,
sondern eine Ruecksetzung - der neue Wert ist dann selbst der Zuwachs seit
der Ruecksetzung, alles davor wurde bereits gezaehlt.
Am 01.09.2026 an zwei echten Fahrten gemessen:
* Fahrt 1, mit Ruecksetzung mitten in der Fahrt (das Geraet uebernahm
seinen konfigurierten Startwert 0, als der CAN-Kilometerstand zum ersten
Mal hochzaehlte): 0,415 km vor der Ruecksetzung, 6,543 km danach - macht
6,958 km. Google Maps sagt 7,1 km fuer dieselbe Strecke. Ohne die
Ruecksprungbehandlung waeren es 6,543 km gewesen, also 0,4 km zu wenig.
* Fahrt 2, ohne Ruecksetzung: 6,816 km. Dort degeneriert die Summe zu
Ende minus Anfang, wie es sein soll.
Bewusst NICHT ueber die Teilstrecken je Datensatz (segment_mileage), die
dasselbe Geraet mitliefert: die verlieren einzelne Datensaetze. In Fahrt 2
waren 4 von 50 Teilstrecken null, obwohl gefahren wurde, und die Summe lag
dadurch 0,2 km zu niedrig. Der Zaehler zaehlt ueber solche Aussetzer hinweg.
None, wenn weniger als zwei Zahlenwerte vorliegen."""
werte = [zahl(wert) for _ts, wert in punkte]
werte = [w for w in werte if w is not None]
if len(werte) < 2:
return None
strecke = 0.0
for vorher, jetzt in zip(werte, werte[1:]):
strecke += jetzt if jetzt < vorher else jetzt - vorher
return round(strecke, 3)
def hoechstwert_im_fenster(
verlauf: list[Verlaufspunkt],
start: datetime.datetime,