diff --git a/ajax/room.php b/ajax/room.php index 1fbb0ca..647cd55 100644 --- a/ajax/room.php +++ b/ajax/room.php @@ -14,6 +14,11 @@ * POST ?action=temp {"room": "EG_Bad", "temp": 21} - Solltemperatur */ +// Ein "Zu" an einer Jalousie faehrt herunter, wartet und neigt danach - das +// dauert in der Summe ueber eine Minute. Die Vorgabe von PHP reicht dafuer +// nicht; ajax/tahoma.php macht es an derselben Stelle genauso. +set_time_limit(180); + require_once("../helper.php"); require_once("../restricted/automations.php"); require_once("../restricted/commands.php"); diff --git a/restricted/autoActions/autoaction_runner.py b/restricted/autoActions/autoaction_runner.py index f7d6c31..83da21e 100644 --- a/restricted/autoActions/autoaction_runner.py +++ b/restricted/autoActions/autoaction_runner.py @@ -47,7 +47,9 @@ import configparser import json import logging import os +import queue import sys +import threading import time from datetime import date, datetime, timedelta @@ -367,6 +369,79 @@ def tag_passt(automatik, jetzt, kalender): return True +# =========================================================================== +# Versand +# =========================================================================== + +class Versand: + """ + Schickt Kommandos im Hintergrund - je Geraet der Reihe nach, ueber + Geraete hinweg nebeneinander. + + Ohne das blockiert ein einziges Kommando die ganze Auswertung: eine + Jalousie zuzufahren dauert ueber eine Minute, weil zwischen den beiden + Schwenkbefehlen auf das Ende der Fahrt gewartet werden muss (siehe + TahomaTransport). So lange kaeme kein Zeit-Ausloeser mehr durch, kein + Messwert wuerde zurueckgeschrieben, und mehrere Rollladen in einer + Automatik wuerden sich aufaddieren. + + Je Geraet eine Warteschlange mit einem eigenen Faden: zwei Kommandos an + dieselbe Jalousie duerfen sich nicht ueberholen - "Neigung 0" und + "Neigung 100" sind sonst wirkungslos oder vertauscht -, zwei Kommandos an + verschiedene Jalousien duerfen ruhig gleichzeitig laufen. + + Die Datenbank bleibt aussen vor: die Faeden melden ihr Ergebnis nur + zurueck, geschrieben wird im Hauptfaden. Eine pymysql-Verbindung ist + nicht fuer mehrere Faeden gedacht. + """ + + def __init__(self): + self.warteschlangen = {} # actor_url -> Queue + self.ergebnisse = queue.Queue() + self.laeuft = True + + def einreihen(self, automation_id, beschreibung, transport, auftrag): + schlange = self.warteschlangen.get(auftrag["actor_url"]) + if schlange is None: + schlange = queue.Queue() + self.warteschlangen[auftrag["actor_url"]] = schlange + faden = threading.Thread(target=self._arbeiten, args=(schlange,), + name="versand", daemon=True) + faden.start() + schlange.put((automation_id, beschreibung, transport, auftrag)) + + def _arbeiten(self, schlange): + while self.laeuft: + posten = schlange.get() + if posten is None: + return + automation_id, beschreibung, transport, auftrag = posten + try: + transport.senden(auftrag) + self.ergebnisse.put((automation_id, beschreibung, None)) + except Exception as fehler: + self.ergebnisse.put((automation_id, beschreibung, str(fehler))) + finally: + schlange.task_done() + + def abholen(self): + """Alles, was seit dem letzten Mal fertig geworden ist.""" + fertig = [] + while True: + try: + fertig.append(self.ergebnisse.get_nowait()) + except queue.Empty: + return fertig + + def offen(self): + return sum(s.unfinished_tasks for s in self.warteschlangen.values()) + + def beenden(self): + self.laeuft = False + for schlange in self.warteschlangen.values(): + schlange.put(None) + + # =========================================================================== # Der Runner # =========================================================================== @@ -387,6 +462,7 @@ class Runner: self._kalender = (None, {"feiertag": False, "ferien": False}) self._letzte_saeuberung = None + self.versand = Versand() self.mqtt = self._mqtt_verbinden() self.transporte = [ MQTTTransport(self.mqtt, self.dry_run), @@ -427,6 +503,16 @@ class Runner: def regelwerk_laden(self): self.regelwerk = Regelwerk.laden(self.db) + + # Welche Geraete Position und Neigung zusammen koennen. Nur die haben + # die Kugelschreiber-Mechanik, und nur bei ihnen wird ein "Zu" zum + # zweifachen Schwenken - siehe TahomaTransport. + kombi = {k["actor_url"] for k in self.regelwerk.kommandos.values() + if k["command_url"] == "setClosureAndOrientation"} + for transport in self.transporte: + if isinstance(transport, TahomaTransport): + transport.kombigeraete_setzen(kombi) + # Jeder Transport bekommt die Messwerte, fuer die er zustaendig ist. for transport in self.transporte: passende = [{"id": s["id"], "actor_url": s["actor_url"], @@ -523,7 +609,15 @@ class Runner: # --- Ausfuehren ------------------------------------------------------ def ausloesen(self, automatik, anlass): + """ + Die Aktionen einer Automatik in den Versand geben. + + Geschickt wird im Hintergrund - eine Jalousie zuzufahren dauert ueber + eine Minute, und so lange darf die Auswertung nicht stehen. Was dabei + schiefgeht, kommt spaeter ueber ergebnisse_verbuchen() ins Protokoll. + """ fehlerText = [] + eingereiht = 0 for aktion in automatik["aktionen"]: kommando = self.regelwerk.kommandos.get(aktion["command_id"]) if kommando is None: @@ -544,17 +638,14 @@ class Runner: "wert": aktion["werte"].get(p["id"], "")} for p in kommando["params"]], } - try: - transport.senden(auftrag) - logger.info("%s: %s -> %s", automatik["name"], kommando["actor_name"], - kommando["command_name"]) - except Exception as fehler: - fehlerText.append("%s: %s" % (kommando["command_name"], fehler)) - logger.error("%s: %s konnte nicht geschickt werden: %s", - automatik["name"], kommando["command_name"], fehler) + beschreibung = "%s: %s" % (kommando["actor_name"], kommando["command_name"]) + logger.info("%s: %s (unterwegs)", automatik["name"], beschreibung) + self.versand.einreihen(automatik["id"], beschreibung, transport, auftrag) + eingereiht += 1 ergebnis = "error" if fehlerText else anlass - detail = "; ".join(fehlerText)[:255] + detail = "; ".join(fehlerText)[:255] if fehlerText \ + else ("%d Kommando(s) unterwegs" % eingereiht) try: with self.db.cursor() as c: c.execute("""UPDATE automations SET last_run = NOW(), changed = changed @@ -566,6 +657,27 @@ class Runner: automatik["last_run"] = datetime.now() self.lief_im_fenster[automatik["id"]] = True + def ergebnisse_verbuchen(self): + """ + Was der Versand inzwischen erledigt hat ins Protokoll schreiben. + + Nur Fehlschlaege bekommen eine eigene Zeile - der Lauf selbst steht + schon drin, und ein Protokoll, das jedes gelungene Kommando einzeln + auffuehrt, findet niemand mehr etwas darin. + """ + for automation_id, beschreibung, fehler in self.versand.abholen(): + if fehler is None: + logger.debug("erledigt: %s", beschreibung) + continue + logger.error("%s konnte nicht geschickt werden: %s", beschreibung, fehler) + try: + with self.db.cursor() as c: + c.execute("""INSERT INTO automation_log (automation_id, result, detail) + VALUES (%s, 'error', %s)""", + (automation_id, ("%s: %s" % (beschreibung, fehler))[:255])) + except Exception as schreibfehler: + logger.warning("Protokoll nicht schreibbar: %s", schreibfehler) + def flanke_merken(self, automatik, erfuellt): if bool(automatik["cond_met"]) == bool(erfuellt): return @@ -674,6 +786,7 @@ class Runner: self.werte_einsammeln(mit_pollen) self.durchlauf() + self.ergebnisse_verbuchen() self.protokoll_saeubern() if jetzt - letztes_pruefen >= neuladen: @@ -690,6 +803,10 @@ class Runner: time.sleep(takt) def beenden(self): + offen = self.versand.offen() + if offen: + logger.info("%d Kommando(s) noch unterwegs - wird nicht abgewartet", offen) + self.versand.beenden() try: self.mqtt.loop_stop() self.mqtt.disconnect() diff --git a/restricted/autoActions/transports.py b/restricted/autoActions/transports.py index 51a429d..e1e75f1 100644 --- a/restricted/autoActions/transports.py +++ b/restricted/autoActions/transports.py @@ -398,15 +398,20 @@ class TahomaTransport(Transport): # Dasselbe steht in restricted/commands.php: beide Versender brauchen es. NEIGUNG_DIREKT_MAX = 30 - # So heisst der Neigungsparameter im Geraetemodell. + # So heissen die beiden Parameter einer Jalousie im Geraetemodell. NEIGUNG_PARAMETER = "Neigung" + POSITION_PARAMETER = "Position" - # So lange wird hoechstens auf das Ende einer Neigungsfahrt gewartet. - # Auf core:MovingState ist dabei kein Verlass - gemessen hat die Box - # waehrend einer Neigungsfahrt kein einziges Mal "faehrt" gemeldet, der - # Zustandswert selbst ist die verlaessliche Auskunft. - # Gemessen braucht eine Jalousie von 100 % auf 0 % gut fuenfzehn Sekunden. - NEIGUNG_WARTE_SEKUNDEN = 25 + # So lange wird hoechstens auf das Ende einer Fahrt gewartet. Gemessen: + # eine Neigung von 100 % auf 0 % dauert gut fuenfzehn Sekunden, eine + # volle Fahrt von oben nach unten rund sechzig. Die Grenze ist die + # Notbremse, nicht die uebliche Dauer. + JALOUSIE_WARTE_SEKUNDEN = 120 + + # So lange gilt ein "faehrt nicht" direkt nach dem Absenden als noch + # nicht aussagekraeftig: die Box meldet core:MovingState traege, kurz + # nach einem Kommando steht dort noch der alte Wert. + JALOUSIE_VORLAUF_SEKUNDEN = 8 def __init__(self, requests_modul, pin, token, timeout=10, dry_run=False): self.requests = requests_modul @@ -415,6 +420,7 @@ class TahomaTransport(Transport): self.timeout = timeout self.dry_run = dry_run self.states = [] + self.kombigeraete = set() def passt(self, actor_url): return bool(self.pin) and ("://" + self.pin + "/") in actor_url @@ -432,6 +438,14 @@ class TahomaTransport(Transport): logger.info("Tahoma: %d Messwerte an %d Geraeten", len(self.states), len({s["actor_url"] for s in self.states})) + def kombigeraete_setzen(self, urls): + """ + Welche Geraete Position und Neigung zusammen koennen - vom Runner aus + dem Geraetemodell gesetzt, damit der Transport dafuer nicht selbst in + die Datenbank greifen muss. + """ + self.kombigeraete = set(urls) + def zustaende_lesen(self): if not self.token: return {} @@ -458,29 +472,53 @@ class TahomaTransport(Transport): raise RuntimeError("Kein Tahoma-Token in der config.ini") # Die Reihenfolge der Parameter ist die aus command_parameters - bei # setClosureAndOrientation also erst Position, dann Winkel. + befehl = aktion["command_url"] parameter = [self._zahl(p["wert"]) for p in aktion["params"]] - - neigungsziel = None - for p in aktion["params"]: + neigung_index = None + position_index = None + for i, p in enumerate(aktion["params"]): if p["name"] == self.NEIGUNG_PARAMETER: - wert = self._zahl(p["wert"]) - if isinstance(wert, (int, float)): - neigungsziel = wert + neigung_index = i + elif p["name"] == self.POSITION_PARAMETER: + position_index = i - # Kugelschreiber-Mechanik, siehe NEIGUNG_DIREKT_MAX. Der Umweg gehoert - # hierher und nicht in die Automatik - sonst muesste ihn jeder kennen, - # der eine Jalousie anspricht. - umweg = neigungsziel is not None and neigungsziel > self.NEIGUNG_DIREKT_MAX + # "Zu" allein macht diese Jalousien nicht dicht: sie faehrt herunter, + # die Lamellen bleiben durch die Mechanik aber bei etwa 30 % offen. + # Gemeint ist "ganz unten, Lamellen geschlossen" - also dasselbe wie + # Position 100 mit Neigung 100, und damit ein Fall fuer die Regel. + if befehl == "down" and self._kannKombi(aktion["actor_url"]): + befehl = "setClosureAndOrientation" + parameter = [100, 100] + position_index, neigung_index = 0, 1 + + # Kugelschreiber-Mechanik, siehe NEIGUNG_DIREKT_MAX. Geschickt wird + # derselbe Befehl zweimal - erst mit Neigung 0, dann mit dem + # gewuenschten Wert. Die Position bleibt dabei stehen, die Jalousie + # faehrt also nur einmal. + umweg = (neigung_index is not None + and isinstance(parameter[neigung_index], (int, float)) + and parameter[neigung_index] > self.NEIGUNG_DIREKT_MAX) + vorstufe = list(parameter) + if umweg: + vorstufe[neigung_index] = 0 if self.dry_run: - logger.info("[dry-run] Tahoma %s %s%s", aktion["command_url"], parameter, - " (mit Umweg ueber Neigung 0)" if umweg else "") + logger.info("[dry-run] Tahoma %s %s%s", befehl, parameter, + " (zuerst %s, dann warten)" % vorstufe if umweg else "") return if umweg: - self._apply(aktion["actor_url"], "setOrientation", [0]) - self._warteAufNeigung(aktion["actor_url"], 0) - self._apply(aktion["actor_url"], aktion["command_url"], parameter) + self._apply(aktion["actor_url"], befehl, vorstufe) + self._warteAufJalousie(aktion["actor_url"], 0, + None if position_index is None else int(parameter[position_index])) + self._apply(aktion["actor_url"], befehl, parameter) + + def _kannKombi(self, actor_url): + """ + Hat das Geraet ein Kommando fuer Position und Neigung zusammen? + Nur solche Geraete sind Jalousien mit der Kugelschreiber-Mechanik. + """ + return actor_url in self.kombigeraete def _apply(self, actor_url, name, parameter): """Ein Kommando an die Box schicken.""" @@ -493,27 +531,45 @@ class TahomaTransport(Transport): raise RuntimeError("Tahoma antwortete mit %d: %s" % (antwort.status_code, antwort.text[:120])) - def _warteAufNeigung(self, actor_url, ziel): - """Wartet, bis die Neigung den Zielwert zeigt.""" - for _ in range(self.NEIGUNG_WARTE_SEKUNDEN): - time.sleep(1) + def _warteAufJalousie(self, actor_url, neigung_ziel, schliessung_ziel=None): + """ + Wartet, bis die Jalousie ihre Fahrt beendet hat und die Ziele zeigt. + + Zwei Auskuenfte zusammen, weil einzeln keine traegt: + core:MovingState taugt fuer die lange Fahrt hoch und runter, wird bei + kurzen Neigungsfahrten aber nie gesetzt; die Zustandswerte sind die + eigentliche Wahrheit, zeigen kurz nach dem Kommando aber noch den + alten Stand. Fertig ist die Fahrt, wenn nichts mehr faehrt, die Ziele + erreicht sind und entweder ein "faehrt" gesehen wurde oder der + Vorlauf um ist. + """ + start = time.time() + gestartet = False + while time.time() - start < self.JALOUSIE_WARTE_SEKUNDEN: + time.sleep(2) try: antwort = self.requests.get( self.basis + "/setup/devices/" + quote(actor_url, safe="") + "/states", headers=self._kopf(), timeout=self.timeout, verify=False) - for zustand in antwort.json(): - if zustand.get("name") != "core:SlateOrientationState": - continue - wert = zustand.get("value") - # Ein fehlendes Feld darf nicht als 0 durchgehen - das - # waere ausgerechnet beim Ziel 0 ein falsches Erfolgs- - # signal. - if wert is not None and int(wert) == ziel: - return True + z = {x.get("name"): x.get("value") for x in antwort.json()} except Exception as fehler: - logger.debug("Neigung nicht lesbar: %s", fehler) - logger.warning("Neigung von %s hat %s%% nicht innerhalb von %d s erreicht", - actor_url, ziel, self.NEIGUNG_WARTE_SEKUNDEN) + logger.debug("Zustand nicht lesbar: %s", fehler) + continue + 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 ( + gestartet or time.time() - start >= self.JALOUSIE_VORLAUF_SEKUNDEN): + return True + logger.warning("%s hat Neigung %s%% nicht innerhalb von %d s erreicht", + actor_url, neigung_ziel, self.JALOUSIE_WARTE_SEKUNDEN) return False @staticmethod diff --git a/restricted/commands.php b/restricted/commands.php index c744eaf..6986573 100644 --- a/restricted/commands.php +++ b/restricted/commands.php @@ -44,14 +44,29 @@ const MQTT_PORT = 1883; */ const NEIGUNG_DIREKT_MAX = 30; -/** So heisst der Neigungsparameter im Geraetemodell. */ +/** So heissen die beiden Parameter einer Jalousie im Geraetemodell. */ const NEIGUNG_PARAMETER = "Neigung"; +const POSITION_PARAMETER = "Position"; /** - * So lange wird hoechstens auf das Ende einer Neigungsfahrt gewartet. - * Gemessen braucht eine Jalousie von 100 % auf 0 % gut fuenfzehn Sekunden. + * So lange wird hoechstens auf das Ende einer Fahrt gewartet. + * + * Gemessen: eine Neigung von 100 % auf 0 % dauert gut fuenfzehn Sekunden, + * eine volle Fahrt von oben nach unten rund sechzig. Die Grenze ist die + * Notbremse, nicht die uebliche Dauer. */ -const NEIGUNG_WARTE_SEKUNDEN = 25; +const JALOUSIE_WARTE_SEKUNDEN = 120; + +/** + * So lange gilt ein "faehrt nicht" direkt nach dem Absenden als noch nicht + * aussagekraeftig. + * + * Die Box meldet core:MovingState traege: kurz nach einem Kommando steht + * dort noch der alte Wert. Ohne diesen Vorlauf haelt die Warteschleife die + * Fahrt fuer beendet, bevor sie begonnen hat - und das naechste Kommando + * schneidet sie ab. + */ +const JALOUSIE_VORLAUF_SEKUNDEN = 8; /** Verbindung zur Geraetedatenbank. */ function commandDb() @@ -289,59 +304,122 @@ function tahomaZustand($deviceUrl, $feld) } /** - * Wartet, bis die Neigung den Zielwert erreicht hat. + * Wartet, bis die Jalousie ihre Fahrt beendet hat und die Zielwerte zeigt. * - * Auf core:MovingState ist bei Neigungsfahrten kein Verlass - gemessen hat - * die Box waehrend einer solchen Fahrt kein einziges Mal "faehrt" gemeldet. - * Der Zustandswert selbst ist die verlaessliche Auskunft. + * Zwei Auskuenfte zusammen, weil einzeln keine traegt: + * + * core:MovingState taugt fuer die lange Fahrt hoch und runter, wird + * bei kurzen Neigungsfahrten aber nie gesetzt. + * die Zustandswerte sind die eigentliche Wahrheit, brauchen aber + * einen Vorlauf, weil sie kurz nach dem Kommando + * noch den alten Stand zeigen. + * + * Fertig ist die Fahrt, wenn nichts mehr faehrt, die Ziele erreicht sind und + * entweder ein "faehrt" gesehen wurde oder der Vorlauf um ist. + * + * $schliessungZiel darf null sein, wenn das Kommando die Position nicht + * anfasst. */ -function warteAufNeigung($deviceUrl, $ziel) +function warteAufJalousie($deviceUrl, $neigungZiel, $schliessungZiel = null) { - for ($i = 0; $i < NEIGUNG_WARTE_SEKUNDEN; $i++) { - sleep(1); - $wert = tahomaZustand($deviceUrl, "core:SlateOrientationState"); + $start = time(); + $gestartet = false; + while (time() - $start < JALOUSIE_WARTE_SEKUNDEN) { + sleep(2); + $faehrt = tahomaZustand($deviceUrl, "core:MovingState"); + $neigung = tahomaZustand($deviceUrl, "core:SlateOrientationState"); + $schliessung = tahomaZustand($deviceUrl, "core:ClosureState"); + if ($faehrt === true) { + $gestartet = true; + continue; + } // null heisst "nicht gelesen". intval() daraus waere 0 - und damit - // ausgerechnet beim Ziel 0 ein falsches Erfolgssignal, das die - // Warteschleife sofort beendet. - if ($wert !== null && intval($wert) === $ziel) { + // ausgerechnet beim Ziel 0 ein falsches Erfolgssignal. + $neigungOk = $neigung !== null && intval($neigung) === $neigungZiel; + $schliessungOk = $schliessungZiel === null + || ($schliessung !== null && intval($schliessung) === $schliessungZiel); + if ($neigungOk && $schliessungOk + && ($gestartet || time() - $start >= JALOUSIE_VORLAUF_SEKUNDEN)) { return true; } } return false; } +/** + * Hat das Geraet ein Kommando, das Position und Neigung zusammen setzt? + * Nur solche Geraete sind Jalousien mit der Kugelschreiber-Mechanik. + */ +function jalousieKombiKommando($actorUrl) +{ + $db = commandDb(); + $stmt = $db->prepare("SELECT c.command_url FROM actor_commands c + JOIN actors a ON a.id = c.actor_id + WHERE a.url = ? AND c.command_url = 'setClosureAndOrientation'"); + $stmt->bind_param("s", $actorUrl); + $stmt->execute(); + $treffer = $stmt->get_result()->fetch_assoc(); + $stmt->close(); + return $treffer ? $treffer["command_url"] : null; +} + /** * Tahoma: exec/apply an die Box - dieselbe Form wie in ajax/tahoma.php, nur * ohne dessen Sonderbehandlung fuer "faehrt gerade". */ function sendeTahoma($cmd, $werte, $probe) { + $befehl = $cmd["command_url"]; $parameter = []; - $neigungsziel = null; - foreach (parameterWerte($cmd, $werte) as $p) { + $neigungIndex = null; + $positionIndex = null; + foreach (parameterWerte($cmd, $werte) as $i => $p) { // Die Box erwartet Zahlen als Zahlen, Text als Text. $parameter[] = is_numeric($p["wert"]) ? $p["wert"] + 0 : $p["wert"]; - if ($p["name"] === NEIGUNG_PARAMETER && is_numeric($p["wert"])) { - $neigungsziel = $p["wert"] + 0; + if ($p["name"] === NEIGUNG_PARAMETER) { + $neigungIndex = $i; + } elseif ($p["name"] === POSITION_PARAMETER) { + $positionIndex = $i; } } - // Kugelschreiber-Mechanik: ueber NEIGUNG_DIREKT_MAX hinaus rastet die - // Mechanik nur um, wenn sie vorher einmal auf 0 % war. Der Umweg gehoert - // hierher und nicht in die Bedienung - sonst muesste ihn jeder kennen, - // der eine Jalousie anspricht. - $umweg = ($neigungsziel !== null && $neigungsziel > NEIGUNG_DIREKT_MAX); + // "Zu" allein macht diese Jalousien nicht dicht: sie faehrt herunter, die + // Lamellen bleiben durch die Mechanik aber bei etwa 30 % offen stehen. + // Gemeint ist "ganz unten, Lamellen geschlossen" - also dasselbe wie + // Position 100 mit Neigung 100, und damit ein Fall fuer die Regel unten. + if ($befehl === "down" && jalousieKombiKommando($cmd["actor_url"])) { + $befehl = "setClosureAndOrientation"; + $parameter = [100, 100]; + $positionIndex = 0; + $neigungIndex = 1; + } + + // Kugelschreiber-Mechanik: ueber NEIGUNG_DIREKT_MAX hinaus rastet sie nur + // um, wenn sie vorher einmal auf 0 % war. Geschickt wird deshalb derselbe + // Befehl zweimal - erst mit Neigung 0, dann mit dem gewuenschten Wert. + // Die Position bleibt dabei stehen, die Jalousie faehrt also nur einmal. + $umweg = $neigungIndex !== null + && is_numeric($parameter[$neigungIndex]) + && $parameter[$neigungIndex] > NEIGUNG_DIREKT_MAX; + + $vorstufe = $parameter; + if ($umweg) { + $vorstufe[$neigungIndex] = 0; + } if ($probe) { - $text = "Tahoma: " . $cmd["command_url"] . " " . json_encode($parameter); - return $umweg ? $text . " (mit Umweg über Neigung 0)" : $text; + $text = "Tahoma: " . $befehl . " " . json_encode($parameter); + return $umweg + ? $text . " (zuerst " . json_encode($vorstufe) . ", dann warten)" + : $text; } if ($umweg) { - tahomaKommando($cmd["actor_url"], "setOrientation", [0]); - warteAufNeigung($cmd["actor_url"], 0); + tahomaKommando($cmd["actor_url"], $befehl, $vorstufe); + warteAufJalousie($cmd["actor_url"], 0, + $positionIndex === null ? null : intval($parameter[$positionIndex])); } - tahomaKommando($cmd["actor_url"], $cmd["command_url"], $parameter); + tahomaKommando($cmd["actor_url"], $befehl, $parameter); return $cmd["actor_name"] . ": " . $cmd["command_name"] - . ($umweg ? " (über 0 % geneigt)" : ""); + . ($umweg ? " (über Neigung 0 gefahren)" : ""); }