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 <[email protected]>
This commit is contained in:
@@ -0,0 +1,525 @@
|
||||
"""Liest und schreibt die Konfiguration des FMM003 über flespi.
|
||||
|
||||
WARUM ÜBERHAUPT
|
||||
---------------
|
||||
Zwei Zahlen dieser Integration gehören eigentlich dem Gerät und stehen bei uns
|
||||
nur als Konstante bzw. Einstellung: `NACHLAUF_S` (Trip: Ignition OFF Timeout)
|
||||
und die Wartezeit bis zum Fahrtende. Ändert jemand die Gegenstücke am Dongle,
|
||||
rechnet die Fahrterkennung still weiter mit dem alten Wert - und niemand merkt
|
||||
es. Genau das ist am 01.09.2026 passiert, als `400` von 0 auf 180 ging: seither
|
||||
endeten alle Fahrten 180 s zu spät, gefunden erst zwei Tage später beim
|
||||
Nachmessen.
|
||||
|
||||
DER REGLER IST DIE EINE STELLE
|
||||
------------------------------
|
||||
"Fahrt beenden" in den Einstellungen ist der Schlaf-Timeout des Dongles
|
||||
(Parameter `103`, in Minuten) - und zugleich unsere eigene Wartezeit, bevor wir
|
||||
eine Fahrt schliessen. Beides ist derselbe Sachverhalt:
|
||||
|
||||
* So lange bleibt der Dongle nach dem Zündungs-Aus wach und wartet darauf, dass
|
||||
es unmittelbar weitergeht. Schläft er erst, dauert das Aufwachen einige
|
||||
Sekunden - bei einem Kaltstart egal, beim Fortsetzen einer Fahrt nicht.
|
||||
* Genau so lange darf für uns eine zurückkehrende Zündung noch dieselbe Fahrt
|
||||
sein.
|
||||
|
||||
Vor dem Timeout laufen zusätzlich die 180 s `Ignition OFF Delay` (`400`) des
|
||||
Geräts. Die muss der Nutzer nicht kennen - sie werden von der Fahrtdauer
|
||||
abgezogen (`fahrterkennung.ZUENDUNG_NACHLAUF_S`), nicht angezeigt.
|
||||
|
||||
Denselben Sachverhalt an zwei Stellen einzustellen ist die Sorte Doppelung, die
|
||||
in diesem Projekt schon mehrfach auseinandergelaufen ist. Der Regler schreibt
|
||||
deshalb beides: unsere Wartezeit und `103`.
|
||||
|
||||
FLESPI PUFFERT SELBST - DESHALB KEINE EIGENE WARTESCHLANGE
|
||||
-----------------------------------------------------------
|
||||
Erst war hier ein eigenes Auftragsbuch gebaut, weil "Lesen und Schreiben gehen
|
||||
nur bei aktiver Zündung" - der Dongle schläft ja. **Das gilt für das Gerät, aber
|
||||
nicht für diese Schnittstelle**, und das ist gemessen, nicht angenommen: die
|
||||
vollständige Konfiguration kam am 03.09.2026 herein, während das Fahrzeug seit
|
||||
dem Vortag stand.
|
||||
|
||||
flespi ist eine Ebene über dem Gerät. Es kennt zu jeder Einstellung `current`
|
||||
(was das Gerät zuletzt gemeldet hat) und `pending` (was noch zu ihm unterwegs
|
||||
ist), und `"address": "connection"` heisst ausdrücklich: zustellen, sobald sich
|
||||
das Gerät das nächste Mal meldet. Ein zweites Auftragsbuch bei uns wäre eine
|
||||
zweite Warteschlange für dieselbe Aufgabe - mit dem zusätzlichen Nachteil, dass
|
||||
sie nur raten könnte, was die erste schon weiss.
|
||||
|
||||
Geschrieben wird deshalb sofort, und was noch aussteht, steht in `pending` und
|
||||
wird so auch angezeigt.
|
||||
|
||||
WAS DIE ANTWORT WIRKLICH ENTHÄLT
|
||||
--------------------------------
|
||||
Auch das ist gemessen, und es hat zwei Annahmen widerlegt (deshalb steht es hier
|
||||
und nicht nur im Code):
|
||||
|
||||
* Der Wert steht in **`current`**, nicht in `value`.
|
||||
* Die Einstellung heisst **`sleep_mode`**, nicht `sleep_settings`, und der Wert
|
||||
liegt eine Ebene tiefer unter `mode`.
|
||||
* Ein PUT braucht `{"properties": ..., "address": ...}` - ohne `address`
|
||||
antwortet flespi mit `400 the following properties are missing: address`.
|
||||
* `GET .../settings/all` liefert die gefüllten Einträge; ein Abruf einer
|
||||
einzelnen Einstellung kam leer zurück. Gelesen wird deshalb immer `all`.
|
||||
|
||||
Der `Ignition OFF Delay` (`400`) taucht unter keinem Namen auf, den wir sicher
|
||||
zuordnen könnten. Er wird deshalb **nicht** angezeigt - lieber eine Zeile
|
||||
weniger als eine falsch beschriftete.
|
||||
|
||||
WAS DER TOKEN DARF
|
||||
------------------
|
||||
Ein flespi-Token lässt sich per ACL begrenzen. Dieser Weg braucht Lesen **und**
|
||||
Schreiben der Geräte-Konfiguration; mehr sollte er nicht dürfen, dann kann auch
|
||||
ein Fehler in diesem Modul nichts anderes am Fahrzeug verändern.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import aiohttp
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_FLESPI_GERAET, CONF_FLESPI_TOKEN
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .koordinator import Koordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
BASIS = "https://flespi.io"
|
||||
ZEITLIMIT = aiohttp.ClientTimeout(total=20)
|
||||
|
||||
# Die Einstellung, die der Regler schreibt. Der Wert liegt verschachtelt:
|
||||
# current = {"mode": {"timeout": 15, "type": 2, ...}}.
|
||||
SCHLAF_NAME = "sleep_mode"
|
||||
SCHLAF_OBJEKT = "mode"
|
||||
SCHLAF_FELD = "timeout"
|
||||
SCHLAF_SCHLUESSEL = f"{SCHLAF_NAME}.{SCHLAF_OBJEKT}.{SCHLAF_FELD}"
|
||||
|
||||
# Grenzen des Geräts (Angabe des Eigentümers): 1 bis 3000 Minuten. Unser Regler
|
||||
# bleibt deutlich enger; diese Grenzen sind nur der Riegel davor, dass eine 0
|
||||
# oder ein Ausreisser an das Gerät geht.
|
||||
TIMEOUT_MIN = 1
|
||||
TIMEOUT_MAX = 3000
|
||||
|
||||
# "sobald sich das Gerät das nächste Mal meldet" - die Zustellart, die den
|
||||
# schlafenden Dongle überhaupt erreichbar macht. Siehe Modulkopf.
|
||||
ZUSTELLUNG = "connection"
|
||||
|
||||
# Was angezeigt wird, mit der Nummer aus dem Configurator daneben - danach sucht
|
||||
# der Eigentümer dort. Die Pfade sind die gemessenen, nicht die vermuteten.
|
||||
#
|
||||
# "unser" nennt die Konstante dieser Integration, gegen die verglichen wird;
|
||||
# None heisst: nur anzeigen, es gibt nichts zu vergleichen.
|
||||
INTERESSANT: dict[str, dict] = {
|
||||
SCHLAF_SCHLUESSEL: {
|
||||
"nummer": "103",
|
||||
"titel": "Sleep Timeout",
|
||||
"einheit": "Min.",
|
||||
"unser": "regler",
|
||||
},
|
||||
"sleep_mode.mode.type": {
|
||||
"nummer": "102",
|
||||
"titel": "Sleep Mode",
|
||||
"einheit": "",
|
||||
"unser": None,
|
||||
},
|
||||
"trip_scenario.ign_off_timeout": {
|
||||
"nummer": "11804",
|
||||
"titel": "Trip: Ignition OFF Timeout",
|
||||
"einheit": "s",
|
||||
"unser": "NACHLAUF_S",
|
||||
},
|
||||
"trip_scenario.start_speed": {
|
||||
"nummer": "11803",
|
||||
"titel": "Trip: Start Speed",
|
||||
"einheit": "km/h",
|
||||
"unser": None,
|
||||
},
|
||||
"odometer_fmb.source": {
|
||||
"nummer": "11806",
|
||||
"titel": "Odometer-Quelle",
|
||||
"einheit": "",
|
||||
"unser": None,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class FlespiFehler(Exception):
|
||||
"""Der Vorgang ist gescheitert - mit einem Satz, der dem Nutzer etwas sagt."""
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ Zugang
|
||||
|
||||
|
||||
def zugang(k: Koordinator) -> tuple[str, int | None] | None:
|
||||
"""Token und Geräte-Nummer, oder None wenn kein Token hinterlegt ist.
|
||||
|
||||
Die Nummer darf fehlen - dann wird sie einmal ermittelt, sofern der Token
|
||||
genau ein Gerät sieht (siehe `geraet_finden`).
|
||||
"""
|
||||
token = (k.entry.options.get(CONF_FLESPI_TOKEN) or "").strip()
|
||||
if not token:
|
||||
return None
|
||||
roh = k.entry.options.get(CONF_FLESPI_GERAET)
|
||||
try:
|
||||
geraet = int(roh) if roh else None
|
||||
except (TypeError, ValueError):
|
||||
geraet = None
|
||||
return token, geraet
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ HTTP
|
||||
|
||||
|
||||
def _kopf(token: str) -> dict[str, str]:
|
||||
return {"Authorization": f"FlespiToken {token}"}
|
||||
|
||||
|
||||
def _fehlertext(status: int) -> str | None:
|
||||
if status in (401, 403):
|
||||
return (
|
||||
"flespi hat den Token abgelehnt. Er muss die Geräte-Konfiguration "
|
||||
"lesen und schreiben dürfen."
|
||||
)
|
||||
if status == 404:
|
||||
return "flespi kennt dieses Gerät nicht. Stimmt die Geräte-Nummer?"
|
||||
return None
|
||||
|
||||
|
||||
async def _anfrage(
|
||||
hass: HomeAssistant, token: str, verb: str, pfad: str, nutzlast: dict | None = None
|
||||
) -> dict:
|
||||
session = async_get_clientsession(hass)
|
||||
try:
|
||||
async with session.request(
|
||||
verb,
|
||||
f"{BASIS}{pfad}",
|
||||
headers=_kopf(token),
|
||||
json=nutzlast,
|
||||
timeout=ZEITLIMIT,
|
||||
) as antwort:
|
||||
text = _fehlertext(antwort.status)
|
||||
if text:
|
||||
raise FlespiFehler(text)
|
||||
if antwort.status != 200:
|
||||
# flespi nennt im Rumpf den Grund - der ist für die Fehlersuche
|
||||
# mehr wert als die blosse Nummer (so kam die fehlende
|
||||
# "address" heraus, siehe Modulkopf).
|
||||
grund = (await antwort.text())[:200]
|
||||
raise FlespiFehler(f"flespi antwortete mit {antwort.status}: {grund}")
|
||||
return await antwort.json()
|
||||
except asyncio.TimeoutError as fehler:
|
||||
raise FlespiFehler("flespi hat nicht rechtzeitig geantwortet.") from fehler
|
||||
except aiohttp.ClientError as fehler:
|
||||
raise FlespiFehler(f"flespi war nicht erreichbar ({fehler}).") from fehler
|
||||
|
||||
|
||||
async def geraet_finden(hass: HomeAssistant, token: str) -> int:
|
||||
"""Die Geräte-Nummer, wenn der Token genau ein Gerät sieht.
|
||||
|
||||
Sieht er mehrere, wird **nicht** geraten - dann muss die Nummer eingetragen
|
||||
werden. Ein falsch gewähltes Gerät läse und schriebe die Konfiguration
|
||||
eines fremden Trackers, und das fiele niemandem auf.
|
||||
"""
|
||||
daten = await _anfrage(hass, token, "GET", "/gw/devices/all")
|
||||
geraete = daten.get("result") or []
|
||||
if not geraete:
|
||||
raise FlespiFehler("Dieser Token sieht kein einziges Gerät.")
|
||||
if len(geraete) > 1:
|
||||
namen = ", ".join(str(g.get("name") or g.get("id")) for g in geraete[:5])
|
||||
raise FlespiFehler(
|
||||
f"Dieser Token sieht {len(geraete)} Geräte ({namen}) - bitte die "
|
||||
"Geräte-Nummer in den Einstellungen der Integration eintragen."
|
||||
)
|
||||
return int(geraete[0]["id"])
|
||||
|
||||
|
||||
# ------------------------------------------------------ Antwort aufbereiten
|
||||
|
||||
|
||||
def _flachen(ziel: dict[str, object], praefix: str, wert: object) -> None:
|
||||
"""Verschachtelte Objekte als "a.b.c" ablegen, Blätter als Wert.
|
||||
|
||||
`sleep_mode` bringt seinen Wert eine Ebene tiefer unter `mode`; ohne diese
|
||||
Abflachung wäre der Timeout nicht einzeln ansprechbar.
|
||||
"""
|
||||
ziel[praefix] = wert
|
||||
if isinstance(wert, dict):
|
||||
for name, unter in wert.items():
|
||||
_flachen(ziel, f"{praefix}.{name}", unter)
|
||||
|
||||
|
||||
def _sicht(eintraege: list[dict], feld: str) -> dict[str, object]:
|
||||
"""Alle Einstellungen als "Name -> Wert", gelesen aus `current` bzw. `pending`."""
|
||||
flach: dict[str, object] = {}
|
||||
for eintrag in eintraege:
|
||||
name = eintrag.get("name")
|
||||
if not name:
|
||||
continue
|
||||
wert = eintrag.get(feld)
|
||||
if wert is None:
|
||||
continue
|
||||
_flachen(flach, name, wert)
|
||||
return flach
|
||||
|
||||
|
||||
def _roh(eintraege: list[dict], name: str) -> dict | None:
|
||||
for eintrag in eintraege:
|
||||
if eintrag.get("name") == name:
|
||||
wert = eintrag.get("current")
|
||||
return wert if isinstance(wert, dict) else None
|
||||
return None
|
||||
|
||||
|
||||
def vergleichen(
|
||||
aktuell: dict[str, object],
|
||||
offen: dict[str, object],
|
||||
wunsch: int | None,
|
||||
) -> list[dict]:
|
||||
"""Die interessanten Einstellungen mit unserem eigenen Wert daneben."""
|
||||
from . import fahrterkennung
|
||||
|
||||
zeilen = []
|
||||
for schluessel, angabe in INTERESSANT.items():
|
||||
if schluessel not in aktuell and schluessel not in offen:
|
||||
continue
|
||||
geraet = aktuell.get(schluessel)
|
||||
if angabe["unser"] == "regler":
|
||||
unser: object = wunsch
|
||||
elif angabe["unser"]:
|
||||
unser = getattr(fahrterkennung, angabe["unser"], None)
|
||||
else:
|
||||
unser = None
|
||||
# Nur vergleichen, wenn beide Zahlen sind - ein Modus wie "Deep Sleep"
|
||||
# hat kein Gegenstück bei uns.
|
||||
abweichung = (
|
||||
isinstance(geraet, (int, float))
|
||||
and isinstance(unser, (int, float))
|
||||
and int(geraet) != int(unser)
|
||||
)
|
||||
zeilen.append(
|
||||
{
|
||||
"schluessel": schluessel,
|
||||
"nummer": angabe["nummer"],
|
||||
"titel": angabe["titel"],
|
||||
"einheit": angabe["einheit"],
|
||||
"geraet": geraet,
|
||||
# Was noch zum Gerät unterwegs ist - None, wenn nichts aussteht.
|
||||
"offen": offen.get(schluessel),
|
||||
"unser": unser,
|
||||
"abweichung": abweichung,
|
||||
}
|
||||
)
|
||||
return zeilen
|
||||
|
||||
|
||||
def jetzt_iso() -> str:
|
||||
"""Jetzt, als ISO-Zeichenkette in UTC - der Zeitstempel jedes Anzeigestands."""
|
||||
return datetime.datetime.now(datetime.UTC).isoformat()
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ Lesen
|
||||
|
||||
|
||||
async def _geraet_und_token(k: Koordinator) -> tuple[str, int]:
|
||||
daten = zugang(k)
|
||||
if daten is None:
|
||||
raise FlespiFehler(
|
||||
"Kein flespi-Token hinterlegt - unter Einstellungen → Zugänge eintragen."
|
||||
)
|
||||
token, geraet = daten
|
||||
if not geraet:
|
||||
geraet = await geraet_finden(k.hass, token)
|
||||
return token, geraet
|
||||
|
||||
|
||||
async def _alle_einstellungen(
|
||||
hass: HomeAssistant, token: str, geraet: int
|
||||
) -> list[dict]:
|
||||
daten = await _anfrage(hass, token, "GET", f"/gw/devices/{geraet}/settings/all")
|
||||
eintraege = daten.get("result") or []
|
||||
if not eintraege:
|
||||
raise FlespiFehler("flespi hat keine Einstellungen zu diesem Gerät.")
|
||||
return eintraege
|
||||
|
||||
|
||||
def _stand(geraet: int, eintraege: list[dict], wunsch: int | None) -> dict:
|
||||
aktuell = _sicht(eintraege, "current")
|
||||
offen = _sicht(eintraege, "pending")
|
||||
werte = vergleichen(aktuell, offen, wunsch)
|
||||
return {
|
||||
"geraet": geraet,
|
||||
"gelesen_am": jetzt_iso(),
|
||||
"anzahl": len(eintraege),
|
||||
"werte": werte,
|
||||
"abweichungen": sum(1 for z in werte if z["abweichung"]),
|
||||
"offen": sum(1 for z in werte if z["offen"] is not None),
|
||||
"fehler": None,
|
||||
}
|
||||
|
||||
|
||||
async def lesen(k: Koordinator) -> dict:
|
||||
"""Holt die Konfiguration und gibt den fertigen Anzeigestand zurück."""
|
||||
token, geraet = await _geraet_und_token(k)
|
||||
eintraege = await _alle_einstellungen(k.hass, token, geraet)
|
||||
return _stand(geraet, eintraege, wunsch_timeout(k))
|
||||
|
||||
|
||||
# --------------------------------------------------------------- Schreiben
|
||||
|
||||
|
||||
def wunsch_timeout(k: Koordinator) -> int | None:
|
||||
"""Der Wert, den `103` haben soll - aus der Reglerstellung.
|
||||
|
||||
None, solange das Profil noch nicht gelesen wurde; dann gibt es nichts zu
|
||||
vergleichen und nichts zu schreiben.
|
||||
"""
|
||||
minuten = k.pausenzeit_min
|
||||
if minuten is None:
|
||||
return None
|
||||
return max(TIMEOUT_MIN, min(TIMEOUT_MAX, int(minuten)))
|
||||
|
||||
|
||||
async def schlaf_timeout_schreiben(k: Koordinator, minuten: int) -> dict:
|
||||
"""Setzt `103` am Gerät - lesen, schreiben, gegenlesen.
|
||||
|
||||
Gegengelesen wird, weil eine Änderung am Fahrzeug nur dann vertretbar ist,
|
||||
wenn nachher belegt ist, dass sich genau die eine gewollte Stelle geändert
|
||||
hat (Vorgabe des Eigentümers). "Geändert" heisst hier zweierlei, und beides
|
||||
zählt als Erfolg:
|
||||
|
||||
* `current` trägt den neuen Wert - das Gerät war wach und hat ihn schon.
|
||||
* `pending` trägt ihn - flespi hält ihn vor und stellt ihn zu, sobald sich
|
||||
das Gerät meldet. Das ist der Normalfall bei stehendem Fahrzeug.
|
||||
|
||||
Steht er hinterher in keinem von beiden, ist der Schreibvorgang nicht
|
||||
angekommen und das wird als Fehler gemeldet, nicht als Erfolg.
|
||||
"""
|
||||
minuten = max(TIMEOUT_MIN, min(TIMEOUT_MAX, int(minuten)))
|
||||
token, geraet = await _geraet_und_token(k)
|
||||
|
||||
vorher_roh = await _alle_einstellungen(k.hass, token, geraet)
|
||||
vorher = _sicht(vorher_roh, "current")
|
||||
offen_vorher = _sicht(vorher_roh, "pending")
|
||||
if (
|
||||
vorher.get(SCHLAF_SCHLUESSEL) == minuten
|
||||
and offen_vorher.get(SCHLAF_SCHLUESSEL) is None
|
||||
):
|
||||
_LOGGER.debug("Sleep Timeout steht bereits auf %s Min. - nichts zu tun", minuten)
|
||||
return _stand(geraet, vorher_roh, wunsch_timeout(k))
|
||||
|
||||
# Das ganze Objekt zurückschreiben, nur mit geändertem Feld: `type` und die
|
||||
# übrigen Felder gehören dazu und dürfen beim Schreiben nicht verlorengehen.
|
||||
alt = (_roh(vorher_roh, SCHLAF_NAME) or {}).get(SCHLAF_OBJEKT)
|
||||
neu = (
|
||||
{**alt, SCHLAF_FELD: minuten}
|
||||
if isinstance(alt, dict)
|
||||
else {SCHLAF_FELD: minuten}
|
||||
)
|
||||
await _anfrage(
|
||||
k.hass,
|
||||
token,
|
||||
"PUT",
|
||||
f"/gw/devices/{geraet}/settings/{SCHLAF_NAME}",
|
||||
{"properties": {SCHLAF_OBJEKT: neu}, "address": ZUSTELLUNG},
|
||||
)
|
||||
|
||||
nachher_roh = await _alle_einstellungen(k.hass, token, geraet)
|
||||
nachher = _sicht(nachher_roh, "current")
|
||||
offen = _sicht(nachher_roh, "pending")
|
||||
|
||||
if nachher.get(SCHLAF_SCHLUESSEL) == minuten:
|
||||
_LOGGER.info(
|
||||
"Sleep Timeout (103) am Gerät %s auf %s Min. gesetzt - das Gerät hat ihn",
|
||||
geraet,
|
||||
minuten,
|
||||
)
|
||||
elif offen.get(SCHLAF_SCHLUESSEL) == minuten:
|
||||
_LOGGER.info(
|
||||
"Sleep Timeout (103) auf %s Min. bei flespi hinterlegt - das Gerät "
|
||||
"bekommt ihn, sobald es sich meldet",
|
||||
minuten,
|
||||
)
|
||||
else:
|
||||
raise FlespiFehler(
|
||||
"Der Schreibvorgang ist nicht angekommen: das Gerät steht weiter auf "
|
||||
f"{vorher.get(SCHLAF_SCHLUESSEL)} und es liegt nichts bereit."
|
||||
)
|
||||
|
||||
# Nur melden, nicht abbrechen: das Schreiben ist zu diesem Zeitpunkt
|
||||
# geschehen, und ein Abbruch würde daran nichts mehr ändern - eine Warnung
|
||||
# dagegen führt zu der Stelle, an der jemand nachsehen muss.
|
||||
fremd = [
|
||||
s
|
||||
for s in sorted(set(vorher) | set(nachher))
|
||||
if vorher.get(s) != nachher.get(s) and not s.startswith(SCHLAF_NAME)
|
||||
]
|
||||
if fremd:
|
||||
_LOGGER.warning(
|
||||
"Am Gerät hat sich mehr geändert als gewollt: %s", ", ".join(fremd[:5])
|
||||
)
|
||||
|
||||
return _stand(geraet, nachher_roh, wunsch_timeout(k))
|
||||
|
||||
|
||||
async def wunsch_uebernehmen(k: Koordinator) -> None:
|
||||
"""Bringt `103` auf die Reglerstellung, wenn beide auseinanderliegen.
|
||||
|
||||
Wird nach jedem Profil-Schreiben und einmal nach dem Start aufgerufen und
|
||||
ist deshalb bewusst zustandsvergleichend statt ereignisgetrieben: sie muss
|
||||
auch dann zum Ziel führen, wenn eine frühere Änderung nie ankam (flespi
|
||||
nicht erreichbar, Neustart mittendrin).
|
||||
|
||||
Ohne hinterlegten Token passiert gar nichts - dann ist der Regler wie bisher
|
||||
nur unsere eigene Wartezeit.
|
||||
"""
|
||||
if zugang(k) is None:
|
||||
return
|
||||
ziel = wunsch_timeout(k)
|
||||
if ziel is None:
|
||||
return
|
||||
|
||||
# Erst der billige Vergleich gegen den zuletzt gelesenen Stand: stimmt er
|
||||
# schon, kostet das Speichern eines Profils keine einzige Anfrage. Ein noch
|
||||
# ausstehender Wert (`offen`) zählt dabei wie ein gesetzter - er ist schon
|
||||
# unterwegs.
|
||||
if k.flespi_stand:
|
||||
for zeile in k.flespi_stand.get("werte", []):
|
||||
if zeile["schluessel"] != SCHLAF_SCHLUESSEL:
|
||||
continue
|
||||
steht = zeile["offen"] if zeile["offen"] is not None else zeile["geraet"]
|
||||
if isinstance(steht, (int, float)) and int(steht) == ziel:
|
||||
return
|
||||
|
||||
if k.flespi_sperre.locked():
|
||||
return
|
||||
async with k.flespi_sperre:
|
||||
try:
|
||||
stand = await schlaf_timeout_schreiben(k, ziel)
|
||||
except FlespiFehler as fehler:
|
||||
_LOGGER.warning("Sleep Timeout konnte nicht gesetzt werden: %s", fehler)
|
||||
await k.flespi_stand_setzen(
|
||||
{"fehler": str(fehler), "gelesen_am": jetzt_iso()}
|
||||
)
|
||||
return
|
||||
await k.flespi_stand_setzen(stand)
|
||||
|
||||
|
||||
async def jetzt_lesen(k: Koordinator) -> None:
|
||||
"""Der Knopf "Jetzt lesen": Stand holen und hinterlegen.
|
||||
|
||||
Fehler fliegen weiter - hier hat jemand gedrückt und wartet auf eine
|
||||
Antwort, auch auf ein "ging nicht".
|
||||
"""
|
||||
async with k.flespi_sperre:
|
||||
stand = await lesen(k)
|
||||
await k.flespi_stand_setzen(stand)
|
||||
# Ein frischer Stand kann eine Abweichung erst sichtbar machen - dann gehört
|
||||
# sie gleich behoben, statt bis zum nächsten Speichern zu warten.
|
||||
await wunsch_uebernehmen(k)
|
||||
Reference in New Issue
Block a user