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:
@@ -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"}
|
||||
Binary file not shown.
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user