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,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"],