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:
@@ -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
@@ -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)" : "");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user