diff --git a/autoActions/autoaction_runner.py b/autoActions/autoaction_runner.py index b718027..015d62d 100644 --- a/autoActions/autoaction_runner.py +++ b/autoActions/autoaction_runner.py @@ -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"], "")} diff --git a/autoActions/transports.py b/autoActions/transports.py index 5bb2c5a..20fcf8f 100644 --- a/autoActions/transports.py +++ b/autoActions/transports.py @@ -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."""