Commit Graph

21 Commits

Author SHA1 Message Date
tobias bcfd54489b Das Ende einer verschlafenen Fahrt richtig bestimmen
Drei Fehler, die zusammen eine 5-Minuten-Fahrt als 15 Stunden mit 0,1 km/h
im Bestand stehen liessen:

- nach_neustart_fortsetzen() schloss eine laufende Fahrt bei ALLEM ausser
  "on" - auch bei Funkstille. Jetzt nur noch bei ausdruecklichem "off".
- _letztes_lebenszeichen() las nur die Zuendungsentitaet, die nur bei einem
  Wechsel schreibt. Zusaetzliche Quelle ist die Meldezeit-Entitaet, deren
  Werte die Geraetezeit jedes Datensatzes sind.
- Strecke und Dauer decken bei einer Luecke verschiedene Zeitraeume ab: der
  Durchschnitt entfaellt, und die Plausibilitaetspruefung rechnet ohne die
  verschlafenen Kilometer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 17:08:52 +02:00
tobias d6e27666a3 Verschlafene Kilometer am Fahrtanfang zurueckholen
Der Dongle wacht nach dem Motorstart erst nach 28 s bis 19,5 min auf; was in
der Zeit gefahren wird, zeichnet er nicht auf. Die Kilometer holt das
Screening jetzt aus dem Endkilometerstand der Vorgaengerin zurueck und haelt
in luecke_km fest, wie viel davon betroffen ist.

Keine Sperre der GNSS-Verfeinerung, sondern eine Zerlegung: verschlafener
Anfang in ganzen Kilometern, aufgezeichneter Rest metergenau. Sonst waere die
feine Zahl systematisch zu klein und wuerde bei kleinen Luecken die Korrektur
stillschweigend wieder kassieren.

Zwei Schutzgitter, beide am Bestand gelernt: LUECKE_MAX_KM = 50 gegen
Differenzen, die keine Weckverzoegerung mehr sein koennen, und ein Rueckfall,
wenn _vollstaendig() die Werte daraufhin verwerfen wuerde.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 16:56:14 +02:00
tobias 863284e540 Hauptuntersuchung: Ableitung aus Erstzulassung und Wartungsplan, drittes Feld entfernt
Gemeldet: Wartungsplan leer, Erstzulassung 01.03.2025, erwartet 01.03.2028 -
gezeigt wurden drei verschiedene Antworten (Uebersicht 01.08.2026, Mein Audi
"August 2026", Service "kein Eintrag im Wartungsplan").

Ursache war das Profilfeld hauptuntersuchung_faellig ("08/2026"), das die
Ableitung ueberstimmte, in keiner Oberflaeche ein Eingabefeld hatte und im
Backend kein Schema. Drei Bildschirme verarbeiteten denselben Wert
verschieden. Das Feld ist samt Lese- und Schreibweg entfernt; es gibt jetzt
zwei Quellen: Wartungsplan + 24 Monate, sonst Erstzulassung + 36 Monate.

Dabei mitbehoben:

* Die Service-Kachel verschluckte abgeleitete Termine, sobald kein
  Wartungsplan-Eintrag dahinterstand.
* monatePlus() in der App verlor einen Tag ueber die Zeitumstellung (drei von
  fuenf gemessenen Faellen) - betraf Oelwechsel- und Inspektionsprognose
  genauso. Das Panel rechnete dort seit jeher richtig.
* Die App verlangte fuer jeden Wartungsplan-Eintrag eine Kilometerangabe. Eine
  Hauptuntersuchung ist rein datumsbasiert; ohne km waere der erste HU-Eintrag
  ignoriert worden. Ausserdem nahm das Panel den ersten Treffer der Liste, die
  App den juengsten - beide nehmen jetzt den juengsten.
* Monatsgenauigkeit ("03/2025") bleibt auf beiden Seiten erhalten.

Neu: paritaet_hu.test.ts vergleicht die App gegen die aus der Panel-Quelle
herausgeschnittenen Originalfunktionen, 15 Faelle. Gesamt 292 App-Tests, 73
Python-Tests mit 231 Untertests, 0 Tracebacks. Live an der Testinstanz auf
allen drei gemeldeten Bildschirmen geprueft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 01:56:11 +02:00
tobias 9ac69cdf44 Eine Neuschreibung ist kein Wechsel - Wartezeit startet nicht mehr neu
Home Assistant schreibt denselben Zustandswert auch neu, wenn sich nur ein
Attribut geruehrt hat. Am 04.09.2026 gezaehlt: von 22 Zustandszeilen der
Zuendung waren nur 6 echte Wechsel, die uebrigen 16 Neuschreibungen - und
alle 6 Wechsel lagen ausserhalb der Fahrten. Das Zuendungssignal ist
unterwegs stabil.

Der Beobachter unterschied das nicht. Fiel eine Neuschreibung von "aus" in
die laufende Wartezeit, brach Zweig 2 die wartende Bestaetigung ab und
begann die 15 Minuten von vorn. Am 04.09. ist es nicht passiert - die
Neuschreibungen lagen hinter dem Fenster -, aber das Loch war da.

