Files
Smart-Dashboard/ajax/room.php
T
adminandClaude Opus 5 8914133d01 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>
2026-09-05 12:18:04 +02:00

186 lines
8.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}
// --- 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") {
$text = executeCommand(intval($body["command_id"] ?? 0),
is_array($body["params"] ?? null) ? $body["params"] : [],
!empty($body["probe"]));
raumAntwort(["ok" => $text]);
}
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 1030 °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>";