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 <noreply@anthropic.com>
This commit is contained in:
2026-08-31 19:18:55 +02:00
co-authored by Claude Opus 5
parent 98a1da6cd5
commit 1b9bbc9857
2 changed files with 171 additions and 24 deletions
+65 -5
View File
@@ -32,6 +32,7 @@ id, `aktion` ein Dict mit actor_url, command_url und params (Liste aus
import json import json
import logging import logging
import re import re
import time
import urllib3 import urllib3
from datetime import datetime from datetime import datetime
from urllib.parse import quote from urllib.parse import quote
@@ -382,6 +383,24 @@ class TahomaTransport(Transport):
schema = "io://" 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): def __init__(self, requests_modul, pin, token, timeout=10, dry_run=False):
self.requests = requests_modul self.requests = requests_modul
self.pin = pin self.pin = pin
@@ -432,19 +451,60 @@ class TahomaTransport(Transport):
raise RuntimeError("Kein Tahoma-Token in der config.ini") raise RuntimeError("Kein Tahoma-Token in der config.ini")
# Die Reihenfolge der Parameter ist die aus command_parameters - bei # Die Reihenfolge der Parameter ist die aus command_parameters - bei
# setClosureAndOrientation also erst Position, dann Winkel. # setClosureAndOrientation also erst Position, dann Winkel.
befehl = {"name": aktion["command_url"], parameter = [self._zahl(p["wert"]) for p in aktion["params"]]
"parameters": [self._zahl(p["wert"]) for p in aktion["params"]]}
rumpf = {"label": "AutoAction", # Kugelschreiber-Mechanik, siehe NEIGUNG_DIREKT_MAX. Der Umweg gehoert
"actions": [{"deviceURL": aktion["actor_url"], "commands": [befehl]}]} # 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: 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 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(), antwort = self.requests.post(self.basis + "/exec/apply", headers=self._kopf(),
data=json.dumps(rumpf), timeout=self.timeout, verify=False) data=json.dumps(rumpf), timeout=self.timeout, verify=False)
if antwort.status_code >= 400: if antwort.status_code >= 400:
raise RuntimeError("Tahoma antwortete mit %d: %s" raise RuntimeError("Tahoma antwortete mit %d: %s"
% (antwort.status_code, antwort.text[:120])) % (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 @staticmethod
def _zahl(wert): def _zahl(wert):
"""Tahoma erwartet Zahlen als Zahlen, Text als Text.""" """Tahoma erwartet Zahlen als Zahlen, Text als Text."""
+106 -19
View File
@@ -26,6 +26,26 @@ require_once(__DIR__ . "/../ajax/phpMQTT.php");
const MQTT_BROKER = "localhost"; const MQTT_BROKER = "localhost";
const MQTT_PORT = 1883; 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. */ /** Verbindung zur Geraetedatenbank. */
function commandDb() function commandDb()
{ {
@@ -210,30 +230,18 @@ function sendeHttp($cmd, $werte, $probe)
return $cmd["actor_name"] . ": " . $cmd["command_name"]; return $cmd["actor_name"] . ": " . $cmd["command_name"];
} }
/** /** Ein Kommando an die Tahoma-Box schicken. */
* Tahoma: exec/apply an die Box - dieselbe Form wie in ajax/tahoma.php, nur function tahomaKommando($deviceUrl, $name, $parameter)
* 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"];
}
$rumpf = [ $rumpf = [
"label" => "Raumbedienung", "label" => "Raumbedienung",
"actions" => [[ "actions" => [[
"deviceURL" => $cmd["actor_url"], "deviceURL" => $deviceUrl,
"commands" => [["name" => $cmd["command_url"], "parameters" => $parameter]], "commands" => [["name" => $name, "parameters" => $parameter]],
]], ]],
]; ];
$ziel = "https://gateway-" . $GLOBALS["tahoma_PIN"] $ch = curl_init("https://gateway-" . $GLOBALS["tahoma_PIN"]
. ":8443/enduser-mobile-web/1/enduserAPI/exec/apply"; . ":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);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json", curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json",
"Authorization: Bearer " . $GLOBALS["tahoma_token"]]); "Authorization: Bearer " . $GLOBALS["tahoma_token"]]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($rumpf)); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($rumpf));
@@ -247,5 +255,84 @@ function sendeTahoma($cmd, $werte, $probe)
if ($code >= 400 || $code === 0) { if ($code >= 400 || $code === 0) {
throw new RuntimeException("Tahoma antwortete mit " . $code . ": " . substr(strval($antwort), 0, 120)); 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)" : "");
} }