_signalwechsel() bekommt jetzt mit, ob es ein echter Wechsel war. Eine
Neuschreibung, waehrend fuer diese Fahrt schon eine Bestaetigung wartet,
laesst sie stehen.

Bewusst NICHT als Filter ganz oben: _spaetes_aus() verlaesst sich darauf,
ein Zuendungs-Aus auch als off -> off zu sehen - daran haengt das
Nachtragen vorlaeufiger Enden. Die Pruefung sitzt nur in Zweig 2, und ein
eigener Test belegt, dass der andere Weg weiter offen ist.

21 Tests (von 18), Gegenprobe mit ausgeschalteter Pruefung scheitert wie
erwartet an einem. py_compile sauber, audi_ha_test auf 2026.9.4.25 ohne
Traceback.

AGENTS.md haelt ausserdem fest: die Beschreibung des Beenden-Knopfes war zu
weit gefasst (die Korrektur zieht nur nach hinten, nie nach vorn), und der
flespi-Cache laesst sich ueber DELETE auf die Einstellung zum Neulesen
zwingen - mit dem Haken, dass dabei ausstehende Werte verloren gingen.
2026-09-05 01:02:11 +02:00
tobias 242312441d Pausenregel rechnet in Geraetezeit - zwei Fahrten bleiben zwei
Am 04.09.2026 war der Dongle von 18:25 bis 23:36 ohne Verbindung. Als er
sich meldete, kamen das Zuendungs-Aus der ersten Fahrt und das Ein der
zweiten in DERSELBEN Sekunde bei Home Assistant an - ihre Geraetezeiten
lagen 5 Stunden 12 auseinander (18:23:32 und 23:35:43).

Die Pausenregel mass an der Ankunft: 0 Sekunden, also dieselbe Fahrt.
Ergebnis war ein Eintrag von 18:10 bis 23:42 ueber 5 h 33 ohne Strecke,
und die Fahrt um 23:30 gab es gar nicht.

Zwei Stellen im Code trugen dazu bei:
- zuendung_geaendert() rief bedingungslos warte_ende_ab_abbrechen(); das
  "an" loeschte damit das wartende Ende des "aus" derselben Sekunde.
- Der Fall "an, waehrend eine Fahrt laeuft" fiel wortlos durch alle drei
  Zweige von _signalwechsel(). Dazu steht seit dem 02.09. ein Kommentar im
  Code - damals wurde eine Sperre eingebaut, die die Reihenfolge richtig
  stellt, aber nicht den Ausgang.

Neu entscheidet _zuendung_kehrt_zurueck() in Geraetezeit. Der Koordinator
fuehrt mit, welches Ende gerade seine Pausenzeit absitzt (ende_wartet).
Liegt zwischen dem echten Ende und dem neuen "an" mehr als die Pausenzeit,
wird die alte Fahrt auf ihr berechnetes Ende geschlossen und eine neue
begonnen. Gemessen wird ab dem echten Ende, nicht ab dem Signal. Ohne
wartendes Ende passiert nichts - ein Schnitt auf Verdacht waere schlimmer
als eine zu lange Fahrt.

Das behebt die Verspaetung nicht, es macht sie harmlos.

Die Tests mussten zwei Dinge trennen, die vorher eine Zahl waren: PAUSE_S
(900, das Fenster der Regel) und WARTEN (0,15 s, die Wanduhrzeit des Tests).

18 Tests in test_zuendungspause.py (von 14), py_compile sauber,
audi_ha_test auf 2026.9.4.24 ohne Traceback. Gegenprobe am alten Stand:
2 der 4 neuen Tests scheitern dort, mit der Korrektur laufen alle 18.
2026-09-05 00:40:59 +02:00
tobias 7f386d19ea Geraetezeit statt Ankunftszeit - die Wurzel hinter der 3-km-Fahrt (2026.9.4.1)
Das Fahrtfenster stand seit dem 01.09. in der Gerätezeit, der Verlauf war nach
Ankunftszeit sortiert. Zwei Uhren, und das Gerät puffert: was verspätet ankam,
fiel aus einem Fenster heraus, in dem es der Sache nach lag. Am 03.09. hat das
eine Fahrt gekostet - 26 von 48 GPS-Punkten und 3,0 statt 7 km.

verlauf_lesen() stempelt jeden Punkt jetzt mit der Gerätezeit seines
Datensatzes. Eine Stelle, an der alle Verbraucher vorbeikommen. Dazu zwei
Riegel aus derselben Untersuchung: ein Rückschritt von 43 Metern ist keine
Zählerrücksetzung mehr (aus 87 Metern wurden vorher 13,7 km), und das
Fahrtende wird auf das letzte Lebenszeichen des Geräts geklemmt - der Nachlauf
ist eine Rechnung, kein Messwert.

Am echten Recorder nachgerechnet: Tacho 3,0 -> 7,0 km, GNSS 2,952 -> 6,877 km,
Route 26 -> 47 Punkte, Ende 23:21:57 -> 23:17:48. Beim ersten Lauf haben zwei
Fahrten eine Strecke bekommen, die vorher gar keine hatten.

