Regler stellt den Schlaf-Timeout des Dongles (2026.9.3.10)

Der Regler "Fahrt beenden" ist jetzt eine Zahl fuer zwei Dinge: unsere eigene
Wartezeit bis zum Fahrtende und der Schlaf-Timeout des FMM003 (103). So lange
bleibt der Dongle wach und wartet darauf, dass es gleich weitergeht; danach
schlaeft er. Die 180 s Ignition OFF Delay laufen davor und bleiben unsichtbar -
sie werden nur von der Fahrtdauer abgezogen. Der Regler geht deshalb von 1 bis
60 in Einerschritten; das Geraet kennt kein Timeout 0.

flespi.py liest und schreibt die Geraete-Konfiguration. Drei Annahmen sind an
der echten API gefallen und stehen als Messung im Modulkopf: der Wert liegt in
current (nicht value), die Einstellung heisst sleep_mode mit dem Wert eine Ebene
tiefer unter mode, und ein PUT braucht {"properties": ..., "address": ...}.

Die eigene Auftrags-Warteschlange ist wieder entfallen. Sie war fuer "das Geraet
schlaeft" gebaut - das gilt aber nur fuer das Geraet, nicht fuer die
Schnittstelle: die vollstaendige Konfiguration kam herein, waehrend das Fahrzeug
seit dem Vortag stand. flespi puffert selbst (pending + address=connection), ein
zweites Auftragsbuch waere eine zweite Warteschlange fuer dieselbe Aufgabe.

Gegengelesen wird nach jedem Schreiben; als Erfolg zaehlt current ODER pending -
letzteres ist bei stehendem Fahrzeug der Normalfall.

Live gegen den echten Account belegt: Token sieht genau ein Geraet, 127
Einstellungen bei parkendem Fahrzeug gelesen, Regler 15 geschrieben und als
pending bestaetigt (Nachbarfelder unveraendert), zweiter Neustart ohne erneute
Anfrage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 20:00:39 +02:00
parent 8c96d9b78e
commit 4d4890034a
18 changed files with 999 additions and 30 deletions
@@ -485,18 +485,47 @@ async def _spaetes_aus(
ende = await _geraetezeit(k, ereigniszeit, jetzt)
echt = ende - datetime.timedelta(seconds=ZUENDUNG_NACHLAUF_S)
juengste = None
# Zwei verschiedene Fragen, deshalb zwei Suchen ueber denselben Bestand:
#
# juengstes_ende beantwortet "gehoert das Aus zur zuletzt geschlossenen
# Fahrt?" - das ist der Verwerfen-Zweig unten.
#
# kandidat beantwortet "gibt es eine von Hand beendete Fahrt, deren Ende
# dieses Aus nachtraegt?". Bewusst NICHT auf die juengste Fahrt beschraenkt:
# trifft das gepufferte Aus erst ein, nachdem schon eine weitere Fahrt
# aufgezeichnet wurde, waere die vorlaeufige sonst fuer immer zu kurz. Es
# gewinnt die vorlaeufige Fahrt, deren Ende dem Aus am naechsten VOR ihm
# liegt - jede spaetere kann nicht gemeint sein.
fahrten = await k.ablage.fahrten_lesen()
juengstes_ende = None
for fahrt in await k.ablage.fahrten_lesen():
kandidat = None
kandidat_ende = None
for fahrt in fahrten:
roh = fahrt.get("ts_end")
if not roh:
continue
ts = zeit_normalisiert(roh)
if juengstes_ende is None or ts > juengstes_ende:
juengste, juengstes_ende = fahrt, ts
juengstes_ende = ts
if fahrt.get("ende_vorlaeufig") and ts < echt:
if kandidat_ende is None or ts > kandidat_ende:
kandidat, kandidat_ende = fahrt, ts
if juengste is not None and await _vorlaeufiges_ende_korrigieren(
k, juengste, juengstes_ende, echt
# Eine Fahrt darf nicht ueber eine andere hinweg verlaengert werden. Faengt
# zwischen dem vorlaeufigen Ende und dem Aus eine Fahrt an, gehoert das Aus
# zu ihr oder zu etwas Spaeterem - jedenfalls nicht mehr zur vorlaeufigen,
# und zwei sich ueberlappende Fahrten waeren schlimmer als eine zu kurze.
if kandidat is not None and _fahrt_dazwischen(fahrten, kandidat, kandidat_ende, echt):
_LOGGER.info(
"Zuendungs-Aus (Geraetezeit %s) koennte die vorlaeufige Fahrt %s "
"verlaengern, aber dazwischen liegt bereits eine andere Fahrt - "
"nicht korrigiert",
echt.isoformat(), kandidat["trip_id"],
)
kandidat = None
if kandidat is not None and await _vorlaeufiges_ende_korrigieren(
k, kandidat, kandidat_ende, echt
):
return
@@ -517,6 +546,25 @@ async def _spaetes_aus(
)
def _fahrt_dazwischen(
fahrten: list[dict],
kandidat: dict,
kandidat_ende: datetime.datetime,
echt: datetime.datetime,
) -> bool:
"""Faengt zwischen dem vorlaeufigen Ende und dem Aus eine andere Fahrt an?"""
for fahrt in fahrten:
if fahrt.get("trip_id") == kandidat.get("trip_id"):
continue
roh = fahrt.get("ts_start")
if not roh:
continue
beginn = zeit_normalisiert(roh)
if kandidat_ende < beginn <= echt:
return True
return False
async def _vorlaeufiges_ende_korrigieren(
k: Koordinator,
fahrt: dict,
@@ -532,10 +580,17 @@ async def _vorlaeufiges_ende_korrigieren(
verworfen.
Drei Bedingungen, jede aus einem eigenen Grund: nur eine als vorlaeufig
gekennzeichnete Fahrt (eine regulaer geschlossene ist bereits richtig),
nur ein SPAETERES Ende (frueher waere kein Nachtrag, sondern eine
Verkuerzung), und nur innerhalb von KORREKTUR_FENSTER_S (sonst gehoert das
Aus wahrscheinlich zu einer ganz anderen Fahrt).
gekennzeichnete Fahrt (eine regulaer geschlossene ist bereits richtig - ihr
Ende IST dieses Signal, ein zweites waere nur die gepufferte Kopie), nur ein
SPAETERES Ende (frueher waere kein Nachtrag, sondern eine Verkuerzung), und
nur innerhalb von KORREKTUR_FENSTER_S.
Zum Fenster: gemessen wird zwischen dem vorlaeufigen Ende und der
GERAETEZEIT des Aus - nicht zwischen Knopfdruck und Eintreffen. Ein
Datensatz, der eine Woche in der Tiefgarage liegt, traegt trotzdem seinen
eigenen Zeitstempel von damals (siehe verlauf.geraetezeit); das Fenster
begrenzt also, wie lange nach dem letzten Lebenszeichen noch gefahren
worden sein kann, nicht wie lange das Geraet puffern darf.
ts_end wandert bewusst NICHT in edited_fields: das Feld ist keine
Handeingabe, und dort eingetragen wuerde es genau diese Korrektur beim