Wer das Raum-Modal gleich nach einem Fahrbefehl zumachte, nahm den einzigen Zuschauer mit: die Verfolgung im Browser endet mit dem Modal, und niemand hielt fest, wo die Jalousie stehen blieb. In actor_states stand bis zum naechsten Poll des Runners der alte Wert - genau die fuenf Minuten, die eigentlich weg sein sollten. ajax/room.php schickt die Antwort auf ein Tahoma-Kommando jetzt ab, schliesst die Verbindung und wartet danach selbst auf das Ende der Fahrt (fastcgi_finish_request, dieselbe Bauart wie in ajax/tahoma.php). Ob der Browser noch da ist, spielt keine Rolle mehr; ignore_user_abort deckt auch den Fall ab, dass er mitten im Aufruf verschwindet. Die Verfolgung im Modal bleibt, sie ist jetzt aber nur noch fuer die Anzeige zustaendig, solange jemand hinsieht. Das Aufschreiben haengt nicht mehr daran. Geprueft an der Wozi-Schiebetuer: Kommando auf Position 70, 0,7 Sekunden spaeter die Seite neu geladen. 48 Sekunden danach zeigte das frisch geoeffnete Modal 70 - und eine Neigung von 59 statt 62, die sich beim Fahren von selbst verstellt hatte und die nur die Box kannte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
228 lines
9.9 KiB
PHP
228 lines
9.9 KiB
PHP
<?php
|
||
/**
|
||
* Raum-Modal: alles, was in einem Raum haengt, auf einer Seite.
|
||
*
|
||
* Loest das frühere Thermostat-Modal ab. Der Grundriss zeigt Raeume, also
|
||
* sollte ein Klick darauf den Raum oeffnen und nicht nur seine Heizung - die
|
||
* Rollladen im selben Zimmer waren bisher gar nicht erreichbar.
|
||
*
|
||
* Welche Geraete zu einem Raum gehoeren, sagt die Spalte actors.room (siehe
|
||
* den Zuordnungsdialog in der Karte "Automatismen").
|
||
*
|
||
* GET ?room=EG_Bad Inhalt des Modals
|
||
* POST ?action=command {"command_id": N, "params": {...}} - ein Kommando
|
||
* 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");
|
||
require_once("../restricted/roomControls.php");
|
||
require_once("./phpMQTT.php");
|
||
|
||
if (!checkLogin()) {
|
||
http_response_code(403);
|
||
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)
|
||
{
|
||
http_response_code($code);
|
||
header("Content-Type: application/json; charset=utf-8");
|
||
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* Die Antwort abschicken, aber weiterarbeiten.
|
||
*
|
||
* Nach einem Fahrbefehl muss noch jemand abwarten, wo die Jalousie stehen
|
||
* bleibt, und das aufschreiben - sonst steht in actor_states bis zum
|
||
* naechsten Poll des Runners der alte Wert. Das dem Browser zu ueberlassen
|
||
* ging schief: wer das Modal gleich nach dem Fahrbefehl zumachte, nahm den
|
||
* einzigen Zuschauer mit.
|
||
*
|
||
* Dieselbe Bauart wie in ajax/tahoma.php: Content-Length setzen, ausliefern,
|
||
* die Verbindung schliessen, weiterlaufen. Ohne FPM bleibt nur, die Puffer zu
|
||
* leeren - dann hat der Browser seine Daten zwar, wartet aber trotzdem.
|
||
*/
|
||
function raumAntwortUndWeiter($data)
|
||
{
|
||
http_response_code(200);
|
||
header("Content-Type: application/json; charset=utf-8");
|
||
$text = json_encode($data, JSON_UNESCAPED_UNICODE);
|
||
header("Content-Length: " . strlen($text));
|
||
echo $text;
|
||
// Der Aufrufer ist weg, das Skript soll trotzdem zu Ende kommen.
|
||
ignore_user_abort(true);
|
||
if (function_exists("fastcgi_finish_request")) {
|
||
fastcgi_finish_request();
|
||
return;
|
||
}
|
||
while (ob_get_level() > 0) {
|
||
ob_end_flush();
|
||
}
|
||
flush();
|
||
}
|
||
|
||
// --- Schaltende Aufrufe ---------------------------------------------------
|
||
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||
$body = json_decode(file_get_contents("php://input"), true);
|
||
if (!is_array($body)) {
|
||
raumAntwort(["error" => "Konnte die Anfrage nicht lesen."], 400);
|
||
}
|
||
try {
|
||
if (($_GET["action"] ?? "") === "command") {
|
||
$id = intval($body["command_id"] ?? 0);
|
||
$text = executeCommand($id,
|
||
is_array($body["params"] ?? null) ? $body["params"] : [],
|
||
!empty($body["probe"]));
|
||
// Bei einem Tahoma-Geraet ist mit dem Absenden noch nichts
|
||
// festgehalten: die Box meldet die neue Stellung erst, wenn die
|
||
// Fahrt zu Ende ist. Also Antwort raus und in Ruhe nachsehen.
|
||
$url = empty($body["probe"]) ? tahomaUrlZuKommando($id) : null;
|
||
if ($url === null) {
|
||
raumAntwort(["ok" => $text]);
|
||
}
|
||
raumAntwortUndWeiter(["ok" => $text]);
|
||
tahomaNachlesen($url);
|
||
exit;
|
||
}
|
||
if (($_GET["action"] ?? "") === "temp") {
|
||
// Bewusst derselbe Weg wie im alten Thermostat-Modal: das Topic
|
||
// Raumtemp/<Etage>/<Raum>/changeSetTemp.
|
||
// Das Leerzeichen muss durch: Raumnamen wie "Carport EG" haben
|
||
// eines, und ohne es findet der Vergleich weiter unten den Raum
|
||
// nicht mehr.
|
||
$raum = preg_replace('/[^A-Za-z0-9_ ]/', '', strval($body["room"] ?? ""));
|
||
$temp = intval($body["temp"] ?? 0);
|
||
if ($temp < 10 || $temp > 30) {
|
||
raumAntwort(["error" => "Temperatur außerhalb 10–30 °C."], 400);
|
||
}
|
||
$mqtt = new Bluerhinos\phpMQTT(MQTT_BROKER, MQTT_PORT, "RoomTempSetter_" . getmypid());
|
||
if (!$mqtt->connect(true, null, "", "")) {
|
||
raumAntwort(["error" => "Keine Verbindung zum MQTT-Broker."], 500);
|
||
}
|
||
$mqtt->publish("Raumtemp/" . str_replace("_", "/", $raum) . "/changeSetTemp",
|
||
strval($temp), 0, false);
|
||
$mqtt->close();
|
||
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);
|
||
} catch (InvalidArgumentException $e) {
|
||
raumAntwort(["error" => $e->getMessage()], 400);
|
||
} catch (Throwable $e) {
|
||
raumAntwort(["error" => $e->getMessage()], 500);
|
||
}
|
||
}
|
||
|
||
|
||
// --- Inhalt des Modals ----------------------------------------------------
|
||
// Das Leerzeichen gehoert zu den erlaubten Zeichen. Ohne es wurde aus
|
||
// "AG_Carport EG" ein "AG_CarportEG", und der Vergleich gegen die
|
||
// gespeicherte Zuordnung "AG/Carport EG" ging ins Leere - das Modal meldete
|
||
// dann "Diesem Raum ist noch nichts zugeordnet". Aufgefallen ist es erst mit
|
||
// dem Aussengelaende: dort haben sechs von neun Raeumen ein Leerzeichen im
|
||
// Namen, auf den anderen Etagen keiner von achtzehn.
|
||
$schluessel = preg_replace('/[^A-Za-z0-9_ ]/', '', strval($_GET["room"] ?? ""));
|
||
$teile = explode("_", $schluessel, 2);
|
||
$etage = $teile[0] ?? "";
|
||
$name = $teile[1] ?? "";
|
||
$raumKey = $etage . "/" . $name;
|
||
|
||
// Alles im Raum, sortiert nach Bedienform. Das Thermostat selbst wird nicht
|
||
// als Karte gezeichnet: es wird zum Schieberegler im Heizungs-Reiter und
|
||
// braucht daneben keine zweite Bedienung - "Solltemperatur" stand sonst
|
||
// zweimal im Modal.
|
||
//
|
||
// Ob es einen Heizungs-Reiter gibt, entscheidet genau das: haengt in diesem
|
||
// Raum ein Thermostat? Frueher stand hier "hat der Raum eine Kachel" - das
|
||
// stimmte, solange nur Raeume mit Thermostat eine bekamen. Seit auch Kueche
|
||
// und Technik eine haben, bekamen sie einen Heizungsregler fuer eine
|
||
// Heizung, die es dort nicht gibt.
|
||
$katalog = deviceCatalog();
|
||
$hatThermostat = false;
|
||
$gruppen = [];
|
||
foreach ($katalog as $d) {
|
||
if ($d["room"] !== $raumKey) {
|
||
continue;
|
||
}
|
||
$form = bedienform($d);
|
||
if ($form === "heizung") {
|
||
$hatThermostat = true;
|
||
continue;
|
||
}
|
||
$gruppen[$form][] = $d;
|
||
}
|
||
|
||
// Die Heizung ist eine Gruppe wie die anderen, nur baut sie sich nicht aus
|
||
// einer Geraeteliste: ein Regler fuer den Raum statt einer Karte je Geraet.
|
||
$fertig = [];
|
||
if ($hatThermostat) {
|
||
$karte = "<div class='card mb-2 raum-geraet'><div class='card-body py-2'>"
|
||
. "<div class='d-flex justify-content-between align-items-baseline'>"
|
||
. "<label class='form-label mb-0 small' for='modal-slider'>Solltemperatur</label>"
|
||
. "<span id='modal-slider-label' class='fw-semibold text-nowrap ms-2' style='color:#297195'>–</span>"
|
||
. "</div>"
|
||
. "<input type='range' id='modal-slider' class='form-range range-color-track' min='10' max='30'>"
|
||
// Der Ist-Zustand kommt live ueber MQTT, nicht aus der Datenbank -
|
||
// der Browser hoert ohnehin schon auf Raumtemp/#, und ein Wert von
|
||
// vorhin waere neben dem Regler nur irrefuehrend.
|
||
. "<div class='d-flex flex-wrap gap-3 small text-body-secondary'>"
|
||
. "<span><i class='bi bi-thermometer-half me-1'></i><span data-mqtt='Temp[degC]'>–</span> °C</span>"
|
||
. "<span><i class='bi bi-droplet me-1'></i><span data-mqtt='rHum[%]'>–</span> %rF</span>"
|
||
. "<span data-mqtt-heizung class='d-none'><i class='bi bi-fire text-danger me-1'></i>heizt</span>"
|
||
. "</div></div></div>";
|
||
$fertig["heizung"] = $karte;
|
||
}
|
||
|
||
echo "<div class='p-1' id='roomView' data-room='" . htmlspecialchars($schluessel, ENT_QUOTES) . "'>";
|
||
|
||
if (!$gruppen && !$fertig) {
|
||
echo "<p class='text-body-secondary mb-0'>Diesem Raum ist noch nichts zugeordnet. "
|
||
. "Das geht über das Haus-Symbol in der Karte „Automatismen“.</p>";
|
||
}
|
||
|
||
echo zeichneGruppen($gruppen, $fertig);
|
||
|
||
echo "<div id='roomFeedback' class='small mt-2'></div>";
|
||
echo "</div>";
|