Streckenberechnung: zuletzt davor statt naechstgelegen, keine Rueckwaertsspruenge (2026.8.31.6)

_fahrt_screenen() holte beide Kilometerstaende mit naechster_wert(), also dem
zeitlich naechstgelegenen Wert - gleich ob davor oder danach. Fuer einen
Zaehlerstand ist das falsch, und wert_bei()s eigener Docstring sagt es
woertlich: der Stand bei Fahrtbeginn ist der zuletzt gemeldete, nicht der
naechste, der schon Strecke enthaelt. historienimport.py hat immer wert_bei()
benutzt - dieselbe Fahrt wurde also je nach Weg unterschiedlich bewertet,
genau das, was die gemeinsame Herkunft von UNPLAUSIBLE_KMH und MINDESTDAUER_S
in verlauf.py ausschliesst.

Am echten recorder-Verlauf nachgewiesen: fuer den Fahrtbeginn 16:39:42 UTC
lieferte naechster_wert() 21325 (der Stand von 16:40:53, also 71 s nach dem
Beginn und aus einem anderen Fahrzeug), wert_bei() dagegen 209177.

_vollstaendig() prueft ausserdem nach unten. Bisher galt nur > UNPLAUSIBLE_KMH;
ein Rueckwaertssprung ergab eine negative Strecke und wurde als "vollstaendig"
gespeichert. historienimport.py verlangt odo_end >= odo_start seit jeher. Die
Pruefung sitzt an der Strecke, nicht an der Geschwindigkeit: durchschnitt_kmh()
liefert bei Dauer 0 None, eine Fahrt mit null Sekunden kaeme sonst vorbei.

Dem Fahrzeugwechsel des Dongles waehrend der Testphase steht das nicht im Weg:
verglichen werden Anfang und Ende derselben Fahrt, und jedes Fahrzeug zaehlt
innerhalb seiner eigenen Fahrt aufwaerts. In allen 23 gespeicherten Fahrten
haette die Pruefung nie ausgeloest. Greift sie doch, werden beide Werte
verworfen und die Fahrt bleibt "offen" - der naechste Screening-Lauf versucht
es erneut.

