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:
@@ -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