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:
@@ -11117,3 +11117,116 @@ Standort-Blatt sei fehlerhaft, weil "Route" und "Teilen" hinter der Tab-Leiste
|
||||
liegen. Das war falsch - die 96-px-Vorschau ist Absicht, aufgezogen sitzt das
|
||||
Blatt sauber 30 px ueber der Leiste. Mein Klick-Test war der Fehler, nicht die
|
||||
App: das Blatt hoert auf Zeigerereignisse, nicht auf `click`.
|
||||
|
||||
## CQ. Zwei Fahrten wurden zu einer, weil die Pausenregel an der Wanduhr hing (2026.9.4.24)
|
||||
|
||||
Gemeldet als "erneut Fehler/Einschlafen unterwegs". Die Untersuchung hat drei
|
||||
meiner eigenen Erklaerungen widerlegt, bevor die richtige uebrig blieb - die
|
||||
falschen stehen hier mit, weil sie der lehrreiche Teil sind.
|
||||
|
||||
### Was NICHT die Ursache war
|
||||
|
||||
* **"Der Dongle schlaeft unterwegs ein."** Nachgerechnet: das Geraet war nach
|
||||
dem Abstellen noch rund zwoelf Minuten wach (Schlaf-Timeout `103` = 15 Min.
|
||||
ab Zuendungs-Aus) und hatte einen Datensatz hoher Prioritaet in der Hand.
|
||||
Es hat in dieser Zeit keine Verbindung aufgebaut. Nicht der Schlaf.
|
||||
* **"`close_code 17` ist neu, das kommt vom frisch gesetzten Ping."** Ueber
|
||||
alle 4190 Protokolleintraege gezaehlt: erstmals **11.08.2026**, 23 Mal.
|
||||
Nichts Neues.
|
||||
* **"Der Sendetakt im Stand steht auf 24 h."** Das stand in flespis `current` -
|
||||
und `current` ist, was das Geraet ZULETZT GEMELDET hat. Die aufgespielte
|
||||
Konfiguration hat dort 0. Der Eigentuemer hat es richtiggestellt: **"die
|
||||
gelesenen werte sind veraltet"**.
|
||||
|
||||
**Merkposten:** `flespi.INTERESSANT` vergleicht `trip_scenario.ign_off_timeout`
|
||||
gegen unsere Konstante `NACHLAUF_S`. Es las 900, unsere Konstante ist 900, also
|
||||
meldete der Waechter `abweichung: false`. Das Geraet hat dort **0** - das
|
||||
Trip-Szenario ist abgeschaltet. Der Waechter, der genau dieses Auseinanderlaufen
|
||||
melden soll, ist auf einen veralteten Wert hereingefallen, weil er nie fragt,
|
||||
wie alt `current` ist. **Ein stiller Gleichstand mit einem veralteten Wert ist
|
||||
schlimmer als gar kein Vergleich.** (Noch offen.)
|
||||
|
||||
### Was die Ursache war - gemessen, nicht erschlossen
|
||||
|
||||
Das Geraet war von 18:25 bis 23:36 ohne Verbindung (fest im geparkten Auto,
|
||||
niemand hat etwas angefasst). Als es sich meldete, kamen zwei Zuendungs-
|
||||
ereignisse in **derselben Sekunde** bei Home Assistant an:
|
||||
|
||||
| Ankunft | Geraetezeit | Rueckstand |
|
||||
|---|---|---|
|
||||
| 23:36:50 | **18:23:32** | 5 h 13 - das Aus der ersten Fahrt |
|
||||
| 23:36:50 | **23:35:43** | 67 s - das Ein der zweiten |
|
||||
|
||||
Die Pausenregel fragt: "kommt die Zuendung innerhalb von 15 Minuten zurueck?"
|
||||
Sie mass das an der **Ankunft**. Nach Ankunft: 0 Sekunden, also dieselbe Fahrt.
|
||||
Nach **Geraetezeit**: 5 Stunden 12, also zwei Fahrten.
|
||||
|
||||
Ergebnis: **ein Eintrag von 18:10 bis 23:42, 5 h 33, ohne Strecke** - und die
|
||||
Fahrt um 23:30 gab es gar nicht. Sie steckte darin.
|
||||
|
||||
Dazu kamen zwei Stellen im Code:
|
||||
|
||||
1. `zuendung_geaendert()` rief **bedingungslos** `warte_ende_ab_abbrechen()`.
|
||||
Das "an" loeschte damit das wartende Ende des "aus" derselben Sekunde.
|
||||
2. Der Fall "**an, waehrend eine Fahrt laeuft**" fiel durch alle drei Zweige
|
||||
von `_signalwechsel()` - wortlos.
|
||||
|
||||
Zu 2. steht im Code seit dem 02.09. ein langer Kommentar: damals hat genau das
|
||||
"eine ganze Fahrt gekostet". Eingebaut wurde daraufhin eine **Sperre**, damit
|
||||
die Ereignisse sich nicht ueberholen. Die Reihenfolge stimmt seither - **der
|
||||
Ausgang nicht.** Die Sperre hat das Symptom behandelt.
|
||||
|
||||
### Die Korrektur
|
||||
|
||||
`_zuendung_kehrt_zurueck()` entscheidet jetzt in **Geraetezeit**:
|
||||
|
||||
* Der Koordinator fuehrt mit, welches Ende gerade seine Pausenzeit absitzt
|
||||
(`ende_wartet`, Beginn und Ende beide in Geraetezeit). Ohne diese Angabe
|
||||
konnte der Beobachter nur abbrechen.
|
||||
* Liegt zwischen dem echten Ende und dem neuen "an" **mehr** als die
|
||||
Pausenzeit, wird die alte Fahrt auf ihr bereits berechnetes Ende geschlossen
|
||||
und eine neue begonnen. Sonst bleibt es wie bisher dieselbe Fahrt.
|
||||
* Gemessen wird ab dem **echten Ende** (Zuendungs-Aus minus Nachlauf), nicht ab
|
||||
dem Signal - sonst waere die Grenze um 180 s verschoben.
|
||||
* Ohne wartendes Ende passiert weiterhin nichts. Fehlt das "aus" ganz, waere
|
||||
ein Schnitt auf Verdacht schlimmer als eine zu lange Fahrt.
|
||||
|
||||
Der bedingungslose Abbruch ist weg; abgebrochen wird jetzt in jedem Zweig
|
||||
einzeln, dort wo die Geraetezeit bekannt ist.
|
||||
|
||||
**Das behebt die Verspaetung nicht - es macht sie harmlos.** Ein fuenf Stunden
|
||||
zu spaet gelieferter Stapel ergibt jetzt zwei richtige Fahrten statt einer
|
||||
falschen.
|
||||
|
||||
### Die Tests mussten zwei Dinge trennen
|
||||
|
||||
`PAUSE = 0.15` war bisher beides zugleich: das Fenster der Pausenregel und die
|
||||
Wanduhrzeit, die der Test absitzt. Seit die Regel in Geraetezeit rechnet, geht
|
||||
das nicht mehr - mit 0,15 s als Fenster haette der Test etwas anderes geprueft
|
||||
als der Betrieb tut. Jetzt: `PAUSE_S = 900` fuer die Entscheidung,
|
||||
`WARTEN = 0.15` fuer das Warten (ueber ein kurzes `_nach_pause_beenden`).
|
||||
|
||||
### Verifiziert
|
||||
|
||||
**18 Tests** in `test_zuendungspause.py` (von 14), dazu 5 + 13 in den beiden
|
||||
anderen Dateien - alle gruen. `py_compile` sauber. `audi_ha_test` auf
|
||||
`2026.9.4.24`, **0 Tracebacks**.
|
||||
|
||||
**Gegenprobe am alten Stand gelaufen** (der neue Zweig durch das alte
|
||||
`warte_ende_ab_abbrechen()` ersetzt): **2 der 4 neuen Tests scheitern**, mit der
|
||||
Korrektur laufen alle 18. Die beiden anderen neuen Tests sind Gegenproben in
|
||||
die andere Richtung (kurze Rueckkehr, fehlendes "aus") und gruen in beiden
|
||||
Staenden - so sollen sie sein.
|
||||
|
||||
### Was offen bleibt
|
||||
|
||||
* Der flespi-Waechter meldet Gleichstand gegen veraltete Werte (oben).
|
||||
* `NACHLAUF_S = 900` ist als "Trip: Ignition OFF Timeout" beschriftet; das
|
||||
Geraet hat dort 0. Die 900, die es wirklich gibt, sind der Schlaf-Timeout -
|
||||
ein anderer Parameter.
|
||||
* **Am Dongle wurde nichts geaendert.** Der Eigentuemer hat am Morgen des
|
||||
04.09. `1003` (Network Ping) von 0 auf 60 und `1004` (Ack Type) auf 1
|
||||
gesetzt; zugestellt wurden sie um 18:08, mitten in der ersten Fahrt. Bei der
|
||||
zweiten Fahrt hielt die Sitzung 18 Minuten durch und schloss normal, der
|
||||
Rueckstand lag durchgehend bei 1-3 Sekunden. **Das wirkt** - die
|
||||
Zustellluecke selbst ist damit aber noch nicht als behoben bewiesen.
|
||||
|
||||
@@ -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"}
|
||||
Binary file not shown.
@@ -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": [
|
||||
|
||||
@@ -56,6 +56,10 @@ class FakeKoordinator:
|
||||
# die Wartezeit - die hat test_zuendungspause.py.
|
||||
self.zuordnung = FakeZuordnung()
|
||||
|
||||
# Seit die Pausenregel in Geraetezeit rechnet, liest _signalwechsel() das
|
||||
# wartende Ende. Hier wartet nie eines - None heisst genau das.
|
||||
ende_wartet = None
|
||||
|
||||
def warte_ende_ab_abbrechen(self):
|
||||
pass
|
||||
|
||||
|
||||
@@ -33,9 +33,16 @@ from custom_components.audi_dashboard import fahrterkennung as f # noqa: E402
|
||||
|
||||
T0 = datetime.datetime(2026, 9, 2, 16, 32, 18, tzinfo=datetime.UTC)
|
||||
AUS = datetime.datetime(2026, 9, 2, 16, 54, 21, tzinfo=datetime.UTC)
|
||||
# So kurz, dass der Test schnell bleibt - die Laenge ist hier nicht der Punkt,
|
||||
# sondern ob sie abgebrochen wird.
|
||||
PAUSE = 0.15
|
||||
# Zwei verschiedene Dinge, seit die Pausenregel in GERAETEZEIT rechnet:
|
||||
#
|
||||
# PAUSE_S ist das Fenster, in dem eine zurueckkehrende Zuendung noch dieselbe
|
||||
# Fahrt ist - gemessen an den Zeitstempeln des Geraets. Es muss realistisch
|
||||
# sein, sonst prueft der Test etwas anderes als der Betrieb tut.
|
||||
#
|
||||
# WARTEN ist die Wanduhrzeit, die der Test tatsaechlich absitzt. Sie darf kurz
|
||||
# sein; ob abgebrochen wird, haengt nicht an ihrer Laenge.
|
||||
PAUSE_S = 900
|
||||
WARTEN = 0.15
|
||||
|
||||
|
||||
class FakeWerte:
|
||||
@@ -72,15 +79,18 @@ class FakeKoordinator:
|
||||
self.ablage = FakeAblage(minuten, fahrten)
|
||||
self.beendet = []
|
||||
self._aufgabe = None
|
||||
self.ende_wartet = None
|
||||
|
||||
def warte_ende_ab(self, koroutine):
|
||||
def warte_ende_ab(self, koroutine, wartet=None):
|
||||
self.warte_ende_ab_abbrechen()
|
||||
self.ende_wartet = wartet
|
||||
self._aufgabe = asyncio.ensure_future(koroutine)
|
||||
|
||||
def warte_ende_ab_abbrechen(self):
|
||||
if self._aufgabe is not None and not self._aufgabe.done():
|
||||
self._aufgabe.cancel()
|
||||
self._aufgabe = None
|
||||
self.ende_wartet = None
|
||||
|
||||
async def fahrt_start_setzen(self, ts):
|
||||
self.fahrt_start_ts = ts
|
||||
@@ -101,7 +111,7 @@ class FakeKoordinator:
|
||||
class Basis(unittest.IsolatedAsyncioTestCase):
|
||||
def _pause_kurz(self):
|
||||
async def pausenzeit(k):
|
||||
return 0 if k.zuordnung.werte.TRIP_SENSOR else PAUSE
|
||||
return 0 if k.zuordnung.werte.TRIP_SENSOR else PAUSE_S
|
||||
|
||||
return pausenzeit
|
||||
|
||||
@@ -122,15 +132,24 @@ class Basis(unittest.IsolatedAsyncioTestCase):
|
||||
# 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.
|
||||
await asyncio.sleep(WARTEN)
|
||||
await asyncio.shield(f._jetzt_schliessen(k, start_ts, ende_ts))
|
||||
|
||||
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._nach_pause_beenden = self._echt_nach_pause
|
||||
|
||||
|
||||
class Wartezeit(Basis):
|
||||
@@ -148,9 +167,9 @@ class Wartezeit(Basis):
|
||||
"""Der eigentliche Zweck der Wartezeit."""
|
||||
k = FakeKoordinator()
|
||||
await f.zuendung_geaendert(k, "off", "on", AUS)
|
||||
await asyncio.sleep(PAUSE / 3)
|
||||
await asyncio.sleep(WARTEN / 3)
|
||||
await f.zuendung_geaendert(k, "on", "off", AUS + datetime.timedelta(seconds=20))
|
||||
await asyncio.sleep(PAUSE * 2)
|
||||
await asyncio.sleep(WARTEN * 2)
|
||||
self.assertEqual(k.beendet, [], "eine zurueckgekehrte Zuendung beendet nichts")
|
||||
self.assertEqual(k.fahrt_start_ts, T0, "es bleibt dieselbe Fahrt")
|
||||
|
||||
@@ -161,7 +180,7 @@ class Wartezeit(Basis):
|
||||
t = AUS
|
||||
for _ in range(4):
|
||||
await f.zuendung_geaendert(k, "off", "on", t)
|
||||
await asyncio.sleep(PAUSE / 5)
|
||||
await asyncio.sleep(WARTEN / 5)
|
||||
t += datetime.timedelta(seconds=45)
|
||||
await f.zuendung_geaendert(k, "on", "off", t)
|
||||
t += datetime.timedelta(seconds=200)
|
||||
@@ -222,6 +241,102 @@ class SpaetesAus(Basis):
|
||||
)
|
||||
|
||||
|
||||
|
||||
class GepufferterStapel(Basis):
|
||||
"""Der Fehler vom 04.09.2026: zwei Fahrten wurden zu einer.
|
||||
|
||||
Das Geraet war fuenf Stunden ohne Verbindung. Als es sich meldete, kamen
|
||||
das Zuendungs-Aus der ersten Fahrt und das Zuendungs-Ein der zweiten in
|
||||
DERSELBEN Sekunde bei Home Assistant an (23:36:50). Ihre Geraetezeiten
|
||||
lagen 5 Stunden 12 auseinander: 18:23:32 und 23:35:43.
|
||||
|
||||
Nach der Ankunft gerechnet sieht das aus wie eine Zuendung, die sofort
|
||||
zurueckkommt - also dieselbe Fahrt. Genau so wurde gerechnet, und dabei ist
|
||||
aus zwei Fahrten eine geworden: 18:10 bis 23:42, 5 Stunden 33, ohne
|
||||
Strecke. Die zweite Fahrt gab es gar nicht.
|
||||
|
||||
Die Ankunftszeit spielt hier bewusst KEINE Rolle - beide Aufrufe folgen
|
||||
unmittelbar aufeinander, so wie sie es an dem Abend taten.
|
||||
"""
|
||||
|
||||
async def test_zwei_fahrten_bleiben_zwei(self):
|
||||
beginn1 = datetime.datetime(2026, 9, 4, 16, 10, 27, tzinfo=datetime.UTC)
|
||||
aus1 = datetime.datetime(2026, 9, 4, 18, 23, 32, tzinfo=datetime.UTC)
|
||||
an2 = datetime.datetime(2026, 9, 4, 21, 35, 43, tzinfo=datetime.UTC)
|
||||
k = FakeKoordinator(start_ts=beginn1)
|
||||
|
||||
await f.zuendung_geaendert(k, "off", "on", aus1)
|
||||
await f.zuendung_geaendert(k, "on", "off", an2)
|
||||
|
||||
self.assertEqual(
|
||||
len(k.beendet), 1, "die erste Fahrt muss geschlossen worden sein"
|
||||
)
|
||||
start, ende = k.beendet[0]
|
||||
self.assertEqual(start, beginn1)
|
||||
self.assertEqual(
|
||||
ende, aus1 - datetime.timedelta(seconds=f.ZUENDUNG_NACHLAUF_S),
|
||||
"geschlossen wird auf das Zuendungs-Aus minus Nachlauf",
|
||||
)
|
||||
self.assertEqual(
|
||||
k.fahrt_start_ts, an2, "und die zweite Fahrt beginnt mit ihrem eigenen an"
|
||||
)
|
||||
|
||||
async def test_kurze_rueckkehr_bleibt_eine_fahrt(self):
|
||||
"""Die Gegenprobe - sonst wuerde jeder Tankstopp die Fahrt zerreissen.
|
||||
|
||||
Derselbe Ablauf, nur liegen die Geraetezeiten acht Minuten
|
||||
auseinander statt fuenf Stunden.
|
||||
"""
|
||||
beginn = datetime.datetime(2026, 9, 4, 16, 10, 27, tzinfo=datetime.UTC)
|
||||
aus = datetime.datetime(2026, 9, 4, 18, 23, 32, tzinfo=datetime.UTC)
|
||||
an = aus + datetime.timedelta(minutes=8)
|
||||
k = FakeKoordinator(start_ts=beginn)
|
||||
|
||||
await f.zuendung_geaendert(k, "off", "on", aus)
|
||||
await f.zuendung_geaendert(k, "on", "off", an)
|
||||
|
||||
self.assertEqual(k.beendet, [], "innerhalb der Pause wird nichts geschlossen")
|
||||
self.assertEqual(k.fahrt_start_ts, beginn, "es bleibt dieselbe Fahrt")
|
||||
|
||||
async def test_genau_an_der_grenze_bleibt_es_eine_fahrt(self):
|
||||
"""Die Pausenzeit gehoert noch zur Fahrt, nicht schon zur naechsten.
|
||||
|
||||
Gemessen wird ab dem ECHTEN Ende (Zuendungs-Aus minus Nachlauf), nicht
|
||||
ab dem Zuendungssignal - sonst waere die Grenze um 180 s verschoben.
|
||||
"""
|
||||
beginn = datetime.datetime(2026, 9, 4, 16, 10, 27, tzinfo=datetime.UTC)
|
||||
aus = datetime.datetime(2026, 9, 4, 18, 23, 32, tzinfo=datetime.UTC)
|
||||
echtes_ende = aus - datetime.timedelta(seconds=f.ZUENDUNG_NACHLAUF_S)
|
||||
k = FakeKoordinator(start_ts=beginn)
|
||||
|
||||
await f.zuendung_geaendert(k, "off", "on", aus)
|
||||
await f.zuendung_geaendert(
|
||||
k, "on", "off", echtes_ende + datetime.timedelta(seconds=PAUSE_S)
|
||||
)
|
||||
self.assertEqual(k.beendet, [], "genau auf der Grenze noch dieselbe Fahrt")
|
||||
|
||||
k2 = FakeKoordinator(start_ts=beginn)
|
||||
await f.zuendung_geaendert(k2, "off", "on", aus)
|
||||
await f.zuendung_geaendert(
|
||||
k2, "on", "off", echtes_ende + datetime.timedelta(seconds=PAUSE_S + 1)
|
||||
)
|
||||
self.assertEqual(len(k2.beendet), 1, "eine Sekunde darueber sind es zwei")
|
||||
|
||||
async def test_ohne_wartendes_ende_wird_nichts_zerschnitten(self):
|
||||
"""Ein "an" ohne vorheriges "aus" darf keine Fahrt beenden.
|
||||
|
||||
Fehlt das Aus ganz - weil es nie ankam -, waere ein Schnitt auf
|
||||
Verdacht schlimmer als eine zu lange Fahrt. Dann passiert nichts,
|
||||
wie bisher.
|
||||
"""
|
||||
beginn = datetime.datetime(2026, 9, 4, 16, 10, 27, tzinfo=datetime.UTC)
|
||||
k = FakeKoordinator(start_ts=beginn)
|
||||
await f.zuendung_geaendert(
|
||||
k, "on", "off", beginn + datetime.timedelta(hours=5)
|
||||
)
|
||||
self.assertEqual(k.beendet, [])
|
||||
self.assertEqual(k.fahrt_start_ts, beginn)
|
||||
|
||||
class Einstellung(unittest.IsolatedAsyncioTestCase):
|
||||
"""_pausenzeit_s selbst - hier ungepatcht."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user