Das Ende einer verschlafenen Fahrt richtig bestimmen
Drei Fehler, die zusammen eine 5-Minuten-Fahrt als 15 Stunden mit 0,1 km/h im Bestand stehen liessen: - nach_neustart_fortsetzen() schloss eine laufende Fahrt bei ALLEM ausser "on" - auch bei Funkstille. Jetzt nur noch bei ausdruecklichem "off". - _letztes_lebenszeichen() las nur die Zuendungsentitaet, die nur bei einem Wechsel schreibt. Zusaetzliche Quelle ist die Meldezeit-Entitaet, deren Werte die Geraetezeit jedes Datensatzes sind. - Strecke und Dauer decken bei einer Luecke verschiedene Zeitraeume ab: der Durchschnitt entfaellt, und die Plausibilitaetspruefung rechnet ohne die verschlafenen Kilometer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12407,3 +12407,55 @@ Das gepufferte Zündung-Aus kam am 06.09. um 14:28 mit Gerätezeit 23:21:29 an,
|
|||||||
Schnitt. Die Umstempelung auf Gerätezeit hat hier nicht gegriffen
|
Schnitt. Die Umstempelung auf Gerätezeit hat hier nicht gegriffen
|
||||||
(`ZUORDNUNG_MAX_S = 1.0`). Das ist dieselbe Familie wie der Befund zu
|
(`ZUORDNUNG_MAX_S = 1.0`). Das ist dieselbe Familie wie der Befund zu
|
||||||
`_letztes_lebenszeichen()` und noch nicht behoben.
|
`_letztes_lebenszeichen()` und noch nicht behoben.
|
||||||
|
|
||||||
|
## DF. Das Ende einer Fahrt, die das Gerät verschlafen hat (2026.9.6.3)
|
||||||
|
|
||||||
|
Abschnitt DE hat die Kilometer gerettet, aber im selben Durchgang stand die
|
||||||
|
Rückfahrt vom 05.09. mit **15 h 14 min Dauer und 0,1 km/h Schnitt** im
|
||||||
|
Bestand. Drei Fehler, alle behoben.
|
||||||
|
|
||||||
|
### 1. „Keine Meldung" ist kein „Zündung aus" — auch beim Neustart-Nachlauf
|
||||||
|
|
||||||
|
`nach_neustart_fortsetzen()` prüfte `== "on"` und schloss bei **allem
|
||||||
|
anderen** die laufende Fahrt — also auch bei `None`, `unavailable`,
|
||||||
|
`unknown`. Genau die Regel, die `zuendung_geaendert()` seit jeher befolgt
|
||||||
|
(„Funkstille ist keine Aussage"), fehlte hier. Steht die flespi-Anbindung beim
|
||||||
|
Hochfahren noch nicht bereit, las die Funktion „nicht on" und beendete eine
|
||||||
|
laufende Fahrt mit einem Ende aus `_letztes_lebenszeichen()` — im schlimmsten
|
||||||
|
Fall „jetzt". Jetzt wird nur bei einem ausdrücklichen `"off"` geschlossen.
|
||||||
|
|
||||||
|
### 2. Das letzte Lebenszeichen kam aus der falschen Entität
|
||||||
|
|
||||||
|
`_letztes_lebenszeichen()` las nur den Verlauf der **Zündungsentität**. Die
|
||||||
|
schreibt aber nur bei einem WECHSEL eine Zeile: bleibt die Zündung stundenlang
|
||||||
|
an, ist ihr letzter Verlaufspunkt der Fahrtbeginn — und ein Ende daraus wäre
|
||||||
|
eine Fahrtdauer von null. Am 06.09.2026 gemessen: die Fahrt vom Vorabend hatte
|
||||||
|
seit 23:13:08 **genau drei Zeilen, alle aus derselben Sekunde**, obwohl das
|
||||||
|
Gerät danach noch zwölf Minuten lang sendete.
|
||||||
|
|
||||||
|
Zusätzliche Quelle ist jetzt die **Meldezeit-Entität** (`_meldezeiten()`): sie
|
||||||
|
bekommt zu JEDEM Datensatz einen neuen Wert, und dieser Wert *ist* die
|
||||||
|
Gerätezeit. Genommen wird das späteste Lebenszeichen aus beiden Quellen.
|
||||||
|
|
||||||
|
### 3. Strecke und Dauer decken verschiedene Zeiträume ab
|
||||||
|
|
||||||
|
Mit einer Lücke enthält `distance_km` die verschlafenen Kilometer, `duration_s`
|
||||||
|
aber nicht — ihr Quotient ist bedeutungslos. Zwei Folgen:
|
||||||
|
|
||||||
|
* **Der Durchschnitt entfällt.** Die Rückfahrt käme auf 92 km/h für eine
|
||||||
|
33-km/h-Fahrt. „Liegt nicht vor" ist die einzige ehrliche Angabe.
|
||||||
|
* **Die Plausibilitätsprüfung in `_vollstaendig()` rechnet ohne die Lücke.**
|
||||||
|
Sonst verwirft eine Phantomgeschwindigkeit die Kilometerstände — am
|
||||||
|
06.09.2026 genau so passiert.
|
||||||
|
|
||||||
|
### Stand
|
||||||
|
|
||||||
|
Live in `audi_ha_test` am gerenderten Panel nachgemessen: *Startkilometer
|
||||||
|
21.347 km · Endkilometer 21.355 km · Nicht aufgezeichnet 6 km am Anfang ·
|
||||||
|
Dauer 5 Min. · Ø Geschwindigkeit liegt nicht vor.* Vorher: 15 h 14 min und
|
||||||
|
0,1 km/h. 0 Tracebacks, 303 App-Tests und alle fünf Backend-Testdateien grün.
|
||||||
|
|
||||||
|
**Was offen bleibt:** der Fahrtbeginn steht weiter dort, wo das Gerät aufwachte
|
||||||
|
— neun Minuten und sechs Kilometer zu spät. Ihn aus der Motorlaufzeit
|
||||||
|
zurückzurechnen hat der Eigentümer abgelehnt, solange es eine gerechnete statt
|
||||||
|
gemeldete Zahl wäre.
|
||||||
|
|||||||
@@ -776,8 +776,21 @@ async def nach_neustart_fortsetzen(k: Koordinator) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
sensor = k.zuordnung.werte.ZUENDUNG_SENSOR
|
sensor = k.zuordnung.werte.ZUENDUNG_SENSOR
|
||||||
if zustand_oder_none(k.hass, sensor) == "on":
|
zustand = zustand_oder_none(k.hass, sensor)
|
||||||
return # fährt noch - der Beobachter übernimmt wie sonst auch
|
if zustand != "off":
|
||||||
|
# "on" heisst: faehrt noch, der Beobachter uebernimmt wie sonst auch.
|
||||||
|
#
|
||||||
|
# Alles ANDERE - None, "unavailable", "unknown" - heisst Funkstille,
|
||||||
|
# nicht "aus". Genau diese Regel steht in zuendung_geaendert() seit
|
||||||
|
# jeher, hier fehlte sie: stand die Entitaet beim Hochfahren noch nicht
|
||||||
|
# bereit (die flespi-Anbindung baut ihre MQTT-Verbindung erst auf),
|
||||||
|
# las diese Funktion "nicht on" und schloss eine laufende Fahrt - mit
|
||||||
|
# einem Ende aus _letztes_lebenszeichen(), im schlimmsten Fall "jetzt".
|
||||||
|
#
|
||||||
|
# Eine wirklich beendete Fahrt geht dadurch nicht verloren: kommt das
|
||||||
|
# gepufferte "aus" spaeter an, schliesst der Beobachter sie, und
|
||||||
|
# _spaetes_aus() faengt sogar den Fall ab, dass sie da schon zu ist.
|
||||||
|
return
|
||||||
|
|
||||||
# Dieselbe Sperre wie im Beobachter: der erste Wechsel des Fahrtsignals
|
# Dieselbe Sperre wie im Beobachter: der erste Wechsel des Fahrtsignals
|
||||||
# kann eintreffen, während dieser Nachlauf noch schreibt.
|
# kann eintreffen, während dieser Nachlauf noch schreibt.
|
||||||
@@ -822,11 +835,30 @@ async def _letztes_lebenszeichen(
|
|||||||
Kilometerstände ohnehin nach.
|
Kilometerstände ohnehin nach.
|
||||||
"""
|
"""
|
||||||
jetzt = datetime.datetime.now(datetime.UTC)
|
jetzt = datetime.datetime.now(datetime.UTC)
|
||||||
|
kandidaten: list[datetime.datetime] = []
|
||||||
|
|
||||||
punkte = await verlauf_lesen(
|
punkte = await verlauf_lesen(
|
||||||
k.hass, sensor, seit, jetzt, k.zuordnung.werte.MELDEZEIT_SENSOR
|
k.hass, sensor, seit, jetzt, k.zuordnung.werte.MELDEZEIT_SENSOR
|
||||||
)
|
)
|
||||||
if punkte:
|
if punkte:
|
||||||
return punkte[-1][0]
|
kandidaten.append(punkte[-1][0])
|
||||||
|
|
||||||
|
# Die Zuendungsentitaet schreibt nur bei einem WECHSEL eine Zeile. Bleibt
|
||||||
|
# die Zuendung stundenlang an, ist ihr letzter Verlaufspunkt der Beginn der
|
||||||
|
# Fahrt - und ein Ende daraus waere die Fahrtdauer null.
|
||||||
|
#
|
||||||
|
# Am 06.09.2026 gemessen: die Fahrt vom Vorabend hatte seit 23:13:08 genau
|
||||||
|
# drei Zeilen, alle aus derselben Sekunde, obwohl das Geraet danach noch
|
||||||
|
# zwoelf Minuten lang Daten schickte.
|
||||||
|
#
|
||||||
|
# Die Meldezeit-Entitaet dagegen bekommt zu JEDEM Datensatz einen neuen
|
||||||
|
# Wert - und dieser Wert IST die Geraetezeit. Sie ist damit die ehrlichste
|
||||||
|
# Antwort auf "bis wann haben wir das Fahrzeug beobachtet".
|
||||||
|
for zeitpunkt in await _meldezeiten(k, seit, jetzt):
|
||||||
|
kandidaten.append(zeitpunkt)
|
||||||
|
|
||||||
|
if kandidaten:
|
||||||
|
return max(kandidaten)
|
||||||
|
|
||||||
zustand = k.hass.states.get(sensor) if sensor else None
|
zustand = k.hass.states.get(sensor) if sensor else None
|
||||||
if zustand is not None:
|
if zustand is not None:
|
||||||
@@ -848,6 +880,30 @@ async def _letztes_lebenszeichen(
|
|||||||
return jetzt
|
return jetzt
|
||||||
|
|
||||||
|
|
||||||
|
async def _meldezeiten(
|
||||||
|
k: Koordinator, seit: datetime.datetime, bis: datetime.datetime
|
||||||
|
) -> list[datetime.datetime]:
|
||||||
|
"""Die Geraetezeiten aller Datensaetze im Fenster, aufsteigend.
|
||||||
|
|
||||||
|
Die Werte der Meldezeit-Entitaet SIND Unix-Sekunden in UTC (siehe
|
||||||
|
verlauf.geraetezeit()) - hier wird deshalb nicht umgestempelt, sondern der
|
||||||
|
Wert selbst gelesen. Werte ausserhalb des Fensters fallen heraus: ein
|
||||||
|
gepufferter Datensatz kann eine Geraetezeit vor dem Fahrtbeginn tragen."""
|
||||||
|
sensor = k.zuordnung.werte.MELDEZEIT_SENSOR
|
||||||
|
if not sensor:
|
||||||
|
return []
|
||||||
|
zeiten: list[datetime.datetime] = []
|
||||||
|
for _ankunft, roh in await verlauf_lesen(k.hass, sensor, seit, bis):
|
||||||
|
try:
|
||||||
|
wert = datetime.datetime.fromtimestamp(float(roh), datetime.UTC)
|
||||||
|
except (TypeError, ValueError, OSError, OverflowError):
|
||||||
|
continue
|
||||||
|
if seit <= wert <= bis:
|
||||||
|
zeiten.append(wert)
|
||||||
|
zeiten.sort()
|
||||||
|
return zeiten
|
||||||
|
|
||||||
|
|
||||||
async def fahrt_beenden(
|
async def fahrt_beenden(
|
||||||
k: Koordinator,
|
k: Koordinator,
|
||||||
start_ts: datetime.datetime,
|
start_ts: datetime.datetime,
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"version":"2026.9.6.2","sha256":"9c9f09af98149f065e1f874c452d71f0ebb5232334a4740a8f2fe272e161a10b","bytes":378683,"gebaut":"2026-09-06T14:47:37Z"}
|
{"version":"2026.9.6.3","sha256":"03801eda3b7914d3dd9a51262edd1462df47747919742828f446f79323384ddf","bytes":378674,"gebaut":"2026-09-06T15:06:31Z"}
|
||||||
Binary file not shown.
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "audi_dashboard",
|
"domain": "audi_dashboard",
|
||||||
"name": "Audi Dashboard",
|
"name": "Audi Dashboard",
|
||||||
"version": "2026.9.6.2",
|
"version": "2026.9.6.3",
|
||||||
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
||||||
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
||||||
"codeowners": [
|
"codeowners": [
|
||||||
|
|||||||
@@ -115,13 +115,20 @@ def _vollstaendig(aenderungen: dict, fahrt: dict) -> dict:
|
|||||||
aenderungen["odo_start"] = None
|
aenderungen["odo_start"] = None
|
||||||
aenderungen["odo_end"] = None
|
aenderungen["odo_end"] = None
|
||||||
return aenderungen
|
return aenderungen
|
||||||
geschwindigkeit = durchschnitt_kmh(distanz, fahrt.get("duration_s"))
|
# Nur der aufgezeichnete Teil darf gegen die Dauer geprueft werden.
|
||||||
|
# Die verschlafenen Kilometer sind zwar gefahren worden, aber in einer
|
||||||
|
# Zeit, die NICHT in duration_s steckt - der Beginn liegt ja erst dort,
|
||||||
|
# wo das Geraet aufgewacht ist. Beides zusammen ergaebe eine
|
||||||
|
# Phantomgeschwindigkeit; am 06.09.2026 hat genau das eine Fahrt aus
|
||||||
|
# der Testphase verworfen (siehe _aufzeichnungsluecken_schliessen).
|
||||||
|
gemessen = round(distanz - (fahrt.get("luecke_km") or 0), 1)
|
||||||
|
geschwindigkeit = durchschnitt_kmh(gemessen, fahrt.get("duration_s"))
|
||||||
if geschwindigkeit is not None and geschwindigkeit > UNPLAUSIBLE_KMH:
|
if geschwindigkeit is not None and geschwindigkeit > UNPLAUSIBLE_KMH:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Fahrt %s: Kilometerstand verworfen - unmögliche %s km/h im "
|
"Fahrt %s: Kilometerstand verworfen - unmögliche %s km/h im "
|
||||||
"Schnitt (%s km in %s s), vermutlich ein Ausreißer im "
|
"Schnitt (%s km in %s s), vermutlich ein Ausreißer im "
|
||||||
"Verlauf oder eine falsch abgegrenzte Fahrt.",
|
"Verlauf oder eine falsch abgegrenzte Fahrt.",
|
||||||
fahrt.get("trip_id"), geschwindigkeit, distanz, fahrt.get("duration_s"),
|
fahrt.get("trip_id"), geschwindigkeit, gemessen, fahrt.get("duration_s"),
|
||||||
)
|
)
|
||||||
aenderungen["odo_start"] = None
|
aenderungen["odo_start"] = None
|
||||||
aenderungen["odo_end"] = None
|
aenderungen["odo_end"] = None
|
||||||
@@ -180,6 +187,18 @@ async def durchfuehren(k: Koordinator) -> None:
|
|||||||
# die beide schon dastehen. Deshalb vor dem Geschwindigkeitssensor und
|
# die beide schon dastehen. Deshalb vor dem Geschwindigkeitssensor und
|
||||||
# unabhaengig davon, ob einer zugeordnet ist.
|
# unabhaengig davon, ob einer zugeordnet ist.
|
||||||
for fahrt in fahrten:
|
for fahrt in fahrten:
|
||||||
|
if fahrt.get("luecke_km"):
|
||||||
|
# Strecke und Dauer decken hier verschiedene Zeitraeume ab: die
|
||||||
|
# Strecke enthaelt die verschlafenen Kilometer, die Dauer nicht.
|
||||||
|
# Ein Quotient daraus waere eine Zahl ohne Bedeutung - die Fahrt
|
||||||
|
# vom 05.09.2026 kaeme auf 92 km/h fuer eine 33-km/h-Fahrt.
|
||||||
|
# "Liegt nicht vor" ist die einzige ehrliche Angabe.
|
||||||
|
if fahrt.get("avg_speed_kmh") is not None:
|
||||||
|
await k.ablage.fahrt_aktualisieren(
|
||||||
|
fahrt["trip_id"], {"avg_speed_kmh": None}
|
||||||
|
)
|
||||||
|
fahrt["avg_speed_kmh"] = None
|
||||||
|
continue
|
||||||
if fahrt.get("avg_speed_kmh") is not None:
|
if fahrt.get("avg_speed_kmh") is not None:
|
||||||
continue
|
continue
|
||||||
schnitt = durchschnitt_kmh(fahrt.get("distance_km"), fahrt.get("duration_s"))
|
schnitt = durchschnitt_kmh(fahrt.get("distance_km"), fahrt.get("duration_s"))
|
||||||
|
|||||||
Reference in New Issue
Block a user