Files
audi-app/tests/verlauf/test_geraetezeit.py
T
tobias 7f386d19ea Geraetezeit statt Ankunftszeit - die Wurzel hinter der 3-km-Fahrt (2026.9.4.1)
Das Fahrtfenster stand seit dem 01.09. in der Gerätezeit, der Verlauf war nach
Ankunftszeit sortiert. Zwei Uhren, und das Gerät puffert: was verspätet ankam,
fiel aus einem Fenster heraus, in dem es der Sache nach lag. Am 03.09. hat das
eine Fahrt gekostet - 26 von 48 GPS-Punkten und 3,0 statt 7 km.

verlauf_lesen() stempelt jeden Punkt jetzt mit der Gerätezeit seines
Datensatzes. Eine Stelle, an der alle Verbraucher vorbeikommen. Dazu zwei
Riegel aus derselben Untersuchung: ein Rückschritt von 43 Metern ist keine
Zählerrücksetzung mehr (aus 87 Metern wurden vorher 13,7 km), und das
Fahrtende wird auf das letzte Lebenszeichen des Geräts geklemmt - der Nachlauf
ist eine Rechnung, kein Messwert.

Am echten Recorder nachgerechnet: Tacho 3,0 -> 7,0 km, GNSS 2,952 -> 6,877 km,
Route 26 -> 47 Punkte, Ende 23:21:57 -> 23:17:48. Beim ersten Lauf haben zwei
Fahrten eine Strecke bekommen, die vorher gar keine hatten.

Nicht das Gerät war schuld: es hat lückenlos aufgezeichnet. Die zehn Minuten
Funkstille waren eine offene, aber tote TCP-Sitzung (flespi-Log: 703 s, 16
Nachrichten) - kein Funkloch. Begründung und die Empfehlung fürs Gerät stehen
in AGENTS.md, Abschnitt CB.

Dazu die neun Paritätsbefunde, alle Richtung Panel gelöst - darunter ein
unmaskierter Punkt in beiden Codebasen, der aus "vor 3 Tage" ein "vor 3 Tag"
machte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 02:18:12 +02:00

195 lines
8.5 KiB
Python

