Jalousien waehrend der Fahrt nachfuehren
actor_states.current_value schrieb bisher nur der AutoAction-Runner fort, und der fragt die Tahoma-Box alle fuenf Minuten ab (poll_tahoma). Nach einem Fahrbefehl stand im Raum-Modal deshalb bis zu fuenf Minuten lang der Stand von vor der Fahrt. Das Modal verfolgt die Jalousie jetzt selbst: alle zwei Sekunden fragt es ueber ajax/room.php?action=jalousie genau dieses eine Geraet, zieht die beiden Regler und das Lamellenbild nach und hoert auf, sobald die Box keinen Fahrbefehl mehr meldet. Der gelesene Stand landet dabei gleich in actor_states, damit auch das naechste Oeffnen und der Automatik-Editor ihn haben. Wer gerade selbst am Schieber zieht, behaelt ihn. Drei Dinge mussten dafuer aus dem Weg: - Die Sitzung. helper.php startet sie und schliesst sie nie, PHP sperrt ihre Datei bis zum Ende der Anfrage. Ein "Zu" wartet ueber eine Minute auf das Ende der Fahrt - solange stand jede weitere Anfrage desselben Browsers still. ajax/room.php gibt sie jetzt frei, sobald checkLogin() durch ist; danach wird $_SESSION nur noch gelesen. - Drei Abfragen je Warterunde. warteAufJalousie() holte MovingState, Neigung und Position einzeln, obwohl eine Abfrage die ganze Liste liefert. Jetzt eine. - Kein Ort, an dem der frische Stand haengenbleibt. Dafuer tahomaZustaende(), tahomaZustaendeSchreiben() und tahomaAktorUrl(). Wahrheitswerte werden dabei als "True"/"False" geschrieben - so macht es der Runner mit str(), und zwei Schreibweisen fuer denselben Zustand waeren schlimmer als eine schraege. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,14 @@ if (!checkLogin()) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Die Sitzung wird ab hier nur noch gelesen, und PHP haelt ihre Datei bis
|
||||||
|
// zum Ende der Anfrage exklusiv gesperrt. Ein "Zu" an einer Jalousie wartet
|
||||||
|
// weiter unten ueber eine Minute auf das Ende der Fahrt - solange stand jede
|
||||||
|
// weitere Anfrage desselben Browsers still, auch die, die waehrend der Fahrt
|
||||||
|
// nachsehen will, wie weit sie ist. Alle Schreibzugriffe auf $_SESSION
|
||||||
|
// stecken in checkLogin() und sind hier durch.
|
||||||
|
session_write_close();
|
||||||
|
|
||||||
function raumAntwort($data, $code = 200)
|
function raumAntwort($data, $code = 200)
|
||||||
{
|
{
|
||||||
http_response_code($code);
|
http_response_code($code);
|
||||||
@@ -71,6 +79,31 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|||||||
$mqtt->close();
|
$mqtt->close();
|
||||||
raumAntwort(["ok" => "Solltemperatur " . $temp . " °C"]);
|
raumAntwort(["ok" => "Solltemperatur " . $temp . " °C"]);
|
||||||
}
|
}
|
||||||
|
// Eine Jalousie waehrend ihrer Fahrt nachfuehren. actor_states wird
|
||||||
|
// sonst nur vom Runner fortgeschrieben, und der fragt die Box alle
|
||||||
|
// fuenf Minuten ab - so lange stand nach einem Fahrbefehl der alte
|
||||||
|
// Wert im Modal. Hier wird genau ein Geraet gelesen, das kostet die
|
||||||
|
// Box wenig, und der frische Stand landet gleich in der Tabelle.
|
||||||
|
if (($_GET["action"] ?? "") === "jalousie") {
|
||||||
|
$url = tahomaAktorUrl(intval($body["actor"] ?? 0));
|
||||||
|
if ($url === null) {
|
||||||
|
raumAntwort(["error" => "Kein Tahoma-Gerät mit dieser Nummer."], 404);
|
||||||
|
}
|
||||||
|
$zustaende = tahomaZustaende($url);
|
||||||
|
if ($zustaende === null) {
|
||||||
|
raumAntwort(["error" => "Tahoma-Box antwortet nicht."], 503);
|
||||||
|
}
|
||||||
|
tahomaZustaendeSchreiben($url, $zustaende);
|
||||||
|
$zahl = function ($feld) use ($zustaende) {
|
||||||
|
return isset($zustaende[$feld]) && is_numeric($zustaende[$feld])
|
||||||
|
? intval($zustaende[$feld]) : null;
|
||||||
|
};
|
||||||
|
raumAntwort([
|
||||||
|
"faehrt" => ($zustaende["core:MovingState"] ?? null) === true,
|
||||||
|
"position" => $zahl("core:ClosureState"),
|
||||||
|
"neigung" => $zahl("core:SlateOrientationState"),
|
||||||
|
]);
|
||||||
|
}
|
||||||
raumAntwort(["error" => "Unbekannte Aktion."], 400);
|
raumAntwort(["error" => "Unbekannte Aktion."], 400);
|
||||||
} catch (InvalidArgumentException $e) {
|
} catch (InvalidArgumentException $e) {
|
||||||
raumAntwort(["error" => $e->getMessage()], 400);
|
raumAntwort(["error" => $e->getMessage()], 400);
|
||||||
|
|||||||
@@ -513,6 +513,11 @@ function switchFloor(floor){
|
|||||||
function raumKommando(cmdId, params, element) {
|
function raumKommando(cmdId, params, element) {
|
||||||
const karte = element.closest(".raum-geraet") || element;
|
const karte = element.closest(".raum-geraet") || element;
|
||||||
karte.classList.add("raum-warte");
|
karte.classList.add("raum-warte");
|
||||||
|
// Die Verfolgung beginnt sofort, nicht erst mit der Antwort: ein "Zu"
|
||||||
|
// faehrt herunter, wartet und neigt danach - der Aufruf kommt also erst
|
||||||
|
// zurueck, wenn die Jalousie schon steht. Beides nebeneinander geht,
|
||||||
|
// seit ajax/room.php die Sitzung freigibt.
|
||||||
|
jalousieVerfolgen(karte);
|
||||||
raumRequest("command", { command_id: cmdId, params: params })
|
raumRequest("command", { command_id: cmdId, params: params })
|
||||||
.then(antwort => {
|
.then(antwort => {
|
||||||
karte.classList.remove("raum-warte");
|
karte.classList.remove("raum-warte");
|
||||||
@@ -520,6 +525,83 @@ function switchFloor(floor){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Takt und Grenzen der Verfolgung. Dieselben Zahlen wie die Konstanten
|
||||||
|
// JALOUSIE_* in restricted/commands.php: die Box meldet core:MovingState
|
||||||
|
// traege, kurz nach dem Kommando steht dort noch "faehrt nicht" - deshalb
|
||||||
|
// der Vorlauf, in dem ein Stillstand nichts bedeutet.
|
||||||
|
const JALOUSIE_TAKT_MS = 2000;
|
||||||
|
const JALOUSIE_VORLAUF_MS = 8000;
|
||||||
|
const JALOUSIE_MAX_MS = 120000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eine Jalousie waehrend ihrer Fahrt nachfuehren.
|
||||||
|
*
|
||||||
|
* actor_states schreibt sonst nur der Runner fort, und der fragt die
|
||||||
|
* Tahoma-Box alle fuenf Minuten ab - so lange zeigte das Modal nach einem
|
||||||
|
* Fahrbefehl den Stand von davor. Hier wird genau dieses eine Geraet
|
||||||
|
* gefragt; ajax/room.php haelt den frischen Wert gleich in der Tabelle
|
||||||
|
* fest, damit auch das naechste Oeffnen und der Automatik-Editor ihn haben.
|
||||||
|
*/
|
||||||
|
function jalousieVerfolgen(karte) {
|
||||||
|
if (!karte || !karte.querySelector(".raum-jalousie")) return;
|
||||||
|
const actor = Number(karte.dataset.actor);
|
||||||
|
if (!actor) return;
|
||||||
|
|
||||||
|
// Ein zweiter Fahrbefehl schiebt nur das Ende hinaus. Zwei Schleifen
|
||||||
|
// wuerden dieselbe Box doppelt so oft fragen, ohne mehr zu zeigen.
|
||||||
|
karte.dataset.jalousieBis = String(Date.now() + JALOUSIE_MAX_MS);
|
||||||
|
if (karte.dataset.jalousieLaeuft) return;
|
||||||
|
karte.dataset.jalousieLaeuft = "1";
|
||||||
|
|
||||||
|
const beginn = Date.now();
|
||||||
|
let gefahren = false;
|
||||||
|
|
||||||
|
function aufhoeren() {
|
||||||
|
delete karte.dataset.jalousieLaeuft;
|
||||||
|
delete karte.dataset.jalousieBis;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runde(letzte) {
|
||||||
|
raumRequest("jalousie", { actor: actor }).then(stand => {
|
||||||
|
// Modal zu, Box stumm, Geraet unbekannt - in jedem Fall aufhoeren
|
||||||
|
// statt zwei Minuten lang ins Leere zu fragen.
|
||||||
|
if (!stand || stand.error || !document.body.contains(karte)) {
|
||||||
|
aufhoeren();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
jalousieAnzeigen(karte, stand);
|
||||||
|
if (stand.faehrt) gefahren = true;
|
||||||
|
if (letzte || Date.now() > Number(karte.dataset.jalousieBis)) {
|
||||||
|
aufhoeren();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Steht sie still, ist die Fahrt vorbei - aber erst, wenn wir sie
|
||||||
|
// haben fahren sehen oder der Vorlauf um ist. Eine letzte Runde
|
||||||
|
// danach holt den Wert, den die Box beim Anhalten setzt.
|
||||||
|
const fertig = !stand.faehrt
|
||||||
|
&& (gefahren || Date.now() - beginn >= JALOUSIE_VORLAUF_MS);
|
||||||
|
setTimeout(() => runde(fertig), JALOUSIE_TAKT_MS);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setTimeout(() => runde(false), JALOUSIE_TAKT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Die gemeldete Stellung in die beiden Regler und das Bild uebernehmen. */
|
||||||
|
function jalousieAnzeigen(karte, stand) {
|
||||||
|
[["position", stand.position], ["neigung", stand.neigung]].forEach(paar => {
|
||||||
|
const wert = paar[1];
|
||||||
|
if (wert === null || typeof wert === "undefined") return;
|
||||||
|
const regler = karte.querySelector(".raum-regler[data-rolle='" + paar[0] + "']");
|
||||||
|
if (!regler) return;
|
||||||
|
const schieber = regler.querySelector("input[type=range]");
|
||||||
|
// Wer gerade zieht, bestimmt. Sonst spraenge ihm der Schieber unter
|
||||||
|
// dem Daumen weg, waehrend die Jalousie noch das alte Ziel anfaehrt.
|
||||||
|
if (document.activeElement === schieber) return;
|
||||||
|
schieber.value = wert;
|
||||||
|
reglerAnzeigen(regler);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Die neue Solltemperatur schicken.
|
* Die neue Solltemperatur schicken.
|
||||||
*
|
*
|
||||||
|
|||||||
+129
-8
@@ -279,8 +279,18 @@ function tahomaKommando($deviceUrl, $name, $parameter)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Einen Zustandswert eines Tahoma-Geraets lesen, null wenn unbekannt. */
|
/**
|
||||||
function tahomaZustand($deviceUrl, $feld)
|
* Alle Zustaende eines Tahoma-Geraets in einem Zug lesen.
|
||||||
|
*
|
||||||
|
* Die Box liefert auf eine Abfrage ohnehin die ganze Liste. Drei einzelne
|
||||||
|
* Aufrufe fuer drei Felder waren dreimal derselbe Weg dorthin - und
|
||||||
|
* ausgerechnet in der Warteschleife, die alle zwei Sekunden nachsieht.
|
||||||
|
*
|
||||||
|
* Rueckgabe: Feldname => Wert, oder null, wenn die Box nicht antwortet. Ein
|
||||||
|
* leeres Array waere hier falsch: es hiesse "Geraet kennt keine Zustaende"
|
||||||
|
* und liesse eine Warteschleife durchlaufen, statt sie warten zu lassen.
|
||||||
|
*/
|
||||||
|
function tahomaZustaende($deviceUrl)
|
||||||
{
|
{
|
||||||
$ch = curl_init("https://gateway-" . $GLOBALS["tahoma_PIN"]
|
$ch = curl_init("https://gateway-" . $GLOBALS["tahoma_PIN"]
|
||||||
. ":8443/enduser-mobile-web/1/enduserAPI/setup/devices/"
|
. ":8443/enduser-mobile-web/1/enduserAPI/setup/devices/"
|
||||||
@@ -295,12 +305,87 @@ function tahomaZustand($deviceUrl, $feld)
|
|||||||
if (!is_array($antwort)) {
|
if (!is_array($antwort)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
$werte = [];
|
||||||
foreach ($antwort as $zustand) {
|
foreach ($antwort as $zustand) {
|
||||||
if (($zustand["name"] ?? "") === $feld) {
|
if (isset($zustand["name"])) {
|
||||||
return $zustand["value"];
|
$werte[$zustand["name"]] = $zustand["value"] ?? null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return $werte;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Einen Zustandswert eines Tahoma-Geraets lesen, null wenn unbekannt. */
|
||||||
|
function tahomaZustand($deviceUrl, $feld)
|
||||||
|
{
|
||||||
|
$zustaende = tahomaZustaende($deviceUrl);
|
||||||
|
return $zustaende === null ? null : ($zustaende[$feld] ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einen Zustandswert so in Text fassen, wie der Runner es tut.
|
||||||
|
*
|
||||||
|
* In actor_states.current_value schrieb bisher nur der Runner, und der macht
|
||||||
|
* str() daraus - aus einem Wahrheitswert wird dort "True" bzw. "False", nicht
|
||||||
|
* "1" und "". Das sieht aus PHP heraus schraeg aus, ist aber wichtig: sonst
|
||||||
|
* stuenden fuer denselben Zustand zwei Schreibweisen in der Tabelle, je
|
||||||
|
* nachdem wer zuletzt geschrieben hat. istAn() in roomControls.php kennt
|
||||||
|
* beide, eine Bedingung im Automatik-Editor vergleicht aber stur.
|
||||||
|
*/
|
||||||
|
function tahomaWertText($wert)
|
||||||
|
{
|
||||||
|
if (is_bool($wert)) {
|
||||||
|
return $wert ? "True" : "False";
|
||||||
|
}
|
||||||
|
return strval($wert);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die frisch gelesenen Zustaende eines Tahoma-Geraets in actor_states
|
||||||
|
* festhalten.
|
||||||
|
*
|
||||||
|
* Sonst schreibt dort nur der Runner, und der fragt die Box alle fuenf
|
||||||
|
* Minuten ab (poll_tahoma in seiner config.ini) - nach einem Fahrbefehl
|
||||||
|
* zeigte jedes Modal bis dahin den Stand von vor der Fahrt. Die Werte liegen
|
||||||
|
* hier ohnehin schon vor, weil die Warteschleife sie gelesen hat.
|
||||||
|
*
|
||||||
|
* Geschrieben wird nur, was sich geaendert hat. Der Runner haelt einen
|
||||||
|
* eigenen Merkzettel darueber, was er zuletzt geschrieben hat; er wird
|
||||||
|
* unsere Zeile beim naechsten Poll hoechstens einmal ueberfluessig
|
||||||
|
* ueberschreiben, denn er liest dieselbe Box.
|
||||||
|
*
|
||||||
|
* Rueckgabe: wie viele Zeilen geaendert wurden.
|
||||||
|
*/
|
||||||
|
function tahomaZustaendeSchreiben($deviceUrl, $zustaende)
|
||||||
|
{
|
||||||
|
if (!is_array($zustaende) || !$zustaende) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
$db = commandDb();
|
||||||
|
$stmt = $db->prepare("SELECT s.id, s.url, s.current_value FROM actor_states s
|
||||||
|
JOIN actors a ON a.id = s.actor_id
|
||||||
|
WHERE a.url = ? AND s.url IS NOT NULL AND s.url <> ''");
|
||||||
|
$stmt->bind_param("s", $deviceUrl);
|
||||||
|
$stmt->execute();
|
||||||
|
$zeilen = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
$geaendert = 0;
|
||||||
|
$schreib = $db->prepare("UPDATE actor_states SET current_value = ? WHERE id = ?");
|
||||||
|
foreach ($zeilen as $zeile) {
|
||||||
|
if (!array_key_exists($zeile["url"], $zustaende)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$text = tahomaWertText($zustaende[$zeile["url"]]);
|
||||||
|
if (strval($zeile["current_value"]) === $text) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$id = intval($zeile["id"]);
|
||||||
|
$schreib->bind_param("si", $text, $id);
|
||||||
|
$schreib->execute();
|
||||||
|
$geaendert++;
|
||||||
|
}
|
||||||
|
$schreib->close();
|
||||||
|
return $geaendert;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -326,9 +411,13 @@ function warteAufJalousie($deviceUrl, $neigungZiel, $schliessungZiel = null)
|
|||||||
$gestartet = false;
|
$gestartet = false;
|
||||||
while (time() - $start < JALOUSIE_WARTE_SEKUNDEN) {
|
while (time() - $start < JALOUSIE_WARTE_SEKUNDEN) {
|
||||||
sleep(2);
|
sleep(2);
|
||||||
$faehrt = tahomaZustand($deviceUrl, "core:MovingState");
|
$zustaende = tahomaZustaende($deviceUrl);
|
||||||
$neigung = tahomaZustand($deviceUrl, "core:SlateOrientationState");
|
if ($zustaende === null) {
|
||||||
$schliessung = tahomaZustand($deviceUrl, "core:ClosureState");
|
continue;
|
||||||
|
}
|
||||||
|
$faehrt = $zustaende["core:MovingState"] ?? null;
|
||||||
|
$neigung = $zustaende["core:SlateOrientationState"] ?? null;
|
||||||
|
$schliessung = $zustaende["core:ClosureState"] ?? null;
|
||||||
if ($faehrt === true) {
|
if ($faehrt === true) {
|
||||||
$gestartet = true;
|
$gestartet = true;
|
||||||
continue;
|
continue;
|
||||||
@@ -340,12 +429,44 @@ function warteAufJalousie($deviceUrl, $neigungZiel, $schliessungZiel = null)
|
|||||||
|| ($schliessung !== null && intval($schliessung) === $schliessungZiel);
|
|| ($schliessung !== null && intval($schliessung) === $schliessungZiel);
|
||||||
if ($neigungOk && $schliessungOk
|
if ($neigungOk && $schliessungOk
|
||||||
&& ($gestartet || time() - $start >= JALOUSIE_VORLAUF_SEKUNDEN)) {
|
&& ($gestartet || time() - $start >= JALOUSIE_VORLAUF_SEKUNDEN)) {
|
||||||
|
// Die Fahrt ist zu Ende und die Werte sind gerade gelesen - der
|
||||||
|
// guenstigste Moment, sie festzuhalten.
|
||||||
|
tahomaZustaendeSchreiben($deviceUrl, $zustaende);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Geraete-URL eines Aktors, null wenn es ihn nicht gibt oder er nicht an
|
||||||
|
* der Tahoma-Box haengt.
|
||||||
|
*
|
||||||
|
* Die Pruefung auf die Box-Kennung ist dieselbe wie im Runner: am Schema
|
||||||
|
* laesst sich ein Tahoma-Geraet nicht erkennen, dieselbe Box liefert io://
|
||||||
|
* fuer die Jalousien, rts:// fuer die Dachfenster und internal:// fuer die
|
||||||
|
* Alarmanlage. Ohne sie wuerde eine Anfrage mit der Nummer einer Shelly die
|
||||||
|
* Box nach einer URL fragen, die sie nicht kennt.
|
||||||
|
*/
|
||||||
|
function tahomaAktorUrl($actorId)
|
||||||
|
{
|
||||||
|
$pin = strval($GLOBALS["tahoma_PIN"] ?? "");
|
||||||
|
if ($pin === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$db = commandDb();
|
||||||
|
$stmt = $db->prepare("SELECT url FROM actors WHERE id = ?");
|
||||||
|
$id = intval($actorId);
|
||||||
|
$stmt->bind_param("i", $id);
|
||||||
|
$stmt->execute();
|
||||||
|
$zeile = $stmt->get_result()->fetch_assoc();
|
||||||
|
$stmt->close();
|
||||||
|
if (!$zeile || strpos(strval($zeile["url"]), "://" . $pin . "/") === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $zeile["url"];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hat das Geraet ein Kommando, das Position und Neigung zusammen setzt?
|
* Hat das Geraet ein Kommando, das Position und Neigung zusammen setzt?
|
||||||
* Nur solche Geraete sind Jalousien mit der Kugelschreiber-Mechanik.
|
* Nur solche Geraete sind Jalousien mit der Kugelschreiber-Mechanik.
|
||||||
|
|||||||
Reference in New Issue
Block a user