From 1b9bbc9857e5f06b44e5f50f706c16ad60509986 Mon Sep 17 00:00:00 2001 From: Moirtz Wagner Date: Mon, 31 Aug 2026 19:18:55 +0200 Subject: [PATCH] Jalousien: Neigung ueber 30 % nur mit Umweg ueber 0 % Die Aussenjalousien haben eine Kugelschreiber-Mechanik: beim Herunterfahren stehen die Lamellen bei etwa 30 %. In Richtung 0 % laesst sich von jeder Stellung aus direkt neigen; darueber hinaus rastet die Mechanik nur um, wenn sie vorher einmal auf 0 % war. Ein "Neigung 80 %" ohne diesen Umweg bleibt wirkungslos. Beide Versender wissen das jetzt: restricted/commands.php fuer die Bedienung im Raum-Modal, transports.py fuer den Runner. Bei setOrientation mit einem Ziel ueber 30 % geht erst eine 0 raus, dann wird gewartet, bis die Box diese 0 auch meldet, und erst danach der eigentliche Wert. Der Umweg gehoert dorthin und nicht in die Bedienung - sonst muesste ihn jeder kennen, der eine Jalousie anspricht, auch beim Anlegen einer Automatik. Gewartet wird auf core:SlateOrientationState und nicht auf core:MovingState: bei Neigungsfahrten meldet die Box waehrend der ganzen Bewegung "faehrt nicht". Genau darauf war ich vorher hereingefallen. Zwei Messungen aus der Erprobung stecken in den Konstanten: von 100 % auf 0 % braucht eine Jalousie gut fuenfzehn Sekunden, deshalb die Grenze von 25 s. Im Versuch ging ein "Neigung 100 %" von 79 % aus nach 14 s durch - echtes Warten, nicht die Notbremse. Dabei ein Fehler, den ausgerechnet das Ziel 0 verdeckt haette: die PHP-Leseroutine liefert null, wenn die Abfrage fehlschlaegt, und intval(null) ist 0. Die Warteschleife hielt einen fehlgeschlagenen Lesevorgang damit fuer "Ziel erreicht" und brach sofort ab - der Umweg fand also gar nicht statt, es sah nur so aus. In der Python-Fassung ist dieselbe Stelle jetzt ebenfalls ausdruecklich gegen ein fehlendes Feld abgesichert. Co-Authored-By: Claude Opus 5 --- restricted/autoActions/transports.py | 70 +++++++++++++-- restricted/commands.php | 125 +++++++++++++++++++++++---- 2 files changed, 171 insertions(+), 24 deletions(-) diff --git a/restricted/autoActions/transports.py b/restricted/autoActions/transports.py index f615679..8af1641 100644 --- a/restricted/autoActions/transports.py +++ b/restricted/autoActions/transports.py @@ -32,6 +32,7 @@ id, `aktion` ein Dict mit actor_url, command_url und params (Liste aus import json import logging import re +import time import urllib3 from datetime import datetime from urllib.parse import quote @@ -382,6 +383,24 @@ class TahomaTransport(Transport): schema = "io://" + # Bis zu dieser Neigung fahren die Aussenjalousien direkt. + # + # Sie haben eine Kugelschreiber-Mechanik: beim Herunterfahren stehen die + # Lamellen bei etwa 30 %. In Richtung 0 % laesst sich von jeder Stellung + # aus direkt neigen; darueber hinaus muss die Mechanik erst einmal auf + # 0 % zurueck, sonst rastet sie nicht um - ein "Neigung 80 %" ohne + # diesen Umweg bleibt wirkungslos. + # + # Dasselbe steht in restricted/commands.php: beide Versender brauchen es. + NEIGUNG_DIREKT_MAX = 30 + + # 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 + def __init__(self, requests_modul, pin, token, timeout=10, dry_run=False): self.requests = requests_modul self.pin = pin @@ -432,19 +451,60 @@ 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 = {"name": aktion["command_url"], - "parameters": [self._zahl(p["wert"]) for p in aktion["params"]]} - rumpf = {"label": "AutoAction", - "actions": [{"deviceURL": aktion["actor_url"], "commands": [befehl]}]} + parameter = [self._zahl(p["wert"]) for p in aktion["params"]] + + # 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 = (aktion["command_url"] == "setOrientation" + and len(parameter) == 1 + and isinstance(parameter[0], (int, float)) + and parameter[0] > self.NEIGUNG_DIREKT_MAX) + if self.dry_run: - logger.info("[dry-run] Tahoma %s", json.dumps(rumpf, ensure_ascii=False)) + logger.info("[dry-run] Tahoma %s %s%s", aktion["command_url"], parameter, + " (mit Umweg ueber Neigung 0)" 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) + + def _apply(self, actor_url, name, parameter): + """Ein Kommando an die Box schicken.""" + rumpf = {"label": "AutoAction", + "actions": [{"deviceURL": actor_url, + "commands": [{"name": name, "parameters": parameter}]}]} antwort = self.requests.post(self.basis + "/exec/apply", headers=self._kopf(), data=json.dumps(rumpf), timeout=self.timeout, verify=False) if antwort.status_code >= 400: 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) + 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 + 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) + return False + @staticmethod def _zahl(wert): """Tahoma erwartet Zahlen als Zahlen, Text als Text.""" diff --git a/restricted/commands.php b/restricted/commands.php index ee0b145..3260200 100644 --- a/restricted/commands.php +++ b/restricted/commands.php @@ -26,6 +26,26 @@ require_once(__DIR__ . "/../ajax/phpMQTT.php"); const MQTT_BROKER = "localhost"; const MQTT_PORT = 1883; +/** + * Bis zu dieser Neigung fahren die Aussenjalousien direkt. + * + * Sie haben eine Kugelschreiber-Mechanik: beim Herunterfahren stehen die + * Lamellen bei etwa 30 %. In Richtung 0 % laesst sich von jeder Stellung aus + * direkt neigen; ueber 30 % hinaus muss die Mechanik erst einmal auf 0 % + * zurueck, sonst rastet sie nicht um. Ein "Neigung 80 %" ohne diesen Umweg + * bleibt wirkungslos. + * + * Dasselbe steht in restricted/autoActions/transports.py - beide Versender + * brauchen es, und beide muessen es wissen. + */ +const NEIGUNG_DIREKT_MAX = 30; + +/** + * So lange wird hoechstens auf das Ende einer Neigungsfahrt gewartet. + * Gemessen braucht eine Jalousie von 100 % auf 0 % gut fuenfzehn Sekunden. + */ +const NEIGUNG_WARTE_SEKUNDEN = 25; + /** Verbindung zur Geraetedatenbank. */ function commandDb() { @@ -210,30 +230,18 @@ function sendeHttp($cmd, $werte, $probe) return $cmd["actor_name"] . ": " . $cmd["command_name"]; } -/** - * 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) +/** Ein Kommando an die Tahoma-Box schicken. */ +function tahomaKommando($deviceUrl, $name, $parameter) { - $parameter = []; - foreach (parameterWerte($cmd, $werte) as $p) { - // Die Box erwartet Zahlen als Zahlen, Text als Text. - $parameter[] = is_numeric($p["wert"]) ? $p["wert"] + 0 : $p["wert"]; - } $rumpf = [ "label" => "Raumbedienung", "actions" => [[ - "deviceURL" => $cmd["actor_url"], - "commands" => [["name" => $cmd["command_url"], "parameters" => $parameter]], + "deviceURL" => $deviceUrl, + "commands" => [["name" => $name, "parameters" => $parameter]], ]], ]; - $ziel = "https://gateway-" . $GLOBALS["tahoma_PIN"] - . ":8443/enduser-mobile-web/1/enduserAPI/exec/apply"; - if ($probe) { - return "Tahoma: " . json_encode($rumpf, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - } - $ch = curl_init($ziel); + $ch = curl_init("https://gateway-" . $GLOBALS["tahoma_PIN"] + . ":8443/enduser-mobile-web/1/enduserAPI/exec/apply"); curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json", "Authorization: Bearer " . $GLOBALS["tahoma_token"]]); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($rumpf)); @@ -247,5 +255,84 @@ function sendeTahoma($cmd, $werte, $probe) if ($code >= 400 || $code === 0) { throw new RuntimeException("Tahoma antwortete mit " . $code . ": " . substr(strval($antwort), 0, 120)); } - return $cmd["actor_name"] . ": " . $cmd["command_name"]; +} + +/** Einen Zustandswert eines Tahoma-Geraets lesen, null wenn unbekannt. */ +function tahomaZustand($deviceUrl, $feld) +{ + $ch = curl_init("https://gateway-" . $GLOBALS["tahoma_PIN"] + . ":8443/enduser-mobile-web/1/enduserAPI/setup/devices/" + . rawurlencode($deviceUrl) . "/states"); + curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer " . $GLOBALS["tahoma_token"]]); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + $antwort = json_decode(curl_exec($ch), true); + curl_close($ch); + if (!is_array($antwort)) { + return null; + } + foreach ($antwort as $zustand) { + if (($zustand["name"] ?? "") === $feld) { + return $zustand["value"]; + } + } + return null; +} + +/** + * Wartet, bis die Neigung den Zielwert erreicht hat. + * + * 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. + */ +function warteAufNeigung($deviceUrl, $ziel) +{ + for ($i = 0; $i < NEIGUNG_WARTE_SEKUNDEN; $i++) { + sleep(1); + $wert = tahomaZustand($deviceUrl, "core:SlateOrientationState"); + // 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) { + return true; + } + } + return false; +} + +/** + * 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) +{ + $parameter = []; + foreach (parameterWerte($cmd, $werte) as $p) { + // Die Box erwartet Zahlen als Zahlen, Text als Text. + $parameter[] = is_numeric($p["wert"]) ? $p["wert"] + 0 : $p["wert"]; + } + + // 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 = ($cmd["command_url"] === "setOrientation" + && count($parameter) === 1 + && $parameter[0] > NEIGUNG_DIREKT_MAX); + + if ($probe) { + $text = "Tahoma: " . $cmd["command_url"] . " " . json_encode($parameter); + return $umweg ? $text . " (mit Umweg über Neigung 0)" : $text; + } + + if ($umweg) { + tahomaKommando($cmd["actor_url"], "setOrientation", [0]); + warteAufNeigung($cmd["actor_url"], 0); + } + tahomaKommando($cmd["actor_url"], $cmd["command_url"], $parameter); + return $cmd["actor_name"] . ": " . $cmd["command_name"] + . ($umweg ? " (über 0 % geneigt)" : ""); }