Nicht das Gerät war schuld: es hat lückenlos aufgezeichnet. Die zehn Minuten
Funkstille waren eine offene, aber tote TCP-Sitzung (flespi-Log: 703 s, 16
Nachrichten) - kein Funkloch. Begründung und die Empfehlung fürs Gerät stehen
in AGENTS.md, Abschnitt CB.

Dazu die neun Paritätsbefunde, alle Richtung Panel gelöst - darunter ein
unmaskierter Punkt in beiden Codebasen, der aus "vor 3 Tage" ein "vor 3 Tag"
machte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 02:18:12 +02:00
tobias 6ec31b5275 Audit, zweite Runde: der Knopf schrieb ein erfundenes Ende (2026.9.3.14)
_letztes_lebenszeichen() fiel auf "jetzt" zurueck, sobald verlauf_lesen() eine
leere Liste lieferte - und die bedeutet sowohl "nichts aufgezeichnet" als auch
"nicht lesbar". Am 03.09. trat der zweite Fall ein: der Knopf traf einen
Neustart, der recorder war noch nicht bereit, die Abfrage lief 2:47 und kam leer
zurueck. Die Fahrt bekam 18:43 statt 17:59 als Ende, 74 Minuten statt 30.

Drei Quellen in fester Reihenfolge statt einer: Aufzeichnung, dann hass.states
(last_reported/last_updated - weiss dasselbe ohne Datenbank), dann "jetzt", und
das nur noch mit Warnung. Vier Regressionstests, drei davon gegen den alten
Stand nachweislich rot.

Dazu aus der Bestandspruefung: odometer_km liegt in drei alten Tankvorgaengen
als Zeichenkette vor, ablage.distanz_seit_letzter_tankung() rechnete damit
float - str. Laeuft jetzt durch als_kilometerstand(). Der Eingang war schon
zu; das ist Altbestand.

Und acht Dienste hatten keinen Klarnamen in translations/de.json - jetzt 28
Dienste, 28 Klarnamen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 21:15:42 +02:00
tobias 4d4890034a Regler stellt den Schlaf-Timeout des Dongles (2026.9.3.10)
Der Regler "Fahrt beenden" ist jetzt eine Zahl fuer zwei Dinge: unsere eigene
Wartezeit bis zum Fahrtende und der Schlaf-Timeout des FMM003 (103). So lange
bleibt der Dongle wach und wartet darauf, dass es gleich weitergeht; danach
schlaeft er. Die 180 s Ignition OFF Delay laufen davor und bleiben unsichtbar -
sie werden nur von der Fahrtdauer abgezogen. Der Regler geht deshalb von 1 bis
60 in Einerschritten; das Geraet kennt kein Timeout 0.

flespi.py liest und schreibt die Geraete-Konfiguration. Drei Annahmen sind an
der echten API gefallen und stehen als Messung im Modulkopf: der Wert liegt in
current (nicht value), die Einstellung heisst sleep_mode mit dem Wert eine Ebene
tiefer unter mode, und ein PUT braucht {"properties": ..., "address": ...}.

Die eigene Auftrags-Warteschlange ist wieder entfallen. Sie war fuer "das Geraet
schlaeft" gebaut - das gilt aber nur fuer das Geraet, nicht fuer die
Schnittstelle: die vollstaendige Konfiguration kam herein, waehrend das Fahrzeug
seit dem Vortag stand. flespi puffert selbst (pending + address=connection), ein
zweites Auftragsbuch waere eine zweite Warteschlange fuer dieselbe Aufgabe.

Gegengelesen wird nach jedem Schreiben; als Erfolg zaehlt current ODER pending -
letzteres ist bei stehendem Fahrzeug der Normalfall.

Live gegen den echten Account belegt: Token sieht genau ein Geraet, 127
Einstellungen bei parkendem Fahrzeug gelesen, Regler 15 geschrieben und als
pending bestaetigt (Nachbarfelder unveraendert), zweiter Neustart ohne erneute
Anfrage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 20:00:39 +02:00
tobias 5ccbfec92d Fahrt beenden auf Knopfdruck, Schieberegler, Zuendungspause (2026.9.3.5-.7)
Vier Themen aus einer Sitzung.

1. Die Fahrt endet wieder bei uns statt im Dongle (Abschnitt BW). Der
   Ignition-OFF-Timeout des FMM003 (900 s) und sein Schlaf-Timeout (ebenfalls
   900 s) starten beide beim Zuendungs-Aus und fallen in derselben Sekunde -
   am 02.09. zweimal beobachtet, einmal ging das Trip-Ende verloren, einmal kam
   es eine Sekunde vor dem Schlaf an. Ausloeser ist jetzt die Zuendung, die
   Wartezeit laeuft in Home Assistant. ZUENDUNG_NACHLAUF_S = 180 wird in beiden
   Wegen abgezogen (ueber das Trip-Signal 1080 s, weil dessen Ende selbst an der
   verzoegerten Zuendung haengt).

