Files
audi-app/tests/batterie/test_messzeitpunkt.py
T
tobias 7bbd236a17 Batteriehistorie: stehende Werte gelten nicht mehr als frische Messungen
Der Vorschlag aus Abschnitt CB (CAN-Wert innerhalb 300 s verlangen) wurde
gemessen und trifft nicht: er haette zwei eindeutige Werkbank-Tage
durchgelassen (CAN 5 Minuten neben dem Minimum) und echte Fahrzeugtage
verworfen - am stehenden Fahrzeug schweigt CAN, eine CAN-Frist verwirft also
ausgerechnet die Ruhespannung.

Der wirkliche Mechanismus: zustand_oder_none() liefert den zuletzt bekannten
Zustand unabhaengig vom Alter, und pruefen() stempelte ihn mit "jetzt". Ein
schweigender Dongle "misst" damit alle fuenf Minuten weiter seinen letzten
Wert. Am Bestand belegt: der 16.08.2026 trug einen Tageseintrag, obwohl der
Sensor an dem Tag kein einziges Mal gemeldet hatte (naechste echte Meldung 885
Minuten entfernt); sieben von 19 Eintraegen standen mehr als fuenf Minuten von
jeder echten Meldung entfernt.

messzeitpunkt() beantwortet jetzt beide Fragen an einer Stelle: ist das eine
neue Messung, und wann wurde sie gemacht. Gestempelt wird mit der Geraetezeit,
wenn zuordenbar - so wie der Rueckblick es laengst tut, der damit die richtige
Umsetzung war und keine Aenderung brauchte. Der Tag kommt aus der Messung, nicht
aus date.today(). Die Frischepruefung steht vor der Plausibilitaetsgrenze, sonst
warnt ein offline gegangener Dongle alle fuenf Minuten ueber denselben Wert (77
identische Zeilen in 3000 Protokollzeilen gezaehlt).

Die erste Fassung mass das Alter an last_updated und hatte damit ein Loch, das
erst die Live-Pruefung zeigte: Home Assistant setzt beim Hochfahren jede
Entitaet neu, last_updated steht danach auf der Neustartzeit. Gemessen: nach dem
Neustart um 21:12:54 trug der Spannungssensor last_updated 21:12:54, waehrend
die Meldezeit desselben Datensatzes 14:46:47 sagte. Das Alter wird deshalb an
der Messzeit gemessen.

Geprueft: 18 Tests in tests/batterie/, davon fallen 16 gegen HEAD; alle 116
Backend-Tests gruen; live im Testcontainer belegt - der erste Takt nach dem
Neustart meldet "keine neue Messung" statt der bisherigen Warnung.

Offen: Werkbank und Fahrzeug lassen sich aus diesen Daten nicht sicher trennen.
Abgestellt sind die erfundenen Eintraege, nicht die Frage, wo der Dongle steckt.
2026-09-06 23:30:08 +02:00

144 lines
5.8 KiB
Python