Verifiziert gegen die echte Aufzeichnung in audi_ha_test: sieben Faelle,
darunter drei Regressionen (normale Fahrt, unmoegliches Tempo, echte 0-km-Fahrt
bei stillstehendem Zaehler). py_compile sauber, 2026.8.31.6 sauber gestartet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-31 23:27:11 +02:00
parent 9b0e390dfd
commit 6f23dfebbf
3 changed files with 109 additions and 6 deletions
+65 -1
View File
@@ -1,6 +1,8 @@
# AGENTS.md — Project state, review findings, open items, and working rules
**Last updated: 2026-08-31** (Audit über Panel und iOS-App: sieben Befunde, sechs behoben,
**Last updated: 2026-08-31** (Streckenberechnung: `wert_bei()` statt `naechster_wert()` und
Rückwärtssprünge verworfen, Manifest `2026.8.31.6`, siehe Abschnitt AW. Davor am selben Tag:
Audit über Panel und iOS-App: sieben Befunde, sechs behoben,
`ZUENDUNG_SENSOR` auf das Trip-Signal des FMM003 umgelegt; Manifest `2026.8.31.5`, siehe
Abschnitt AV. Davor am selben Tag: Neustart-Festhänger der App, OTA-Prüfsumme, Statistik-Kopfzahl,
Setup-Filter und die gemessene Wahrheit über die Dongle-Konfiguration; Manifest `2026.8.31.4`,
@@ -6294,6 +6296,68 @@ Modul-Grammatik, `tsc --noEmit` sauber, 165/165, `vite build` sauber,
`audi_ha_test` auf `2026.8.31.5` sauber gestartet, und jede der fünf Codeänderungen
einzeln live nachgewiesen.
## AW. Der Kilometerstand einer Fahrt: zuletzt davor, und nie rückwärts (2026.8.31.6)
Gemessen an der Fahrt vom 31.08. abends, 22:03-22:13 Ortszeit. Vier unabhängige
Messungen desselben Wegs: GPS-Spur 6,74 km, Geschwindigkeit integriert 8,32 km,
geräteeigener Zähler 7 km — und der CAN-Kilometerstand, den die App nimmt, **3 km**.
Der Parameter `Total Vehicle Mileage (CAN)` fiel ab 22:06 aus den Datensätzen, während
alle übrigen CAN-Werte bis 22:12 weiterliefen. Das ist eine Sache des Geräts, nicht der
App; zurückgestellt auf Entscheidung des Eigentümers, ebenso die GPS-Strecke als zweite
Messung (`km_quelle="gps"`, der offene Vorbehalt im Modulkopf von `screening.py`).
Zwei Befunde im Code wurden dabei behoben.
### wert_bei() statt naechster_wert()
`_fahrt_screenen()` holte beide Kilometerstände mit `naechster_wert()` — dem zeitlich
nächstgelegenen Wert, gleich ob davor oder danach. Für einen Zählerstand ist das falsch,
und `wert_bei()`s eigener Docstring sagt es wörtlich. Der Rückblick
(`historienimport.py:182`) hat immer `wert_bei()` benutzt: dieselbe Fahrt wurde je nach
Weg unterschiedlich bewertet — genau das, was die gemeinsame Herkunft von
`UNPLAUSIBLE_KMH` und `MINDESTDAUER_S` in `verlauf.py` ausschließt.
Am echten Verlauf nachgewiesen, nicht an erfundenen Zahlen: für den Fahrtbeginn um
16:39:42 UTC lieferte `naechster_wert()` **21325** (der Stand von 16:40:53, also 71 s
*nach* dem Beginn — und aus einem anderen Fahrzeug), `wert_bei()` dagegen **209177**,
den letzten Stand davor.
### Rückwärtssprünge werden verworfen
`_vollstaendig()` prüfte nur nach oben (`> UNPLAUSIBLE_KMH`). Ein Rückwärtssprung
ergab eine **negative** Strecke und wurde als „vollständig" gespeichert.
`historienimport.py:185` verlangt seit jeher `odo_end >= odo_start`.
Die Prüfung sitzt an der **Strecke**, nicht an der Geschwindigkeit: `durchschnitt_kmh()`
liefert bei Dauer 0 `None`, eine Fahrt mit null Sekunden käme also an einer
Tempoprüfung vorbei — und solche Fahrten gibt es im Bestand.
**Sie steht dem Fahrzeugwechsel nicht im Weg.** Der Eigentümer bewegt den Dongle während
der Testphase zwischen Fahrzeugen, daher die springenden Stände (am 31.08. meldete der
CAN-Kilometerstand 21325 zwischen 209177 und 209267). Die Prüfung vergleicht Anfang und
Ende *derselben* Fahrt, und jedes Fahrzeug zählt innerhalb seiner eigenen Fahrt aufwärts;
in allen 23 gespeicherten Fahrten hätte sie nie ausgelöst. Sie greift nur, wenn die
beiden Enden aus verschiedenen Fahrzeugen stammen — und verwirft dann beide Werte, statt
Unsinn festzuschreiben: die Fahrt bleibt „offen" und wird beim nächsten Screening erneut
versucht.
Verifiziert im Testcontainer gegen den echten recorder-Verlauf: sieben Fälle, darunter
drei Regressionen (normale Fahrt, unmögliches Tempo, echte 0-km-Fahrt bei stillstehendem
Zähler). `py_compile` sauber, `audi_ha_test` auf `2026.8.31.6` sauber gestartet.
### Zwei Beobachtungen am Rande, nicht angefasst
**Die App legt bei jedem HA-Neustart eine Fahrt an**, wenn das Trip-Signal gerade „an"
ist — nicht über `nach_neustart_fortsetzen()` (das schließt nur), sondern weil die
Entität beim Start neu registriert wird und der Zuhörer das als Wechsel nach „an" liest.
Als Beginn nimmt er **jetzt**. Genau daraus stammt die Fahrt `30.08. 19:43 -> 31.08.
08:37, 12,9 h, 0 km`. Das ist die offene Frage vom 31.08. („woher kommt der Start?") —
in der Praxis beantwortet sie der Code bereits, nur mit der falschen Antwort.
**`_verbrauch_screenen()` liest den Tankstand weiter mit `naechster_wert()`**
(`screening.py:246`), während der Import dort `wert_bei()` nimmt. Dieselbe Doppelung
wie oben, eine Ebene tiefer. Bewusst außerhalb des Auftrags gelassen.
## Working conventions (observed — keep them)
- German is the project language: identifiers, comments, commits, UI texts. Exceptions:
@@ -1,7 +1,7 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.8.31.5",
"version": "2026.8.31.6",
"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"],
+43 -4
View File
@@ -4,8 +4,17 @@ Endkilometerstand vervollständigen (SPECIFICATION.md §7.2).
Der Kilometerstand kommt laut Datenquelle nicht sicher mit Fahrtende, sondern
teils erst mit Beginn oder während der nächsten Fahrt. Statt auf einen festen
Zeitpunkt zu warten, wird deshalb der aufgezeichnete Verlauf des
Kilometerstand-Sensors nach dem Wert durchsucht, dessen Zeitstempel am
nächsten am Fahrtbeginn bzw. -ende liegt.
Kilometerstand-Sensors gelesen und daraus der Stand zu Fahrtbeginn bzw. -ende
bestimmt - mit wert_bei(), also dem zuletzt DAVOR gemeldeten Wert.
Bis zum 31.08.2026 stand hier naechster_wert(): der zeitlich nächstgelegene
Wert, gleich ob davor oder danach. Für einen Zählerstand ist das falsch, und
wert_bei()s eigener Docstring sagt es wörtlich - der Kilometerstand bei
Fahrtbeginn ist der zuletzt gemeldete, nicht der nächste, der schon Strecke
enthält. Der Rückblick (historienimport.py) hat immer wert_bei() benutzt;
dieselbe Fahrt wurde also je nach Weg unterschiedlich bewertet - genau das,
was die gemeinsame Herkunft von UNPLAUSIBLE_KMH und MINDESTDAUER_S in
verlauf.py ausschließt.
Läuft nach jedem Fahrtende und zusätzlich bei jeder Änderung des
Kilometerstand-Sensors - unabhängig vom Fahrtende-Ereignis selbst, eben weil
@@ -30,6 +39,7 @@ from .verlauf import (
route_aus_verlauf,
verbrauch_aus_literstaenden,
verlauf_lesen,
wert_bei,
)
if TYPE_CHECKING:
@@ -67,6 +77,31 @@ def _vollstaendig(aenderungen: dict, fahrt: dict) -> dict:
einen unmöglichen Wert dauerhaft zu speichern."""
if fahrt.get("odo_start") is not None and fahrt.get("odo_end") is not None:
distanz = round(fahrt["odo_end"] - fahrt["odo_start"], 1)
if distanz < 0:
# Ein Zähler läuft nicht rückwärts - der Wert stammt also nicht
# aus derselben Quelle wie sein Gegenstück. Während der Testphase
# wandert der Dongle zwischen Fahrzeugen; am 31.08.2026 meldete der
# CAN-Kilometerstand deshalb 21325 zwischen 209177 und 209267.
#
# Die Prüfung vergleicht Anfang und Ende DERSELBEN Fahrt und steht
# dem Fahrzeugwechsel nicht im Weg: jedes Fahrzeug zählt innerhalb
# seiner eigenen Fahrt aufwärts. Sie greift nur, wenn die beiden
# Enden aus verschiedenen Fahrzeugen stammen.
#
# Warum an der Strecke und nicht an der Geschwindigkeit: die
# Plausibilitätsgrenze darunter prüft nur nach oben, und
# durchschnitt_kmh() liefert bei Dauer 0 None - eine Fahrt mit null
# Sekunden Dauer käme also an einer Geschwindigkeitsprüfung vorbei.
#
# historienimport.py verlangt seit jeher odo_end >= odo_start.
_LOGGER.warning(
"Fahrt %s: Kilometerstand verworfen - Rückwärtssprung (%s -> %s, "
"%s km), vermutlich ein Fahrzeugwechsel des Geräts.",
fahrt.get("trip_id"), fahrt["odo_start"], fahrt["odo_end"], distanz,
)
aenderungen["odo_start"] = None
aenderungen["odo_end"] = None
return aenderungen
geschwindigkeit = durchschnitt_kmh(distanz, fahrt.get("duration_s"))
if geschwindigkeit is not None and geschwindigkeit > UNPLAUSIBLE_KMH:
_LOGGER.warning(
@@ -159,14 +194,18 @@ async def _fahrt_screenen(k: Koordinator, km_sensor: str, fahrt: dict) -> None:
if not punkte:
return
# wert_bei() statt naechster_wert(): der zuletzt DAVOR gemeldete Stand, nicht
# der zeitlich nächstgelegene. Siehe Modulkopf - für einen Zähler ist das die
# richtige Wahl, und der Rückblick in historienimport.py macht es genauso.
# Steht davor nichts, nimmt wert_bei() den ersten Wert danach.
geaendert = False
if fahrt.get("odo_end") is None:
wert = naechster_wert(ende, punkte)
wert = wert_bei(punkte, ende)
if wert is not None:
fahrt["odo_end"] = wert
geaendert = True
if fahrt.get("odo_start") is None:
wert = naechster_wert(start, punkte)
wert = wert_bei(punkte, start)
if wert is not None:
fahrt["odo_start"] = wert
geaendert = True