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>
This commit is contained in:
2026-09-01 11:29:17 +02:00
parent 4cc91c461b
commit cef11b26f3
3 changed files with 147 additions and 6 deletions
+50 -1
View File
@@ -1,6 +1,7 @@
# AGENTS.md — Project state, review findings, open items, and working rules
**Last updated: 2026-09-01** (Fahrtbeginn und -ende kommen jetzt vom Gerät (`MELDEZEIT_SENSOR`),
**Last updated: 2026-09-01** (Fahrtende ohne den 900-s-Nachlauf des Geräts, Manifest `2026.9.1.4`,
siehe Abschnitt AZ. Davor am selben Tag: Fahrtbeginn und -ende kommen vom Gerät (`MELDEZEIT_SENSOR`),
Manifest `2026.9.1.3`, siehe Abschnitt AY: dazu die Trennung von
Trip- und Zündungsrolle und die Gerätezeit für Tankvorgänge. Davor am selben Tag: Phantomfahrten beim Neustart abgestellt, Tankstand auf `wert_bei()`
nachgezogen, zwölf Nullfahrten gelöscht, „Open items" von vier MQTT-Karteileichen befreit;
@@ -6518,6 +6519,54 @@ sonst testet man gegen ganze Kilometer.
`11807` (Odometer Value) steht auf 0. Für die geplante Rechnung ist das gleichgültig, weil sie mit
Zuwächsen arbeitet; wer den absoluten Wert lesbar haben will, trägt dort den echten Kilometerstand ein.
## AZ. Das Fahrtende ohne den Nachlauf des Geräts (2026.9.1.4)
Das Trip-Signal endet nicht mit der Zündung, sondern `Ignition OFF Timeout` später — beim
FMM003 900 s. Als Auslöser ist es deshalb ideal (es prellt nicht, es bringt die Pausentoleranz
mit), aber das Fahrtende lag systematisch **15 Minuten zu spät**.
`_echtes_ende()` zieht `NACHLAUF_S = 900` ab. Die Rechnung stammt vollständig aus der Logik des
Geräts und überlebt deshalb ein Funkloch: der Datensatz kommt später, trägt aber seine eigene
Zeit (Abschnitt AY).
**Die Klemme ist der wichtigere Teil.** War das Signal nicht länger als der Nachlauf, wurde nicht
gefahren — dann wird das Ende auf den Beginn geklemmt und `MINDESTDAUER_S` verwirft den Vorgang.
Ohne sie entstünde aus dreißig Sekunden Zündung eine Viertelstunde Fahrt, also genau die Sorte
Nulleintrag, die Abschnitt AW ausgeräumt hat.
**Warum keine Einstellung in der Oberfläche.** Die Zahl gehört zum Gerät, nicht zum Fahrzeug und
nicht zum Nutzer — dieselbe Linie wie `UNPLAUSIBLE_KMH` und `MIN_STRECKE_VERBRAUCH_KM`. flespi
kann die Gerätekonfiguration zwar liefern (`GET /gw/devices/{id}/settings/all`, 127 benannte
Einstellungen, darunter `trip_scenario.ign_off_timeout`), aber dafür bräuchte die Integration
einen zweiten Zugang samt Token; sie liest heute ausschließlich Telemetrie. Verworfen als
unverhältnismäßig.
### Die Gegenprobe meldet, statt zu korrigieren
`_nachlauf_gegenpruefen()` vergleicht das gerechnete Ende mit dem letzten beobachteten
Zündungswechsel auf „aus" und **warnt ab 120 s Abweichung, ohne etwas zu ändern**.
Der Grund für „nur warnen": wie genau „Zündung aus" bekannt ist, hängt am Operanden des
Zündungs-I/O-Elements. Er steht in der `.cfg` auf **3**; nach der Reihenfolge in der
Teltonika-Dokumentation (On Exit, On Entrance, On Both, Monitoring, On Hysteresis, On Change, On
Delta Change) ist das **Monitoring** — laut Wiki „No event at all. Values are recorded only when
other triggers worked". Der Wechsel erzeugte dann keinen eigenen Datensatz, sondern führe im
nächsten mit, und der beobachtete Zeitpunkt wäre zu spät statt exakt. Der Standard ist 5 (On
Change). **Offen und am Gerät zu prüfen** — explizit nummeriert steht die Operandenliste nicht in
der Dokumentation, und die Messung ist durch eigene Testschaltvorgänge verunreinigt. Steht der
Operand tatsächlich auf Monitoring, wäre `On Change` die bessere Einstellung: ein zusätzlicher
Datensatz je Zündungswechsel, keine zusätzliche Übertragung (Priorität bleibt Low).
Verifiziert in `audi_ha_test`:
| Fall | Ergebnis |
|---|---|
| Signal 1200 s (5 min Fahrt + Nachlauf) | Fahrt **300 s** gespeichert |
| Signal 930 s (30 s Zündung) | **keine** Fahrt, Bestand 12 → 12 |
| Gegenprobe | Warnung mit 887 s Abweichung ausgelöst und protokolliert |
Testfahrt anschließend über `fahrt_loeschen` entfernt; Bestand unverändert 11 Fahrten.
## Working conventions (observed — keep them)
- German is the project language: identifiers, comments, commits, UI texts. Exceptions:
@@ -50,6 +50,29 @@ _LOGGER = logging.getLogger(__name__)
# Einstellung fahrten_pausenzeit_min in beiden Oberflächen wieder anbieten.
# Der Nachlauf des Geraets zwischen Zuendung aus und Trip-Ende.
#
# Das Trip-Signal des FMM003 endet nicht mit der Zuendung, sondern erst nach
# seinem "Ignition OFF Timeout" (Trip \ Odometer). Es ist damit als Ausloeser
# ideal - es prellt nicht und bringt die Pausentoleranz mit -, liefert aber ein
# Fahrtende, das systematisch um genau diese Spanne zu spaet liegt.
#
# Der Wert ist die Konfiguration des Geraets, nicht unsere Wahl: er steht dort
# auf 900 s. Aendert er sich, gehoert diese Zahl mitgeaendert - sie laesst sich
# nicht aus den Daten ableiten (siehe _nachlauf_gegenpruefen unten, die genau
# das versucht und meldet, wenn es nicht mehr zusammenpasst).
#
# Bewusst KEINE Einstellung in der Oberflaeche: die Zahl gehoert zum Geraet,
# nicht zum Fahrzeug oder zum Nutzer - dieselbe Linie wie bei UNPLAUSIBLE_KMH
# und MIN_STRECKE_VERBRAUCH_KM in verlauf.py.
NACHLAUF_S = 900
# Ab welcher Abweichung zwischen gerechnetem und beobachtetem Ende gewarnt
# wird. Grosszuegig, weil die Zuendung nur so genau bekannt ist, wie das Geraet
# Datensaetze schreibt - beim Fahren alle 10 s, im Stand deutlich seltener.
NACHLAUF_TOLERANZ_S = 120
def leere_fahrt(start_ts: datetime.datetime, ende_ts: datetime.datetime, quelle: str) -> dict:
"""Das vollständige Fahrt-Schema mit allem, was noch nicht bekannt ist,
auf None. Bewusst alle Felder ausgeschrieben: die Oberfläche darf sich auf
@@ -83,6 +106,71 @@ def leere_fahrt(start_ts: datetime.datetime, ende_ts: datetime.datetime, quelle:
async def _echtes_ende(
k: Koordinator,
start_ts: datetime.datetime,
signal_ende: datetime.datetime,
) -> datetime.datetime:
"""Das Fahrtende ohne den Nachlauf des Geraets.
Das Trip-Signal endet NACHLAUF_S nach der Zuendung; abgezogen ergibt das den
Zeitpunkt, zu dem wirklich abgestellt wurde. Die Rechnung stammt vollstaendig
aus der Logik des Geraets und ueberlebt deshalb auch ein Funkloch: der
Datensatz kommt spaeter, traegt aber seine eigene Zeit (siehe geraetezeit()).
War die ganze Fahrt kuerzer als der Nachlauf, bleibt nichts uebrig - dann war
die Zuendung nur kurz an, ohne dass gefahren wurde. Das Ende wird auf den
Beginn geklemmt, und MINDESTDAUER_S in fahrt_beenden() verwirft den Vorgang
anschliessend. Ohne diese Klemme entstuende aus dreissig Sekunden Zuendung
eine Viertelstunde Fahrt."""
gerechnet = signal_ende - datetime.timedelta(seconds=NACHLAUF_S)
if gerechnet <= start_ts:
_LOGGER.info(
"Trip-Signal lief %s s, also nicht laenger als der Nachlauf (%s s) - "
"es wurde nicht gefahren",
int((signal_ende - start_ts).total_seconds()), NACHLAUF_S,
)
return start_ts
await _nachlauf_gegenpruefen(k, start_ts, signal_ende, gerechnet)
return gerechnet
async def _nachlauf_gegenpruefen(
k: Koordinator,
start_ts: datetime.datetime,
signal_ende: datetime.datetime,
gerechnet: datetime.datetime,
) -> None:
"""Vergleicht das gerechnete Ende mit dem beobachteten Zuendungswechsel und
meldet, wenn beide nicht zusammenpassen. Aendert nichts - die Rechnung
bleibt massgeblich.
Warum nur eine Meldung und keine Korrektur: wie genau der Zeitpunkt "Zuendung
aus" bekannt ist, haengt an der I/O-Einstellung des Geraets. Steht der
Operand des Zuendungselements auf "Monitoring", erzeugt ein Wechsel keinen
eigenen Datensatz, sondern faehrt nur im naechsten mit - dann ist der
beobachtete Zeitpunkt zu spaet, oft genau der des Trip-Endes. Auf "On Change"
waere er exakt. Solange das nicht feststeht, waere ein Umschalten auf den
beobachteten Wert eine Verschlechterung; eine Abweichung zu protokollieren
ist es nie."""
sensor = k.zuordnung.werte.ZUENDUNG_SENSOR
if not sensor or sensor == fahrtsignal(k.zuordnung.werte):
return
punkte = await verlauf_lesen(k.hass, sensor, start_ts, signal_ende)
aus = [ts for ts, wert in punkte if wert == "off"]
if not aus:
return
abweichung = abs((aus[-1] - gerechnet).total_seconds())
if abweichung > NACHLAUF_TOLERANZ_S:
_LOGGER.warning(
"Fahrtende gerechnet auf %s (Trip-Ende minus %s s), die Zuendung ging "
"laut Aufzeichnung aber um %s aus - %.0f s Abweichung. Entweder stimmt "
"NACHLAUF_S nicht mehr mit dem Geraet ueberein, oder das "
"Zuendungselement schreibt keine eigenen Datensaetze.",
gerechnet.isoformat(), NACHLAUF_S, aus[-1].isoformat(), abweichung,
)
async def _geraetezeit(
k: Koordinator,
ereigniszeit: datetime.datetime | None,
@@ -163,16 +251,20 @@ async def zuendung_geaendert(
# zu warten haette daraus eine halbe Stunde gemacht und zwei
# aufeinanderfolgende Fahrten verschmolzen, die das Geraet sauber
# getrennt hatte.
ende = await _geraetezeit(k, ereigniszeit, jetzt)
signal_ende = await _geraetezeit(k, ereigniszeit, jetzt)
# Ein Ende vor dem Beginn kann es nicht geben. Passiert, wenn die
# Meldezeit aus einem älteren Datensatz stammt als der Fahrtbeginn -
# dann ist die Ankunftszeit die ehrlichere Angabe.
if ende < k.fahrt_start_ts:
if signal_ende < k.fahrt_start_ts:
_LOGGER.warning(
"Meldezeit %s liegt vor dem Fahrtbeginn %s - nehme die Ankunft",
ende.isoformat(), k.fahrt_start_ts.isoformat(),
signal_ende.isoformat(), k.fahrt_start_ts.isoformat(),
)
ende = jetzt
signal_ende = jetzt
# Erst jetzt den Nachlauf des Geraets abziehen: signal_ende ist der
# Zeitpunkt, zu dem das TRIP-SIGNAL endete, nicht der, zu dem das
# Fahrzeug abgestellt wurde.
ende = await _echtes_ende(k, k.fahrt_start_ts, signal_ende)
await fahrt_beenden(k, k.fahrt_start_ts, ende)
await k.fahrzeugstatus_veroeffentlichen()
@@ -1,7 +1,7 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.9.1.3",
"version": "2026.9.1.4",
"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"],