#!/usr/bin/env python3
"""Nur eine NEUE Messung wird aufgezeichnet - und mit ihrer eigenen Zeit.
Warum es das gibt (06.09.2026): zustand_oder_none() liefert den zuletzt
bekannten Zustand unabhängig von seinem Alter, und pruefen() stempelte ihn mit
"jetzt". Ein schweigender Dongle "misst" damit alle fünf Minuten weiter seinen
letzten Wert.
Am Bestand des Testcontainers nachgemessen, nicht vermutet:
2026-08-16 Tageseintrag vorhanden (13.024 V @ 08:37:43)
Spannungsmeldungen an diesem Tag: 0
Abstand zur nächsten echten Meldung: 885 Minuten
Sieben von 19 Einträgen standen mehr als fünf Minuten von jeder echten Meldung
entfernt; die unstrittigen Fahrzeugtage dagegen exakt bei 0 s.
Geprüft wird deshalb: dass eine veraltete Messung gar nicht mehr erfasst wird,
dass eine frische ihren eigenen Zeitstempel behält (Gerätezeit, wenn zuordenbar
- so wie der Rückblick es tut), und dass der Tag aus der Messung stammt und
nicht aus der Uhr des Servers.
Aufruf: python3 tests/batterie/test_messzeitpunkt.py
(braucht das homeassistant-Paket, weil die Integration es importiert - also
z. B. im Test-Container)
"""
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 batterie as b # noqa: E402
JETZT = datetime.datetime(2026, 8, 16, 8, 37, 43, tzinfo=datetime.UTC)
class Zustand:
"""Das Wenige, das messzeitpunkt() von einem HA-State braucht."""
def __init__(self, last_updated, state=""):
self.last_updated = last_updated
self.state = state
def vor(sekunden):
return JETZT - datetime.timedelta(seconds=sekunden)
class NurNeueMessungen(unittest.TestCase):
def test_frische_messung_wird_erfasst(self):
z = Zustand(vor(12))
self.assertEqual(b.messzeitpunkt(z, None, JETZT), vor(12))
def test_veraltete_messung_wird_nicht_erfasst(self):
"""Der Fall 16.08.2026: 885 Minuten seit der letzten echten Meldung."""
z = Zustand(vor(885 * 60))
self.assertIsNone(b.messzeitpunkt(z, None, JETZT))
def test_genau_am_fenster_zaehlt_noch(self):
z = Zustand(vor(b.MESSUNG_MAX_ALTER_S))
self.assertIsNotNone(b.messzeitpunkt(z, None, JETZT))
def test_eine_sekunde_darueber_nicht_mehr(self):
z = Zustand(vor(b.MESSUNG_MAX_ALTER_S + 1))
self.assertIsNone(b.messzeitpunkt(z, None, JETZT))
def test_fenster_deckt_mindestens_einen_takt_ab(self):
"""Sonst ginge eine Messung verloren, die kurz nach einem Takt eintrifft."""
self.assertGreaterEqual(b.MESSUNG_MAX_ALTER_S, 300)
def test_ohne_zustand_nichts(self):
self.assertIsNone(b.messzeitpunkt(None, None, JETZT))
class GeraetezeitStattAnkunftszeit(unittest.TestCase):
def test_zuordenbare_geraetezeit_gewinnt(self):
ankunft = vor(30)
geraet = vor(200)
mz = Zustand(ankunft, str(geraet.timestamp()))
self.assertEqual(b.messzeitpunkt(Zustand(ankunft), mz, JETZT), geraet)
def test_zu_weit_entfernte_meldezeit_zaehlt_nicht(self):
"""Sie gehört dann zu einem anderen Datensatz - siehe ZUORDNUNG_MAX_S."""
ankunft = vor(30)
mz = Zustand(vor(90), str(vor(200).timestamp()))
self.assertEqual(b.messzeitpunkt(Zustand(ankunft), mz, JETZT), ankunft)
def test_unplausible_geraetezeit_zaehlt_nicht(self):
"""Eine falsch gestellte Geräteuhr darf die Messung nicht mitreißen."""
ankunft = vor(30)
zukunft = JETZT + datetime.timedelta(days=3)
mz = Zustand(ankunft, str(zukunft.timestamp()))
self.assertEqual(b.messzeitpunkt(Zustand(ankunft), mz, JETZT), ankunft)
def test_unlesbare_meldezeit_zaehlt_nicht(self):
ankunft = vor(30)
mz = Zustand(ankunft, "unavailable")
self.assertEqual(b.messzeitpunkt(Zustand(ankunft), mz, JETZT), ankunft)
class NachEinemNeustart(unittest.TestCase):
"""Am 06.09.2026 im laufenden Testcontainer aufgefallen, nicht am Quelltext.
Nach dem Neustart um 21:12:54 stand die neue Frischeprüfung im Container -
und der erste Takt um 21:17:54 warnte trotzdem über den seit 15:30
stehenden 0-V-Wert. Der Grund: Home Assistant setzt beim Hochfahren jede
Entität neu, last_updated steht danach auf der Neustartzeit. Gemessen:
Spannung last_updated 21:12:54 Wert 0
Meldezeit last_updated 21:12:54 Wert 1788706007 -> 14:46:47
Die Ankunftszeit log also, die Gerätezeit sagte die Wahrheit: 395 Minuten.
"""
def test_frische_ankunft_aber_alte_geraetezeit_zaehlt_nicht(self):
ankunft = JETZT # der Neustart hat sie gerade gesetzt
geraet = JETZT - datetime.timedelta(minutes=395)
mz = Zustand(ankunft, str(geraet.timestamp()))
self.assertIsNone(b.messzeitpunkt(Zustand(ankunft), mz, JETZT))
def test_ohne_meldezeit_bleibt_die_luecke_und_das_ist_dokumentiert(self):
"""Ohne MELDEZEIT_SENSOR gibt es nur die Ankunftszeit - dann geht nach
einem Neustart einmalig ein veralteter Wert durch. Der Test hält fest,
dass das bekannt ist und nicht aus Versehen passiert."""
self.assertIsNotNone(b.messzeitpunkt(Zustand(JETZT), None, JETZT))
class DerTagKommtAusDerMessung(unittest.TestCase):
def test_messung_kurz_vor_mitternacht_bleibt_im_richtigen_tag(self):
"""Um 00:03 verarbeitet, um 23:58 gemessen - der Eintrag gehört zum Vortag."""
jetzt = datetime.datetime(2026, 9, 7, 0, 3, 0, tzinfo=datetime.UTC)
gemessen = datetime.datetime(2026, 9, 6, 23, 58, 0, tzinfo=datetime.UTC)
ergebnis = b.messzeitpunkt(Zustand(gemessen), None, jetzt)
self.assertEqual(ergebnis.date().isoformat(), "2026-09-06")
self.assertNotEqual(ergebnis.date().isoformat(), jetzt.date().isoformat())
if __name__ == "__main__":
unittest.main(verbosity=2)