7bbd236a17
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.
144 lines
5.8 KiB
Python
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)
|