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>
This commit is contained in:
2026-09-01 10:04:55 +02:00
parent 6f23dfebbf
commit ff5a334e95
4 changed files with 108 additions and 51 deletions
+84 -49
View File
@@ -1,6 +1,9 @@
# AGENTS.md — Project state, review findings, open items, and working rules
**Last updated: 2026-08-31** (Streckenberechnung: `wert_bei()` statt `naechster_wert()` und
**Last updated: 2026-09-01** (Phantomfahrten beim Neustart abgestellt, Tankstand auf `wert_bei()`
nachgezogen, zwölf Nullfahrten gelöscht, „Open items" von vier MQTT-Karteileichen befreit;
Manifest `2026.9.1.1`, siehe Abschnitt AX. Davor am 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
@@ -439,15 +442,26 @@ wraps the web app for iPhone; a PWA home-screen install is the accepted intermed
- [x] Offline UX per design brief (offline marker, visible pending queue)
- [ ] **QR-Code-Scanner fürs Token-Onboarding (angefragt 2026-08-28, noch nicht geplant/gebaut)**
2026-08-11 bewusst weggelassen ("nur wenn es einfach bleibt"), der Owner will das jetzt doch als
Alternative zum manuellen Einfügen. Ursprünglicher Plan aus `UMSETZUNGSPLAN.md` Phase 10 Stufe 2,
Schritt 6: Barcode-Plugin (`@capacitor-mlkit/barcode-scanning`) + Kamera-Berechtigung in der App,
Gegenstück eine kleine statische Seite unter HA `/local/dm360-qr.html`, die den Token **clientseitig**
(Offline-JS-QR-Bibliothek, keine Netzabfrage) als QR anzeigt — Inhalt `{"url": "...", "token": "..."}`.
Noch nicht scoped/umgesetzt.
Alternative zum manuellen Einfügen.
### B) Infrastructure / commissioning (partly waits for FMM003 hardware)
**Umfang halbiert am 2026-09-01:** Home Assistant zeigt den Token beim Anlegen bereits selbst
als QR-Code an. Die geplante eigene Seite unter `/local/dm360-qr.html` entfällt damit ersatzlos —
gebraucht wird nur noch die **Scanner-Seite in der App** (Barcode-Plugin
`@capacitor-mlkit/barcode-scanning` + Kamera-Berechtigung). Offen bleibt eine Frage, die vor dem
Bauen zu klären ist: HAs eigener QR trägt nur den Token, nicht die Instanz-URL — die muss also
weiterhin von Hand kommen oder aus dem Netz gefunden werden.
- [ ] Switch `datametric360.de` nameservers at all-inkl to Cloudflare ("full setup") —
### B) Infrastructure / commissioning
> **Aufgeräumt am 2026-09-01.** Vier Einträge sind hier ersatzlos entfallen, weil der
> MQTT-/Mosquitto-Weg seit dem Wechsel auf flespi (2026-08-13) nicht mehr existiert: FMM003
> verkabeln, Broker-Erreichbarkeit, MQTT Client Type, erste Codec-JSON aufzeichnen. Das Gerät
> läuft seit dem 13.08. produktiv über flespi; die Feldzuordnung steht und ist gemessen (siehe
> Abschnitt AU).
- [x] Switch `datametric360.de` nameservers at all-inkl to Cloudflare ("full setup") — **erledigt,
vom Eigentümer bestätigt am 2026-09-01: Domain liegt bei Cloudflare, Nameserver stehen, die
iOS-App ist darüber angebunden.**
prerequisite for the tunnel; domain carries nothing else, so this is consequence-free. Step-by-
step runbook for this and everything below it now exists:
[`homeassistant/INTERNET_ZUGRIFF_EINRICHTEN.md`](homeassistant/INTERNET_ZUGRIFF_EINRICHTEN.md)
@@ -463,43 +477,11 @@ wraps the web app for iPhone; a PWA home-screen install is the accepted intermed
2026-08-28: corrected the stale `/local/dm360/*` assumption (never built) to the app's real
remote need, its OTA bundle at `/audi_dashboard_static/app/*` (`@capgo/capacitor-updater`). See
`homeassistant/REVERSE_PROXY.md`.
- [ ] Install/wire the FMM003; record firmware version (Codec JSON is firmware-dependent)
- [x] Generate TLS certificates for Mosquitto + device (small private CA) — done 2026-08-11, 10-year
validity; Mosquitto configured (`certfile`/`keyfile`/`cafile`/`require_certificate: true`).
Remaining: upload root/client cert/key to the FMM003 Security tab — filenames must end in
`.pem`/`.pem.crt`/`.pem.key` (Configurator rejects plain `.crt`/`.key`, content-agnostic check)
- [ ] Decide broker reachability for the vehicle — **reopened 2026-08-11 evening**: port-forward
8883 on the Speedport Smart 4 Plus looked correctly configured (rule present, right internal
IP, right port) and internal reachability was confirmed (`homeassistant.local:8883` open from
the LAN, Mosquitto TLS listener genuinely up, TLS cert chain end-to-end verified byte-for-byte
against the FMM003's uploaded client cert), but the port stayed **closed from outside**
(confirmed via external port checker, both before and after a router reboot that changed the
dynamic WAN IP — DNS/DuckDNS matched correctly each time, so not a DNS or CGNAT issue).
Community reports (ComputerBase, Telekom Hilft) describe this as a known, Telekom-acknowledged
firmware bug on this router model; a full disable-the-firewall workaround doesn't exist on this
model either. Port-forward rule has been removed again. **New direction: route the FMM003
through flespi instead** (native Teltonika/Codec8 channel, IMEI-based auth, no certs, no
inbound port needed at all — flespi has a stable public endpoint; HA pulls data back out via
flespi's REST API or MQTT, outbound-only). Free flespi tier (10 devices/2 channels) is enough
for one vehicle. Next step: user creates the flespi account + Teltonika channel; a `flespi`
custom integration is already present in this HA instance (unconfigured) — check what it
needs once flespi-side setup exists.
Unrelated but still valid from the same session: DuckDNS hostname
`datametric360.duckdns.org` reliably updating; Let's Encrypt for HA's own local UI works (root
cause of the earlier DNS-01 failures was a stray `aliases` entry in the DuckDNS add-on config,
not DNS/network — see COMPANION_APP_ARCHITECTURE.md §5 item 2a). HA's SSL config now lives in
Settings → System → Network (UI), not `configuration.yaml`'s old `http:` block, which was
removed after HA started migrating/ignoring it. The Mosquitto TLS setup itself (cert chain,
`require_certificate: true`, `certfile`/`keyfile`/`cafile`) is verified correct and can be
reused as-is if a self-hosted broker is ever revisited.
- [ ] MQTT Client Type on the FMM003 ("Custom server" not selectable in practice, "AWS IoT Custom"
pointed at a self-hosted broker was confirmed working by two independent community reports) —
**moot for now** given the flespi pivot above: flespi uses the device's native Codec8/TCP
channel, not MQTT at all, so Server Settings should switch to Protocol: TCP against the flespi
channel host/port instead, and Codec set to "Codec 8 Extended". Revisit this item only if a
self-hosted broker is picked back up later.
- [ ] Capture the first real Codec JSON message (`mosquitto_sub`/MQTT Explorer) and build the
field mapping from it — **do not guess beforehand** (explicit decision)
**Gegenstandslos seit dem Wechsel auf flespi (2026-08-13):** das Gerät spricht dort seinen
nativen Codec-8-Kanal über TCP, ohne Zertifikate. Der Mosquitto-Aufbau bleibt nur als
Bauanleitung stehen, falls je ein eigener Broker zurückkommt.
- [x] Move trip detection to FMM003 ignition — done 2026-08-12. `fahrterkennung.py` rewritten:
trigger is now `einstellungen.ZUENDUNG_SENSOR`
(`binary_sensor.testzone_fmm003_engine_ignition_or_acc_status`, on = trip running) instead
@@ -1730,9 +1712,12 @@ wraps the web app for iPhone; a PWA home-screen install is the accepted intermed
2026-08-11 (`UMSETZUNGSPLAN.md` Phase 2): swipe-delete keyboard fallback, popup keyboard
access, and self-hosting Leaflet stay unfixed in this panel and are addressed only in
DataMetric360.
- [ ] Optional: persist the RAM-only states (trip start, fuel low-water-mark) — deliberately
deferred; may become moot with the FMM003 switch
- [ ] Upload vehicle photos to `www/bilder/`, set `steuer.faellig` (operational data, not code)
- [x] Persist the RAM-only states (trip start, fuel low-water-mark) — erledigt mit dem Umbau zur
echten Integration: beide liegen im Laufzeit-Store des Koordinators
(`…_laufzeit`: `fahrt_start_ts`, `tiefststand_pct`, `tiefststand_liter`) und überleben einen
Neustart. Am 2026-09-01 am laufenden Container nachgesehen.
- [x] Fahrzeugfoto hochladen — erledigt, über die App (`seitenansicht.webp`, 2026-08-31)
- [ ] `steuer.faellig` setzen (Betriebsdaten, kein Code) — steht noch auf `None`
### D) Once the panel is superseded
@@ -6277,12 +6262,14 @@ aus deutschen Kommentaren sehen wie Klassen aus, und `ads-dot--${status}` wird z
Laufzeit zusammengesetzt und taucht in keiner Suche auf — sechs Design-System-Klassen
standen deshalb zunächst falsch auf der Liste und wurden vor dem Melden aussortiert.
### Offen: Befund 07, Erstzulassung
### Geschlossen: Befund 07, Erstzulassung
`fahrzeug.erstzulassung` ist leer, deshalb steht bei der Hauptuntersuchung in beiden
Oberflächen „kein Eintrag im Wartungsplan" — ohne dieses Datum gibt es keinen Anker,
weder aus dem Wartungsplan noch aus einer Fahrzeugmeldung. Kein Codefehler; einzutragen
unter Einstellungen → Fahrzeug einrichten. Bewusst nicht erfunden.
weder aus dem Wartungsplan noch aus einer Fahrzeugmeldung. Kein Codefehler.
**Am 2026-09-01 vom Eigentümer geschlossen:** das ist eine Eingabe des App-Nutzers unter
Einstellungen → Fahrzeug einrichten, kein offener Punkt des Projekts. Nicht wieder aufmachen.
### Was am Panel und an der App in Ordnung war
@@ -6358,6 +6345,54 @@ in der Praxis beantwortet sie der Code bereits, nur mit der falschen Antwort.
(`screening.py:246`), während der Import dort `wert_bei()` nimmt. Dieselbe Doppelung
wie oben, eine Ebene tiefer. Bewusst außerhalb des Auftrags gelassen.
## AX. Phantomfahrten beim Neustart, und der Tankstand zieht nach (2026.9.1.1)
### Jeder Neustart legte eine Fahrt an
`zuendung_geaendert()` prüfte den neuen Zustand, aber nicht den alten. Beim Hochfahren von
Home Assistant taucht die Zündungs-Entität neu auf; das kommt als Zustandswechsel mit
`old_state = None` an. Steht das Trip-Signal ohnehin auf „an" — beim FMM003 der Normalfall,
solange die Zündung nicht sauber aus war —, las der Beobachter das als „Zündung ging gerade an"
und legte eine Fahrt an, deren **Beginn schlicht der Zeitpunkt des Neustarts** war.
Daher der Eintrag `30.08. 19:43 → 31.08. 08:37, 12,9 h, 0 km`, und daher die Fahrt, die am
31.08. um 21:18:45 UTC beim Ausliefern von `2026.8.31.6` entstand.
Die Prüfung ist `if alt is None: return` — dieselbe, die `_kilometerstand_geaendert()` im
Koordinator seit jeher hat. Eine wirklich laufende Fahrt geht dabei 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 im Protokoll und `fahrt_start_ts` bleibt `null`. Vorher
entstand unter genau diesen Bedingungen jedes Mal eine.
**Was das nicht löst, und der Eigentümer ausdrücklich will:** einen Beginn und ein Ende, die
*aus den Daten des Dongles* stammen. Heute ist der Beginn `datetime.now()` im Moment, in dem
Home Assistant den Wechsel verarbeitet — gemessen 0,5 bis 139 s nach dem Zeitstempel des Geräts,
nach einer Funklücke Minuten. Und ein Wechsel, den es nie gab, wird nie erkannt: die echte Fahrt
vom 31.08., 22:0322:13 Ortszeit, hat die Live-Erkennung nicht gesehen, weil `trip_status`
schon seit 18:39 auf „an" stand. Eigenes Arbeitspaket, hängt an der Konfiguration des Geräts.
### Der Tankstand liest jetzt wie der Kilometerstand
`_verbrauch_screenen()` holte die Literstände weiter mit `naechster_wert()`, während
`historienimport.py` `wert_bei()` nimmt — dieselbe Doppelbewertung wie in Abschnitt AW, eine
Ebene tiefer. Der zeitlich nächstgelegene Stand zum Fahrtbeginn kann schon in der Fahrt liegen
und hätte dann einen Teil des Verbrauchs vorweggenommen.
`naechster_wert()` bleibt an einer Stelle richtig und steht dort weiter: Breiten- und
Längengrad in `_position_screenen()` sind keine Zähler.
### Nullfahrten entfernt
Auf Anweisung des Eigentümers („0 km Fahrten sind keine Fahrten und damit zu entfernen") wurden
am 2026-09-01 die zwölf Fahrten mit `distance_km == 0` gelöscht — über den eigenen Dienst
`fahrt_loeschen`, nicht durch Hantieren an der Datei, und nach einem `backup_jetzt`
(`backups/20260901_072541`). Bestand danach: 11 Fahrten, keine mit 0 km.
Das ist eine Abkehr von der Linie in Abschnitt W und AV, wo Nullfahrten bewusst stehen blieben.
Sie gilt, weil der Eigentümer sie ausdrücklich getroffen hat — nicht als neue Gewohnheit.
## Working conventions (observed — keep them)
- German is the project language: identifiers, comments, commits, UI texts. Exceptions:
@@ -91,6 +91,23 @@ async def zuendung_geaendert(k: Koordinator, neu: str | None, alt: str | None) -
if neu not in ("on", "off"):
return
# Kein Vorzustand heißt: die Entität taucht gerade erst auf - beim
# Hochfahren von Home Assistant oder nach einem Neuladen der Integration.
# Das ist kein Wechsel der Zündung, sondern eine Registrierung.
#
# Ohne diese Prüfung las der Beobachter das als "Zündung ging gerade an"
# und legte eine Fahrt an, deren Beginn schlicht der Zeitpunkt des
# Neustarts war. Steht das Trip-Signal ohnehin auf "an", entstand so bei
# JEDEM Neustart eine Phantomfahrt - daher der Eintrag vom 30.08.2026,
# 19:43 bis 08:37 des Folgetags, 12,9 Stunden, 0 km.
#
# Eine wirklich laufende Fahrt geht dabei nicht verloren: deren Beginn
# liegt im Store und wird von nach_neustart_fortsetzen() aufgenommen.
# _kilometerstand_geaendert() im Koordinator prüft aus demselben Grund
# schon immer auf einen Vorzustand.
if alt is None:
return
# Eine noch wartende Ende-Bestätigung aus einer vorherigen Änderung
# abbrechen - das ist der Mechanismus hinter der Pausenregel.
k.warte_ende_ab_abbrechen()
@@ -1,7 +1,7 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.8.31.6",
"version": "2026.9.1.1",
"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"],
@@ -281,8 +281,13 @@ async def _verbrauch_screenen(
if not verlauf:
return
# wert_bei() aus demselben Grund wie beim Kilometerstand (siehe Modulkopf):
# der Tankstand zu Fahrtbeginn ist der zuletzt DAVOR gemeldete, nicht der
# zeitlich nächstgelegene - der läge womöglich schon in der Fahrt und hätte
# einen Teil des Verbrauchs vorweggenommen. historienimport.py liest ihn
# seit jeher so.
verbrauch = verbrauch_aus_literstaenden(
naechster_wert(start, verlauf), naechster_wert(ende, verlauf), fahrt.get("distance_km")
wert_bei(verlauf, start), wert_bei(verlauf, ende), fahrt.get("distance_km")
)
if verbrauch is not None:
await k.ablage.fahrt_aktualisieren(fahrt["trip_id"], {"verbrauch_l_100km": verbrauch})