Jalousien: Zu heisst zweifach schwenken, Versand laeuft im Hintergrund

Die Kugelschreiber-Mechanik betrifft jedes Kommando, nicht nur
setOrientation, und sie betrifft auch das Schliessen: ein "Zu" faehrt die
Jalousie zwar herunter, die Lamellen bleiben aber bei etwa 30 % offen
stehen. Dicht wird sie erst durch das zweifache Schwenken.

Die Regel lautet jetzt einheitlich: derselbe Befehl zweimal - erst mit
Neigung 0, dann warten, bis die Fahrt steht, dann mit dem gewuenschten Wert.
Die Position bleibt dabei erhalten, die Jalousie faehrt also nur einmal.

    Zu                     -> [100,0] warten [100,100]
    Neigung 20             -> direkt, unter der Schwelle
    Neigung 80             -> [0] warten [80]
    Position 40 Neigung 50 -> [40,0] warten [40,50]

"Zu" wird dabei nur umgeschrieben, wenn das Geraet Position und Neigung
zusammen setzen kann - der einfache Rollladen "Terasse" hat keine Lamellen
und bekommt weiter das rohe down.

Gewartet wird auf zwei Auskuenfte zusammen, weil einzeln keine traegt:
core:MovingState trug die lange Fahrt (gemessen 61 s), wird bei kurzen
Neigungsfahrten aber nie gesetzt; die Zustandswerte sind die Wahrheit,
zeigen direkt nach dem Kommando aber noch den alten Stand. Fertig heisst:
nichts faehrt mehr, die Ziele stimmen, und es wurde entweder ein "faehrt"
gesehen oder der Vorlauf von acht Sekunden ist um.

Und weil ein "Zu" damit ueber eine Minute dauert, schickt der Runner nicht
mehr selbst: der neue Versand nimmt die Kommandos entgegen und arbeitet sie
in eigenen Faeden ab - je Geraet der Reihe nach, ueber Geraete hinweg
nebeneinander. Sonst haette eine einzige Jalousie die ganze Auswertung fuer
eine Minute angehalten: keine Zeit-Ausloeser, keine Messwerte, und mehrere
Rollladen in einer Automatik haetten sich aufaddiert. Die Datenbank bleibt
dabei im Hauptfaden - die Faeden melden nur ihr Ergebnis zurueck, eine
pymysql-Verbindung ist nicht fuer mehrere Faeden gedacht.

Nachgemessen: Einreihen kehrt sofort zurueck, zwei Kommandos an dasselbe
Geraet laufen nacheinander (6 s, dann 12 s), drei an verschiedene Geraete
gleichzeitig, Fehler kommen zurueck, und die Schleife lief in 20 Sekunden
20 Takte durch.

Die Bedienung im Modal wartet weiterhin - dort sitzt ein Mensch vor einem
Fortschritt und klickt einmal. ajax/room.php hebt dafuer die Zeitgrenze auf
180 Sekunden an, wie es ajax/tahoma.php an derselben Stelle auch tut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-31 20:29:40 +02:00
co-authored by Claude Opus 5
parent d557a1651f
commit 131d7cc46d
4 changed files with 333 additions and 77 deletions
+5
View File
@@ -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");
+126 -9
View File
@@ -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()
+94 -38
View File
@@ -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
+108 -30
View File
@@ -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)" : "");
}