Raeume ueber ihre feste Nummer: actors.room_id, Raum-Modal, Thermostat-Zweig
Geraete hingen ueber den Text "Etage/Raum" an ihrem Raum, das Raum-Modal zerlegte "EG_Bad" am Unterstrich, und das Thermostat-Topic wurde aus Etage und Name zusammengesetzt. Ein umbenannter Raum haette still alle Geraete, seinen Temperaturregler und sein Heizsymbol verloren. - actors.room (Text) -> actors.room_id, Fremdschluessel auf rooms, beim Loeschen eines Raums SET NULL. Alle 59 Zuordnungen uebernommen. - automations.floor: ENUM(UG,EG,OG,AG) -> Verweis auf floors. Eine neue Etage kann damit auch Automatiken tragen. - room.php nimmt ?room=<Nummer>; Solltemperatur geht an rooms.thermostat + /changeSetTemp. - homeMQTT.js: Heizsymbol, Regler und Ist-Werte ueber mqttZweig(thermostat) statt mqttData.Raumtemp[Etage][Name]. - Raumvorschlaege in den Einstellungen: Messwerte unter dem Thermostat- Zweig eines Raums statt fest Raumtemp/<Etage>/<Name>. - Schluessel in raumListe(), Katalog und Maske ist die Raumnummer als Text. Geprueft gegen den Stand davor: Startseite gleich bis auf das Klickziel, jede der 25 Kacheln oeffnet ihren eigenen Raum; Geraeteliste, Vorschlaege, Editor-Katalog und die eigenen Geraete je Kachel gleich. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+39
-23
@@ -6,14 +6,29 @@
|
|||||||
* sollte ein Klick darauf den Raum oeffnen und nicht nur seine Heizung - die
|
* sollte ein Klick darauf den Raum oeffnen und nicht nur seine Heizung - die
|
||||||
* Rollladen im selben Zimmer waren bisher gar nicht erreichbar.
|
* Rollladen im selben Zimmer waren bisher gar nicht erreichbar.
|
||||||
*
|
*
|
||||||
* Welche Geraete zu einem Raum gehoeren, sagt die Spalte actors.room (siehe
|
* Welche Geraete zu einem Raum gehoeren, sagt die Spalte actors.room_id
|
||||||
* den Zuordnungsdialog in der Karte "Automatismen").
|
* (Einstellungen -> Geraete).
|
||||||
*
|
*
|
||||||
* GET ?room=EG_Bad Inhalt des Modals
|
* GET ?room=12 Inhalt des Modals, 12 = rooms.id
|
||||||
* POST ?action=command {"command_id": N, "params": {...}} - ein Kommando
|
* POST ?action=command {"command_id": N, "params": {...}} - ein Kommando
|
||||||
* POST ?action=temp {"room": "EG_Bad", "temp": 21} - Solltemperatur
|
* POST ?action=temp {"room": 12, "temp": 21} - Solltemperatur
|
||||||
|
*
|
||||||
|
* Frueher hiess der Raum "EG_Bad" und wurde am Unterstrich zerlegt. Das hielt
|
||||||
|
* weder ein Leerzeichen im Namen aus (erst nachgeruestet) noch eine
|
||||||
|
* Umbenennung - die feste Nummer haelt beides.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/** Ein Raum nach seiner festen Nummer, oder null. */
|
||||||
|
function raumNachNummer($nr)
|
||||||
|
{
|
||||||
|
foreach (allRooms() as $r) {
|
||||||
|
if ($r["nr"] === intval($nr)) {
|
||||||
|
return $r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Ein "Zu" an einer Jalousie faehrt herunter, wartet und neigt danach - das
|
// 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
|
// dauert in der Summe ueber eine Minute. Die Vorgabe von PHP reicht dafuer
|
||||||
// nicht; ajax/tahoma.php macht es an derselben Stelle genauso.
|
// nicht; ajax/tahoma.php macht es an derselben Stelle genauso.
|
||||||
@@ -102,12 +117,14 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
if (($_GET["action"] ?? "") === "temp") {
|
if (($_GET["action"] ?? "") === "temp") {
|
||||||
// Bewusst derselbe Weg wie im alten Thermostat-Modal: das Topic
|
// Derselbe Weg wie im alten Thermostat-Modal: <Zweig>/changeSetTemp.
|
||||||
// Raumtemp/<Etage>/<Raum>/changeSetTemp.
|
// Der Zweig steht am Raum (rooms.thermostat) und wird nicht mehr
|
||||||
// Das Leerzeichen muss durch: Raumnamen wie "Carport EG" haben
|
// aus Etage und Name zusammengesetzt - ein Thermostat in einem
|
||||||
// eines, und ohne es findet der Vergleich weiter unten den Raum
|
// anderen Haus meldet vielleicht unter einem ganz anderen.
|
||||||
// nicht mehr.
|
$raum = raumNachNummer($body["room"] ?? 0);
|
||||||
$raum = preg_replace('/[^A-Za-z0-9_ ]/', '', strval($body["room"] ?? ""));
|
if (!$raum || empty($raum["thermostat"])) {
|
||||||
|
raumAntwort(["error" => "Dieser Raum hat kein Thermostat."], 400);
|
||||||
|
}
|
||||||
$temp = intval($body["temp"] ?? 0);
|
$temp = intval($body["temp"] ?? 0);
|
||||||
if ($temp < 10 || $temp > 30) {
|
if ($temp < 10 || $temp > 30) {
|
||||||
raumAntwort(["error" => "Temperatur außerhalb 10–30 °C."], 400);
|
raumAntwort(["error" => "Temperatur außerhalb 10–30 °C."], 400);
|
||||||
@@ -116,7 +133,7 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|||||||
if (!$mqtt->connect(true, null, "", "")) {
|
if (!$mqtt->connect(true, null, "", "")) {
|
||||||
raumAntwort(["error" => "Keine Verbindung zum MQTT-Broker."], 500);
|
raumAntwort(["error" => "Keine Verbindung zum MQTT-Broker."], 500);
|
||||||
}
|
}
|
||||||
$mqtt->publish("Raumtemp/" . str_replace("_", "/", $raum) . "/changeSetTemp",
|
$mqtt->publish(rtrim($raum["thermostat"], "/") . "/changeSetTemp",
|
||||||
strval($temp), 0, false);
|
strval($temp), 0, false);
|
||||||
$mqtt->close();
|
$mqtt->close();
|
||||||
raumAntwort(["ok" => "Solltemperatur " . $temp . " °C"]);
|
raumAntwort(["ok" => "Solltemperatur " . $temp . " °C"]);
|
||||||
@@ -156,17 +173,13 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|||||||
|
|
||||||
|
|
||||||
// --- Inhalt des Modals ----------------------------------------------------
|
// --- Inhalt des Modals ----------------------------------------------------
|
||||||
// Das Leerzeichen gehoert zu den erlaubten Zeichen. Ohne es wurde aus
|
$raum = raumNachNummer($_GET["room"] ?? 0);
|
||||||
// "AG_Carport EG" ein "AG_CarportEG", und der Vergleich gegen die
|
if (!$raum) {
|
||||||
// gespeicherte Zuordnung "AG/Carport EG" ging ins Leere - das Modal meldete
|
echo "<p class='text-body-secondary mb-0'>Diesen Raum gibt es nicht (mehr).</p>";
|
||||||
// dann "Diesem Raum ist noch nichts zugeordnet". Aufgefallen ist es erst mit
|
exit;
|
||||||
// dem Aussengelaende: dort haben sechs von neun Raeumen ein Leerzeichen im
|
}
|
||||||
// Namen, auf den anderen Etagen keiner von achtzehn.
|
// Derselbe Schluessel wie actors.room_id im Katalog - als Text, siehe raumListe().
|
||||||
$schluessel = preg_replace('/[^A-Za-z0-9_ ]/', '', strval($_GET["room"] ?? ""));
|
$raumKey = strval($raum["nr"]);
|
||||||
$teile = explode("_", $schluessel, 2);
|
|
||||||
$etage = $teile[0] ?? "";
|
|
||||||
$name = $teile[1] ?? "";
|
|
||||||
$raumKey = $etage . "/" . $name;
|
|
||||||
|
|
||||||
// Alles im Raum, sortiert nach Bedienform. Das Thermostat selbst wird nicht
|
// Alles im Raum, sortiert nach Bedienform. Das Thermostat selbst wird nicht
|
||||||
// als Karte gezeichnet: es wird zum Schieberegler im Heizungs-Reiter und
|
// als Karte gezeichnet: es wird zum Schieberegler im Heizungs-Reiter und
|
||||||
@@ -214,7 +227,10 @@ if ($hatThermostat) {
|
|||||||
$fertig["heizung"] = $karte;
|
$fertig["heizung"] = $karte;
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "<div class='p-1' id='roomView' data-room='" . htmlspecialchars($schluessel, ENT_QUOTES) . "'>";
|
// Der Thermostat-Zweig reist mit: das Modal fuellt Ist-Werte und Heizsymbol
|
||||||
|
// aus dem laufenden MQTT-Strom und muss wissen, wo es nachsehen soll.
|
||||||
|
echo "<div class='p-1' id='roomView' data-room='" . $raum["nr"] . "'"
|
||||||
|
. " data-thermostat='" . htmlspecialchars((string)$raum["thermostat"], ENT_QUOTES) . "'>";
|
||||||
|
|
||||||
if (!$gruppen && !$fertig) {
|
if (!$gruppen && !$fertig) {
|
||||||
echo "<p class='text-body-secondary mb-0'>Diesem Raum ist noch nichts zugeordnet. "
|
echo "<p class='text-body-secondary mb-0'>Diesem Raum ist noch nichts zugeordnet. "
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ CREATE TABLE `actors` (
|
|||||||
`type` varchar(50) NOT NULL COMMENT 'Gerätetyp z.B. RollerShutter',
|
`type` varchar(50) NOT NULL COMMENT 'Gerätetyp z.B. RollerShutter',
|
||||||
`name` varchar(70) NOT NULL COMMENT 'Name des Geräts',
|
`name` varchar(70) NOT NULL COMMENT 'Name des Geräts',
|
||||||
`url` varchar(100) NOT NULL COMMENT 'Tahoma Device URL',
|
`url` varchar(100) NOT NULL COMMENT 'Tahoma Device URL',
|
||||||
`room` varchar(60) DEFAULT NULL COMMENT 'Raum aus restricted/rooms.php als "Etage/Raum" (z.B. OG/Bad) oder "aussen". Wird von Hand vergeben und deshalb vom Discovery nicht angefasst - der Upsert setzt nur type und name.'
|
`room_id` int(11) DEFAULT NULL COMMENT 'Raum (rooms.id, siehe homeMesh_grundriss.sql). Wird von Hand vergeben und deshalb vom Discovery nicht angefasst - der Upsert setzt nur type und name.'
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||||
|
|
||||||
--
|
--
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ SET NAMES utf8mb4;
|
|||||||
CREATE TABLE IF NOT EXISTS `automations` (
|
CREATE TABLE IF NOT EXISTS `automations` (
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
`name` varchar(100) NOT NULL COMMENT 'Anzeigename, z.B. Kinder zu',
|
`name` varchar(100) NOT NULL COMMENT 'Anzeigename, z.B. Kinder zu',
|
||||||
`floor` enum('UG','EG','OG','AG') NOT NULL DEFAULT 'OG' COMMENT 'Reiter in der Uebersicht. Ohne Etage waere die Automatik in keinem davon zu sehen, deshalb gibt es keinen leeren Wert. AG ist das Aussengelaende - eine Etage ohne Grundriss, siehe restricted/rooms.php',
|
`floor` varchar(4) NOT NULL COMMENT 'Reiter in der Uebersicht (floors.code, Fremdschluessel legt homeMesh_grundriss.sql an). Ohne Etage waere die Automatik in keinem Reiter zu sehen, deshalb kein leerer Wert',
|
||||||
`enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'Aktiv oder pausiert',
|
`enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'Aktiv oder pausiert',
|
||||||
`window_from` time NOT NULL DEFAULT '00:00:00' COMMENT 'Aktiver Zeitraum, Beginn',
|
`window_from` time NOT NULL DEFAULT '00:00:00' COMMENT 'Aktiver Zeitraum, Beginn',
|
||||||
`window_to` time NOT NULL DEFAULT '23:59:00' COMMENT 'Aktiver Zeitraum, Ende. Kleiner als window_from = ueber Mitternacht',
|
`window_to` time NOT NULL DEFAULT '23:59:00' COMMENT 'Aktiver Zeitraum, Ende. Kleiner als window_from = ueber Mitternacht',
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
-- Die Tabellen duerfen leer sein. Dann zeigt die Startseite einen Hinweis auf
|
-- Die Tabellen duerfen leer sein. Dann zeigt die Startseite einen Hinweis auf
|
||||||
-- die Einstellungen statt eines Hauses - das ist der Anfang jeder neuen
|
-- die Einstellungen statt eines Hauses - das ist der Anfang jeder neuen
|
||||||
-- Installation.
|
-- Installation.
|
||||||
|
--
|
||||||
|
-- Einspielen NACH homeMesh_DB-layout.sql und homeMesh_automations.sql: am
|
||||||
|
-- Ende haengt diese Datei die Fremdschluessel von actors.room_id und
|
||||||
|
-- automations.floor an die neuen Tabellen. Ein zweiter Lauf schadet nicht.
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
SET NAMES utf8mb4;
|
SET NAMES utf8mb4;
|
||||||
@@ -63,3 +67,30 @@ CREATE TABLE IF NOT EXISTS `rooms` (
|
|||||||
CONSTRAINT `fk_rooms_floor` FOREIGN KEY (`floor`) REFERENCES `floors` (`code`)
|
CONSTRAINT `fk_rooms_floor` FOREIGN KEY (`floor`) REFERENCES `floors` (`code`)
|
||||||
ON UPDATE CASCADE ON DELETE RESTRICT
|
ON UPDATE CASCADE ON DELETE RESTRICT
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Verweise auf Etagen und Raeume.
|
||||||
|
--
|
||||||
|
-- actors.room_id: ein geloeschter Raum laesst seine Geraete unzugeordnet
|
||||||
|
-- zurueck (SET NULL) - sie tauchen dann unter "ohne Raum" wieder auf.
|
||||||
|
-- automations.floor: eine Etage mit Automatiken laesst sich nicht loeschen
|
||||||
|
-- (RESTRICT); die Einstellungsseite nennt vorher die Namen.
|
||||||
|
--
|
||||||
|
-- Idempotent ueber information_schema, weil MariaDB kein
|
||||||
|
-- ADD CONSTRAINT IF NOT EXISTS fuer Fremdschluessel kennt.
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
SET @da := (SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS
|
||||||
|
WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'fk_actors_room');
|
||||||
|
SET @sql := IF(@da = 0,
|
||||||
|
'ALTER TABLE actors ADD KEY idx_actors_room (room_id),
|
||||||
|
ADD CONSTRAINT fk_actors_room FOREIGN KEY (room_id) REFERENCES rooms (id) ON DELETE SET NULL',
|
||||||
|
'DO 0');
|
||||||
|
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @da := (SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS
|
||||||
|
WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'fk_automations_floor');
|
||||||
|
SET @sql := IF(@da = 0,
|
||||||
|
'ALTER TABLE automations ADD CONSTRAINT fk_automations_floor FOREIGN KEY (floor)
|
||||||
|
REFERENCES floors (code) ON UPDATE CASCADE ON DELETE RESTRICT',
|
||||||
|
'DO 0');
|
||||||
|
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|||||||
+33
-12
@@ -192,7 +192,7 @@ const homeSVG = {
|
|||||||
// Heizsymbol und Pufferbetrieb gibt es nur, wo ein Thermostat
|
// Heizsymbol und Pufferbetrieb gibt es nur, wo ein Thermostat
|
||||||
// haengt - anderswo sind die Elemente gar nicht gezeichnet.
|
// haengt - anderswo sind die Elemente gar nicht gezeichnet.
|
||||||
if (!room.heizung) return;
|
if (!room.heizung) return;
|
||||||
const values = mqttData["Raumtemp"]?.[room.floor]?.[room.mqtt];
|
const values = mqttZweig(room.thermostat);
|
||||||
if (!values) return;
|
if (!values) return;
|
||||||
if (typeof (values["Heating"]) != "undefined")
|
if (typeof (values["Heating"]) != "undefined")
|
||||||
el(room.id + "_heater").setAttribute("display", values["Heating"] == "false" ? "none" : "");
|
el(room.id + "_heater").setAttribute("display", values["Heating"] == "false" ? "none" : "");
|
||||||
@@ -276,8 +276,23 @@ function switchFloor(floor){
|
|||||||
|
|
||||||
var mqttData = {};
|
var mqttData = {};
|
||||||
|
|
||||||
function openHeaterSettings(heater){
|
/**
|
||||||
openRoomView(heater);
|
* Ein Zweig des Nachrichtenbaums nach seinem Topic, z.B. "Raumtemp/EG/Bad".
|
||||||
|
*
|
||||||
|
* Frueher stand hier fest mqttData["Raumtemp"][Etage][Raum] - das setzte
|
||||||
|
* voraus, dass jedes Thermostat unter Raumtemp/<Etage>/<Raumname> meldet.
|
||||||
|
* Jetzt steht der Zweig am Raum (rooms.thermostat).
|
||||||
|
*/
|
||||||
|
function mqttZweig(topic) {
|
||||||
|
if (!topic) return undefined;
|
||||||
|
return String(topic).split("/").filter(t => t !== "")
|
||||||
|
.reduce((knoten, teil) => knoten?.[teil], mqttData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ein Raum aus homeRooms nach seiner festen Nummer. */
|
||||||
|
function raumNachNummer(nr) {
|
||||||
|
return (typeof homeRooms !== "undefined" ? homeRooms : [])
|
||||||
|
.find(r => r.nr === Number(nr));
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('readystatechange', function () {
|
document.addEventListener('readystatechange', function () {
|
||||||
@@ -294,13 +309,19 @@ function switchFloor(floor){
|
|||||||
* oeffnen - vorher kam nur der Temperaturregler, die Rollladen im selben
|
* oeffnen - vorher kam nur der Temperaturregler, die Rollladen im selben
|
||||||
* Zimmer waren von hier gar nicht erreichbar.
|
* Zimmer waren von hier gar nicht erreichbar.
|
||||||
*
|
*
|
||||||
* Welche Geraete dazugehoeren, sagt die Raumzuordnung (actors.room).
|
* Welche Geraete dazugehoeren, sagt die Raumzuordnung (actors.room_id).
|
||||||
|
* `raum` ist die feste Raumnummer (rooms.id).
|
||||||
*/
|
*/
|
||||||
function openRoomView(raum) {
|
function openRoomView(raum) {
|
||||||
const teile = raum.toString().split("_");
|
const eintrag = raumNachNummer(raum);
|
||||||
document.getElementById("modal-title").innerHTML =
|
// Der Name kommt jetzt aus der Datenbank und darf Umlaute tragen. Die
|
||||||
teile[0] + " · " + teile.slice(1).join(" ")
|
// Ersetzung bleibt fuer die alten Namen ("Kueche"), bis sie jemand
|
||||||
.replace("ue", "ü").replace("ae", "ä").replace("oe", "ö");
|
// unter Einstellungen -> Grundriss umbenennt.
|
||||||
|
const titel = eintrag
|
||||||
|
? eintrag.floor + " · " + eintrag.mqtt.replace("ue", "ü").replace("ae", "ä").replace("oe", "ö")
|
||||||
|
: "Raum";
|
||||||
|
const modalTitel = document.getElementById("modal-title");
|
||||||
|
modalTitel.textContent = titel;
|
||||||
|
|
||||||
// Der Speichern-Knopf gehoert allen Modals gemeinsam. Ein Klon ohne
|
// Der Speichern-Knopf gehoert allen Modals gemeinsam. Ein Klon ohne
|
||||||
// Zuhoerer stellt sicher, dass nicht der Handler eines vorher
|
// Zuhoerer stellt sicher, dass nicht der Handler eines vorher
|
||||||
@@ -330,7 +351,7 @@ function switchFloor(floor){
|
|||||||
if (slider) {
|
if (slider) {
|
||||||
bindModalSlider(slider, document.getElementById("modal-slider-label"),
|
bindModalSlider(slider, document.getElementById("modal-slider-label"),
|
||||||
wert => wert + " °C");
|
wert => wert + " °C");
|
||||||
const stand = mqttData["Raumtemp"]?.[teile[0]]?.[teile[1]]?.["Set Temp[degC]"];
|
const stand = mqttZweig(eintrag?.thermostat)?.["Set Temp[degC]"];
|
||||||
slider.value = typeof stand !== "undefined" ? stand : 10;
|
slider.value = typeof stand !== "undefined" ? stand : 10;
|
||||||
// Nur "input": das zeichnet die Anzeige, schickt aber nichts.
|
// Nur "input": das zeichnet die Anzeige, schickt aber nichts.
|
||||||
slider.dispatchEvent(new Event("input"));
|
slider.dispatchEvent(new Event("input"));
|
||||||
@@ -345,7 +366,7 @@ function switchFloor(floor){
|
|||||||
// Fahrt aufmacht, saehe sonst den Stand von davor und nie wieder
|
// Fahrt aufmacht, saehe sonst den Stand von davor und nie wieder
|
||||||
// etwas anderes: gezeichnet wird nur beim Oeffnen.
|
// etwas anderes: gezeichnet wird nur beim Oeffnen.
|
||||||
koerper.querySelectorAll(".raum-geraet").forEach(k => jalousieVerfolgen(k, null));
|
koerper.querySelectorAll(".raum-geraet").forEach(k => jalousieVerfolgen(k, null));
|
||||||
raumMesswerteAktualisieren(teile[0], teile[1]);
|
raumMesswerteAktualisieren(eintrag?.thermostat);
|
||||||
})
|
})
|
||||||
.catch(fehler => { koerper.innerHTML = "Konnte den Raum nicht laden: " + fehler.message; });
|
.catch(fehler => { koerper.innerHTML = "Konnte den Raum nicht laden: " + fehler.message; });
|
||||||
}
|
}
|
||||||
@@ -504,8 +525,8 @@ function switchFloor(floor){
|
|||||||
* Der Browser hoert ohnehin schon auf Raumtemp/#, und ein Wert aus der
|
* Der Browser hoert ohnehin schon auf Raumtemp/#, und ein Wert aus der
|
||||||
* Datenbank waere neben dem Regler nur ein Stand von vorhin.
|
* Datenbank waere neben dem Regler nur ein Stand von vorhin.
|
||||||
*/
|
*/
|
||||||
function raumMesswerteAktualisieren(etage, raum) {
|
function raumMesswerteAktualisieren(thermostat) {
|
||||||
const werte = mqttData["Raumtemp"]?.[etage]?.[raum];
|
const werte = mqttZweig(thermostat);
|
||||||
if (!werte) return;
|
if (!werte) return;
|
||||||
document.querySelectorAll("#roomView [data-mqtt]").forEach(feld => {
|
document.querySelectorAll("#roomView [data-mqtt]").forEach(feld => {
|
||||||
const wert = werte[feld.dataset.mqtt];
|
const wert = werte[feld.dataset.mqtt];
|
||||||
|
|||||||
@@ -498,7 +498,7 @@ function kachelWertZeile(wert, i, kachel) {
|
|||||||
// Ohne Treffer beim ersten Geraet des eigenen Raums beginnen, sonst beim
|
// Ohne Treffer beim ersten Geraet des eigenen Raums beginnen, sonst beim
|
||||||
// ersten ueberhaupt - ein neuer Wert soll nicht im Nachbargeschoss starten.
|
// ersten ueberhaupt - ein neuer Wert soll nicht im Nachbargeschoss starten.
|
||||||
const ersterEigener = katalog.findIndex(
|
const ersterEigener = katalog.findIndex(
|
||||||
(g) => kachel && g.floorKey === kachel.floor && g.raumKey === kachel.name);
|
(g) => kachel && g.raumKey === kachel.nr);
|
||||||
const gi = treffer ? treffer.gi : Math.max(0, ersterEigener);
|
const gi = treffer ? treffer.gi : Math.max(0, ersterEigener);
|
||||||
const wi = treffer ? treffer.wi : -1;
|
const wi = treffer ? treffer.wi : -1;
|
||||||
|
|
||||||
@@ -512,7 +512,9 @@ function kachelWertZeile(wert, i, kachel) {
|
|||||||
* in der gefilterten Liste - sonst zeigte die Auswahl nach dem Umsortieren
|
* in der gefilterten Liste - sonst zeigte die Auswahl nach dem Umsortieren
|
||||||
* auf ein anderes Geraet.
|
* auf ein anderes Geraet.
|
||||||
*/
|
*/
|
||||||
const eigen = (g) => kachel && g.floorKey === kachel.floor && g.raumKey === kachel.name;
|
// Ueber die feste Raumnummer, nicht ueber den Namen - "Bad" gibt es auf
|
||||||
|
// drei Etagen, und ein umbenannter Raum bliebe sonst ohne eigene Geraete.
|
||||||
|
const eigen = (g) => kachel && g.raumKey === kachel.nr;
|
||||||
const option = (g, index) => '<option value="' + index + '"'
|
const option = (g, index) => '<option value="' + index + '"'
|
||||||
+ (index === gi ? " selected" : "") + ">"
|
+ (index === gi ? " selected" : "") + ">"
|
||||||
+ textSchuetzen(g.label + (g.raum && !eigen(g) ? " – " + g.raum : "")) + "</option>";
|
+ textSchuetzen(g.label + (g.raum && !eigen(g) ? " – " + g.raum : "")) + "</option>";
|
||||||
|
|||||||
+48
-38
@@ -333,9 +333,12 @@ function anzeigeName($name)
|
|||||||
/**
|
/**
|
||||||
* Waehlbare Raeume: die Raeume der Home-Ansicht.
|
* Waehlbare Raeume: die Raeume der Home-Ansicht.
|
||||||
*
|
*
|
||||||
* Einzige Quelle bleibt restricted/rooms.php - ein neuer Raum ist dort eine
|
* Einzige Quelle bleibt restricted/rooms.php (dahinter homeMesh.rooms) - ein
|
||||||
* Zeile und taucht hier automatisch auf. Der Schluessel ist "Etage/Raum",
|
* neuer Raum taucht hier automatisch auf. Der Schluessel ist die feste
|
||||||
* weil "Bad" allein auf drei Etagen vorkommt.
|
* Raumnummer als Text, so wie actors.room_id sie traegt. Frueher war es
|
||||||
|
* "Etage/Raum" - ein umbenannter Raum verlor damit still alle Geraete. Als
|
||||||
|
* Text, weil er im Browser mit dem Wert einer Auswahlliste verglichen wird,
|
||||||
|
* und der ist immer einer.
|
||||||
*
|
*
|
||||||
* Frueher stand hier ein Pseudo-Raum "aussen" fuer alles, was zu keinem Raum
|
* Frueher stand hier ein Pseudo-Raum "aussen" fuer alles, was zu keinem Raum
|
||||||
* der Wohnung gehoert - Carport, Pergola, Garage, die Wechselrichter. Das war
|
* der Wohnung gehoert - Carport, Pergola, Garage, die Wechselrichter. Das war
|
||||||
@@ -353,7 +356,7 @@ function raumListe()
|
|||||||
$liste = [];
|
$liste = [];
|
||||||
foreach (allRooms() as $raum) {
|
foreach (allRooms() as $raum) {
|
||||||
$liste[] = [
|
$liste[] = [
|
||||||
"key" => $raum["floor"] . "/" . $raum["mqtt"],
|
"key" => strval($raum["nr"]),
|
||||||
"label" => $raum["floor"] . " · " . $raum["mqtt"],
|
"label" => $raum["floor"] . " · " . $raum["mqtt"],
|
||||||
"floor" => $raum["floor"],
|
"floor" => $raum["floor"],
|
||||||
"room" => $raum["mqtt"],
|
"room" => $raum["mqtt"],
|
||||||
@@ -365,17 +368,17 @@ function raumListe()
|
|||||||
/** Anzeigename eines gespeicherten Raumschluessels. */
|
/** Anzeigename eines gespeicherten Raumschluessels. */
|
||||||
function raumBeschriftung($key)
|
function raumBeschriftung($key)
|
||||||
{
|
{
|
||||||
if (!$key) {
|
if ($key === null || $key === "") {
|
||||||
return "Nicht zugeordnet";
|
return "Nicht zugeordnet";
|
||||||
}
|
}
|
||||||
foreach (raumListe() as $raum) {
|
foreach (raumListe() as $raum) {
|
||||||
if ($raum["key"] === $key) {
|
if ($raum["key"] === strval($key)) {
|
||||||
return $raum["label"];
|
return $raum["label"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Raum aus rooms.php entfernt - der Eintrag am Geraet bleibt stehen,
|
// Kann nur noch an der Datenbank vorbei entstehen: fk_actors_room setzt
|
||||||
// damit eine Umbenennung dort keine Zuordnung stillschweigend loescht.
|
// die Zuordnung beim Loeschen eines Raums auf NULL.
|
||||||
return $key . " (unbekannt)";
|
return "Raum " . $key . " (unbekannt)";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -389,43 +392,46 @@ function raumBeschriftung($key)
|
|||||||
*/
|
*/
|
||||||
function raumEinordnung($key)
|
function raumEinordnung($key)
|
||||||
{
|
{
|
||||||
if (!$key) {
|
if ($key === null || $key === "") {
|
||||||
return ["floor" => "Ohne Raum", "room" => ""];
|
return ["floor" => "Ohne Raum", "room" => ""];
|
||||||
}
|
}
|
||||||
foreach (raumListe() as $raum) {
|
foreach (raumListe() as $raum) {
|
||||||
if ($raum["key"] === $key) {
|
if ($raum["key"] === strval($key)) {
|
||||||
return ["floor" => $raum["floor"], "room" => $raum["room"]];
|
return ["floor" => $raum["floor"], "room" => $raum["room"]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ["floor" => "Unbekannter Raum", "room" => $key];
|
return ["floor" => "Unbekannter Raum", "room" => strval($key)];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Raumvorschlaege aus den MQTT-Topics.
|
* Raumvorschlaege aus den MQTT-Topics.
|
||||||
*
|
*
|
||||||
* Die Thermostate tragen ihren Raum im Topic: "Raumtemp/OG/Bad/Temp[degC]".
|
* Ein Thermostat meldet unter dem Zweig, der am Raum eingetragen ist
|
||||||
* Das ist keine Rateaufgabe, sondern dieselbe Schreibweise wie in rooms.php,
|
* (rooms.thermostat, etwa "Raumtemp/OG/Bad"). Ein Messwert darunter gehoert
|
||||||
* und deckt dreizehn Geraete ohne Zutun ab. Bei allen anderen wird bewusst
|
* also zu diesem Raum - das ist keine Rateaufgabe und deckt die Thermostate
|
||||||
* nicht geraten: "Bad Links" sagt nichts darueber, auf welcher der drei
|
* ohne Zutun ab. Bei allen anderen wird bewusst nicht geraten: "Bad Links"
|
||||||
* Etagen dieses Bad liegt.
|
* sagt nichts darueber, auf welcher Etage dieses Bad liegt.
|
||||||
*/
|
*/
|
||||||
function raumVorschlaege()
|
function raumVorschlaege()
|
||||||
{
|
{
|
||||||
$bekannt = [];
|
$zweige = [];
|
||||||
foreach (raumListe() as $raum) {
|
foreach (allRooms() as $raum) {
|
||||||
$bekannt[strtolower($raum["key"])] = $raum["key"];
|
if (!empty($raum["thermostat"])) {
|
||||||
|
$zweige[strtolower(rtrim($raum["thermostat"], "/")) . "/"] = strval($raum["nr"]);
|
||||||
}
|
}
|
||||||
$res = meshDb()->query("SELECT actor_id, url FROM actor_states
|
}
|
||||||
WHERE url LIKE 'Raumtemp/%/%/%'");
|
if (!$zweige) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$res = meshDb()->query("SELECT actor_id, url FROM actor_states WHERE url LIKE '%/%'");
|
||||||
$vorschlag = [];
|
$vorschlag = [];
|
||||||
while ($row = $res->fetch_assoc()) {
|
while ($row = $res->fetch_assoc()) {
|
||||||
$teile = explode("/", $row["url"]);
|
$url = strtolower($row["url"]);
|
||||||
if (count($teile) < 3) {
|
foreach ($zweige as $zweig => $key) {
|
||||||
continue;
|
if (strpos($url, $zweig) === 0) {
|
||||||
|
$vorschlag[intval($row["actor_id"])] = $key;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
$key = strtolower($teile[1] . "/" . $teile[2]);
|
|
||||||
if (isset($bekannt[$key])) {
|
|
||||||
$vorschlag[intval($row["actor_id"])] = $bekannt[$key];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $vorschlag;
|
return $vorschlag;
|
||||||
@@ -436,7 +442,7 @@ function saveRooms($zuordnung)
|
|||||||
{
|
{
|
||||||
$db = meshDb();
|
$db = meshDb();
|
||||||
$erlaubt = array_column(raumListe(), "key");
|
$erlaubt = array_column(raumListe(), "key");
|
||||||
$stmt = $db->prepare("UPDATE actors SET room = ? WHERE id = ?");
|
$stmt = $db->prepare("UPDATE actors SET room_id = ? WHERE id = ?");
|
||||||
$anzahl = 0;
|
$anzahl = 0;
|
||||||
foreach ($zuordnung as $actorId => $key) {
|
foreach ($zuordnung as $actorId => $key) {
|
||||||
$actorId = intval($actorId);
|
$actorId = intval($actorId);
|
||||||
@@ -447,8 +453,8 @@ function saveRooms($zuordnung)
|
|||||||
if ($key !== "" && !in_array($key, $erlaubt, true)) {
|
if ($key !== "" && !in_array($key, $erlaubt, true)) {
|
||||||
throw new InvalidArgumentException("Unbekannter Raum: " . $key);
|
throw new InvalidArgumentException("Unbekannter Raum: " . $key);
|
||||||
}
|
}
|
||||||
$wert = $key === "" ? null : $key;
|
$wert = $key === "" ? null : intval($key);
|
||||||
$stmt->bind_param("si", $wert, $actorId);
|
$stmt->bind_param("ii", $wert, $actorId);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
$anzahl++;
|
$anzahl++;
|
||||||
}
|
}
|
||||||
@@ -475,7 +481,8 @@ function geraeteListe()
|
|||||||
$tipps = raumVorschlaege();
|
$tipps = raumVorschlaege();
|
||||||
|
|
||||||
$geraete = [];
|
$geraete = [];
|
||||||
$res = $db->query("SELECT id, name, type, url, room FROM actors");
|
// room_id als Text: derselbe Schluessel wie in raumListe(), siehe dort.
|
||||||
|
$res = $db->query("SELECT id, name, type, url, CAST(room_id AS CHAR) AS room FROM actors");
|
||||||
while ($row = $res->fetch_assoc()) {
|
while ($row = $res->fetch_assoc()) {
|
||||||
$id = intval($row["id"]);
|
$id = intval($row["id"]);
|
||||||
$wo = raumEinordnung($row["room"]);
|
$wo = raumEinordnung($row["room"]);
|
||||||
@@ -488,6 +495,7 @@ function geraeteListe()
|
|||||||
"room" => $row["room"],
|
"room" => $row["room"],
|
||||||
"roomLabel" => raumBeschriftung($row["room"]),
|
"roomLabel" => raumBeschriftung($row["room"]),
|
||||||
"floor" => $wo["floor"],
|
"floor" => $wo["floor"],
|
||||||
|
"raumName" => $wo["room"],
|
||||||
"tipp" => $tipps[$id] ?? null,
|
"tipp" => $tipps[$id] ?? null,
|
||||||
"messwerte" => 0,
|
"messwerte" => 0,
|
||||||
"kommandos" => 0,
|
"kommandos" => 0,
|
||||||
@@ -542,17 +550,19 @@ function geraeteListe()
|
|||||||
$reihenfolge = array_flip($GLOBALS["floors"]);
|
$reihenfolge = array_flip($GLOBALS["floors"]);
|
||||||
$liste = array_values($geraete);
|
$liste = array_values($geraete);
|
||||||
usort($liste, function ($a, $b) use ($reihenfolge) {
|
usort($liste, function ($a, $b) use ($reihenfolge) {
|
||||||
$wa = explode("/", (string)$a["room"], 2);
|
|
||||||
$wb = explode("/", (string)$b["room"], 2);
|
|
||||||
// Geraete ohne Raum nach oben: sie sind das, was Arbeit macht.
|
// Geraete ohne Raum nach oben: sie sind das, was Arbeit macht.
|
||||||
$ra = ($a["room"] === null || $a["room"] === "") ? -1 : ($reihenfolge[$wa[0]] ?? 98);
|
$ra = ($a["room"] === null || $a["room"] === "") ? -1 : ($reihenfolge[$a["floor"]] ?? 98);
|
||||||
$rb = ($b["room"] === null || $b["room"] === "") ? -1 : ($reihenfolge[$wb[0]] ?? 98);
|
$rb = ($b["room"] === null || $b["room"] === "") ? -1 : ($reihenfolge[$b["floor"]] ?? 98);
|
||||||
if ($ra !== $rb) {
|
if ($ra !== $rb) {
|
||||||
return $ra <=> $rb;
|
return $ra <=> $rb;
|
||||||
}
|
}
|
||||||
$raum = strcoll($wa[1] ?? "", $wb[1] ?? "");
|
$raum = strcoll($a["raumName"], $b["raumName"]);
|
||||||
return $raum !== 0 ? $raum : strcoll($a["label"], $b["label"]);
|
return $raum !== 0 ? $raum : strcoll($a["label"], $b["label"]);
|
||||||
});
|
});
|
||||||
|
// raumName war nur fuer die Sortierung da; die Maske liest roomLabel.
|
||||||
|
foreach ($liste as $i => $g) {
|
||||||
|
unset($liste[$i]["raumName"]);
|
||||||
|
}
|
||||||
return $liste;
|
return $liste;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,7 +667,7 @@ function deviceCatalog()
|
|||||||
$devices = [];
|
$devices = [];
|
||||||
|
|
||||||
$labels = geraeteBeschriftungen();
|
$labels = geraeteBeschriftungen();
|
||||||
$res = $db->query("SELECT id, name, type, url, room FROM actors");
|
$res = $db->query("SELECT id, name, type, url, CAST(room_id AS CHAR) AS room FROM actors");
|
||||||
$zeilen = [];
|
$zeilen = [];
|
||||||
while ($row = $res->fetch_assoc()) {
|
while ($row = $res->fetch_assoc()) {
|
||||||
$zeilen[] = $row;
|
$zeilen[] = $row;
|
||||||
|
|||||||
+1
-1
@@ -125,7 +125,7 @@ require_once(__DIR__ . "/reiter.php");
|
|||||||
$zeilen = [1 => [12], 2 => [9, 16], 3 => [5, 11.5, 16.5]];
|
$zeilen = [1 => [12], 2 => [9, 16], 3 => [5, 11.5, 16.5]];
|
||||||
$y = $zeilen[count($room["werte"])] ?? [];
|
$y = $zeilen[count($room["werte"])] ?? [];
|
||||||
?>
|
?>
|
||||||
<g transform="translate(<?= $room["x"] ?>,<?= $room["y"] ?>)" onclick="openHeaterSettings('<?= $floor ?>_<?= $room["mqtt"] ?>');" cursor="pointer">
|
<g transform="translate(<?= $room["x"] ?>,<?= $room["y"] ?>)" onclick="openRoomView(<?= $room["nr"] ?>);" cursor="pointer">
|
||||||
<rect width="24" height="19" rx="5" fill="#0006" />
|
<rect width="24" height="19" rx="5" fill="#0006" />
|
||||||
<?php if ($room["heizung"]): ?>
|
<?php if ($room["heizung"]): ?>
|
||||||
<use id="<?= $floor ?>_<?= $room["id"] ?>_heater" href="#heater" display="none" />
|
<use id="<?= $floor ?>_<?= $room["id"] ?>_heater" href="#heater" display="none" />
|
||||||
|
|||||||
+10
-6
@@ -156,6 +156,9 @@ function kachelListe()
|
|||||||
"floor" => $room["floor"],
|
"floor" => $room["floor"],
|
||||||
"floorLabel" => floorLabel($room["floor"]),
|
"floorLabel" => floorLabel($room["floor"]),
|
||||||
"room" => $room["id"],
|
"room" => $room["id"],
|
||||||
|
// Die feste Raumnummer, als Text wie in raumListe(): die Maske
|
||||||
|
// stellt damit die Geraete des eigenen Raums nach oben.
|
||||||
|
"nr" => strval($room["nr"]),
|
||||||
"name" => $room["mqtt"],
|
"name" => $room["mqtt"],
|
||||||
"eigen" => isset($room["werte"]),
|
"eigen" => isset($room["werte"]),
|
||||||
"werte" => $room["werte"] ?? kachelVorgabe($room),
|
"werte" => $room["werte"] ?? kachelVorgabe($room),
|
||||||
@@ -219,18 +222,19 @@ function kachelKatalog()
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
* Etage und Raum auch als Kuerzel, nicht nur ausgeschrieben: die
|
* Etage und Raum auch als Schluessel, nicht nur ausgeschrieben: die
|
||||||
* Maske stellt die Geraete des eigenen Raums nach oben und muss dafuer
|
* Maske stellt die Geraete des eigenen Raums nach oben und muss dafuer
|
||||||
* genau vergleichen koennen. actors.room steht als "EG/Technik" da -
|
* genau vergleichen koennen. Der Raum ist die feste Raumnummer
|
||||||
* dieselben Kuerzel, die auch rooms.php benutzt.
|
* (actors.room_id), dieselbe wie "nr" in kachelListe(); die Etage das
|
||||||
|
* Kuerzel, das deviceCatalog() schon als floorLabel fuehrt.
|
||||||
*/
|
*/
|
||||||
$wo = explode("/", (string)($g["room"] ?? ""), 2);
|
$hatRaum = ($g["room"] ?? null) !== null && $g["room"] !== "";
|
||||||
$geraete[] = [
|
$geraete[] = [
|
||||||
"label" => $g["label"],
|
"label" => $g["label"],
|
||||||
"floor" => $g["floorLabel"],
|
"floor" => $g["floorLabel"],
|
||||||
"floorKey" => $wo[0] ?? "",
|
"floorKey" => $hatRaum ? $g["floorLabel"] : "",
|
||||||
"raum" => $g["roomName"],
|
"raum" => $g["roomName"],
|
||||||
"raumKey" => $wo[1] ?? "",
|
"raumKey" => $hatRaum ? strval($g["room"]) : "",
|
||||||
"werte" => $werte,
|
"werte" => $werte,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
* solange die Vorgabe gilt (siehe kachelVorgabe())
|
* solange die Vorgabe gilt (siehe kachelVorgabe())
|
||||||
*
|
*
|
||||||
* Ein Raum ohne x/y ist trotzdem ein Raum: Geräte lassen sich ihm zuordnen
|
* Ein Raum ohne x/y ist trotzdem ein Raum: Geräte lassen sich ihm zuordnen
|
||||||
* (actors.room), er erscheint nur nicht als Kachel im Grundriss.
|
* (actors.room_id), er erscheint nur nicht als Kachel im Grundriss.
|
||||||
*
|
*
|
||||||
* Ohne Datenbank oder ohne die beiden Tabellen gibt es keine Etagen - die
|
* Ohne Datenbank oder ohne die beiden Tabellen gibt es keine Etagen - die
|
||||||
* Startseite zeigt dann einen Hinweis statt eines Fatal Errors.
|
* Startseite zeigt dann einen Hinweis statt eines Fatal Errors.
|
||||||
|
|||||||
Reference in New Issue
Block a user