7f386d19ea
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>
195 lines
8.5 KiB
Python
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)
|