Tahoma: verlorene Funkbefehle erkennen und nachsenden
Der Funk von der Box zum Motor ist unbestaetigt. Die Box nimmt einen Befehl an und meldet Erfolg, auch wenn er den Motor nie erreicht - dann faehrt der Behang gar nicht. Erkennbar ist das allein daran, dass er hinterher nicht dort steht, wo er stehen soll. Genau das ist jetzt die Bedingung fuer den zweiten Versuch. Der Ansatz stand schon in der Datei, konnte aber nicht wirken: Die Endstellung wurde gegen Neigung 0 geprueft, obwohl gerade die gewuenschte Neigung geschickt worden war. Damit lieferte die Wartefunktion immer "nicht erreicht" - verlorener Befehl und geglueckte Fahrt sahen gleich aus, jedes Kommando lief zweimal 120 Sekunden und ging zweimal raus. aktion["actor_name"] gibt es im Auftrag nicht; der Name steht eine Ebene hoeher. Der KeyError flog nach der Vorstufe, also nachdem "Neigung 0" schon draussen war: der Behang fuhr auf Position und liess die Lamellen offen. 23 Mal im Protokoll, zuletzt am 21.09. um 18:49 an acht Jalousien. Jetzt: _positionsZiel() leitet das Ziel auch fuer "up" und "down" ab (0 bzw. 100 Prozent Schliessung). Ohne das haette ausgerechnet der haeufigste Befehl kein pruefbares Ziel und der Wiederholversuch liefe fuer ihn leer. _zieleErreicht() vergleicht mit zwei Prozentpunkten Spielraum: io-Motoren melden fuer befohlene 100 gern 99 oder 101. Ohne Toleranz gaelte eine geglueckte Fahrt als verloren. None heisst "ist egal" - damit funktionieren auch Rollladen ohne Lamellen und reine Neigungsbefehle. _fahren() haelt die Schleife an einer Stelle. Vor der Wiederholung wird noch einmal nachgesehen, weil der Stand nur alle zwei Sekunden gelesen wird und der Behang in der letzten Sekunde angekommen sein kann. Ohne pruefbares Ziel (stop, my, wink) wird einmal geschickt und nicht gewartet. Wiederholt wird ausdruecklich immer, wenn das Ziel nicht erreicht ist - auch wenn der Behang unterwegs war und woanders stehengeblieben ist. Die Unterscheidung waere ueber core:MovingState moeglich und ist bewusst nicht gewollt. Geprueft ohne Schaltbefehle: Ziel erreicht -> einmal gesendet; Befehl verloren -> zweimal; erster verloren, zweiter kommt an -> zweimal, Erfolg; 99 statt 100 -> einmal; Rollladen ohne Lamellen, stop und reine Neigung jeweils richtig. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1242,6 +1242,9 @@ class Runner:
|
||||
continue
|
||||
auftrag = {
|
||||
"actor_url": kommando["actor_url"],
|
||||
# Nur fuer Protokollmeldungen des Transports: "io://1215-.../332898"
|
||||
# sagt niemandem, welcher Rollladen gemeint ist.
|
||||
"actor_name": kommando["actor_name"],
|
||||
"command_url": kommando["command_url"],
|
||||
"params": [{"url": p["url"], "name": p["parameter_name"],
|
||||
"wert": aktion["werte"].get(p["id"], "")}
|
||||
|
||||
+106
-24
@@ -455,6 +455,14 @@ class TahomaTransport(Transport):
|
||||
# nach einem Kommando steht dort noch der alte Wert.
|
||||
JALOUSIE_VORLAUF_SEKUNDEN = 8
|
||||
|
||||
# So oft wird ein Kommando hoechstens geschickt, wenn der Behang danach
|
||||
# nicht auf seinem Ziel steht. Zwei Versuche, weil ein dritter bei einem
|
||||
# wirklich stummen Motor nur Zeit kostet - siehe _fahren().
|
||||
JALOUSIE_VERSUCHE = 2
|
||||
|
||||
# Wie weit der gemeldete Stand vom Ziel abweichen darf, in Prozentpunkten.
|
||||
JALOUSIE_TOLERANZ = 2
|
||||
|
||||
def __init__(self, requests_modul, pin, token, timeout=10, dry_run=False):
|
||||
self.requests = requests_modul
|
||||
self.pin = pin
|
||||
@@ -608,20 +616,78 @@ class TahomaTransport(Transport):
|
||||
" (zuerst %s, dann warten)" % vorstufe if umweg else "")
|
||||
return
|
||||
|
||||
if umweg:
|
||||
self._apply(aktion["actor_url"], befehl, vorstufe)
|
||||
if(not self._warteAufJalousie(aktion["actor_url"], aktion["actor_name"]+" 1.Versuch", 0,
|
||||
None if position_index is None else int(parameter[position_index]))):
|
||||
self._apply(aktion["actor_url"], befehl, vorstufe)
|
||||
self._warteAufJalousie(aktion["actor_url"], aktion["actor_name"]+" 2.Versuch", 0,
|
||||
None if position_index is None else int(parameter[position_index]))
|
||||
self._apply(aktion["actor_url"], befehl, parameter)
|
||||
if(not self._warteAufJalousie(aktion["actor_url"], aktion["actor_name"]+" 1.VersuchEndPos", 0,
|
||||
None if position_index is None else int(parameter[position_index]))):
|
||||
self._apply(aktion["actor_url"], befehl, parameter)
|
||||
self._warteAufJalousie(aktion["actor_url"], aktion["actor_name"]+" 2.VersuchEndPos", 0,
|
||||
None if position_index is None else int(parameter[position_index]))
|
||||
position_ziel = self._positionsZiel(befehl, parameter, position_index)
|
||||
neigung_ziel = (int(parameter[neigung_index])
|
||||
if neigung_index is not None else None)
|
||||
|
||||
if umweg:
|
||||
# Dieselbe Position, aber Neigung 0 - das ist der ganze Sinn der
|
||||
# Vorstufe.
|
||||
self._fahren(aktion, befehl, vorstufe, 0, position_ziel, "Vorstufe")
|
||||
self._fahren(aktion, befehl, parameter, neigung_ziel, position_ziel, "Endstellung")
|
||||
|
||||
def _positionsZiel(self, befehl, parameter, position_index):
|
||||
"""
|
||||
Wo der Behang danach stehen soll - oder None, wenn das niemand weiss.
|
||||
|
||||
Bei setClosure und setClosureAndOrientation steht es im Parameter.
|
||||
"up" und "down" tragen keinen, meinen aber null bzw. hundert Prozent
|
||||
Schliessung; ohne diese Zuordnung haette ausgerechnet der haeufigste
|
||||
Befehl kein pruefbares Ziel, und der Wiederholversuch liefe fuer ihn
|
||||
leer.
|
||||
|
||||
Bei "my", "stop" und "wink" gibt es wirklich keines. Dort wird nichts
|
||||
geprueft und nichts wiederholt - siehe _fahren().
|
||||
"""
|
||||
if position_index is not None:
|
||||
return int(parameter[position_index])
|
||||
return {"up": 0, "down": 100}.get(befehl)
|
||||
|
||||
def _fahren(self, aktion, befehl, parameter, neigung_ziel, position_ziel, schritt):
|
||||
"""
|
||||
Ein Kommando schicken und wiederholen, bis der Behang sein Ziel zeigt.
|
||||
|
||||
Der Funk von der Box zum Motor ist unbestaetigt: die Box nimmt den
|
||||
Befehl an und meldet Erfolg, auch wenn er den Motor nie erreicht -
|
||||
dann faehrt der Behang gar nicht. Erkennbar ist das allein daran,
|
||||
dass er hinterher nicht dort steht, wo er stehen soll. Genau das ist
|
||||
die Bedingung fuer den zweiten Versuch.
|
||||
|
||||
Wiederholt wird in jedem Fall, in dem das Ziel nicht erreicht ist.
|
||||
Ob der Behang unterwegs war, liesse sich an core:MovingState ablesen
|
||||
und koennte einen Abbruch begruenden (Hindernis, Wandschalter) - das
|
||||
ist hier bewusst nicht gewollt: ein Behang, der nicht dort steht, wo
|
||||
er stehen soll, soll es noch einmal versuchen.
|
||||
|
||||
Ohne pruefbares Ziel wird einmal geschickt und nicht gewartet. Bei
|
||||
"stop" auf das Ende einer Fahrt zu warten waere ein Widerspruch in
|
||||
sich, und zu pruefen gaebe es nichts.
|
||||
"""
|
||||
name = aktion.get("actor_name") or aktion["actor_url"]
|
||||
if neigung_ziel is None and position_ziel is None:
|
||||
self._apply(aktion["actor_url"], befehl, parameter)
|
||||
return True
|
||||
|
||||
for versuch in range(1, self.JALOUSIE_VERSUCHE + 1):
|
||||
self._apply(aktion["actor_url"], befehl, parameter)
|
||||
if self._warteAufJalousie(aktion["actor_url"],
|
||||
"%s, %s, Versuch %d" % (name, schritt, versuch),
|
||||
neigung_ziel, position_ziel):
|
||||
return True
|
||||
if versuch >= self.JALOUSIE_VERSUCHE:
|
||||
break
|
||||
# Vor der zweiten Fahrt noch einmal hinsehen. Der Stand wird alle
|
||||
# zwei Sekunden gelesen und von der Box traege gemeldet - er kann
|
||||
# in der letzten Sekunde des Fensters angekommen sein, und dann
|
||||
# waere die Wiederholung eine Fahrt zuviel.
|
||||
if self._zieleErreicht(self._zustaende(aktion["actor_url"]),
|
||||
neigung_ziel, position_ziel):
|
||||
return True
|
||||
logger.info("%s (%s): nicht auf dem Ziel, %d. Versuch",
|
||||
name, schritt, versuch + 1)
|
||||
logger.warning("%s (%s): Ziel nach %d Versuchen nicht erreicht",
|
||||
name, schritt, self.JALOUSIE_VERSUCHE)
|
||||
return False
|
||||
def _kannKombi(self, actor_url):
|
||||
"""
|
||||
Hat das Geraet ein Kommando fuer Position und Neigung zusammen?
|
||||
@@ -640,6 +706,29 @@ class TahomaTransport(Transport):
|
||||
raise RuntimeError("Tahoma antwortete mit %d: %s"
|
||||
% (antwort.status_code, antwort.text[:120]))
|
||||
|
||||
def _zieleErreicht(self, zustaende, neigung_ziel, schliessung_ziel):
|
||||
"""
|
||||
Zeigt das Geraet die gewuenschten Werte? None heisst "ist egal".
|
||||
|
||||
Verglichen wird mit Spielraum: io-Motoren melden fuer befohlene 100 %
|
||||
gern 99 oder 101. Ohne Toleranz gaelte eine geglueckte Fahrt als
|
||||
verloren, und der Behang fuehre ein zweites Mal - genau das, was der
|
||||
Wiederholversuch verhindern soll.
|
||||
|
||||
Ein fehlendes Feld gilt nie als erreicht. Als 0 durchgehen zu lassen
|
||||
waere ausgerechnet beim Ziel 0 ein falsches Erfolgssignal.
|
||||
"""
|
||||
if zustaende is None:
|
||||
return False
|
||||
|
||||
def passt(wert, ziel):
|
||||
if ziel is None:
|
||||
return True
|
||||
return wert is not None and abs(int(wert) - ziel) <= self.JALOUSIE_TOLERANZ
|
||||
|
||||
return (passt(zustaende.get("core:SlateOrientationState"), neigung_ziel)
|
||||
and passt(zustaende.get("core:ClosureState"), schliessung_ziel))
|
||||
|
||||
def _warteAufJalousie(self, actor_url, actor_name, neigung_ziel, schliessung_ziel=None):
|
||||
"""
|
||||
Wartet, bis die Jalousie ihre Fahrt beendet hat und die Ziele zeigt.
|
||||
@@ -662,20 +751,13 @@ class TahomaTransport(Transport):
|
||||
if z.get("core:MovingState") is True:
|
||||
gestartet = True
|
||||
continue
|
||||
neigung = z.get("core:SlateOrientationState")
|
||||
schliessung = z.get("core:ClosureState")
|
||||
# Ein fehlendes Feld darf nicht als 0 durchgehen - das waere
|
||||
# ausgerechnet beim Ziel 0 ein falsches Erfolgssignal.
|
||||
neigung_ok = neigung is not None and int(neigung) == neigung_ziel
|
||||
schliessung_ok = (schliessung_ziel is None
|
||||
or (schliessung is not None and int(schliessung) == schliessung_ziel))
|
||||
if neigung_ok and schliessung_ok and (
|
||||
if self._zieleErreicht(z, neigung_ziel, schliessung_ziel) and (
|
||||
gestartet or time.time() - start >= self.JALOUSIE_VORLAUF_SEKUNDEN):
|
||||
return True
|
||||
logger.warning("%s(%s) hat Neigung %s%% nicht innerhalb von %d s erreicht",
|
||||
actor_url,actor_name, neigung_ziel, self.JALOUSIE_WARTE_SEKUNDEN)
|
||||
logger.warning("%s (%s) steht nach %d s nicht auf Neigung %s / Position %s",
|
||||
actor_url, actor_name, self.JALOUSIE_WARTE_SEKUNDEN,
|
||||
neigung_ziel, schliessung_ziel)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _zahl(wert):
|
||||
"""Tahoma erwartet Zahlen als Zahlen, Text als Text."""
|
||||
|
||||
Reference in New Issue
Block a user