4d4890034a
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>
526 lines
19 KiB
Python
526 lines
19 KiB
Python
"""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)
|