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:
@@ -29,7 +29,6 @@ from .ablage import neue_id
|
||||
from .veroeffentlichung import zustand_oder_none
|
||||
from .verlauf import (
|
||||
MINDESTDAUER_S,
|
||||
fahrtsignal,
|
||||
geraetezeit,
|
||||
geraetezeit_plausibel,
|
||||
verlauf_lesen,
|
||||
@@ -52,22 +51,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
# Einstellung fahrten_pausenzeit_min in beiden Oberflächen wieder anbieten.
|
||||
|
||||
|
||||
# Der Nachlauf des Geraets zwischen Zuendung aus und Trip-Ende.
|
||||
#
|
||||
# Das Trip-Signal des FMM003 endet nicht mit der Zuendung, sondern erst nach
|
||||
# seinem "Ignition OFF Timeout" (Trip \ Odometer). Es ist damit als Ausloeser
|
||||
# ideal - es prellt nicht und bringt die Pausentoleranz mit -, liefert aber ein
|
||||
# Fahrtende, das systematisch um genau diese Spanne zu spaet liegt.
|
||||
#
|
||||
# Der Wert ist die Konfiguration des Geraets, nicht unsere Wahl: er steht dort
|
||||
# auf 900 s. Aendert er sich, gehoert diese Zahl mitgeaendert - sie laesst sich
|
||||
# nicht aus den Daten ableiten (siehe _nachlauf_gegenpruefen unten, die genau
|
||||
# das versucht und meldet, wenn es nicht mehr zusammenpasst).
|
||||
#
|
||||
# Bewusst KEINE Einstellung in der Oberflaeche: die Zahl gehoert zum Geraet,
|
||||
# nicht zum Fahrzeug oder zum Nutzer - dieselbe Linie wie bei UNPLAUSIBLE_KMH
|
||||
# und MIN_STRECKE_VERBRAUCH_KM in verlauf.py.
|
||||
NACHLAUF_S = 900
|
||||
|
||||
# Ab welcher Abweichung zwischen gerechnetem und beobachtetem Ende gewarnt
|
||||
# wird. Grosszuegig, weil die Zuendung nur so genau bekannt ist, wie das Geraet
|
||||
@@ -83,19 +66,20 @@ NACHLAUF_TOLERANZ_S = 120
|
||||
# 02.09.2026 kippte die Zuendung je einmal in den ersten 25 Sekunden fuer unter
|
||||
# eine Sekunde weg und kam sofort zurueck.
|
||||
#
|
||||
# Der gemeldete Zeitpunkt liegt damit systematisch zu spaet, und zwar in BEIDEN
|
||||
# Wegen: auch das Trip-Ende des Geraets haengt an der verzoegerten Zuendung.
|
||||
# Nachgemessen am 02.09.2026: Zuendung aus 21:43:15, Trip aus 21:58:16 - genau
|
||||
# NACHLAUF_S spaeter. Wer das Trip-Signal als Ausloeser nimmt, muss deshalb
|
||||
# beide Spannen abziehen.
|
||||
# Der gemeldete Zeitpunkt liegt damit systematisch zu spaet und wird abgezogen.
|
||||
#
|
||||
# (Bis zum 05.09.2026 gab es einen zweiten Weg ueber das Trip-Signal des
|
||||
# Geraets, dessen Ende an derselben verzoegerten Zuendung hing und deshalb noch
|
||||
# einmal 900 s spaeter kam. Das Trip-Szenario ist am Geraet abgeschaltet und
|
||||
# der Sensor aus der App entfernt - siehe AGENTS.md.)
|
||||
#
|
||||
# Der Wert stand bis zum 31.08.2026 auf 0 und seit dem 01.09.2026 09:38 auf
|
||||
# 180 s (aus den Geraetekonfigurationen rekonstruiert). Fahrten aus der Zeit
|
||||
# dazwischen enden 180 s zu spaet - korrigiert wird ab jetzt, rueckwirkend nur
|
||||
# von Hand.
|
||||
#
|
||||
# Wie NACHLAUF_S die Konfiguration des Geraets und bewusst keine Einstellung in
|
||||
# der Oberflaeche.
|
||||
# Die Konfiguration des Geraets (Parameter 400) und bewusst keine Einstellung
|
||||
# in der Oberflaeche.
|
||||
ZUENDUNG_NACHLAUF_S = 180
|
||||
|
||||
# Wie lange nach einem vorlaeufigen Ende ein spaeter eintreffendes Zuendungs-Aus
|
||||
@@ -216,7 +200,7 @@ async def _echtes_ende(
|
||||
) -> datetime.datetime:
|
||||
"""Das Fahrtende ohne den Nachlauf des Geraets.
|
||||
|
||||
Das Trip-Signal endet NACHLAUF_S nach der Zuendung; abgezogen ergibt das den
|
||||
Die Zuendung wird vom Geraet verzoegert gemeldet; abgezogen ergibt das den
|
||||
Zeitpunkt, zu dem wirklich abgestellt wurde. Die Rechnung stammt vollstaendig
|
||||
aus der Logik des Geraets und ueberlebt deshalb auch ein Funkloch: der
|
||||
Datensatz kommt spaeter, traegt aber seine eigene Zeit (siehe geraetezeit()).
|
||||
@@ -227,16 +211,11 @@ async def _echtes_ende(
|
||||
anschliessend. Ohne diese Klemme entstuende aus dreissig Sekunden Zuendung
|
||||
eine Viertelstunde Fahrt.
|
||||
|
||||
ZWEI Nachlaeufe, nicht einer. Das Zuendungselement meldet selbst schon
|
||||
verzoegert (ZUENDUNG_NACHLAUF_S), und das Trip-Ende haengt an dieser bereits
|
||||
verzoegerten Zuendung - wer ueber das Trip-Signal kommt, muss deshalb beide
|
||||
Spannen abziehen, wer ueber die Zuendung kommt, nur die eine. Bis zum
|
||||
03.09.2026 wurde nur NACHLAUF_S abgezogen; seit der Ignition OFF Delay am
|
||||
Das Zuendungselement meldet selbst schon verzoegert
|
||||
(ZUENDUNG_NACHLAUF_S); genau diese Spanne wird abgezogen. Bis zum
|
||||
03.09.2026 geschah das nicht, und seit der Ignition OFF Delay am
|
||||
01.09.2026 auf 180 s steht, endeten die Fahrten dadurch 180 s zu spaet."""
|
||||
nachlauf = ZUENDUNG_NACHLAUF_S
|
||||
ueber_trip = bool(k.zuordnung.werte.TRIP_SENSOR)
|
||||
if ueber_trip:
|
||||
nachlauf += NACHLAUF_S
|
||||
if not nachlauf:
|
||||
return signal_ende
|
||||
gerechnet = signal_ende - datetime.timedelta(seconds=nachlauf)
|
||||
@@ -247,8 +226,6 @@ async def _echtes_ende(
|
||||
int((signal_ende - start_ts).total_seconds()), nachlauf,
|
||||
)
|
||||
return start_ts
|
||||
if ueber_trip:
|
||||
await _nachlauf_gegenpruefen(k, start_ts, signal_ende, gerechnet)
|
||||
|
||||
# Nie hinter dem letzten Lebenszeichen des Geraets.
|
||||
#
|
||||
@@ -273,49 +250,6 @@ async def _echtes_ende(
|
||||
return gerechnet
|
||||
|
||||
|
||||
async def _nachlauf_gegenpruefen(
|
||||
k: Koordinator,
|
||||
start_ts: datetime.datetime,
|
||||
signal_ende: datetime.datetime,
|
||||
gerechnet: datetime.datetime,
|
||||
) -> None:
|
||||
"""Vergleicht das gerechnete Ende mit dem beobachteten Zuendungswechsel und
|
||||
meldet, wenn beide nicht zusammenpassen. Aendert nichts - die Rechnung
|
||||
bleibt massgeblich.
|
||||
|
||||
Warum nur eine Meldung und keine Korrektur: wie genau der Zeitpunkt "Zuendung
|
||||
aus" bekannt ist, haengt an der I/O-Einstellung des Geraets. Steht der
|
||||
Operand des Zuendungselements auf "Monitoring", erzeugt ein Wechsel keinen
|
||||
eigenen Datensatz, sondern faehrt nur im naechsten mit - dann ist der
|
||||
beobachtete Zeitpunkt zu spaet, oft genau der des Trip-Endes. Auf "On Change"
|
||||
waere er exakt. Solange das nicht feststeht, waere ein Umschalten auf den
|
||||
beobachteten Wert eine Verschlechterung; eine Abweichung zu protokollieren
|
||||
ist es nie."""
|
||||
sensor = k.zuordnung.werte.ZUENDUNG_SENSOR
|
||||
if not sensor or sensor == fahrtsignal(k.zuordnung.werte):
|
||||
return
|
||||
punkte = await verlauf_lesen(
|
||||
k.hass, sensor, start_ts, signal_ende, k.zuordnung.werte.MELDEZEIT_SENSOR
|
||||
)
|
||||
aus = [ts for ts, wert in punkte if wert == "off"]
|
||||
if not aus:
|
||||
return
|
||||
# Der beobachtete Wechsel traegt den Nachlauf des Zuendungselements noch in
|
||||
# sich, das gerechnete Ende nicht mehr - erst ohne ihn meinen beide Zahlen
|
||||
# denselben Zeitpunkt. Ohne diese Zeile meldete die Pruefung seit dem
|
||||
# 01.09.2026 bei JEDER Fahrt eine Abweichung von 180 s.
|
||||
beobachtet = aus[-1] - datetime.timedelta(seconds=ZUENDUNG_NACHLAUF_S)
|
||||
abweichung = abs((beobachtet - gerechnet).total_seconds())
|
||||
if abweichung > NACHLAUF_TOLERANZ_S:
|
||||
_LOGGER.warning(
|
||||
"Fahrtende gerechnet auf %s (Trip-Ende minus %s s), die Zuendung ging "
|
||||
"laut Aufzeichnung aber um %s aus - %.0f s Abweichung. Entweder stimmt "
|
||||
"NACHLAUF_S nicht mehr mit dem Geraet ueberein, oder das "
|
||||
"Zuendungselement schreibt keine eigenen Datensaetze.",
|
||||
gerechnet.isoformat(), NACHLAUF_S, aus[-1].isoformat(), abweichung,
|
||||
)
|
||||
|
||||
|
||||
async def _geraetezeit(
|
||||
k: Koordinator,
|
||||
ereigniszeit: datetime.datetime | None,
|
||||
@@ -352,7 +286,7 @@ async def zuendung_geaendert(
|
||||
#
|
||||
# Ohne diese Prüfung las der Beobachter das als "Zündung ging gerade an"
|
||||
# und legte eine Fahrt an, deren Beginn schlicht der Zeitpunkt des
|
||||
# Neustarts war. Steht das Trip-Signal ohnehin auf "an", entstand so bei
|
||||
# Neustarts war. Stand die Zuendung ohnehin auf "an", entstand so bei
|
||||
# JEDEM Neustart eine Phantomfahrt - daher der Eintrag vom 30.08.2026,
|
||||
# 19:43 bis 08:37 des Folgetags, 12,9 Stunden, 0 km.
|
||||
#
|
||||
@@ -459,11 +393,8 @@ async def _signalwechsel(
|
||||
return
|
||||
# Ein neues "aus" ersetzt ein aelteres, das noch wartet.
|
||||
k.warte_ende_ab_abbrechen()
|
||||
# Ob hier noch gewartet wird, entscheidet _pausenzeit_s(): liegt ein
|
||||
# Trip-Signal an, bringt das Geraet seine Pausentoleranz selbst mit,
|
||||
# und eine zweite obendrauf haette zwei getrennte Fahrten verschmolzen.
|
||||
# Loest die Zuendung aus, gibt es diese Toleranz nicht - dann ist die
|
||||
# Wartezeit unsere.
|
||||
# Wie lange gewartet wird, entscheidet _pausenzeit_s() - die
|
||||
# Wartezeit ist unsere, das Geraet bringt keine mit.
|
||||
signal_ende = await _geraetezeit(k, ereigniszeit, jetzt)
|
||||
# Ein Ende vor dem Beginn kann es nicht geben. Passiert, wenn die
|
||||
# Meldezeit aus einem älteren Datensatz stammt als der Fahrtbeginn -
|
||||
@@ -474,10 +405,9 @@ async def _signalwechsel(
|
||||
signal_ende.isoformat(), k.fahrt_start_ts.isoformat(),
|
||||
)
|
||||
signal_ende = jetzt
|
||||
# Erst jetzt die Nachlaeufe des Geraets abziehen: signal_ende ist der
|
||||
# Zeitpunkt, zu dem das SIGNAL endete - die verzoegerte Zuendung oder
|
||||
# das noch spaetere Trip-Ende -, nicht der, zu dem das Fahrzeug
|
||||
# abgestellt wurde.
|
||||
# Erst jetzt den Nachlauf des Geraets abziehen: signal_ende ist der
|
||||
# Zeitpunkt, zu dem das SIGNAL endete - die verzoegerte Zuendung -,
|
||||
# nicht der, zu dem das Fahrzeug abgestellt wurde.
|
||||
ende = await _echtes_ende(k, k.fahrt_start_ts, signal_ende)
|
||||
pause_s = await _pausenzeit_s(k)
|
||||
if pause_s:
|
||||
@@ -568,14 +498,13 @@ async def _zuendung_kehrt_zurueck(
|
||||
async def _pausenzeit_s(k: Koordinator) -> int:
|
||||
"""Unsere eigene Wartezeit nach dem Zuendungs-Aus, in Sekunden.
|
||||
|
||||
Null, solange ein Trip-Signal zugeordnet ist: dann wartet das Geraet
|
||||
bereits (Ignition OFF Timeout), und beides zusammen haette aus einer
|
||||
Viertelstunde eine halbe gemacht. Genau daran ist die Wartezeit am
|
||||
31.08.2026 entfallen - sie kommt hier nur fuer den Weg ueber die Zuendung
|
||||
zurueck.
|
||||
So lange darf eine zurueckkehrende Zuendung noch dieselbe Fahrt sein -
|
||||
Tanken, kurze Besorgung. Der Wert kommt aus dem Regler "Fahrt beenden",
|
||||
derselbe, der auch den Schlaf-Timeout des Dongles stellt (flespi.py).
|
||||
|
||||
Gemessen wird er in GERAETEZEIT, nicht an der Wanduhr - siehe
|
||||
_zuendung_kehrt_zurueck().
|
||||
"""
|
||||
if k.zuordnung.werte.TRIP_SENSOR:
|
||||
return 0
|
||||
try:
|
||||
profil = await k.ablage.profil_lesen()
|
||||
wert = (profil.get("einstellungen") or {}).get("fahrten_pausenzeit_min")
|
||||
@@ -812,7 +741,7 @@ async def jetzt_beenden(k: Koordinator) -> bool:
|
||||
_LOGGER.info("\"Fahrt beenden\" gedrueckt, aber es laeuft keine Fahrt")
|
||||
return False
|
||||
start_ts = k.fahrt_start_ts
|
||||
ende = await _letztes_lebenszeichen(k, fahrtsignal(k.zuordnung.werte), start_ts)
|
||||
ende = await _letztes_lebenszeichen(k, k.zuordnung.werte.ZUENDUNG_SENSOR, start_ts)
|
||||
_LOGGER.info(
|
||||
"Fahrt seit %s von Hand beendet, vorlaeufiges Ende %s",
|
||||
start_ts.isoformat(), ende.isoformat(),
|
||||
@@ -839,7 +768,7 @@ async def nach_neustart_fortsetzen(k: Koordinator) -> None:
|
||||
if k.fahrt_start_ts is None:
|
||||
return
|
||||
|
||||
sensor = fahrtsignal(k.zuordnung.werte)
|
||||
sensor = k.zuordnung.werte.ZUENDUNG_SENSOR
|
||||
if zustand_oder_none(k.hass, sensor) == "on":
|
||||
return # fährt noch - der Beobachter übernimmt wie sonst auch
|
||||
|
||||
|
||||
Reference in New Issue
Block a user