Pausenregel rechnet in Geraetezeit - zwei Fahrten bleiben zwei

Am 04.09.2026 war der Dongle von 18:25 bis 23:36 ohne Verbindung. Als er
sich meldete, kamen das Zuendungs-Aus der ersten Fahrt und das Ein der
zweiten in DERSELBEN Sekunde bei Home Assistant an - ihre Geraetezeiten
lagen 5 Stunden 12 auseinander (18:23:32 und 23:35:43).

Die Pausenregel mass an der Ankunft: 0 Sekunden, also dieselbe Fahrt.
Ergebnis war ein Eintrag von 18:10 bis 23:42 ueber 5 h 33 ohne Strecke,
und die Fahrt um 23:30 gab es gar nicht.

Zwei Stellen im Code trugen dazu bei:
- zuendung_geaendert() rief bedingungslos warte_ende_ab_abbrechen(); das
  "an" loeschte damit das wartende Ende des "aus" derselben Sekunde.
- Der Fall "an, waehrend eine Fahrt laeuft" fiel wortlos durch alle drei
  Zweige von _signalwechsel(). Dazu steht seit dem 02.09. ein Kommentar im
  Code - damals wurde eine Sperre eingebaut, die die Reihenfolge richtig
  stellt, aber nicht den Ausgang.

Neu entscheidet _zuendung_kehrt_zurueck() in Geraetezeit. Der Koordinator
fuehrt mit, welches Ende gerade seine Pausenzeit absitzt (ende_wartet).
Liegt zwischen dem echten Ende und dem neuen "an" mehr als die Pausenzeit,
wird die alte Fahrt auf ihr berechnetes Ende geschlossen und eine neue
begonnen. Gemessen wird ab dem echten Ende, nicht ab dem Signal. Ohne
wartendes Ende passiert nichts - ein Schnitt auf Verdacht waere schlimmer
als eine zu lange Fahrt.

Das behebt die Verspaetung nicht, es macht sie harmlos.

Die Tests mussten zwei Dinge trennen, die vorher eine Zahl waren: PAUSE_S
(900, das Fenster der Regel) und WARTEN (0,15 s, die Wanduhrzeit des Tests).