2. Schieberegler, neu in beiden Codebasen. Schrittweite 5 Minuten; der Daumen
   war im Tagmodus weiss auf weiss und traegt jetzt einen Ring aus
   --line-strong - ein Token, das genau dort sichtbar ist, wo es gebraucht wird.

3. Knopf "Fahrt beenden" in der Zuletzt-Kachel (Abschnitt BX). Schliesst auf
   das letzte Lebenszeichen, nicht auf "jetzt", und kennzeichnet das Ende als
   vorlaeufig. Ein spaet eintreffendes Zuendungs-Aus zieht es nach - innerhalb
   von sechs Stunden, nur nach vorn, und nur bei einer vorlaeufigen Fahrt.
   ts_end wandert bewusst NICHT in edited_fields, sonst blockierte der Schutz
   fuer Handeingaben genau diese Korrektur.

4. Das OTA-Buendel kann auf dem Mac gar nicht entstehen (Abschnitt BV):
   npm run ota ist eine Windows-PowerShell-Datei, ios-signieren.sh baut ein
   frisches dist/ und fasst das Buendel nie an. Ausgeliefert war deshalb eine
   Oberflaeche ohne den Versionshinweis unter richtiger Nummer.

Verifiziert: Backend 27/27 (5 Signalwechsel, 14 Zuendungspause, 8 Fahrtende),
companion-app tsc sauber und 179/179, Panel als Modul geparst, audi_ha_test auf
2026.9.3.7 sauber gestartet. Live am laufenden Panel und ohne Rueckstand
belegt: Wartezeit samt Abbruch, die 180-s-Rechnung, der Regler im Tagmodus und
der Knopf von der laufenden Fahrt bis zum verworfenen Kurzvorgang - Fahrten
vorher 16, nachher 16.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 15:00:38 +02:00
tobias 6016053a92 Kalender ueber EventKit, Orte nach dem Import, Fotos verkleinert, Signalsperre (2026.9.3.1-.3)
- Kalendertermine gehen nativ ueber EventKit statt ueber das Teilen-Blatt:
  Apples Kalender meldet sich beim System gar nicht als Teilen-Ziel an, ein
  anderes Dateiformat haette also nie geholfen (Abschnitt BS).
- Der Rueckblick stoesst jetzt selbst das Screening an, wenn er Fahrten
  angelegt hat - sonst blieb eine importierte Fahrt ohne Ort liegen, solange
  das Fahrzeug steht (Abschnitt BT).
- Fahrzeugfotos werden vor dem Upload im Browser auf 2000 px/WebP gebracht.
  Gemessen: 3.099.022 -> 325.994 Bytes. Damit ist die Nachrichtengrenze der
  WebSocket-Verbindung kein Thema mehr ("connection lost" auf der realen
  Instanz). Formate, die der Browser weder umwandeln noch anzeigen kann,
  werden mit klarer Meldung abgelehnt statt roh gespeichert.
- Zwei Wechsel des Fahrtsignals in derselben Sekunde kosteten eine ganze
  Fahrt: das "on" ueberholte den noch laufenden Beende-Vorgang und fiel durch
  beide Zweige. Neue asyncio-Sperre plus Regressionstest, der ohne sie
  nachweislich rot ist (Abschnitt BU).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 01:59:32 +02:00
tobias 2af5c469d6 Ortsnamen aus dem Backend, Verbrauchsfaktor aus dem Tankbeleg, Radzaehler ueber Fahrten
Ortsauflösung serverseitig (geokodierung.py): der Ort steht jetzt in der Fahrt
statt im Zwischenspeicher jedes Geräts. Beide Oberflächen fragen nichts mehr ab.

Verbrauchskorrektur (verbrauchskorrektur.py): Faktor = Beleg-Liter / Summe der
Einzelfahrten, voll zu voll. Statt einer Schranke am Faktor wird der Nenner
geprüft - decken die erkannten Fahrten 90-110 % der Tacho-Spanne ab? Ausgelöst
beim Schreiben eines Tankvorgangs, nicht erst bei der nächsten Fahrt.

Radzähler: summiert die Fahrten des montierten Satzes statt Tacho-Deltas. Der
alte Weg hatte Fahrzeugwechsel als gefahrene Strecke verbucht (375.962 km bei
einem Tacho von 61.823).

Null heißt unbekannt: ein Kilometerstand von 0 oder ein leeres Feld werden zu
None normalisiert, statt an sechs Lesestellen als "Tachostand null" zu gelten.
Momentanwerte der Sensoren gelten nur für einen Tankvorgang von jetzt.

Oberfläche, beide Codebasen: Wertespalte der Fahrtenliste ausgerichtet, Ort ->
Ort in "Zuletzt", "Räder" statt "Reifen", "Termin vereinbart" entfernt,
Markenlogo 20 % größer, Übersicht-Symbol auf volle Rasterbreite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 11:28:37 +02:00
tobias 0124254ea9 Nachlauf-Abzug nur, wenn das Trip-Element ihn erzeugt (2026.9.1.9)
_echtes_ende() zog NACHLAUF_S bedingungslos ab. Richtig, solange die Fahrten
vom Trip-Signal ausgeloest werden - aber verlauf.fahrtsignal() faellt auf die
Zuendung zurueck, wenn TRIP_SENSOR nicht zugeordnet ist, und die hat keinen
Nachlauf. Jede Fahrt haette fuenfzehn Minuten verloren, jede kuerzere waere
ganz verschwunden.

