Rueckblick misst auf Meter, Streckenwahl gemeinsam (2026.9.1.14)
Der erste Wurf (2026.9.1.12) verwarf jedes Importfenster mit distanz == 0. Zu grob: seit der GNSS-Verfeinerung messen wir Meter, und eine Fahrt von 400 Metern steht im CAN-Wert als Null. Der Eigentuemer hat es auf den Punkt gebracht - "0,0 moechte ich nicht, ab 0,1 schon". Der Rueckblick liest jetzt ebenfalls den GNSS-Zaehler und entscheidet auf eine Nachkommastelle. Dabei stand die Toleranzlogik kurz doppelt im Code - in screening.py und in historienimport.py. Genau die Doppelung, die dieses Projekt bei UNPLAUSIBLE_KMH und MINDESTDAUER_S schon einmal teuer bezahlt hat. Sie liegt jetzt gemeinsam in verlauf.py: strecke_waehlen(grob, fein) - die feine Zahl gilt, solange sie in der Rundungsunschaerfe des Ankers liegt, oder wenn es keinen Anker gibt. ist_gefahren(distanz) - ab 0,1 km ja. Eine unbekannte Strecke gilt als gefahren; nur die gemessene Null ist ein Nein. VERIFIZIERT: 13 Faelle gegen beide Funktionen, darunter die zwei echten Fahrten, die 400-Meter-Fahrt, die 100-Meter-Untergrenze, 40 Meter, fehlender Anker, fehlender GNSS-Wert, gedrifteter Zaehler und beide Seiten der Grenze. Import live: 1 Fenster uebersprungen, null 0-km-Fahrten im Bestand. Livepfad live: vier alte Fahrten korrekt abgelehnt. NICHT live gezeigt: ein Fenster mit 0,1-0,9 km, das der Import nun behaelt - der Recorder enthaelt kein geschlossenes Fenster dieser Groesse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# AGENTS.md — Project state, review findings, open items, and working rules
|
||||
|
||||
**Last updated: 2026-09-01** (Fahrtstrecke auf 100 m: CAN als Anker, GNSS als Nachkommastelle,
|
||||
**Last updated: 2026-09-01** (Rueckblick ohne Nullfahrten, Streckenwahl gemeinsam in `verlauf.py`,
|
||||
`2026.9.1.14`, Abschnitt BF. Davor: Fahrtstrecke auf 100 m: CAN als Anker, GNSS als Nachkommastelle,
|
||||
`2026.9.1.10`, Abschnitt BE. Davor: Nachlauf-Abzug nur mit zugeordnetem `TRIP_SENSOR`, `2026.9.1.9`;
|
||||
davor „502" beim Neustart entschärft, OTA-Bündel auf `2026.9.1.8`
|
||||
nachgezogen, Abschnitt BC. Davor: „Geparkt seit" kommt von der Zündung und wird nach einem Neustart
|
||||
@@ -6833,6 +6834,51 @@ Verifiziert: `strecke_aus_zaehler()` gegen beide echten Fahrten plus sechs Randf
|
||||
(zwei Rücksetzungen, ein Punkt, leer, unlesbare Werte); `py_compile`, `tsc --noEmit` sauber,
|
||||
Bündel und Manifest auf `2026.9.1.10` mit demselben SHA-256.
|
||||
|
||||
## BF. Der Rueckblick legt keine Nullfahrten mehr an — und misst jetzt auf Meter (2026.9.1.12–.14)
|
||||
|
||||
Der Eigentuemer hatte auf der realen Instanz alle Nullfahrten geloescht, und **bei jedem Import kam
|
||||
eine davon wieder**: 31.08.2026, 14:48–15:21, 33 Minuten, 209177 → 209177 km, vmax 3 km/h.
|
||||
`MINDESTDAUER_S` liess sie durch, weil sie mit 33 Minuten lang genug war; eine Pruefung auf die
|
||||
Strecke gab es im Rueckblick nie.
|
||||
|
||||
### Erst zu grob gedacht, dann korrigiert
|
||||
|
||||
Mein erster Wurf verwarf jedes Fenster mit `distanz == 0`. Der Eigentuemer hat richtig eingewandt:
|
||||
seit der GNSS-Verfeinerung (Abschnitt BE) messen wir Meter — `„0,0 möchte ich nicht, ab 0,1 schon"`.
|
||||
Eine Fahrt von 400 Metern steht im CAN-Wert als Null und waere verlorengegangen, obwohl gefahren
|
||||
wurde.
|
||||
|
||||
Der Rueckblick liest deshalb jetzt ebenfalls den GNSS-Zaehler und entscheidet auf **eine
|
||||
Nachkommastelle**.
|
||||
|
||||
### Eine Entscheidung, eine Stelle
|
||||
|
||||
Dabei stand die Toleranzlogik kurz doppelt im Code — in `screening.py` und in
|
||||
`historienimport.py`. Genau die Doppelung, die dieses Projekt bei `UNPLAUSIBLE_KMH` und
|
||||
`MINDESTDAUER_S` schon einmal teuer bezahlt hat. Sie liegt jetzt in `verlauf.py`:
|
||||
|
||||
- **`strecke_waehlen(grob, fein)`** — die feine Zahl gilt, solange sie in der Rundungsunschaerfe des
|
||||
Ankers liegt, oder wenn es gar keinen Anker gibt. Sonst hat der Fahrzeugwert recht.
|
||||
- **`ist_gefahren(distanz)`** — ab 0,1 km ja, darunter nein. Eine **unbekannte** Strecke gilt als
|
||||
gefahren: eine Fahrt zu verwerfen, die man nicht gemessen hat, waere schlechter als eine ohne
|
||||
Streckenangabe zu behalten. Nur die gemessene Null ist ein Nein.
|
||||
|
||||
Beide Wege rufen dieselben Funktionen; der Rueckblick meldet die uebersprungenen Fenster als eigene
|
||||
Zahl (`„Zuendungen ohne gefahrene Strecke"`) in beiden Oberflaechen, damit er nichts stillschweigend
|
||||
weglaesst.
|
||||
|
||||
### Verifiziert — und was nicht
|
||||
|
||||
- **13 Faelle gegen `strecke_waehlen`/`ist_gefahren`**, darunter die beiden echten Fahrten, die
|
||||
400-Meter-Fahrt, die 100-Meter-Untergrenze, 40 Meter (keine Fahrt), fehlender Anker, fehlender
|
||||
GNSS-Wert, gedrifteter Zaehler und beide Seiten der Toleranzgrenze.
|
||||
- **Import live**: `fahrten_nicht_gefahren: 1`, null Fahrten mit 0 km im Bestand.
|
||||
- **Livepfad live**: vier alte Fahrten korrekt abgelehnt, Meldung jetzt ohne die Konstante im Text.
|
||||
- **Nicht live gezeigt:** ein Fenster mit 0,1–0,9 km, das der Import nun *behaelt*. Der Recorder
|
||||
enthaelt kein geschlossenes Fenster dieser Groesse — die Rangierfahrten vom 31.08. liegen alle
|
||||
innerhalb einer einzigen langen `trip_status`-Phase. Nachweisbar bleibt es an den 13 Faellen; live
|
||||
zeigt es sich bei der naechsten kurzen Fahrt.
|
||||
|
||||
## Working conventions (observed — keep them)
|
||||
|
||||
- German is the project language: identifiers, comments, commits, UI texts. Exceptions:
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":"2026.9.1.12","sha256":"5a2d5547cbc6db6850e46892ea422de08642f86fc634b5887322389b4e0af589","bytes":265724,"gebaut":"2026-09-01T16:35:22Z"}
|
||||
{"version":"2026.9.1.14","sha256":"f1fa3a91435bfc80f3f4245e52c488e3487fe12e5031349e1d7d20ca757d4b7d","bytes":265727,"gebaut":"2026-09-01T16:47:50Z"}
|
||||
Binary file not shown.
@@ -48,6 +48,9 @@ from .verlauf import (
|
||||
MINDESTDAUER_S,
|
||||
durchschnitt_kmh,
|
||||
fahrtsignal,
|
||||
ist_gefahren,
|
||||
strecke_aus_zaehler,
|
||||
strecke_waehlen,
|
||||
hoechstwert_im_fenster,
|
||||
route_aus_verlauf,
|
||||
UNPLAUSIBLE_KMH,
|
||||
@@ -163,6 +166,7 @@ async def _fahrten_importieren(k: Koordinator, verlaeufe: dict) -> dict:
|
||||
|
||||
bestehende = await k.ablage.fahrten_lesen()
|
||||
km_verlauf = verlaeufe["km"]
|
||||
gnss_verlauf = verlaeufe.get("gnss_km") or []
|
||||
lat_verlauf = verlaeufe["lat"]
|
||||
lon_verlauf = verlaeufe["lon"]
|
||||
tank_liter_verlauf = verlaeufe["tank_liter"]
|
||||
@@ -186,10 +190,22 @@ async def _fahrten_importieren(k: Koordinator, verlaeufe: dict) -> dict:
|
||||
if odo_start is not None and odo_end is not None and odo_end >= odo_start:
|
||||
distanz = round(odo_end - odo_start, 1)
|
||||
|
||||
if distanz == 0:
|
||||
# Der Kilometerstand stand an beiden Enden gleich: das Fahrzeug hat
|
||||
# sich nicht bewegt. Zündung an, aber nicht gefahren - Radio, Tür
|
||||
# öffnen, warten. Keine Fahrt.
|
||||
# Der Kilometerstand des Fahrzeugs löst nur ganze Kilometer auf. Wo der
|
||||
# geräteeigene GNSS-Zähler zugeordnet ist, liefert er Meter - dieselbe
|
||||
# Verfeinerung wie im Livepfad (screening._gnss_verfeinern), damit
|
||||
# Rückblick und Livepfad dieselbe Fahrt nicht unterschiedlich bewerten.
|
||||
#
|
||||
# Sie entscheidet hier zusätzlich über Sein oder Nichtsein: eine Fahrt
|
||||
# von 400 Metern steht im CAN-Wert als Null und wäre sonst verworfen
|
||||
# worden, obwohl gefahren wurde.
|
||||
fein = strecke_aus_zaehler(
|
||||
[p for p in gnss_verlauf if f_start <= p[0] <= f_ende]
|
||||
)
|
||||
distanz, gnss_verwendet = strecke_waehlen(distanz, fein)
|
||||
|
||||
if not ist_gefahren(distanz):
|
||||
# Gemessene Null: das Fahrzeug hat sich nicht bewegt. Zündung an,
|
||||
# aber nicht gefahren - Radio, Tür öffnen, warten. Keine Fahrt.
|
||||
#
|
||||
# Die Dauer allein reicht als Prüfung nicht: der Eigentümer hatte
|
||||
# nach dem Löschen aller Nullfahrten bei JEDEM Import wieder eine
|
||||
@@ -197,12 +213,14 @@ async def _fahrten_importieren(k: Koordinator, verlaeufe: dict) -> dict:
|
||||
# 209177 -> 209177 km, vmax 3 km/h). MINDESTDAUER_S ließ sie durch,
|
||||
# weil sie mit 33 Minuten lang genug war.
|
||||
#
|
||||
# Nur die GEMESSENE Null wird verworfen, nicht die unbekannte:
|
||||
# liegt kein Kilometerstand vor, bleibt distanz None und die Fahrt
|
||||
# wird angelegt. Der Preis, bewusst getragen: eine echte Fahrt unter
|
||||
# einem Kilometer, bei der der Zähler nicht umsprang, geht dabei
|
||||
# verloren - sie wäre aber genau der Nulleintrag, den der
|
||||
# Eigentümer nicht im Bestand haben will.
|
||||
# Auf eine Nachkommastelle gerundet, nicht auf Gleichheit mit 0:
|
||||
# ab 0,1 km ist es eine Fahrt (Ansage des Eigentümers, 01.09.2026),
|
||||
# darunter zeigt die Oberfläche ohnehin "0,0 km" an und der Eintrag
|
||||
# sähe aus wie die Nullfahrten, die er gerade ausgeräumt hat.
|
||||
#
|
||||
# Nur die GEMESSENE Null wird verworfen, nicht die unbekannte: liegt
|
||||
# weder Kilometerstand noch GNSS-Zähler vor, bleibt distanz None und
|
||||
# die Fahrt wird angelegt.
|
||||
nicht_gefahren += 1
|
||||
continue
|
||||
|
||||
@@ -221,7 +239,13 @@ async def _fahrten_importieren(k: Koordinator, verlaeufe: dict) -> dict:
|
||||
fahrt = leere_fahrt(f_start, f_ende, "import")
|
||||
fahrt.update({
|
||||
"distance_km": distanz,
|
||||
"km_quelle": "sensor" if distanz is not None else None,
|
||||
"km_quelle": (
|
||||
None
|
||||
if distanz is None
|
||||
else "odometer+gnss"
|
||||
if gnss_verwendet
|
||||
else "sensor"
|
||||
),
|
||||
"odo_start": odo_start,
|
||||
"odo_end": odo_end,
|
||||
"avg_speed_kmh": durchschnitt,
|
||||
@@ -468,6 +492,7 @@ async def importieren(k: Koordinator, start: object, ende: object) -> None:
|
||||
verlaeufe = {
|
||||
"zuendung": await verlauf_lesen(k.hass, fahrtsignal(werte), von, bis),
|
||||
"km": await verlauf_lesen(k.hass, werte.KM_SENSOR, von, bis),
|
||||
"gnss_km": await verlauf_lesen(k.hass, werte.GNSS_KM_SENSOR, von, bis),
|
||||
"tank": await verlauf_lesen(k.hass, werte.TANK_SENSOR, von, bis),
|
||||
"tank_liter": await verlauf_lesen(k.hass, werte.TANK_LITER_SENSOR, von, bis),
|
||||
"tank_distanz": await verlauf_lesen(k.hass, werte.TANK_DISTANZ_SENSOR, von, bis),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "audi_dashboard",
|
||||
"name": "Audi Dashboard",
|
||||
"version": "2026.9.1.12",
|
||||
"version": "2026.9.1.14",
|
||||
"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,11 +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,
|
||||
strecke_waehlen,
|
||||
naechster_wert,
|
||||
route_aus_verlauf,
|
||||
verbrauch_aus_literstaenden,
|
||||
@@ -259,23 +259,22 @@ async def _gnss_verfeinern(k: Koordinator, gnss_sensor: str, fahrt: dict) -> Non
|
||||
|
||||
punkte = await verlauf_lesen(k.hass, gnss_sensor, start, ende)
|
||||
fein = strecke_aus_zaehler(punkte)
|
||||
if fein is None:
|
||||
gewaehlt, genommen = strecke_waehlen(grob, fein)
|
||||
if not genommen:
|
||||
if fein is not None:
|
||||
_LOGGER.info(
|
||||
"Fahrt %s: GNSS-Strecke %s km passt nicht zum Kilometerstand "
|
||||
"(%s km) - der Fahrzeugwert bleibt stehen",
|
||||
fahrt.get("trip_id"), fein, grob,
|
||||
)
|
||||
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"}
|
||||
aenderungen = {"distance_km": gewaehlt, "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,
|
||||
fahrt.get("trip_id"), gewaehlt, grob,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -283,6 +283,46 @@ def route_aus_verlauf(
|
||||
GNSS_TOLERANZ_KM = 1.0
|
||||
|
||||
|
||||
def strecke_waehlen(
|
||||
grob: float | None, fein: float | None
|
||||
) -> tuple[float | None, bool]:
|
||||
"""Welche der beiden Streckenangaben gilt - und wurde die feine genommen?
|
||||
|
||||
`grob` ist die Differenz der Fahrzeug-Kilometerstände: der Anker. Er ist
|
||||
der Tacho, er driftet nicht, und er ist die Zahl im Cockpit - aber er löst
|
||||
nur ganze Kilometer auf. `fein` ist die Strecke aus dem GNSS-Zähler des
|
||||
Geräts: metergenau, dafür anfällig für fehlenden Empfang und Drift.
|
||||
|
||||
Die feine gilt, solange sie innerhalb der Rundungsunschärfe des Ankers
|
||||
liegt (GNSS_TOLERANZ_KM) - oder wenn es gar keinen Anker gibt, denn dann
|
||||
ist irgendeine Messung besser als keine. Liegt sie darüber, hat der Anker
|
||||
recht: dann fehlte Empfang oder der Zähler ist gedriftet.
|
||||
|
||||
Gemeinsam für Livepfad (screening.py) und Rückblick (historienimport.py) -
|
||||
dieselbe Regel wie bei UNPLAUSIBLE_KMH und MINDESTDAUER_S: beide Wege
|
||||
dürfen dieselbe Fahrt nie unterschiedlich bewerten."""
|
||||
if fein is None:
|
||||
return grob, False
|
||||
if grob is None or abs(fein - grob) <= GNSS_TOLERANZ_KM:
|
||||
return fein, True
|
||||
return grob, False
|
||||
|
||||
|
||||
def ist_gefahren(distanz: float | None) -> bool:
|
||||
"""Hat sich das Fahrzeug bewegt?
|
||||
|
||||
Auf eine Nachkommastelle gerundet: ab 0,1 km ist es eine Fahrt, darunter
|
||||
nicht (Ansage des Eigentümers, 01.09.2026). Unter 50 Metern zeigt die
|
||||
Oberfläche ohnehin \"0,0 km\" an, und der Eintrag sähe aus wie die
|
||||
Nullfahrten, die aus dem Bestand geräumt wurden.
|
||||
|
||||
Eine UNBEKANNTE Strecke gilt als gefahren: liegt weder Kilometerstand noch
|
||||
GNSS-Zähler vor, wissen wir es nicht - und eine Fahrt zu verwerfen, die man
|
||||
nicht gemessen hat, wäre schlechter als eine ohne Streckenangabe zu
|
||||
behalten. Nur die gemessene Null ist ein Nein."""
|
||||
return distanz is None or round(distanz, 1) != 0
|
||||
|
||||
|
||||
def strecke_aus_zaehler(punkte: list[Verlaufspunkt]) -> float | None:
|
||||
"""Gefahrene Strecke aus einem laufenden Kilometerzaehler des Geraets.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user