18 Tests in test_zuendungspause.py (von 14), py_compile sauber,
audi_ha_test auf 2026.9.4.24 ohne Traceback. Gegenprobe am alten Stand:
2 der 4 neuen Tests scheitern dort, mit der Korrektur laufen alle 18.
This commit is contained in:
2026-09-05 00:40:59 +02:00
parent b0ec8b4982
commit 242312441d
8 changed files with 337 additions and 15 deletions
@@ -363,9 +363,12 @@ async def zuendung_geaendert(
if alt is None:
return
# Eine noch wartende Ende-Bestätigung aus einer vorherigen Änderung
# abbrechen - das ist der Mechanismus hinter der Pausenregel.
k.warte_ende_ab_abbrechen()
# Frueher stand hier ein bedingungsloses warte_ende_ab_abbrechen(). Das
# war der Fehler vom 04.09.2026: ein gepuffertes "an" loeschte damit das
# wartende Ende eines gepufferten "aus", das in DERSELBEN Sekunde
# angekommen war - obwohl zwischen beiden in Geraetezeit fuenf Stunden
# lagen. Abgebrochen wird jetzt in _signalwechsel(), wo die Geraetezeit
# bekannt ist und die Pausenregel wirklich rechnen kann.
# Von hier an nur einer zur Zeit. Home Assistant startet für jeden
# Zustandswechsel eine eigene Aufgabe, und dieser Rumpf wartet mehrfach:
@@ -405,6 +408,7 @@ async def _signalwechsel(
jetzt = datetime.datetime.now(datetime.UTC)
if an_jetzt and not fahrt_laeuft:
k.warte_ende_ab_abbrechen()
beginn = await _geraetezeit(k, ereigniszeit, jetzt)
await k.fahrt_start_setzen(beginn)
_LOGGER.info(
@@ -418,6 +422,8 @@ async def _signalwechsel(
return
if not an_jetzt and fahrt_laeuft:
# 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.
@@ -444,17 +450,86 @@ async def _signalwechsel(
"Zuendung aus, Fahrtende waere %s - warte %s s, ob sie zurueckkommt",
ende.isoformat(), pause_s,
)
k.warte_ende_ab(_nach_pause_beenden(k, k.fahrt_start_ts, ende, pause_s))
k.warte_ende_ab(
_nach_pause_beenden(k, k.fahrt_start_ts, ende, pause_s),
(k.fahrt_start_ts, ende),
)
await k.fahrzeugstatus_veroeffentlichen()
return
await fahrt_beenden(k, k.fahrt_start_ts, ende)
if an_jetzt and fahrt_laeuft:
await _zuendung_kehrt_zurueck(k, ereigniszeit, jetzt)
if not an_jetzt and not fahrt_laeuft:
k.warte_ende_ab_abbrechen()
await _spaetes_aus(k, ereigniszeit, jetzt)
await k.fahrzeugstatus_veroeffentlichen()
async def _zuendung_kehrt_zurueck(
k: Koordinator, ereigniszeit: datetime.datetime | None, jetzt: datetime.datetime
) -> None:
"""Die Zuendung geht an, waehrend bei uns noch eine Fahrt laeuft.
ZWEI FAELLE, UND SIE UNTERSCHEIDEN SICH NUR IN DER GERAETEZEIT
-------------------------------------------------------------
(a) Der Fahrer war kurz weg - tanken, Brot holen. Dann gehoert das hier
noch zur selben Fahrt, und das wartende Ende faellt weg.
(b) Die Fahrt war laengst zu Ende, aber ihr "aus" kam verspaetet - und das
"an" der NAECHSTEN Fahrt gleich hinterher. Dann sind es zwei Fahrten.
Am 04.09.2026 gemessen: beide Ereignisse kamen in DERSELBEN Sekunde bei
Home Assistant an (23:36:50), ihre Geraetezeiten lagen aber 5 Stunden 12
auseinander - 18:23:32 und 23:35:43. Nach der Ankunft gerechnet ist das
Fall (a), nach der Geraetezeit Fall (b). Gerechnet wurde nach der Ankunft,
und deshalb hat eine Fahrt von 18:10 die naechste um 23:30 verschluckt:
ein Eintrag ueber 5 Stunden 33, ohne Strecke, und die zweite Fahrt gab es
gar nicht.
Entschieden wird deshalb in Geraetezeit - der Zeit, zu der das Fahrzeug
wirklich stand und wirklich wieder losfuhr.
OHNE WARTENDES ENDE passiert weiterhin nichts. Dann gab es kein "aus", auf
das sich ein "an" beziehen koennte: entweder ist die Zuendung nie aus
gewesen (dann ist das hier eine Wiederholung), oder das "aus" fehlt ganz -
und eine Fahrt auf Verdacht zu zerschneiden waere schlimmer, als sie zu
lang stehen zu lassen.
"""
wartet = k.ende_wartet
if wartet is None:
return
start_ts, ende_ts = wartet
if k.fahrt_start_ts != start_ts:
# Zwischenzeitlich hat jemand anders geschlossen - dann gehoert das
# wartende Ende nicht mehr zu dieser Fahrt.
k.warte_ende_ab_abbrechen()
return
beginn = await _geraetezeit(k, ereigniszeit, jetzt)
pause_s = await _pausenzeit_s(k)
stand_s = (beginn - ende_ts).total_seconds()
k.warte_ende_ab_abbrechen()
if stand_s <= pause_s:
_LOGGER.info(
"Zuendung nach %.0f s wieder an - innerhalb der Pause von %s s, "
"dieselbe Fahrt",
stand_s, pause_s,
)
return
_LOGGER.info(
"Zuendung nach %.0f s wieder an - laenger als die Pause von %s s. "
"Fahrt seit %s wird auf %s geschlossen, neue Fahrt ab %s",
stand_s, pause_s, start_ts.isoformat(), ende_ts.isoformat(),
beginn.isoformat(),
)
await fahrt_beenden(k, start_ts, ende_ts)
await k.fahrt_start_setzen(beginn)
async def _pausenzeit_s(k: Koordinator) -> int:
"""Unsere eigene Wartezeit nach dem Zuendungs-Aus, in Sekunden.
@@ -1 +1 @@
{"version":"2026.9.4.23","sha256":"7cc61f10ec846824ae34bc033115c795ab9026e7d56a72cdf9ec4cb6032e8559","bytes":324157,"gebaut":"2026-09-04T16:07:49Z"}
{"version":"2026.9.4.24","sha256":"2d303b02b3d5ba9d2432856a528b32ed30c657a4ff43e1f39744d4f5c9f8c04a","bytes":324162,"gebaut":"2026-09-04T22:39:43Z"}
@@ -168,6 +168,15 @@ class Koordinator:
self._takte: list[CALLBACK_TYPE] = []
self._beobachter: list[CALLBACK_TYPE] = []
self._ende_aufgabe: asyncio.Task | None = None
# Welches Ende gerade seine Pausenzeit absitzt: (Fahrtbeginn, Ende),
# beides in GERAETEZEIT. None = es wartet keines.
#
# Warum das hier steht und nicht nur in der Aufgabe: kommt die Zuendung
# zurueck, muss der Beobachter entscheiden, ob das noch dieselbe Fahrt
# ist - und dafuer braucht er das wartende Ende. Ohne diese Angabe
# konnte er nur abbrechen, und genau daran sind am 04.09.2026 zwei
# Fahrten zu einer verschmolzen (siehe _signalwechsel).
self.ende_wartet: tuple[datetime.datetime, datetime.datetime] | None = None
# ------------------------------------------------------------ Lebenszyklus
@@ -517,8 +526,13 @@ class Koordinator:
self.tiefststand_liter = wert
await self._laufzeit_sichern()
def warte_ende_ab(self, koroutine: Coroutine[Any, Any, None]) -> None:
def warte_ende_ab(
self,
koroutine: Coroutine[Any, Any, None],
wartet: tuple[datetime.datetime, datetime.datetime] | None = None,
) -> None:
self.warte_ende_ab_abbrechen()
self.ende_wartet = wartet
self._ende_aufgabe = self.entry.async_create_background_task(
self.hass, koroutine, "audi_dashboard_fahrtende"
)
@@ -527,6 +541,7 @@ class Koordinator:
if self._ende_aufgabe is not None and not self._ende_aufgabe.done():
self._ende_aufgabe.cancel()
self._ende_aufgabe = None
self.ende_wartet = None
# ---------------------------------------------------------- Veröffentlichen
@@ -1,7 +1,7 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.9.4.23",
"version": "2026.9.4.24",
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
"codeowners": [