Genau die Konstellation, in die eine bestehende Installation nach dem Update
ohne Zutun laeuft: die neue Rolle ist leer, bis jemand sie zuordnet. Gefunden
beim Durchdenken des Update-Wegs fuer die reale Instanz, nicht durch einen
Fehlerbericht.

Nachgewiesen: TRIP_SENSOR geleert, Zuendung fuenf Minuten an -> Fahrt mit
300 s angelegt statt verworfen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 12:24:50 +02:00
tobias cef11b26f3 Fahrtende ohne den Nachlauf des Geraets (2026.9.1.4)
Das Trip-Signal endet nicht mit der Zuendung, sondern "Ignition OFF Timeout"
spaeter - beim FMM003 900 s. Das Fahrtende lag damit systematisch 15 Minuten
zu spaet. _echtes_ende() zieht NACHLAUF_S = 900 ab.

Die Rechnung stammt vollstaendig aus der Logik des Geraets und ueberlebt
deshalb ein Funkloch: der Datensatz kommt spaeter, traegt aber seine eigene
Zeit (2026.9.1.2/.3).

Die Klemme ist der wichtigere Teil: war das Signal nicht laenger als der
Nachlauf, wurde nicht gefahren - Ende auf den Beginn geklemmt, MINDESTDAUER_S
verwirft den Vorgang. Ohne sie entstuende aus dreissig Sekunden Zuendung eine
Viertelstunde Fahrt.

_nachlauf_gegenpruefen() vergleicht das gerechnete Ende mit dem beobachteten
Zuendungswechsel und warnt ab 120 s Abweichung, ohne zu korrigieren. Wie genau
"Zuendung aus" bekannt ist, haengt am Operanden des Zuendungs-I/O-Elements; er
steht auf 3 (nach der Reihenfolge in der Teltonika-Doku "Monitoring", also
kein eigener Datensatz je Wechsel). Solange das nicht feststeht, waere ein
Umschalten auf den beobachteten Wert eine Verschlechterung.

Keine Einstellung in der Oberflaeche: die Zahl gehoert zum Geraet, nicht zum
Nutzer - dieselbe Linie wie UNPLAUSIBLE_KMH. flespi koennte sie liefern
(GET /gw/devices/{id}/settings/all), aber die Integration liest heute nur
Telemetrie; ein zweiter Zugang samt Token waere unverhaeltnismaessig.

Verifiziert in audi_ha_test: Signal 1200 s -> Fahrt 300 s; Signal 930 s ->
keine Fahrt (Bestand 12 -> 12); Gegenprobe mit 887 s Abweichung ausgeloest.
Testfahrt ueber fahrt_loeschen entfernt, Bestand unveraendert 11 Fahrten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 11:29:17 +02:00
tobias 4cc91c461b Fahrtzeiten vom Geraet, Trip und Zuendung getrennt (2026.9.1.3)
Enthaelt 2026.9.1.2, das nie einzeln ausgeliefert wurde.

FAHRT- UND TANKZEITEN KOMMEN VOM GERAET

Fahrtbeginn war datetime.now() - die Uhrzeit, zu der Home Assistant den
Wechsel verarbeitet. Hat das Geraet gepuffert (Funkloch, Tiefgarage,
Tiefschlaf), liegt die Wahrheit beliebig weit davor. Gemessen: ein Datensatz
mit Fahrtende trug die Geraetezeit 07:29:04 und kam um 07:35:23 an.

Neue Sensorrolle MELDEZEIT_SENSOR (message_timestamp, Unix-Sekunden). Der
Wert ist UTC, keine Ortszeit - nachgemessen lag der Versatz zur UTC-Uhr von
HA bei Sekunden, nicht bei zwei Stunden; eine Zeitzonenumrechnung baute einen
Zwei-Stunden-Fehler ein.

geraetezeit() liegt in verlauf.py, weil Fahrt- und Tankerkennung denselben
Zeitstempel bestimmen muessen. tankerkennung._automatisch_anlegen() stempelte
bisher ebenfalls mit now(); ein aus einem gepufferten Datensatz erkannter
Tankvorgang trug damit die Uhrzeit des Auftauchens.

Die flespi-Integration setzt die Entitaeten eines Datensatzes nacheinander -
gemessen Zuendung 07:35:23.633, zugehoerige Meldezeit 2 ms spaeter. Wer
sofort liest, bekommt den vorigen Datensatz: beim Fahren zehn Sekunden
daneben, im Stand Stunden (On Stop Min Period = 43200 s). geraetezeit()
wartet deshalb bis zu 2 s auf den passenden Wert. Zwei Notbremsen, beide live
ausgeloest: Zeitstempel in der Zukunft und Ende vor dem Beginn.

TRIP UND ZUENDUNG SIND ZWEI ROLLEN