#!/usr/bin/env python3
"""Geraetezeit statt Ankunftszeit - der Fehler hinter der Fahrt vom 03.09.2026.
Das Geraet stempelt jeden Datensatz selbst (MELDEZEIT_SENSOR); der recorder
stempelt ihn mit dem Zeitpunkt, zu dem Home Assistant ihn verarbeitet hat.
Solange nichts puffert, sind die beiden Uhren dicht beieinander. Puffert das
Geraet, laufen sie auseinander - und dann schneidet ein Fahrtfenster, das in
Geraetezeit steht, aus einem nach Ankunftszeit sortierten Verlauf das Falsche
heraus.
Die Zahlen in diesen Tests sind gemessen, nicht erfunden: Fahrt Eichstaett -
Adelschlag am 03.09.2026, 23:09:41 bis 23:17:49 Ortszeit.
Aufruf: python3 tests/verlauf/test_geraetezeit.py
"""
import datetime
import os
import sys
import unittest
_HIER = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(os.path.dirname(_HIER)))
from custom_components.audi_dashboard import verlauf as v # noqa: E402
def t(h, m, s=0, us=0):
return datetime.datetime(2026, 9, 3, h, m, s, us, tzinfo=datetime.UTC)
class Zuordnung(unittest.TestCase):
"""geraetezeit_zuordnen(): jeder Wert bekommt die Zeit SEINES Datensatzes."""
def test_normalbetrieb_kaum_versatz(self):
"""Ohne Puffer aendert sich fast nichts - nur die paar Sekunden Laufzeit."""
marken = [(t(21, 13, 47), t(21, 13, 43)), (t(21, 13, 57), t(21, 13, 53))]
punkte = [(t(21, 13, 47), "21335"), (t(21, 13, 57), "21336")]
neu, ohne = v.geraetezeit_zuordnen(punkte, marken)
self.assertEqual(ohne, 0)
self.assertEqual([p[0] for p in neu], [t(21, 13, 43), t(21, 13, 53)])
def test_nachlieferung_landet_im_fahrtfenster(self):
"""Der Kern des Fehlers.
Die Kilometerstaende 21336 bis 21339 entstanden zwischen 21:14:13 und
21:17:03 Geraetezeit, kamen aber erst um 21:24:54 an - also nach dem
gerechneten Fahrtende. Nach Ankunftszeit fallen sie aus dem Fenster,
nach Geraetezeit liegen sie mitten darin."""
ende = t(21, 17, 49)
marken = [
(t(21, 13, 47), t(21, 13, 43)),
(t(21, 24, 54), t(21, 14, 13)),
(t(21, 24, 54, 100000), t(21, 14, 23)),
(t(21, 24, 56), t(21, 16, 43)),
(t(21, 24, 56, 100000), t(21, 17, 3)),
]
punkte = [
(t(21, 13, 47), "21335"),
(t(21, 24, 54), "21336"),
(t(21, 24, 54, 100000), "21337"),
(t(21, 24, 56), "21338"),
(t(21, 24, 56, 100000), "21339"),
]
# Vorher: nur der erste Wert liegt vor dem Fahrtende.
self.assertEqual(len([p for p in punkte if p[0] <= ende]), 1)
neu, ohne = v.geraetezeit_zuordnen(punkte, marken)
self.assertEqual(ohne, 0)
self.assertEqual(len([p for p in neu if p[0] <= ende]), 5)
# Und damit stimmt die Strecke wieder: 21339 - 21332 = 7 km statt 3.
self.assertEqual(v.wert_bei(neu, ende), 21339.0)
def test_verkehrte_reihenfolge_wird_gerade_gezogen(self):
"""Der Rueckstau kommt nicht in Entstehungsreihenfolge an.
Das Geraet schickt beim Verbindungsaufbau erst den aktuellen Datensatz
und danach den Puffer ab dem aeltesten. Nach Ankunftszeit sieht der
GNSS-Zaehler dadurch aus, als sei er zurueckgesprungen - und
strecke_aus_zaehler() liest einen Ruecksprung zu Recht als
Zaehlerruecksetzung. Genau so entstanden am 03.09. die 9,8 km statt
2,6 km."""
marken = [
(t(21, 9, 58), t(21, 9, 41)),
(t(21, 10, 6), t(21, 9, 40)),
(t(21, 10, 6, 100000), t(21, 9, 51)),
]
punkte = [
(t(21, 9, 58), "6.891"),
(t(21, 10, 6), "6.848"),
(t(21, 10, 6, 100000), "6.935"),
]
# Nach Ankunftszeit steht der Zaehler nicht monoton - genau daraus
# entstanden die gemeldeten 9,8 km. Zwei Riegel dagegen: RUECKSETZ_ANTEIL
# (ein Rueckschritt von 43 m ist keine Ruecksetzung) und die
# Umstempelung hier, die die Reihenfolge ueberhaupt erst richtigstellt.
roh = [float(w) for _t, w in punkte]
self.assertFalse(all(a <= b for a, b in zip(roh, roh[1:])))
neu, _ = v.geraetezeit_zuordnen(punkte, marken)
self.assertEqual([p[1] for p in neu], ["6.848", "6.891", "6.935"])
self.assertAlmostEqual(v.strecke_aus_zaehler(neu), 0.087, places=3)
def test_marke_gehoert_zum_datensatz_nicht_zum_neuesten_stand(self):
"""Innerhalb eines Schwalls darf nicht die juengste Marke gewinnen."""
marken = [
(t(21, 24, 54), t(21, 14, 13)),
(t(21, 24, 56), t(21, 17, 3)),
(t(21, 24, 57), t(21, 24, 27)),
]
neu, _ = v.geraetezeit_zuordnen([(t(21, 24, 54), "21336")], marken)
self.assertEqual(neu[0][0], t(21, 14, 13))
def test_ohne_marke_bleibt_die_ankunftszeit(self):
"""Ein Wert einer fremden Integration behaelt seine eigene Zeit."""
marken = [(t(21, 13, 47), t(21, 13, 43))]
punkte = [(t(20, 0, 0), "auf"), (t(21, 13, 47), "21335")]
neu, ohne = v.geraetezeit_zuordnen(punkte, marken)
self.assertEqual(ohne, 1)
self.assertEqual(neu[0], (t(20, 0, 0), "auf"))
def test_grenze_der_zuordnung(self):
"""Genau auf ZUORDNUNG_MAX_S wird noch zugeordnet, darueber nicht."""
marken = [(t(21, 13, 48), t(21, 13, 43))]
knapp, ohne_knapp = v.geraetezeit_zuordnen([(t(21, 13, 47), "a")], marken, 1.0)
self.assertEqual((knapp[0][0], ohne_knapp), (t(21, 13, 43), 0))
weit, ohne_weit = v.geraetezeit_zuordnen([(t(21, 13, 46), "a")], marken, 1.0)
self.assertEqual((weit[0][0], ohne_weit), (t(21, 13, 46), 1))
def test_ohne_marken_bleibt_alles_wie_es_war(self):
punkte = [(t(21, 13, 47), "21335")]
neu, ohne = v.geraetezeit_zuordnen(punkte, [])
self.assertEqual(neu, punkte)
self.assertEqual(ohne, 1)
def test_unplausible_marke_zieht_nichts_nach_1970(self):
"""_marken_aus() wirft eine 0 im Meldezeit-Sensor heraus."""
class Z:
def __init__(self, ts, state):
self.last_updated, self.state = ts, state
jetzt = t(21, 30)
marken = v._marken_aus(
[Z(t(21, 13, 47), "0"), Z(t(21, 13, 57), str(t(21, 13, 53).timestamp()))],
jetzt,
)
self.assertEqual(len(marken), 1)
self.assertEqual(marken[0][1], t(21, 13, 53))
class Ruecksetzung(unittest.TestCase):
"""Ein Rueckschritt ist nicht automatisch eine Zaehlerruecksetzung."""
def test_echte_ruecksetzung_zaehlt_voll(self):
"""Fahrt vom 01.09.2026: 0,415 km, dann Sprung auf 0, dann 6,543."""
punkte = [(t(1, 0), "0.0"), (t(1, 1), "0.415"), (t(1, 2), "0.0"),
(t(1, 3), "6.543")]
self.assertAlmostEqual(v.strecke_aus_zaehler(punkte), 6.958, places=3)
def test_kleiner_rueckschritt_ist_kein_reset(self):
"""6,891 -> 6,848 ist Reihenfolge, nicht Ruecksetzung.
Vorher buchte diese Zeile den vollen Zaehlerstand als Strecke: aus
87 Metern wurden 13,7 km."""
punkte = [(t(1, 0), "6.891"), (t(1, 1), "6.848"), (t(1, 2), "6.935")]
self.assertAlmostEqual(v.strecke_aus_zaehler(punkte), 0.087, places=3)
def test_grenzfall_haelfte(self):
knapp_darunter = [(t(1, 0), "10.0"), (t(1, 1), "4.9")]
self.assertAlmostEqual(v.strecke_aus_zaehler(knapp_darunter), 4.9, places=3)
knapp_darueber = [(t(1, 0), "10.0"), (t(1, 1), "5.1")]
self.assertAlmostEqual(v.strecke_aus_zaehler(knapp_darueber), 0.0, places=3)
class GnssStrecke(unittest.TestCase):
"""Die gemessene Fahrt, einmal ganz."""
def test_volle_strecke_statt_halber(self):
"""48 Datensaetze, 6,877 km - und nach Ankunftszeit blieben davon 26."""
werte = [6.891, 7.335, 8.186, 9.5, 10.61, 11.777, 12.95, 13.698, 13.725]
geraet = [t(21, 9, 41), t(21, 10, 33), t(21, 12, 23), t(21, 13, 43),
t(21, 14, 23), t(21, 15, 23), t(21, 16, 3), t(21, 17, 3), t(21, 17, 34)]
# Alles ab 21:14:13 Geraetezeit kam erst um 21:24:54 an.
ankunft = [g if g <= t(21, 13, 43) else t(21, 24, 54) for g in geraet]
marken = list(zip(ankunft, geraet))
punkte = [(a, str(w)) for a, w in zip(ankunft, werte)]
start, ende = t(21, 9, 41), t(21, 17, 49)
alt = v.zaehlerstrecke(punkte, start, ende)
self.assertAlmostEqual(alt, 2.609, places=3) # der falsche Wert von damals
neu, _ = v.geraetezeit_zuordnen(punkte, marken)
self.assertAlmostEqual(v.zaehlerstrecke(neu, start, ende), 6.834, places=3)
if __name__ == "__main__":
unittest.main(verbosity=2)