Hauptuntersuchung: Ableitung aus Erstzulassung und Wartungsplan, drittes Feld entfernt

Gemeldet: Wartungsplan leer, Erstzulassung 01.03.2025, erwartet 01.03.2028 -
gezeigt wurden drei verschiedene Antworten (Uebersicht 01.08.2026, Mein Audi
"August 2026", Service "kein Eintrag im Wartungsplan").

Ursache war das Profilfeld hauptuntersuchung_faellig ("08/2026"), das die
Ableitung ueberstimmte, in keiner Oberflaeche ein Eingabefeld hatte und im
Backend kein Schema. Drei Bildschirme verarbeiteten denselben Wert
verschieden. Das Feld ist samt Lese- und Schreibweg entfernt; es gibt jetzt
zwei Quellen: Wartungsplan + 24 Monate, sonst Erstzulassung + 36 Monate.

Dabei mitbehoben:

* Die Service-Kachel verschluckte abgeleitete Termine, sobald kein
  Wartungsplan-Eintrag dahinterstand.
* monatePlus() in der App verlor einen Tag ueber die Zeitumstellung (drei von
  fuenf gemessenen Faellen) - betraf Oelwechsel- und Inspektionsprognose
  genauso. Das Panel rechnete dort seit jeher richtig.
* Die App verlangte fuer jeden Wartungsplan-Eintrag eine Kilometerangabe. Eine
  Hauptuntersuchung ist rein datumsbasiert; ohne km waere der erste HU-Eintrag
  ignoriert worden. Ausserdem nahm das Panel den ersten Treffer der Liste, die
  App den juengsten - beide nehmen jetzt den juengsten.
* Monatsgenauigkeit ("03/2025") bleibt auf beiden Seiten erhalten.

