Audit: Livepfad und Rueckblick messen wieder dasselbe (2026.9.1.17)

BEFUND 1 (behoben): dieselbe Fahrt, zwei Strecken

screening.py las den Verlauf ueber verlauf_lesen(), das den Stand zu Beginn
des Fensters mitliefert. historienimport.py schnitt sein Fenster streng heraus
und verlor genau diesen Punkt. An der echten Fahrt nachgemessen: 6,896 km
gegen 6,816 km fuer denselben Zeitraum.

Neu verlauf.zaehlerstrecke(punkte, start, ende) - der Anker ist der letzte
Datensatz am oder vor dem Beginn, dieselbe Ueberlegung wie bei wert_bei().
Beide Wege schneiden jetzt durch dieselbe Funktion. Nachgewiesen: Rueckblick
6,896, Livepfad 6,896.

Dritter Fall dieser Art nach UNPLAUSIBLE_KMH und MINDESTDAUER_S - und ich
hatte ihn am selben Tag selbst eingebaut.

BEFUND 2 (behoben): STANDARDWERTE gingen unnoetig ueber die Leitung.
zuordnung.py schickte sie mit jeder Katalogantwort; gelesen hat sie seit dem
Entfernen des Zuruecksetzen-Knopfes niemand mehr. Die Konstante bleibt, sie
traegt intern die Vorgabewerte und leitet SCHLUESSEL ab.

BEFUND 3 und 4 bleiben offen und gehoeren dem Eigentuemer: die vier Tage alte
Reichweite auf der Uebersicht (RANGE_SENSOR besser leer lassen) und die
Nullfahrt-Regel, die nur der Rueckblick kennt - im Livepfad hiesse sie, einen
bereits gespeicherten Datensatz automatisch zu loeschen.

