#!/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)