ZUENDUNG_SENSOR zeigte auf das Trip-Signal; im Setup stand "Zuendung/ACC" ueber
einer Trip-Entitaet und die echte Zuendung war nirgends zugeordnet. Neu:
TRIP_SENSOR loest Fahrten aus, ZUENDUNG_SENSOR liefert nur noch die Anzeige
"faehrt/steht". verlauf.fahrtsignal() ist die eine Stelle, die entscheidet -
Trip-Status wenn zugeordnet, sonst Zuendung, damit bestehende Installationen
unveraendert weiterlaufen und Livepfad und Rueckblick nicht auseinanderlaufen.

VERIFIZIERT in audi_ha_test, jeder Weg einzeln:
- Fahrtbeginn 08:30:03 (Geraetezeit) statt 08:38:27 (Ankunft)
- Fahrtende 08:04:40 -> 08:09:22 statt 08:11:04 -> 08:11:24
- Tankvorgang 08:18:45 statt 08:38:51 - 20 Minuten frueher
- Meldezeit in der Zukunft und Ende vor Beginn: Warnung, Ankunft genommen
- Zuendung schalten legt keine Fahrt mehr an, Trip-Signal schon

Der Livetest fing dabei einen NameError, den py_compile nicht sehen konnte:
der Import von geraetezeit in tankerkennung.py fehlte. Kompiliert ist nicht
verifiziert. Alle Testdatensaetze wurden ueber die eigenen Dienste wieder
entfernt; Bestand unveraendert 11 Fahrten, 13 Tankvorgaenge, keine offene Fahrt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 10:40:21 +02:00
tobias ff5a334e95 Keine Phantomfahrt beim Neustart, Tankstand auf wert_bei() (2026.9.1.1)
zuendung_geaendert() prueste den neuen Zustand, aber nicht den alten. Beim
Hochfahren taucht die Zuendungs-Entitaet neu auf; das kommt als Wechsel mit
old_state = None an. Steht das Trip-Signal ohnehin auf "an" - beim FMM003 der
Normalfall -, las der Beobachter das als "Zuendung ging gerade an" und legte
eine Fahrt an, deren Beginn schlicht der Zeitpunkt des Neustarts war. Daher
der Eintrag 30.08. 19:43 bis 31.08. 08:37, 12,9 h, 0 km.

Die Pruefung ist "if alt is None: return" - dieselbe, die
_kilometerstand_geaendert() im Koordinator seit jeher hat. Eine wirklich
laufende Fahrt geht nicht verloren: deren Beginn liegt im Store und wird von
nach_neustart_fortsetzen() aufgenommen.

Nachgewiesen: zwei Neustarts hintereinander bei trip_status = on und leerem
Zwischenstand, null "Fahrt gestartet"-Zeilen und fahrt_start_ts bleibt null.
Vorher entstand unter genau diesen Bedingungen jedes Mal eine.

_verbrauch_screenen() liest die Literstaende jetzt ebenfalls mit wert_bei()
statt naechster_wert() - dieselbe Doppelbewertung gegenueber
historienimport.py wie beim Kilometerstand, eine Ebene tiefer.
naechster_wert() bleibt bei Breiten-/Laengengrad: eine Position ist kein
Zaehler.

Auf Anweisung des Eigentuemers ausserdem die zwoelf Fahrten mit
distance_km == 0 geloescht (ueber den Dienst fahrt_loeschen, nach einem
backup_jetzt) und "Open items" in AGENTS.md aufgeraeumt: vier Karteileichen
des MQTT-Wegs entfernt, Cloudflare-Zugang und iPhone-Signatur als erledigt
gebucht, Befund 07 als Nutzereingabe geschlossen, QR-Scanner auf den halben
Umfang gekuerzt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 10:04:55 +02:00
tobias 9b0e390dfd Audit Panel/iOS: sechs Befunde behoben, Zuendung aufs Trip-Signal (2026.8.31.5)
ZUENDUNG_SENSOR liegt jetzt auf dem Trip-Signal des FMM003 statt auf dem rohen,
prellenden Zuendungseingang. Damit kommt die Pausentoleranz vom Geraet - eine
Fahrt beginnt erst bei Zuendung UND Bewegung UND Start Speed.

MINDESTDAUER_S wandert nach verlauf.py und gilt jetzt auch im Livepfad. Vorher
verwarf nur der Import Zuendphasen unter 60 s; live entstanden daraus am 31.08.
elf Fahrten mit 0 km, zwei davon mit null Sekunden Dauer.

Das Panel erfand einen Literpreis von 0,00 EUR, wenn die Kosten noch nicht
feststanden (null/liter ist 0 in JavaScript) - neue Hilfsfunktion literpreis()
prueft beides, wie die App seit jeher. schnittpreis() teilte ohne Nennerpruefung
und konnte "NaN" oder "unendlich" anzeigen.

Der Setup-Dialog ist wirklich modal: die Tab-Leiste hatte einen eigenen Zuhoerer
vor der Sperre und liess einen Tabwechsel bei offenem Fenster zu.

