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>
This commit is contained in:
2026-09-03 15:00:38 +02:00
parent d30bb0e9f0
commit 5ccbfec92d
29 changed files with 1444 additions and 51 deletions
+181 -1
View File
@@ -1,6 +1,10 @@
# AGENTS.md — Project state, review findings, open items, and working rules
**Last updated: 2026-09-03** (Das OTA-Buendel kann auf dem Mac gar nicht entstehen - ausgeliefert war eine
**Last updated: 2026-09-03** („Fahrt beenden“ auf Knopfdruck mit vorlaeufigem Ende, Schieberegler in
5-Minuten-Schritten und im Tagmodus sichtbar, `2026.9.3.7`, Abschnitt BX.
Davor: Die Fahrt endet wieder bei uns statt im Dongle - Zuendung als Ausloeser,
eigene Wartezeit, Nachlauf des Geraets abgezogen, Schieberegler, `2026.9.3.5`, Abschnitt BW.
Davor: Das OTA-Buendel kann auf dem Mac gar nicht entstehen - ausgeliefert war eine
Oberflaeche ohne den Versionshinweis unter richtiger Nummer, repariert mit `2026.9.3.4`,
Abschnitt BV. Davor: Zwei Signalwechsel in derselben Sekunde kosteten eine Fahrt - Sperre
plus Regressionstest, Bildformat-Pruefung, `2026.9.3.3`, Abschnitt BU. Davor: Rueckblick loest Orte
@@ -8762,3 +8766,179 @@ braucht es nicht: die Zip-Einträge schreibt die PowerShell-Fassung ohnehin von
ihrem Kopf), `zlib.deflateRawSync` reicht. Dazu ein Wächter, der in der **Git-Historie** den letzten
Commit an `bundle.zip` gegen den letzten an `companion-app/src` und `design-system/src` hält — über
Git und nicht über Dateidaten, weil ein frischer Klon alle Zeitstempel plattmacht.
---
## BW. Die Fahrt endet wieder bei uns, nicht im Dongle (2026.9.3.5)
### Der Befund: zwei Uhren, dieselbe Sekunde
Der FMM003 beendet den Trip erst nach seinem `Ignition OFF Timeout` (`11804`) von 900 s — und sein
Schlaf-Timeout (`103`) stand auf denselben 900 s. Beide starten beim Zuendungs-Aus. Am 02.09.2026
ist das zweimal beobachtet worden, mit **entgegengesetztem Ausgang**:
| | Zuendung aus | Trip-Ende erwartet | tatsaechlich |
|---|---|---|---|
| Fahrt A | 16:54:21 | 17:09:21 | **nichts** — naechster Datensatz erst 18:42:04 |
| Fahrt B | 21:43:15 | 21:58:15 | 21:58:16 (+901,2 s) |
Ein Wettlauf, gemessen, nicht vermutet. Die Doku entkraeftet dabei die naheliegende Erklaerung: im
Deep Sleep **sendet** das Geraet sehr wohl, High-priority-Datensaetze sogar sofort ("device can save
periodical or eventual records and send data to server based on the selected AVL packet priority").
Die Prioritaet regelt die Zustellung, nicht die Entstehung — und im Deep Sleep sind `Speed`,
`Trip Odometer` und `Total Odometer` aus den Datensaetzen ausgeschlossen, also genau die Groessen, aus
denen das Trip-Szenario lebt.
Der Zielkonflikt ist unaufloesbar, solange das Geraet die Uhr fuehrt: der Eigentuemer will, dass der
Dongle frueh einschlaeft (Strom), und dass das Ende ankommt. Beides zugleich geht nicht.
### Die Loesung: die Wartezeit laeuft, wo nichts schlaeft
Ausloeser ist jetzt die **Zuendung** (`50000:2` High, `50001:5` On Change seit 01.09.2026 — sie wird
im Moment des Wechsels gesendet), die Wartezeit laeuft in Home Assistant. Der Dongle darf danach
frueher schlafen als vorher, nicht spaeter.
**`ZUENDUNG_NACHLAUF_S = 180`** (Geraeteparameter `400`, "Ignition OFF Delay"). Abgezogen wird in
**beiden** Wegen: ueber die Zuendung 180 s, ueber das Trip-Signal 1080 s — denn das Trip-Ende haengt
selbst an der bereits verzoegerten Zuendung (Fahrt B oben belegt es auf die Sekunde). Der Wert stand
bis 31.08. auf 0 und seit **01.09. 09:38** auf 180; **Fahrten aus dieser Zeit enden 180 s zu spaet**,
korrigiert wird ab jetzt. `_nachlauf_gegenpruefen()` musste mit — ohne Anpassung haette sie ab sofort
bei jeder Fahrt 180 s Abweichung gemeldet.
**Die Wartezeit laeuft ausserhalb der Fahrtsperre.** Haette die Hintergrundaufgabe die Sperre, stuende
ein zurueckkehrendes "on" eine Viertelstunde davor, statt die Fahrt fortzusetzen — das Gegenteil des
Zwecks. Das Schliessen selbst ist mit `asyncio.shield()` gegen den Abbruch abgeschirmt: ein Wechsel
genau im Schreibmoment duerfte keine halbe Zeile in `fahrten.jsonl` hinterlassen.
**Die Wartezeit entscheidet nur, OB geschlossen wird, nicht WANN die Fahrt endete.** Das Ende steht
beim Zuendungs-Aus fest.
**Keine doppelte Wartezeit:** `_pausenzeit_s()` liefert 0, solange `TRIP_SENSOR` zugeordnet ist —
dann wartet das Geraet bereits. Genau daran war die Pausenregel am 31.08.2026 entfallen.
**Spaetes Aus** wird ueber die **Geraetezeit** entschieden, nicht ueber die Reihenfolge des
Eintreffens: faellt es in die schon geschlossene Fahrt, wird es verworfen und protokolliert; passt es
zu keiner, gibt es eine Warnung — dann ist ein Fahrtbeginn entgangen.
### Was die Daten ueber die Drehzahlquelle sagen
Aus 21 Geraetekonfigurationen rekonstruiert: `101` ist eine Bitmaske (2 = ACC, 4 = Power Voltage,
8 = RPM). Aussetzer der Zuendung gibt es — aber sie sind **kurz (12 s) und liegen am Fahrtanfang**,
in den ersten 25 Sekunden; fuer einen Ausfall ueber Minuten waehrend der Fahrt gibt es keinen Beleg.
Die Stichprobe der aktuellen Konfiguration ist zwei Fahrten gross, das gehoert dazugesagt. Dazu
kommt **Start-Stopp**: der RS4 stellt an der Ampel den Motor ab, aus einer Drehzahlquelle ist das
nicht vom Abstellen zu unterscheiden. Die abbrechbare Wartezeit verschluckt beides — ein Argument
fuer einen grosszuegigen Standardwert, nicht gegen den Ansatz.
### Der Schieberegler
`Slider` / `.ads-schieber` im Design-System, `.schieber` im Panel — es gab bisher **nirgends** einen.
Ein echtes `input[type=range]` (Tastatur und Vorlesefunktion ohne Zutun), selbst gemalt, weil er
sonst im Panel wie Chrome und in der App wie WebKit aussaehe. Der gefuellte Teil kommt aus
`--fuell` / `--ads-fuell`, weil ein Range-Feld seinen Wert nicht ans Stylesheet weitergibt und
`accent-color` entfaellt, sobald man die Spur selbst gestaltet. Farben wie beim Balken: `--fg` auf
`--ios-fill` — Markenrot bleibt den Stellen vorbehalten, die Aufmerksamkeit verlangen.
Im Panel zieht der `input`-Zweig nur Fuellung und Anzeige nach und ruft **kein** `render()`;
gespeichert wird im `change`-Zweig beim Loslassen. Ein Re-Render mitten in der Geste erzeugt den
Regler neu und der Daumen springt aus der Hand — dieselbe Trennung wie beim Suchfeld im Setup.
Erklaerender Text als **Fusszeile der Gruppe**, nicht als Info-Knopf: das ist Apples eigenes Muster
fuer Einstellungen.
### Verifiziert
Backend **19/19** (5 alte, 14 neue in `tests/fahrterkennung/test_zuendungspause.py`), companion-app
`tsc` sauber und **179/179**, Panel als Modul geparst, `audi_ha_test` auf `2026.9.3.5` sauber
gestartet. Live im Panel gemessen: Regler 28 px hoch, Fuellung 25 % bei 15 von 60, Ziehen aktualisiert
Anzeige und Fuellung **ohne** dass das Element ersetzt wird (`isConnected` bleibt true), Loslassen
schreibt — `fahrten_pausenzeit_min = 25` stand danach in `fahrzeugprofil.json`.
**Ehrliche Luecke:** die Kette "Zuendung aus → Wartezeit → Fahrt geschlossen" ist durch die
Regressionstests belegt, nicht durch eine echte Fahrt im Container. Alle drei Koordinator-Eigenschaften,
die sie anfasst (`warte_ende_ab`, `ablage`, `zuordnung`), werden in demselben Modul bereits am echten
Objekt benutzt.
---
## BX. „Fahrt beenden" auf Knopfdruck, mit vorläufigem Ende (2026.9.3.6/.7)
### Wofür der Knopf da ist
Wunsch des Eigentümers: das Fahrzeug steht in einer Tiefgarage ohne Empfang, das Gerät kann sein
Zündungs-Aus nicht melden, und die Fahrt bliebe offen, bis irgendwann gepufferte Datensätze
eintreffen. Der Knopf schliesst sie sofort.
**Geschlossen wird auf das letzte Lebenszeichen, nicht auf „jetzt".** Das Fahrzeug stand still, als
es still stand, nicht als jemand gedrückt hat — und im Anwendungsfall liegen die beiden weit
auseinander. `_letztes_lebenszeichen()` gab es dafür schon (`nach_neustart_fortsetzen`).
**Genau deshalb ist dieses Ende vorläufig.** Der Eigentümer hat den wunden Punkt selbst benannt:
fährt das Fahrzeug nach dem Knopfdruck noch aus der Garage heraus, kommt der Rest gepuffert nach,
und dann gehört das Ende **nachgezogen** statt verworfen. Neues Feld `ende_vorlaeufig` an der Fahrt;
`_vorlaeufiges_ende_korrigieren()` zieht `ts_end` und `duration_s` nach und stösst das Screening neu
an.
Drei Bedingungen dafür, jede aus einem eigenen Grund: nur eine als vorläufig gekennzeichnete Fahrt
(eine regulär geschlossene ist bereits richtig), nur ein **späteres** Ende (früher wäre kein
Nachtrag, sondern eine Verkürzung), und nur innerhalb von `KORREKTUR_FENSTER_S = 6 h` — jenseits
davon gehört das Aus wahrscheinlich zu einer ganz anderen Fahrt.
**Die Falle, die dabei fast zugeschnappt wäre:** `ts_end` darf **nicht** in `edited_fields` landen.
Der Schutz für Handeingaben würde sonst genau die Korrektur blockieren, für die das Feld gebaut ist.
Ein Test hält das fest.
### „vorläufig" hängt am Wert, nicht an der Fahrt
Entscheidung des Eigentümers zwischen zwei gerenderten Mustern: die Strecke steht mit Tilde da
(`~ 14,2 km`), darunter `vorläufig` — und der **Verbrauch erscheint gar nicht**, bis er feststeht.
Die Alternative (beide Werte mit Tilde, „vorläufig" links an der Fahrt) ist verworfen.
Solange die Fahrt **läuft**, steht rechts **nichts** — keine Strecke, kein Verbrauch, auch kein
Strich. Ein Strich behauptet „hier fehlt ein Wert"; richtig ist „hier gibt es ihn noch nicht".
Dafür hat `Blattzeile` in der App ein optionales `aktion` bekommen und `wert` ist optional geworden.
### Woran die Oberflächen erkennen, dass eine Fahrt läuft
Neues Feld `faehrt_seit` im Fahrzeugstatus — der eigene Zwischenstand der Fahrterkennung
(`fahrt_start_ts`), nicht der Rohwert einer Entität. Die Zündung allein taugt nicht: beim
Trip-Signal steht sie nach dem Abstellen noch 900 s auf „an", und bei laufendem Motor im Stand
ebenfalls.
### Der Knopf
`.zeilenknopf` / `.dm-zeilenknopf`, Pillenform im Umriss — vom Eigentümer aus zwei gerenderten
Fassungen gewählt. 36 px sichtbar, 44 px Trefferhöhe über ein Pseudoelement, damit die Zeile nicht
höher wird als ihre Nachbarin. Der Dienst `fahrt_jetzt_beenden` läuft in der App **nicht** über die
Warteschlange: im Funkloch abgesetzt und Stunden später nachgeholt schlösse er eine Fahrt, die
längst regulär zu Ende ist — dieselbe Begründung wie bei `historieImportieren()`.
### Verifiziert
Backend **27/27** (5 Signalwechsel, 14 Zündungspause, 8 neu in
`tests/fahrterkennung/test_fahrtende_knopf.py`), 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, ohne Rückstand:** Meldezeit frisch gesetzt, Zündung an → Zeile zeigt
„Aktuelle Fahrt / läuft", Knopf 36 px in einer 66 px hohen Zeile, **keine Wertespalte und kein
Chevron**. Knopf gedrückt → `Fahrt seit … von Hand beendet, vorlaeufiges Ende …`, danach
`Zündung war nur 0 s an - keine Fahrt angelegt (Grenze 60 s)`. Fahrten vorher 16, nachher 16. Der
Trick für die Rückstandsfreiheit ist derselbe wie bei der Wartezeit: liegt der Beginn nahe am
letzten Lebenszeichen, verwirft `MINDESTDAUER_S` den Vorgang.
Beim anschliessenden Zurücksetzen der Zündung hat auch der zweite Zweig gefeuert und richtig
gemeldet: `Zuendungs-Aus … ohne laufende Fahrt, und es gehoert zu keiner geschlossenen — ein
Fahrtbeginn ist uns entgangen`. Korrekt, denn die Testfahrt war ja verworfen worden.
**Ehrliche Lücke:** die Korrektur eines vorläufigen Endes durch ein spät eintreffendes Zündungs-Aus
ist durch 8 Regressionstests belegt, nicht durch eine echte Tiefgaragenfahrt.
### Der Regler, im selben Zug
Schrittweite von 1 auf **5 Minuten** — Apples eigene Seite mahnt für weite Wertebereiche einen
Weg zur genauen Eingabe an, und bei 060 in Einerschritten sind das 3,8 px je Minute. 13 Rastpunkte
treffen sich bequem, und minutengenau ist hier ohnehin Scheingenauigkeit.
Der Daumen war im **Tagmodus** kaum zu sehen: weiss auf weisser Kachel, nur ein Schatten. Er hat
jetzt `border: 1px solid var(--line-strong)` — ein Token, zwei Themen: im Tagmodus zeichnet es ihn
ab, im Nachtmodus ist es auf Weiss unsichtbar, wo es nicht gebraucht wird.