diff --git a/AGENTS.md b/AGENTS.md index 9fa874f..bca6fe4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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: diff --git a/custom_components/audi_dashboard/manifest.json b/custom_components/audi_dashboard/manifest.json index 58166f2..0268e18 100644 --- a/custom_components/audi_dashboard/manifest.json +++ b/custom_components/audi_dashboard/manifest.json @@ -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"], diff --git a/custom_components/audi_dashboard/screening.py b/custom_components/audi_dashboard/screening.py index 6a7b424..3d3d31b 100644 --- a/custom_components/audi_dashboard/screening.py +++ b/custom_components/audi_dashboard/screening.py @@ -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