Fahrt beenden auf Knopfdruck, Schieberegler, Zuendungspause (2026.9.3.5-.7)
Vier Themen aus einer Sitzung. 1. Die Fahrt endet wieder bei uns statt im Dongle (Abschnitt BW). Der Ignition-OFF-Timeout des FMM003 (900 s) und sein Schlaf-Timeout (ebenfalls 900 s) starten beide beim Zuendungs-Aus und fallen in derselben Sekunde - am 02.09. zweimal beobachtet, einmal ging das Trip-Ende verloren, einmal kam es eine Sekunde vor dem Schlaf an. Ausloeser ist jetzt die Zuendung, die Wartezeit laeuft in Home Assistant. ZUENDUNG_NACHLAUF_S = 180 wird in beiden Wegen abgezogen (ueber das Trip-Signal 1080 s, weil dessen Ende selbst an der verzoegerten Zuendung haengt). 2. Schieberegler, neu in beiden Codebasen. Schrittweite 5 Minuten; der Daumen war im Tagmodus weiss auf weiss und traegt jetzt einen Ring aus --line-strong - ein Token, das genau dort sichtbar ist, wo es gebraucht wird. 3. Knopf "Fahrt beenden" in der Zuletzt-Kachel (Abschnitt BX). Schliesst auf das letzte Lebenszeichen, nicht auf "jetzt", und kennzeichnet das Ende als vorlaeufig. Ein spaet eintreffendes Zuendungs-Aus zieht es nach - innerhalb von sechs Stunden, nur nach vorn, und nur bei einer vorlaeufigen Fahrt. ts_end wandert bewusst NICHT in edited_fields, sonst blockierte der Schutz fuer Handeingaben genau diese Korrektur. 4. Das OTA-Buendel kann auf dem Mac gar nicht entstehen (Abschnitt BV): npm run ota ist eine Windows-PowerShell-Datei, ios-signieren.sh baut ein frisches dist/ und fasst das Buendel nie an. Ausgeliefert war deshalb eine Oberflaeche ohne den Versionshinweis unter richtiger Nummer. Verifiziert: Backend 27/27 (5 Signalwechsel, 14 Zuendungspause, 8 Fahrtende), companion-app tsc sauber und 179/179, Panel als Modul geparst, audi_ha_test auf 2026.9.3.7 sauber gestartet. Live am laufenden Panel und ohne Rueckstand belegt: Wartezeit samt Abbruch, die 180-s-Rechnung, der Regler im Tagmodus und der Knopf von der laufenden Fahrt bis zum verworfenen Kurzvorgang - Fahrten vorher 16, nachher 16. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Der Knopf "Fahrt beenden" und das Nachziehen des vorlaeufigen Endes.
|
||||
|
||||
Der Fall, fuer den es den Knopf gibt: das Fahrzeug steht in einer Tiefgarage
|
||||
ohne Empfang. Das Geraet kann sein Zuendungs-Aus nicht melden, die Fahrt bliebe
|
||||
offen, bis irgendwann gepufferte Datensaetze eintreffen. Der Knopf schliesst
|
||||
sie auf das letzte Lebenszeichen - mehr weiss in dem Moment niemand.
|
||||
|
||||
Genau deshalb ist dieses Ende VORLAEUFIG: faehrt das Fahrzeug danach noch aus
|
||||
der Garage heraus, kommt der Rest nach, und dann gehoert das Ende nachgezogen
|
||||
statt verworfen. Geprueft wird beides - dass der Knopf das Richtige schliesst,
|
||||
und dass die spaete Meldung die Fahrt korrigiert statt eine neue anzulegen.
|
||||
|
||||
Aufruf: python3 tests/fahrterkennung/test_fahrtende_knopf.py
|
||||
(braucht das homeassistant-Paket, weil die Integration es importiert - also
|
||||
z. B. im Test-Container)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
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 fahrterkennung as f # noqa: E402
|
||||
|
||||
T0 = datetime.datetime(2026, 9, 3, 8, 0, 0, tzinfo=datetime.UTC)
|
||||
LEBENSZEICHEN = datetime.datetime(2026, 9, 3, 8, 20, 0, tzinfo=datetime.UTC)
|
||||
|
||||
|
||||
class FakeWerte:
|
||||
TRIP_SENSOR = ""
|
||||
ZUENDUNG_SENSOR = "binary_sensor.zuendung"
|
||||
|
||||
|
||||
class FakeZuordnung:
|
||||
werte = FakeWerte()
|
||||
|
||||
|
||||
class FakeAblage:
|
||||
def __init__(self, fahrten=None):
|
||||
self.fahrten = fahrten or []
|
||||
self.aktualisiert = []
|
||||
|
||||
async def profil_lesen(self):
|
||||
return {"einstellungen": {"fahrten_pausenzeit_min": 15}}
|
||||
|
||||
async def fahrten_lesen(self):
|
||||
return self.fahrten
|
||||
|
||||
async def fahrt_anhaengen(self, fahrt):
|
||||
self.fahrten.append(fahrt)
|
||||
|
||||
async def fahrt_aktualisieren(self, trip_id, aenderungen):
|
||||
self.aktualisiert.append((trip_id, aenderungen))
|
||||
for fahrt in self.fahrten:
|
||||
if fahrt.get("trip_id") == trip_id:
|
||||
fahrt.update(aenderungen)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class FakeKoordinator:
|
||||
def __init__(self, start_ts=T0, fahrten=None):
|
||||
self.fahrt_start_ts = start_ts
|
||||
self.fahrt_sperre = asyncio.Lock()
|
||||
self.zuordnung = FakeZuordnung()
|
||||
self.ablage = FakeAblage(fahrten)
|
||||
self.hass = object()
|
||||
self.veroeffentlicht = 0
|
||||
self.abgebrochen = 0
|
||||
|
||||
def warte_ende_ab_abbrechen(self):
|
||||
self.abgebrochen += 1
|
||||
|
||||
async def fahrt_start_setzen(self, ts):
|
||||
self.fahrt_start_ts = ts
|
||||
|
||||
async def fahrzeugstatus_veroeffentlichen(self):
|
||||
pass
|
||||
|
||||
async def fahrten_veroeffentlichen(self):
|
||||
self.veroeffentlicht += 1
|
||||
|
||||
|
||||
def fahrt(trip_id, start, ende, vorlaeufig):
|
||||
return {
|
||||
"trip_id": trip_id,
|
||||
"ts_start": start.isoformat(),
|
||||
"ts_end": ende.isoformat(),
|
||||
"duration_s": int((ende - start).total_seconds()),
|
||||
"ende_vorlaeufig": vorlaeufig,
|
||||
}
|
||||
|
||||
|
||||
class Basis(unittest.IsolatedAsyncioTestCase):
|
||||
async def asyncSetUp(self):
|
||||
self._echt = (
|
||||
f._letztes_lebenszeichen,
|
||||
f._geraetezeit,
|
||||
f.screening.durchfuehren,
|
||||
f.reifen.aktiver_satz,
|
||||
)
|
||||
|
||||
async def lebenszeichen(k, sensor, seit):
|
||||
return LEBENSZEICHEN
|
||||
|
||||
async def geraetezeit(k, ereigniszeit, standard):
|
||||
return ereigniszeit or standard
|
||||
|
||||
async def durchfuehren(k):
|
||||
return None
|
||||
|
||||
def aktiver_satz(profil):
|
||||
return "sommer"
|
||||
|
||||
f._letztes_lebenszeichen = lebenszeichen
|
||||
f._geraetezeit = geraetezeit
|
||||
f.screening.durchfuehren = durchfuehren
|
||||
f.reifen.aktiver_satz = aktiver_satz
|
||||
|
||||
async def asyncTearDown(self):
|
||||
(
|
||||
f._letztes_lebenszeichen,
|
||||
f._geraetezeit,
|
||||
f.screening.durchfuehren,
|
||||
f.reifen.aktiver_satz,
|
||||
) = self._echt
|
||||
|
||||
|
||||
class Knopf(Basis):
|
||||
async def test_schliesst_auf_das_letzte_lebenszeichen(self):
|
||||
"""Nicht auf 'jetzt': das Fahrzeug stand still, als es still stand -
|
||||
und genau dann liegen die beiden weit auseinander."""
|
||||
k = FakeKoordinator()
|
||||
self.assertTrue(await f.jetzt_beenden(k))
|
||||
self.assertEqual(len(k.ablage.fahrten), 1)
|
||||
gespeichert = k.ablage.fahrten[0]
|
||||
self.assertEqual(gespeichert["ts_end"], LEBENSZEICHEN.isoformat())
|
||||
self.assertTrue(gespeichert["ende_vorlaeufig"])
|
||||
self.assertIsNone(k.fahrt_start_ts)
|
||||
|
||||
async def test_ohne_laufende_fahrt_passiert_nichts(self):
|
||||
k = FakeKoordinator(start_ts=None)
|
||||
self.assertFalse(await f.jetzt_beenden(k))
|
||||
self.assertEqual(k.ablage.fahrten, [])
|
||||
|
||||
async def test_bricht_eine_wartende_ende_aufgabe_ab(self):
|
||||
"""Sonst schloesse die Wartezeit die Fahrt ein zweites Mal."""
|
||||
k = FakeKoordinator()
|
||||
await f.jetzt_beenden(k)
|
||||
self.assertEqual(k.abgebrochen, 1)
|
||||
|
||||
|
||||
class Nachziehen(Basis):
|
||||
async def _spaetes_aus(self, k, gemeldet):
|
||||
await f._spaetes_aus(k, gemeldet, gemeldet)
|
||||
|
||||
async def test_spaeteres_aus_zieht_das_vorlaeufige_ende_nach(self):
|
||||
"""Der eigentliche Zweck: nach der Tiefgarage kommt der Rest gepuffert
|
||||
nach, und die Fahrt endet dort, wo sie wirklich endete."""
|
||||
k = FakeKoordinator(
|
||||
start_ts=None,
|
||||
fahrten=[fahrt("t-1", T0, LEBENSZEICHEN, True)],
|
||||
)
|
||||
# Gemeldet wird das verzoegerte Zuendungs-Aus; echt ist es
|
||||
# ZUENDUNG_NACHLAUF_S frueher.
|
||||
echt = LEBENSZEICHEN + datetime.timedelta(minutes=12)
|
||||
gemeldet = echt + datetime.timedelta(seconds=f.ZUENDUNG_NACHLAUF_S)
|
||||
await self._spaetes_aus(k, gemeldet)
|
||||
|
||||
self.assertEqual(len(k.ablage.aktualisiert), 1)
|
||||
_, aenderungen = k.ablage.aktualisiert[0]
|
||||
self.assertEqual(aenderungen["ts_end"], echt.isoformat())
|
||||
self.assertEqual(aenderungen["duration_s"], int((echt - T0).total_seconds()))
|
||||
self.assertFalse(aenderungen["ende_vorlaeufig"])
|
||||
self.assertNotIn(
|
||||
"edited_fields",
|
||||
aenderungen,
|
||||
"ts_end darf nicht als Handeingabe gelten - das blockierte die "
|
||||
"naechste Korrektur",
|
||||
)
|
||||
|
||||
async def test_regulaer_geschlossene_fahrt_wird_nicht_angefasst(self):
|
||||
k = FakeKoordinator(
|
||||
start_ts=None,
|
||||
fahrten=[fahrt("t-1", T0, LEBENSZEICHEN, False)],
|
||||
)
|
||||
echt = LEBENSZEICHEN + datetime.timedelta(minutes=12)
|
||||
await self._spaetes_aus(
|
||||
k, echt + datetime.timedelta(seconds=f.ZUENDUNG_NACHLAUF_S)
|
||||
)
|
||||
self.assertEqual(k.ablage.aktualisiert, [])
|
||||
|
||||
async def test_frueheres_aus_verkuerzt_die_fahrt_nicht(self):
|
||||
k = FakeKoordinator(
|
||||
start_ts=None,
|
||||
fahrten=[fahrt("t-1", T0, LEBENSZEICHEN, True)],
|
||||
)
|
||||
echt = LEBENSZEICHEN - datetime.timedelta(minutes=5)
|
||||
await self._spaetes_aus(
|
||||
k, echt + datetime.timedelta(seconds=f.ZUENDUNG_NACHLAUF_S)
|
||||
)
|
||||
self.assertEqual(k.ablage.aktualisiert, [])
|
||||
|
||||
async def test_zu_weit_danach_wird_nicht_korrigiert(self):
|
||||
"""Jenseits des Korrekturfensters gehoert das Aus wahrscheinlich zu
|
||||
einer ganz anderen Fahrt."""
|
||||
k = FakeKoordinator(
|
||||
start_ts=None,
|
||||
fahrten=[fahrt("t-1", T0, LEBENSZEICHEN, True)],
|
||||
)
|
||||
echt = LEBENSZEICHEN + datetime.timedelta(seconds=f.KORREKTUR_FENSTER_S + 60)
|
||||
with self.assertLogs(f._LOGGER, level="WARNING"):
|
||||
await self._spaetes_aus(
|
||||
k, echt + datetime.timedelta(seconds=f.ZUENDUNG_NACHLAUF_S)
|
||||
)
|
||||
self.assertEqual(k.ablage.aktualisiert, [])
|
||||
|
||||
async def test_nur_die_juengste_fahrt_wird_korrigiert(self):
|
||||
"""Eine aeltere vorlaeufige Fahrt darf ein spaetes Aus nicht an sich
|
||||
ziehen - sonst wanderte ihr Ende ueber die naechste Fahrt hinweg."""
|
||||
alt_start = T0 - datetime.timedelta(hours=3)
|
||||
k = FakeKoordinator(
|
||||
start_ts=None,
|
||||
fahrten=[
|
||||
fahrt("t-alt", alt_start, alt_start + datetime.timedelta(minutes=10), True),
|
||||
fahrt("t-neu", T0, LEBENSZEICHEN, False),
|
||||
],
|
||||
)
|
||||
echt = LEBENSZEICHEN + datetime.timedelta(minutes=5)
|
||||
await self._spaetes_aus(
|
||||
k, echt + datetime.timedelta(seconds=f.ZUENDUNG_NACHLAUF_S)
|
||||
)
|
||||
self.assertEqual(
|
||||
k.ablage.aktualisiert, [], "die juengste Fahrt ist regulaer geschlossen"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -33,6 +33,15 @@ T0 = datetime.datetime(2026, 9, 2, 16, 32, 18, tzinfo=datetime.UTC)
|
||||
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"
|
||||
|
||||
|
||||
class FakeZuordnung:
|
||||
werte = FakeWerte()
|
||||
|
||||
|
||||
class FakeKoordinator:
|
||||
"""Nur das, was der Signalpfad anfasst."""
|
||||
|
||||
@@ -41,6 +50,11 @@ 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.
|
||||
self.zuordnung = FakeZuordnung()
|
||||
|
||||
def warte_ende_ab_abbrechen(self):
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Die Wartezeit nach dem Zuendungs-Aus, und die beiden Nachlaeufe 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
|
||||
stand auf denselben 900 s. Beide Uhren starten beim Zuendungs-Aus und fallen in
|
||||
derselben Sekunde. Am 02.09.2026 ist das zweimal beobachtet worden, mit
|
||||
entgegengesetztem Ausgang: die Fahrt um 16:54 verlor ihr Trip-Ende (danach
|
||||
1 h 48 min ohne einen einzigen Datensatz), die um 21:43 bekam es um 21:58:16,
|
||||
genau 901 s spaeter. Ein Wettlauf, den unsere Seite nicht gewinnen kann.
|
||||
|
||||
Deshalb loest jetzt die Zuendung aus, und die Wartezeit laeuft bei uns - auf
|
||||
einer Maschine, die nicht schlaeft. Geprueft wird genau das: dass eine
|
||||
zurueckkehrende Zuendung die Fahrt fortsetzt statt sie zu teilen, dass die
|
||||
Nachlaeufe des Geraets abgezogen werden, und dass ein spaet eintreffendes Aus
|
||||
keine Phantomfahrt erzeugt.
|
||||
|
||||
Aufruf: python3 tests/fahrterkennung/test_zuendungspause.py
|
||||
(braucht das homeassistant-Paket, weil die Integration es importiert - also
|
||||
z. B. im Test-Container)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
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 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
|
||||
|
||||
|
||||
class FakeWerte:
|
||||
def __init__(self, trip=""):
|
||||
self.TRIP_SENSOR = trip
|
||||
self.ZUENDUNG_SENSOR = "binary_sensor.zuendung"
|
||||
|
||||
|
||||
class FakeZuordnung:
|
||||
def __init__(self, trip=""):
|
||||
self.werte = FakeWerte(trip)
|
||||
|
||||
|
||||
class FakeAblage:
|
||||
def __init__(self, minuten=15, fahrten=None):
|
||||
self.profil = {"einstellungen": {"fahrten_pausenzeit_min": minuten}}
|
||||
self.fahrten = fahrten or []
|
||||
|
||||
async def profil_lesen(self):
|
||||
return self.profil
|
||||
|
||||
async def fahrten_lesen(self):
|
||||
return self.fahrten
|
||||
|
||||
|
||||
class FakeKoordinator:
|
||||
def __init__(self, start_ts=T0, trip="", minuten=15, fahrten=None):
|
||||
self.fahrt_start_ts = start_ts
|
||||
self.fahrt_sperre = asyncio.Lock()
|
||||
self.zuordnung = FakeZuordnung(trip)
|
||||
self.ablage = FakeAblage(minuten, fahrten)
|
||||
self.beendet = []
|
||||
self._aufgabe = None
|
||||
|
||||
def warte_ende_ab(self, koroutine):
|
||||
self.warte_ende_ab_abbrechen()
|
||||
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
|
||||
|
||||
async def fahrt_start_setzen(self, ts):
|
||||
self.fahrt_start_ts = ts
|
||||
|
||||
async def fahrzeugstatus_veroeffentlichen(self):
|
||||
pass
|
||||
|
||||
async def ausklingen(self):
|
||||
"""Der wartenden Aufgabe Zeit geben, fertig zu werden."""
|
||||
if self._aufgabe is None:
|
||||
return
|
||||
try:
|
||||
await asyncio.wait_for(asyncio.shield(self._aufgabe), timeout=2)
|
||||
except (asyncio.CancelledError, asyncio.TimeoutError):
|
||||
pass
|
||||
|
||||
|
||||
class Basis(unittest.IsolatedAsyncioTestCase):
|
||||
def _pause_kurz(self):
|
||||
async def pausenzeit(k):
|
||||
return 0 if k.zuordnung.werte.TRIP_SENSOR else PAUSE
|
||||
|
||||
return pausenzeit
|
||||
|
||||
async def asyncSetUp(self):
|
||||
self._echt = (
|
||||
f._geraetezeit, f.fahrt_beenden, f._pausenzeit_s, f._nachlauf_gegenpruefen,
|
||||
)
|
||||
|
||||
async def geraetezeit(k, ereigniszeit, standard):
|
||||
return ereigniszeit or standard
|
||||
|
||||
async def beenden(k, start_ts, ende_ts):
|
||||
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
|
||||
|
||||
f._geraetezeit = geraetezeit
|
||||
f.fahrt_beenden = beenden
|
||||
f._pausenzeit_s = self._pause_kurz()
|
||||
f._nachlauf_gegenpruefen = gegenpruefen
|
||||
|
||||
async def asyncTearDown(self):
|
||||
(
|
||||
f._geraetezeit, f.fahrt_beenden, f._pausenzeit_s, f._nachlauf_gegenpruefen,
|
||||
) = self._echt
|
||||
|
||||
|
||||
class Wartezeit(Basis):
|
||||
async def test_aus_schliesst_nicht_sofort(self):
|
||||
k = FakeKoordinator()
|
||||
await f.zuendung_geaendert(k, "off", "on", AUS)
|
||||
self.assertEqual(
|
||||
k.beendet, [], "waehrend der Wartezeit darf nichts geschrieben sein"
|
||||
)
|
||||
self.assertIsNotNone(k.fahrt_start_ts)
|
||||
await k.ausklingen()
|
||||
self.assertEqual(len(k.beendet), 1, "nach der Wartezeit muss die Fahrt enden")
|
||||
|
||||
async def test_zuendung_kommt_zurueck_und_die_fahrt_laeuft_weiter(self):
|
||||
"""Der eigentliche Zweck der Wartezeit."""
|
||||
k = FakeKoordinator()
|
||||
await f.zuendung_geaendert(k, "off", "on", AUS)
|
||||
await asyncio.sleep(PAUSE / 3)
|
||||
await f.zuendung_geaendert(k, "on", "off", AUS + datetime.timedelta(seconds=20))
|
||||
await asyncio.sleep(PAUSE * 2)
|
||||
self.assertEqual(k.beendet, [], "eine zurueckgekehrte Zuendung beendet nichts")
|
||||
self.assertEqual(k.fahrt_start_ts, T0, "es bleibt dieselbe Fahrt")
|
||||
|
||||
async def test_start_stopp_zerlegt_die_fahrt_nicht(self):
|
||||
"""Der RS4 stellt an der Ampel den Motor ab - aus einer Drehzahlquelle
|
||||
ist das nicht vom Abstellen zu unterscheiden."""
|
||||
k = FakeKoordinator()
|
||||
t = AUS
|
||||
for _ in range(4):
|
||||
await f.zuendung_geaendert(k, "off", "on", t)
|
||||
await asyncio.sleep(PAUSE / 5)
|
||||
t += datetime.timedelta(seconds=45)
|
||||
await f.zuendung_geaendert(k, "on", "off", t)
|
||||
t += datetime.timedelta(seconds=200)
|
||||
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):
|
||||
async def test_zuendungsweg_zieht_180_s_ab(self):
|
||||
k = FakeKoordinator()
|
||||
await f.zuendung_geaendert(k, "off", "on", AUS)
|
||||
await k.ausklingen()
|
||||
_, 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))
|
||||
await f.zuendung_geaendert(k, "off", "on", AUS)
|
||||
await k.ausklingen()
|
||||
start, ende = k.beendet[0]
|
||||
self.assertEqual(ende, start, "kuerzer als der Nachlauf heisst: nicht gefahren")
|
||||
|
||||
|
||||
class SpaetesAus(Basis):
|
||||
async def test_aus_zur_geschlossenen_fahrt_wird_verworfen(self):
|
||||
"""Gepuffert eintreffendes Aus, dessen Geraetezeit in die schon
|
||||
geschlossene Fahrt faellt - es darf nichts ausloesen."""
|
||||
ende = AUS - datetime.timedelta(seconds=180)
|
||||
k = FakeKoordinator(
|
||||
start_ts=None,
|
||||
fahrten=[{"ts_start": T0.isoformat(), "ts_end": ende.isoformat()}],
|
||||
)
|
||||
await f.zuendung_geaendert(k, "off", "on", AUS)
|
||||
self.assertEqual(k.beendet, [])
|
||||
self.assertIsNone(k.fahrt_start_ts, "es darf keine Fahrt entstehen")
|
||||
|
||||
async def test_aus_ohne_jede_fahrt_meldet_sich(self):
|
||||
k = FakeKoordinator(start_ts=None, fahrten=[])
|
||||
with self.assertLogs(f._LOGGER, level="WARNING") as protokoll:
|
||||
await f.zuendung_geaendert(k, "off", "on", AUS)
|
||||
self.assertTrue(
|
||||
any("entgangen" in z for z in protokoll.output),
|
||||
"ein Aus ohne passende Fahrt gehoert ins Protokoll",
|
||||
)
|
||||
|
||||
|
||||
class Einstellung(unittest.IsolatedAsyncioTestCase):
|
||||
"""_pausenzeit_s selbst - hier ungepatcht."""
|
||||
|
||||
async def test_wert_aus_dem_profil(self):
|
||||
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()
|
||||
k.ablage.profil = {"einstellungen": {"fahrten_pausenzeit_min": "viel"}}
|
||||
self.assertEqual(await f._pausenzeit_s(k), f.STANDARD_PAUSENZEIT_MIN * 60)
|
||||
|
||||
async def test_obergrenze_greift(self):
|
||||
k = FakeKoordinator(minuten=9999)
|
||||
self.assertEqual(await f._pausenzeit_s(k), f.PAUSENZEIT_MAX_MIN * 60)
|
||||
|
||||
async def test_null_ist_erlaubt(self):
|
||||
"""Wer keine Pause will, bekommt keine - dann endet die Fahrt sofort."""
|
||||
k = FakeKoordinator(minuten=0)
|
||||
self.assertEqual(await f._pausenzeit_s(k), 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Reference in New Issue
Block a user