Verwaister Code entfernt: drei Funktionen und vier Regeln im Panel, 20 Regeln in
der App.

Offen bleibt allein die leere Erstzulassung - eine Angabe, keine Aenderung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 20:05:59 +02:00
tobias 4af38d7e65 Fahrten, Statistik, Trip-Timer (2026.8.31.3)
Der 15-Minuten-Timer war doppelt: der FMM003 wartet laut Konfiguration
(Trip \ Odometer, Ignition OFF Timeout) selbst 900 s, die App noch einmal
15 Minuten. Zusammen eine halbe Stunde. Die Pausenregel ist damit entfallen -
in der Live-Erkennung, im Historienimport und in beiden Oberflaechen. Der
Schwellwert "ab wann ist es ein Parkplatz" hing an derselben Einstellung, hat
damit aber nichts zu tun und steht jetzt als eigene Zahl.

Hoechst- und Durchschnittsgeschwindigkeit je Fahrt. Der Durchschnitt folgt aus
Strecke und Dauer, vmax aus der neuen Rolle GESCHWINDIGKEIT_SENSOR - laut
Konfiguration liest das Geraet die Geschwindigkeit vom OBD/CAN. Es ist der
groesste gemeldete Wert, nicht die tatsaechliche Spitze: alle zehn Sekunden
ein Datensatz. So ist es auch beschriftet.

Der Langzeitverbrauch war um einen ganzen Tankinhalt zu hoch. Gemessen wird
die Strecke ZWISCHEN erstem und letztem Tankstop; verbraucht wurde darauf,
was ab dem ZWEITEN hineinkam - der erste fuellte die Strecke davor. Bei 13
Tankvorgaengen rund 8 Prozent, im Testfall 16,7 statt 8,3 l/100 km. Der Test
hatte die falsche Rechnung festgeschrieben und ist mitkorrigiert. Eine
negative Strecke (falscher Kilometerstand an einem Tankvorgang) gilt jetzt
als unplausibel statt als Null ohne Erklaerung.

Verbrauch je Fahrt bekommt Grenzen: am Sensor nachgemessen loest
can_fuel_volume 0,1 l auf, ein Schritt entspricht also 10/Strecke l/100 km
Fehler. Unter 3 km sagt die Differenz nichts mehr, ueber 60 l/100 km ist es
kein Verbrauch.

Statistik: zusaetzlich "Absolut", und die Einheit steht jetzt in eckigen
Klammern an der Rubrik statt vier Mal am Zeitraum. Fuenf Spalten passen auf
einem Telefon nicht nebeneinander, das Raster bricht um.

Radfoto in Reifen war ein reines <Bild> - Ersetzen und Loeschen von dort aus
gar nicht erreichbar. Jetzt BildMitMenue wie im Panel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 15:03:28 +02:00
tobias 90fff7b305 Vollaudit: sechs echte Fehler in Backend, Panel und companion-app behoben
- companion-app Batterie.tsx: Generatorspannung zeigte sich faelschlich als
  Ruhespannung/verzerrte den SOH-Trend, Panel-Filter (AGM_RUHE_MAX_V) fehlte
- Manuell angelegte Fahrten/Tankvorgaenge trugen naive Zeitstempel und wurden
  von der Import-Dublettenpruefung stillschweigend uebersprungen; neue
  gemeinsame zeit_normalisiert() in verlauf.py schliesst die Luecke
- Batteriespannungsverlauf fehlte im Backup (sicherung.py)
- Tankvorgangs-Import verlor stillschweigend aeltere Tankvorgaenge vor Beginn
  der Litersensor-Historie; laeuft jetzt zweigleisig (Prozent + Liter)
- Panel-Statistikseite behauptete faelschlich, der Verbrauch je Fahrt komme
  vom Fahrzeug (OBD) - ist eine Naeherung aus dem Tankfuellstand
- companion-app Statistik.tsx: Arbeitsweg-Segment war noch rot statt neutral

Version 2026.8.27.19, OTA-Buendel neu gebaut, im Testcontainer verifiziert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 00:05:58 +02:00
tobias 80d73e8afd Fahrten: Verbrauch-Näherung, 0V-Zustandsanzeige gefiltert, Plausibilitätsgrenze für die Live-Vervollständigung
Verbrauch (l/100km) war bislang totes Schema - kein Codepfad füllte
verbrauch_l_100km je. Jetzt aus der Literstand-Differenz (TANK_LITER_SENSOR)
über die Distanz genähert, live und beim Import, in beiden Frontends.

Die "Zustand"-Kachel zeigte die Batteriespannung ungefiltert direkt vom
Sensor, unabhängig von der Plausibilitätsgrenze der Verlaufsaufzeichnung -
ein Sensorausreißer (0V) zeigte sich dort weiterhin, obwohl die Messwertliste
ihn längst verwarf. Dieselbe Grenze gilt jetzt auch für diesen Anzeigepfad.