Neu: paritaet_hu.test.ts vergleicht die App gegen die aus der Panel-Quelle
herausgeschnittenen Originalfunktionen, 15 Faelle. Gesamt 292 App-Tests, 73
Python-Tests mit 231 Untertests, 0 Tracebacks. Live an der Testinstanz auf
allen drei gemeldeten Bildschirmen geprueft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 01:56:11 +02:00
parent 5621b818cc
commit 863284e540
25 changed files with 505 additions and 243 deletions
@@ -32,7 +32,6 @@ LEBENSZEICHEN = datetime.datetime(2026, 9, 3, 8, 20, 0, tzinfo=datetime.UTC)
class FakeWerte:
TRIP_SENSOR = ""
ZUENDUNG_SENSOR = "binary_sensor.zuendung"
MELDEZEIT_SENSOR = ""
+10 -9
View File
@@ -34,7 +34,6 @@ T_WECHSEL = datetime.datetime(2026, 9, 2, 21, 22, 12, tzinfo=datetime.UTC)
class FakeWerte:
TRIP_SENSOR = "binary_sensor.trip"
ZUENDUNG_SENSOR = "binary_sensor.zuendung"
@@ -50,10 +49,9 @@ class FakeKoordinator:
self.fahrt_sperre = asyncio.Lock()
self.beendet = []
self.veroeffentlicht = 0
# Dieser Test beschreibt die Welt MIT Trip-Signal: dort bringt das
# Geraet seine Pausentoleranz mit, _pausenzeit_s() gibt 0 zurueck und
# es wird sofort geschlossen. Gegenstand ist hier die Sperre, nicht
# die Wartezeit - die hat test_zuendungspause.py.
# Gegenstand ist hier die SPERRE, nicht die Wartezeit - die hat
# test_zuendungspause.py. Deshalb ist die Pausenzeit unten auf 0
# gesetzt, dann schliesst _signalwechsel() sofort.
self.zuordnung = FakeZuordnung()
# Seit die Pausenregel in Geraetezeit rechnet, liest _signalwechsel() das
@@ -72,7 +70,10 @@ class FakeKoordinator:
class SignalwechselTest(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self):
self._echt = (f._geraetezeit, f._echtes_ende, f.fahrt_beenden)
self._echt = (f._geraetezeit, f._echtes_ende, f.fahrt_beenden, f._pausenzeit_s)
async def pausenzeit(k):
return 0
async def geraetezeit(k, ereigniszeit, standard):
# Der eigentliche Punkt: dieser Schritt wartet wirklich (in echt
@@ -88,12 +89,12 @@ class SignalwechselTest(unittest.IsolatedAsyncioTestCase):
k.beendet.append((start_ts, ende_ts))
await k.fahrt_start_setzen(None)
f._geraetezeit, f._echtes_ende, f.fahrt_beenden = (
geraetezeit, echtes_ende, beenden,
f._geraetezeit, f._echtes_ende, f.fahrt_beenden, f._pausenzeit_s = (
geraetezeit, echtes_ende, beenden, pausenzeit,
)
async def asyncTearDown(self):
f._geraetezeit, f._echtes_ende, f.fahrt_beenden = self._echt
f._geraetezeit, f._echtes_ende, f.fahrt_beenden, f._pausenzeit_s = self._echt
async def test_off_und_on_in_derselben_sekunde(self):
"""Der genaue Fall vom 02.09.2026: die neue Fahrt darf nicht verloren
+9 -35
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Die Wartezeit nach dem Zuendungs-Aus, und die beiden Nachlaeufe des Geraets.
"""Die Wartezeit nach dem Zuendungs-Aus und der Nachlauf des Geraets.
Warum es das gibt (03.09.2026): das Trip-Signal des FMM003 endet erst nach
seinem "Ignition OFF Timeout" von 900 s - und der Schlaf-Timeout des Geraets
@@ -46,8 +46,7 @@ WARTEN = 0.15
class FakeWerte:
def __init__(self, trip=""):
self.TRIP_SENSOR = trip
def __init__(self):
self.ZUENDUNG_SENSOR = "binary_sensor.zuendung"
# Ohne Meldezeit-Sensor keine Geraetezeit und keine Klemme - die Tests
# pruefen die Nachlaufrechnung, nicht die Zuordnung.
@@ -55,8 +54,8 @@ class FakeWerte:
class FakeZuordnung:
def __init__(self, trip=""):
self.werte = FakeWerte(trip)
def __init__(self):
self.werte = FakeWerte()
class FakeAblage:
@@ -72,10 +71,10 @@ class FakeAblage:
class FakeKoordinator:
def __init__(self, start_ts=T0, trip="", minuten=15, fahrten=None):
def __init__(self, start_ts=T0, minuten=15, fahrten=None):
self.fahrt_start_ts = start_ts
self.fahrt_sperre = asyncio.Lock()
self.zuordnung = FakeZuordnung(trip)
self.zuordnung = FakeZuordnung()
self.ablage = FakeAblage(minuten, fahrten)
self.beendet = []
self._aufgabe = None
@@ -111,14 +110,12 @@ class FakeKoordinator:
class Basis(unittest.IsolatedAsyncioTestCase):
def _pause_kurz(self):
async def pausenzeit(k):
return 0 if k.zuordnung.werte.TRIP_SENSOR else PAUSE_S
return PAUSE_S
return pausenzeit
async def asyncSetUp(self):
self._echt = (
f._geraetezeit, f.fahrt_beenden, f._pausenzeit_s, f._nachlauf_gegenpruefen,
)
self._echt = (f._geraetezeit, f.fahrt_beenden, f._pausenzeit_s)
async def geraetezeit(k, ereigniszeit, standard):
return ereigniszeit or standard
@@ -127,11 +124,6 @@ class Basis(unittest.IsolatedAsyncioTestCase):
k.beendet.append((start_ts, ende_ts))
await k.fahrt_start_setzen(None)
async def gegenpruefen(k, start_ts, signal_ende, gerechnet):
# Liest den Verlauf und aendert nichts an der Rechnung (siehe dort);
# ohne echtes hass-Objekt ist sie hier nur im Weg.
return None
async def nach_pause(k, start_ts, ende_ts, pause_s):
# Wie das Original, nur mit kurzer Wanduhrzeit - die echte
# Pausenzeit steckt weiterhin in der Entscheidung, nicht im Warten.
@@ -141,14 +133,11 @@ class Basis(unittest.IsolatedAsyncioTestCase):
f._geraetezeit = geraetezeit
f.fahrt_beenden = beenden
f._pausenzeit_s = self._pause_kurz()
f._nachlauf_gegenpruefen = gegenpruefen
self._echt_nach_pause = f._nach_pause_beenden
f._nach_pause_beenden = nach_pause
async def asyncTearDown(self):
(
f._geraetezeit, f.fahrt_beenden, f._pausenzeit_s, f._nachlauf_gegenpruefen,
) = self._echt
(f._geraetezeit, f.fahrt_beenden, f._pausenzeit_s) = self._echt
f._nach_pause_beenden = self._echt_nach_pause
@@ -187,11 +176,6 @@ class Wartezeit(Basis):
self.assertEqual(k.beendet, [])
self.assertEqual(k.fahrt_start_ts, T0)
async def test_ohne_wartezeit_bleibt_es_wie_bisher(self):
"""Mit zugeordnetem Trip-Signal wartet das Geraet - wir nicht."""
k = FakeKoordinator(trip="binary_sensor.trip")
await f.zuendung_geaendert(k, "off", "on", AUS)
self.assertEqual(len(k.beendet), 1, "ohne Pause wird sofort geschlossen")
class Nachlauf(Basis):
@@ -202,13 +186,6 @@ class Nachlauf(Basis):
_, ende = k.beendet[0]
self.assertEqual(ende, AUS - datetime.timedelta(seconds=180))
async def test_tripweg_zieht_beide_nachlaeufe_ab(self):
"""900 s Trip-Nachlauf plus 180 s des Zuendungselements: das Trip-Ende
haengt selbst an der bereits verzoegerten Zuendung."""
k = FakeKoordinator(trip="binary_sensor.trip")
await f.zuendung_geaendert(k, "off", "on", AUS)
_, ende = k.beendet[0]
self.assertEqual(ende, AUS - datetime.timedelta(seconds=1080))
async def test_zu_kurz_wird_auf_den_beginn_geklemmt(self):
k = FakeKoordinator(start_ts=AUS - datetime.timedelta(seconds=60))
@@ -412,9 +389,6 @@ class Einstellung(unittest.IsolatedAsyncioTestCase):
k = FakeKoordinator(minuten=25)
self.assertEqual(await f._pausenzeit_s(k), 25 * 60)
async def test_trip_signal_schlaegt_die_einstellung(self):
k = FakeKoordinator(minuten=25, trip="binary_sensor.trip")
self.assertEqual(await f._pausenzeit_s(k), 0)
async def test_unsinn_faellt_auf_den_standard_zurueck(self):
k = FakeKoordinator()