SAUBER: 24 Backend-Dateien py_compile, Panel als Modul geparst, tsc --noEmit
und vite build sauber, 165/165 Tests, 27 Katalogeintraege gegen 27
Dataclass-Felder ohne Abweichung, jedes Listenfeld mit so vielen Beispielen wie
Positionen, keine verwaisten Verweise, in der Konsole nur das bekannte
ServiceWorker-Rauschen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 19:09:59 +02:00
parent 8e73621d85
commit 9d70192da3
8 changed files with 100 additions and 12 deletions
+61 -2
View File
@@ -1,7 +1,8 @@
# AGENTS.md — Project state, review findings, open items, and working rules
**Last updated: 2026-09-01** (Setup: gruene Bestaetigung und Erwartungswert je Listenposition,
`2026.9.1.16`, Abschnitt BG. Davor: Rueckblick ohne Nullfahrten, Streckenwahl gemeinsam in `verlauf.py`,
**Last updated: 2026-09-01** (Audit ueber Panel, App und Backend: vier Befunde, zwei behoben,
`2026.9.1.17`, Abschnitt BH. Davor: Setup mit gruener Bestaetigung und Erwartungswert je
Listenposition, Abschnitt BG. Davor: 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`
@@ -6935,6 +6936,64 @@ ganze Zeit richtig.
oder Stylesheet - nicht davor. Wer waehrend der Pruefung noch nachbessert, erhoeht danach noch
einmal.
## BH. Audit vom 01.09.2026 — vier Befunde, zwei behoben (2026.9.1.17)
Nach einem Tag mit siebzehn Auslieferungen ein Rundumblick über beide Oberflächen und das Backend.
**Was sauber war:** 24 Backend-Dateien `py_compile`, Panel als **Modul** geparst (Abschnitt AQ),
`tsc --noEmit` und `vite build` sauber, 165/165 Tests grün, 27 Katalogeinträge gegen 27
Dataclass-Felder ohne Abweichung in beide Richtungen, keine doppelten Schlüssel, jedes Feld mit
Beispielnamen, jedes Listenfeld mit so vielen Beispielen wie Positionen. In der Konsole nur das
bekannte ServiceWorker-Rauschen. Keine verwaisten Verweise auf das entfernte
`„Auf Standard zurücksetzen"`.
### Befund 1 (behoben): Livepfad und Rückblick maßen dieselbe Fahrt unterschiedlich
Die schwerste Sorte Fehler in diesem Projekt — und ich hatte sie am selben Tag selbst eingebaut.
`screening.py` las den Verlauf über `verlauf_lesen()`, das den Stand **zu Beginn** des Fensters
mitliefert. `historienimport.py` schnitt sein Fenster streng heraus (`f_start <= p[0] <= f_ende`)
und verlor genau diesen Punkt. An der echten Fahrt nachgemessen: **6,896 km gegen 6,816 km** für
denselben Zeitraum, je nachdem welcher Weg sie bewertete.
Neu `verlauf.zaehlerstrecke(punkte, start, ende)`: der Anker ist der letzte Datensatz **am oder
vor** dem Beginn, dieselbe Überlegung wie bei `wert_bei()`. Beide Wege schneiden jetzt durch
dieselbe Funktion. Nachgewiesen: Rückblick 6,896 — Livepfad 6,896.
Dritter Fall dieser Art nach `UNPLAUSIBLE_KMH` und `MINDESTDAUER_S`. **Regel bestätigt sich:**
sobald zwei Wege dieselbe Größe berechnen, gehört die Rechnung in `verlauf.py` — nicht als
Aufräumarbeit hinterher, sondern beim ersten Mal.
### Befund 2 (behoben, kleiner): `STANDARDWERTE` gingen unnötig über die Leitung
`zuordnung.py` schickte sie mit jeder Katalogantwort ans Frontend. Gelesen hat sie dort seit dem
Entfernen des Zurücksetzen-Knopfes (`2026.9.1.11`) niemand mehr. Die Konstante selbst bleibt — sie
trägt intern die Vorgabewerte und leitet `SCHLUESSEL` ab; nur die Veröffentlichung ist weg.
### Befund 3 (offen, gehört dem Eigentümer): die Reichweite ist vier Tage alt
Auf der Übersicht steht **87 km** als größte Zahl des Bildschirms — daneben 61 % Tank. Der Wert
stammt vom 29.08. und aus einem **anderen Fahrzeug**; die beiden zuletzt genutzten Autos liefern
`can.vehicle.remaining.range` nicht über CAN (an flespis Zeitstempeln nachgewiesen, HAs Recorder
kann tote von konstanten Sensoren nicht unterscheiden).
Kein Codefehler. Solange kein Fahrzeug den Wert liefert, wäre die Rolle `RANGE_SENSOR` besser leer:
dann steht dort „Reichweite unbekannt" statt einer falschen Zahl. Im Endzustand liefert das
Fahrzeug sie wieder — Entscheidung des Eigentümers.
### Befund 4 (offen, braucht eine Entscheidung): die Nullfahrt-Regel gilt nur im Rückblick
`ist_gefahren()` wird nur von `historienimport.py` aufgerufen. Der Livepfad kann weiterhin eine
Fahrt anlegen, die sich später zu 0,0 km ausrechnet — genau das, was der Eigentümer aus dem Bestand
haben wollte. Im aktuellen Bestand gibt es keine solche Fahrt, und unter der neuen Gerätekonfiguration
(`Start Speed = 3 km/h`) sollte sie kaum entstehen. Aber es ist wieder eine Regel, die nur ein Weg
kennt.
**Bewusst nicht einseitig gebaut:** der Livepfad kennt die Strecke erst beim Screening, also
nachträglich. Die Fahrt dann zu verwerfen hieße, einen bereits gespeicherten Datensatz automatisch
zu löschen. Das ist eine Entscheidung über die Daten des Eigentümers, keine über den Code — dieselbe
Linie wie in Abschnitt W.
## Working conventions (observed — keep them)
- German is the project language: identifiers, comments, commits, UI texts. Exceptions:
@@ -1 +1 @@
{"version":"2026.9.1.16","sha256":"500a3e3bf6b2c9663bea77d47adc770aa973797f17724297af4a2438096d7221","bytes":265720,"gebaut":"2026-09-01T17:03:56Z"}
{"version":"2026.9.1.17","sha256":"96fa9737a475fe862c2dd309490f687a3e3e4088a8f356acd3f5c718cc919306","bytes":265731,"gebaut":"2026-09-01T17:08:45Z"}
@@ -49,8 +49,8 @@ from .verlauf import (
durchschnitt_kmh,
fahrtsignal,
ist_gefahren,
strecke_aus_zaehler,
strecke_waehlen,
zaehlerstrecke,
hoechstwert_im_fenster,
route_aus_verlauf,
UNPLAUSIBLE_KMH,
@@ -198,9 +198,7 @@ async def _fahrten_importieren(k: Koordinator, verlaeufe: dict) -> dict:
# 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]
)
fein = zaehlerstrecke(gnss_verlauf, f_start, f_ende)
distanz, gnss_verwendet = strecke_waehlen(distanz, fein)
if not ist_gefahren(distanz):
@@ -1,7 +1,7 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.9.1.16",
"version": "2026.9.1.17",
"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"],
@@ -35,8 +35,8 @@ from .verlauf import (
UNPLAUSIBLE_KMH,
durchschnitt_kmh,
hoechstwert_im_fenster,
strecke_aus_zaehler,
strecke_waehlen,
zaehlerstrecke,
naechster_wert,
route_aus_verlauf,
verbrauch_aus_literstaenden,
@@ -257,8 +257,10 @@ async def _gnss_verfeinern(k: Koordinator, gnss_sensor: str, fahrt: dict) -> Non
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)
# Etwas vor dem Fahrtbeginn mitlesen, damit zaehlerstrecke() ihren Anker
# findet - den letzten Datensatz am oder vor dem Beginn.
punkte = await verlauf_lesen(k.hass, gnss_sensor, start - SUCHFENSTER, ende)
fein = zaehlerstrecke(punkte, start, ende)
gewaehlt, genommen = strecke_waehlen(grob, fein)
if not genommen:
if fein is not None:
@@ -283,6 +283,32 @@ def route_aus_verlauf(
GNSS_TOLERANZ_KM = 1.0
def zaehlerstrecke(
punkte: list[Verlaufspunkt],
start: datetime.datetime,
ende: datetime.datetime,
) -> float | None:
"""Die im Fenster [start, ende] gefahrene Strecke aus einem laufenden
Kilometerzaehler.
Der Anker ist der letzte Datensatz AM ODER VOR `start`, nicht der erste
danach: der Zaehlerstand zu Fahrtbeginn ist der zuletzt gemeldete - dieselbe
Ueberlegung wie bei wert_bei(). Ohne ihn fehlt die Strecke zwischen dem
Beginn und dem ersten Datensatz danach.
WARUM DIESE FUNKTION EXISTIERT: Livepfad und Rueckblick haben dieselbe
Fahrt unterschiedlich gemessen. Das Screening las den Verlauf ueber
verlauf_lesen(), das den Stand ZU BEGINN des Fensters mitliefert; der
Rueckblick schnitt sein Fenster streng heraus und verlor genau diesen
Punkt. Am 01.09.2026 an der echten Fahrt nachgemessen: 6,896 km gegen
6,816 km fuer denselben Zeitraum. Das ist die Doppelbewertung, die dieses
Projekt bei UNPLAUSIBLE_KMH und MINDESTDAUER_S schon zweimal ausgeraeumt
hat - jetzt schneiden beide Wege durch dieselbe Funktion."""
davor = [p for p in punkte if p[0] <= start]
im_fenster = [p for p in punkte if start < p[0] <= ende]
return strecke_aus_zaehler((davor[-1:] if davor else []) + im_fenster)
def strecke_waehlen(
grob: float | None, fein: float | None
) -> tuple[float | None, bool]:
@@ -70,7 +70,10 @@ class Zuordnung:
die Oberfläche bereits angewendete Overrides und eingebaute
Standardwerte einheitlich."""
werte = {feld["key"]: getattr(self.werte, feld["key"], None) for feld in FELDER}
return {"katalog": FELDER, "werte": werte, "standardwerte": STANDARDWERTE}
# "standardwerte" ist mit dem Zuruecksetzen-Knopf (2026.9.1.11)
# entfallen - niemand liest sie mehr, also gehen sie auch nicht mehr
# ueber die Leitung.
return {"katalog": FELDER, "werte": werte}
__all__ = ["Zuordnung", "einstellungen"]