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:
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user