Reale Fahrtendaten zeigten eine Fahrt mit 22km in 67s (~1180 km/h) - die
Live-Vervollständigung (screening.py) hatte anders als der Import keine
Plausibilitätsprüfung der Durchschnittsgeschwindigkeit. Jetzt gemeinsam in
verlauf.py (UNPLAUSIBLE_KMH/durchschnitt_kmh) für beide Pfade. Dabei einen
zweiten echten Bug gefunden: _fahrt_screenen() zog sein Ergebnis nie ins
In-Memory-Objekt nach (nur in die Ablage) - eine im selben Durchlauf gerade
erst ermittelte Distanz blieb für spätere Schritte (z. B. Verbrauch) bis zum
nächsten Screening unsichtbar. Beide Stellen jetzt behoben.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 21:01:57 +02:00
tobias 70253e83a5 Fahrterkennung: FMM003-Funklöcher reißen keine Fahrt mehr auseinander
Die Zündungs-Entität stammt vom FMM003-Tracker; verliert er unterwegs
kurz die Verbindung, meldet Home Assistant "unavailable" statt eines
echten Zündungszustands. Die Live-Erkennung wertete das bisher wie
"aus" - eine Funklücke über der Pausenzeit teilte eine durchgehende
Fahrt in zwei. zuendung_geaendert() ignoriert "unavailable"/"unknown"
jetzt vollständig und leitet "fährt gerade" aus dem eigenen
Zwischenstand (fahrt_start_ts) statt aus dem letzten Rohwert ab.

historienimport.py bekommt zusätzlich eine Plausibilitätsprüfung: eine
errechnete Durchschnittsgeschwindigkeit über 300 km/h deutet auf einen
Kilometerstand-Ausreißer an der Fahrtgrenze hin, nicht auf eine echte
Fahrt - die Strecke wird dann verworfen statt eine unmögliche Fahrt
anzuzeigen.

Tankerkennung: optionaler zweiter Sensor für das Tankvolumen in Litern
(TANK_LITER_SENSOR, direkt vom CAN) neben dem bisherigen
Prozent-Füllstand. Ist er zugeordnet, übernimmt er die automatische
Tankerkennung vollständig - genauer als der Umweg über das im
Fahrzeugprofil hinterlegte Tankvolumen, und der erkannte Anstieg
liefert gleich eine grobe Vorbelegung für die getankte Menge statt
eines leeren Feldes. Ohne den Sensor bleibt alles beim Alten. Der
historische Import zieht mit derselben Präferenz nach.

Manifest auf 2026.8.25.2, beide Änderungsrunden live im Testcontainer
verifiziert (py_compile, Neustart, sauberes Setup-Log).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 22:04:26 +02:00
tobias d8b12da36d pyscript-Backend zur echten HA-Integration umgebaut (HACS-fähig)
Das Backend liegt jetzt als custom_components/audi_dashboard/ vor - eine
normale Home-Assistant-Integration mit Config-Flow, einer sensor-Plattform
und 18 Diensten. Damit ist die App über HACS installierbar; bis das Repo auf
GitHub gespiegelt ist (HACS spricht ausschließlich mit GitHub), installiert
homeassistant/installationspaket/install.ps1 denselben Ordner ohne HACS.

Fünf Installationsschritte entfallen ersatzlos: der pyscript:-Block, der
panel_custom:-Block, das Kopieren der Oberfläche nach www/, das langlebige
Zugriffstoken (der Verlauf wird direkt über die recorder-API gelesen) und
"pip install pypdf" (steht in manifest.json). Das Fahrzeugprofil legt die
Integration beim ersten Start aus ihrer Vorlage an.

Drei alte Schwächen sind dabei mit erledigt:
- Die Nutzlast landet nicht mehr in der Recorder-Datenbank
  (_unrecorded_attributes - das kann nur eine echte Entität).
- Eine laufende Fahrt überlebt einen Neustart (Store statt Arbeitsspeicher);
  fiel sie während eines Ausfalls ins Ende, schließt
  nach_neustart_fortsetzen() sie beim letzten aufgezeichneten Zeitpunkt.
- Sensor-Zuordnungen wirken sofort - die Zustandsbeobachter werden neu
  gebunden, der Neustart-Hinweis und der Neustart-Dienst sind weg.

Namensvertrag geändert, beide Oberflächen mitgezogen:
pyscript.audi_dashboard_x -> sensor.audi_dashboard_x,
pyscript.audi_dashboard_y -> audi_dashboard.y. Eine Companion-App vom alten
Stand findet nach dem Umstieg nichts mehr und muss neu gebaut werden; das
Panel liegt in der Integration und kann nicht driften.

Der selbstgebaute Updater entfällt - HACS ist die Update-Mechanik, die Home
Assistant kennt. Die Versionierung schrumpft auf eine Quelle: manifest.json.

Geprüft am laufenden Testcontainer (Container byteweise identisch mit dem
Repo): alle 18 Dienste, Panel, Config-Entry neu laden, Historienimport,
echter Shell-Beleg in-process, Neuinstallation im Wegwerf-Container blank mit
automatisch nachinstalliertem pypdf. Companion-App: tsc sauber, 112/112
Tests, beide Rauchtests gegen das laufende Backend grün. Belegparser 8/8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 23:53:56 +02:00