From d3c1cb46826d88e1db12054e479b8ac2fa3731be Mon Sep 17 00:00:00 2001
From: Moirtz Wagner
Date: Fri, 4 Sep 2026 17:55:11 +0200
Subject: [PATCH] Aussengelaende wird die vierte Etage AG
Was draussen haengt, stand als Pseudo-Raum "aussen" in automations.php:
"Geraete, die zu keinem Raum der Wohnung gehoeren - Carport, Garage,
Regensensor". Das waren inzwischen sechzehn Geraete in einem einzigen Topf,
der groesste ueberhaupt - in der Geraeteauswahl des Editors standen die fuenf
Carport-Wechselrichter, das Sonnensegel und das Garagentor unsortiert
nebeneinander.
Jetzt ist das Aussengelaende eine Etage wie OG, EG und UG, mit eigenen
Raeumen: Carport, Veranda, Pergola, Terrasse, Garage und Garten. Die Namen
stammen von den Geraeten selbst - die Hoymiles heissen seit jeher "Carport
1-6" und "Veranda OG2-4". Die achtzehn Geraete sind verteilt (die beiden
Ventilsteuerungen hatten noch gar keinen Raum und stehen jetzt im Garten,
zusammen mit dem Regensensor, der ihre Automatiken ausloest); "Ohne Raum"
faellt damit von acht auf sechs, und die sind es zu Recht.
AG ist eine Etage ohne Grundriss. Ihre Raeume haben kein x/y, es gibt ja
kein Bild, auf das eine Kachel zeigen koennte - ein Raum ohne Position war
schon immer vorgesehen. Das Menue und das SVG richten sich deshalb nach der
neuen floorsWithPlan(), die Zuordnung und die Automatiken nach $floors.
Bekommt der erste AG-Raum eine Kachel, erscheint die Etage von selbst auch
im Menue; es gibt keine zweite Liste, die man nachziehen muesste.
Die Etagen standen bisher an sechs Stellen noch einmal im Code. Jetzt gibt
es genau eine Liste, $floors in rooms.php, und alles andere leitet sich ab:
* automations.php prueft gegen $floors statt gegen eine eigene Whitelist
* ajax/AutoAction.php baut seine Etagen-Auswahl daraus
* home.php baut Reiter und Inhaltsbereiche in einer Schleife statt
dreimal fast dasselbe HTML
* header.php baut die Menuepunkte daraus
* switchTab() in homeMQTT.js liest die Reiter aus der Seite, statt OG, EG
und UG sechsmal aufzuzaehlen
* refreshAutomations() ebenso - eine feste Liste haette die vierte Etage
still ausgelassen, die Tabelle waere ohne Fehlermeldung leer geblieben
Dazu die ausgeschriebenen Namen in $floorLabels: das Kuerzel steht in URLs,
SVG-Ids und im MQTT-Baum, "Aussengelaende" nur als Titel am Menuepunkt und
am Reiter.
In der Datenbank ist das Enum um 'AG' erweitert; header.php laedt rooms.php
jetzt selbst, weil es vor home.php eingebunden wird.
Co-Authored-By: Claude Opus 5
---
ajax/AutoAction.php | 8 +++---
homeMesh_automations.sql | 2 +-
js/solar/autoActionFuncs.js | 12 ++++++--
js/solar/homeMQTT.js | 22 +++++++-------
restricted/automations.php | 20 ++++++-------
restricted/header.php | 25 ++++++++--------
restricted/home.php | 40 +++++++++-----------------
restricted/rooms.php | 57 ++++++++++++++++++++++++++++++++++++-
8 files changed, 117 insertions(+), 69 deletions(-)
diff --git a/ajax/AutoAction.php b/ajax/AutoAction.php
index d55b855..e3064d8 100644
--- a/ajax/AutoAction.php
+++ b/ajax/AutoAction.php
@@ -33,7 +33,7 @@ function jsonAntwort($data, $code = 200)
}
$action = $_GET["action"] ?? "editor";
-$floor = in_array($_GET["floor"] ?? "", ["UG", "EG", "OG"], true) ? $_GET["floor"] : "";
+$floor = in_array($_GET["floor"] ?? "", $floors, true) ? $_GET["floor"] : "";
// --- Schreibende Aufrufe -------------------------------------------------
if ($_SERVER["REQUEST_METHOD"] === "POST") {
@@ -234,9 +234,9 @@ $nutzdaten = json_encode([
die Reiter, damit der Rueckfall auf den ersten Eintrag dieselbe
Etage trifft wie die Startseite. -->
diff --git a/homeMesh_automations.sql b/homeMesh_automations.sql
index 71a20d4..412f689 100644
--- a/homeMesh_automations.sql
+++ b/homeMesh_automations.sql
@@ -29,7 +29,7 @@ SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS `automations` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT 'Anzeigename, z.B. Kinder zu',
- `floor` enum('UG','EG','OG') 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',
+ `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',
`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_to` time NOT NULL DEFAULT '23:59:00' COMMENT 'Aktiver Zeitraum, Ende. Kleiner als window_from = ueber Mitternacht',
diff --git a/js/solar/autoActionFuncs.js b/js/solar/autoActionFuncs.js
index 50d279d..e5d9e98 100644
--- a/js/solar/autoActionFuncs.js
+++ b/js/solar/autoActionFuncs.js
@@ -765,9 +765,17 @@ function automationRequest(action, body) {
.catch(error => ({ error: error.message }));
}
-/** Die Tabelle einer Etage neu holen. Ohne Etage alle drei. */
+/**
+ * Die Tabelle einer Etage neu holen. Ohne Etage alle, die es gibt.
+ *
+ * Welche das sind, sagen die Reiter - home.php baut sie aus der Raumtabelle.
+ * Eine feste Liste hier haette beim Aussengelaende still eine Etage
+ * ausgelassen: die Tabelle waere leer geblieben, ohne Fehlermeldung.
+ */
function refreshAutomations(floor) {
- const etagen = floor ? [floor] : ["UG", "EG", "OG"];
+ const etagen = floor ? [floor]
+ : Array.from(document.querySelectorAll("#actions-tabContent .tab-pane"))
+ .map(pane => pane.id.replace(/^actions-/, ""));
etagen.forEach(etage => {
const ziel = document.getElementById("actions-" + etage + "-list");
if (!ziel) return;
diff --git a/js/solar/homeMQTT.js b/js/solar/homeMQTT.js
index 6f6549a..333eebe 100644
--- a/js/solar/homeMQTT.js
+++ b/js/solar/homeMQTT.js
@@ -134,20 +134,20 @@ function removeClass(el, classNameToRemove){
el.className = elClass;
}
+// Die Etagen stehen nicht mehr im Code: was es gibt, sagen die Reiter, die
+// home.php aus der Raumtabelle gebaut hat. Vorher standen OG, EG und UG hier
+// sechsmal, und eine vierte Etage haette sechs neue Zeilen gebraucht.
function switchTab(newtab){
- newContent = document.getElementById(newtab);
- newTabBtn = document.getElementById(newtab+"-tab");
- removeClass(document.getElementById("actions-OG-tab"),"active");
- removeClass(document.getElementById("actions-EG-tab"),"active");
- removeClass(document.getElementById("actions-UG-tab"),"active");
-
- removeClass(document.getElementById("actions-OG"),"active show");
- removeClass(document.getElementById("actions-EG"),"active show");
- removeClass(document.getElementById("actions-UG"),"active show");
+ const newContent = document.getElementById(newtab);
+ const newTabBtn = document.getElementById(newtab+"-tab");
+ document.querySelectorAll("#actions-tab .nav-link").forEach(function(btn){
+ removeClass(btn,"active");
+ });
+ document.querySelectorAll("#actions-tabContent .tab-pane").forEach(function(pane){
+ removeClass(pane,"active show");
+ });
addClass(newTabBtn,"active");
addClass(newContent,"active show");
-
-
}
diff --git a/restricted/automations.php b/restricted/automations.php
index b510224..e848965 100644
--- a/restricted/automations.php
+++ b/restricted/automations.php
@@ -17,10 +17,6 @@
require_once(__DIR__ . "/mysql.php");
require_once(__DIR__ . "/rooms.php");
-// Geraete, die zu keinem Raum der Wohnung gehoeren - Carport, Garage,
-// Regensensor. Kein Raum in rooms.php, aber auch nicht "unzugeordnet".
-const RAUM_AUSSEN = "aussen";
-
/** Verbindung zur Geraete- und Automatik-Datenbank. */
function meshDb()
{
@@ -220,12 +216,17 @@ function anzeigeName($name)
}
/**
- * Waehlbare Raeume: die Raeume der Home-Ansicht plus "Aussen".
+ * Waehlbare Raeume: die Raeume der Home-Ansicht.
*
* Einzige Quelle bleibt restricted/rooms.php - ein neuer Raum ist dort eine
* Zeile und taucht hier automatisch auf. Der Schluessel ist "Etage/Raum",
* weil "Bad" allein auf drei Etagen vorkommt.
*
+ * Frueher stand hier ein Pseudo-Raum "aussen" fuer alles, was zu keinem Raum
+ * der Wohnung gehoert - Carport, Pergola, Garage, die Wechselrichter. Das war
+ * ein einziger Topf mit sechzehn Geraeten. Inzwischen ist das Aussengelaende
+ * die Etage AG mit eigenen Raeumen, und dieser Sonderfall ist keiner mehr.
+ *
* Etage und Raum stehen zusaetzlich einzeln daneben: die Geraeteauswahl im
* Editor gruppiert nach der einen und sortiert nach dem anderen, und der
* Schluessel soll dafuer nicht erst im JavaScript wieder auseinandergenommen
@@ -243,11 +244,6 @@ function raumListe()
"room" => $raum["mqtt"],
];
}
- // Aussen zaehlt als eigene Etage mit genau einem Raum: Carport, Garage und
- // Regensensor liegen auf keinem Stockwerk, sollen aber ihre eigene
- // Ueberschrift bekommen und nicht unter "Ohne Raum" fallen.
- $liste[] = ["key" => RAUM_AUSSEN, "label" => "Außen",
- "floor" => "Außen", "room" => ""];
return $liste;
}
@@ -521,7 +517,7 @@ function emptyAutomation($floor)
"name" => "",
// Aufgerufen wird der Editor immer aus einem Etagenreiter. Fehlt die
// Angabe trotzdem, gilt dieselbe Etage wie auf der Startseite.
- "floor" => in_array($floor, ["UG", "EG", "OG"], true) ? $floor : "OG",
+ "floor" => in_array($floor, $GLOBALS["floors"], true) ? $floor : "OG",
"enabled" => true,
"window_from" => "00:00",
"window_to" => "23:59",
@@ -624,7 +620,7 @@ function saveAutomation($data)
throw new InvalidArgumentException("Bitte einen Namen vergeben.");
}
$floor = strval($data["floor"] ?? "");
- if (!in_array($floor, ["UG", "EG", "OG"], true)) {
+ if (!in_array($floor, $GLOBALS["floors"], true)) {
// Nicht stillschweigend auf "" zurueckfallen: die Automatik wuerde
// dann in keinem der drei Reiter mehr auftauchen.
throw new InvalidArgumentException("Bitte eine Etage wählen.");
diff --git a/restricted/header.php b/restricted/header.php
index fe3cc64..d9f821d 100644
--- a/restricted/header.php
+++ b/restricted/header.php
@@ -3,6 +3,10 @@
+
@@ -224,7 +212,7 @@
// die der Browser dafuer beim Broker anmelden muss.
const homeRooms = = json_encode(roomsWithTile(), JSON_UNESCAPED_UNICODE) ?>;
const homeTopics = = json_encode(tileTopics(), JSON_UNESCAPED_UNICODE) ?>;
-
+
document.addEventListener('readystatechange', function () {
if (event.target.readyState === 'complete') {
switchFloor('= $_GET["floor"] ?>');
diff --git a/restricted/rooms.php b/restricted/rooms.php
index 1fc3bc1..c6a890f 100644
--- a/restricted/rooms.php
+++ b/restricted/rooms.php
@@ -32,7 +32,23 @@
*/
// Reihenfolge im Menü und in der Anzeige; OG ist die Standardetage.
-$floors = ["OG", "EG", "UG"];
+//
+// AG ist das Außengelände - eine Etage ohne Grundriss. Was draußen hängt,
+// stand vorher als Pseudo-Raum "aussen" in automations.php und war damit ein
+// einziger Topf: Carport, Pergola, Garage und die Wechselrichter alle
+// nebeneinander. Als Etage bekommt jeder davon einen Raum, und die
+// Geräteauswahl im Automatik-Editor gruppiert danach wie überall sonst.
+$floors = ["OG", "EG", "UG", "AG"];
+
+// Ausgeschriebene Namen für Beschriftungen, wo Platz ist. Das Kürzel steht in
+// URLs (?floor=AG), in SVG-Element-Ids (AG_Info) und im MQTT-Baum - dort will
+// man weder Umlaute noch Länge.
+$floorLabels = [
+ "OG" => "Obergeschoss",
+ "EG" => "Erdgeschoss",
+ "UG" => "Untergeschoss",
+ "AG" => "Außengelände",
+];
$rooms = [
["floor" => "OG", "mqtt" => "Wohnzimmer", "id" => "wozi", "x" => 180, "y" => 140],
@@ -69,6 +85,21 @@ $rooms = [
"einheit" => "W", "stellen" => 0, "gross" => true],
["topic" => "wasser/main/value", "einheit" => "m³", "stellen" => 1],
]],
+ // Außengelände: noch ohne x/y, es gibt ja keinen Lageplan, auf den eine
+ // Kachel zeigen könnte. Zuordnen und Automatiken anlegen geht trotzdem -
+ // dafür zählt allein, dass der Raum hier steht. Kommt der Lageplan, sind
+ // es zwei Zahlen je Zeile, gemessen mit tools/kachelpositionen.php.
+ //
+ // Die Namen stammen von den Geräten selbst: die Hoymiles heißen seit jeher
+ // "Carport 1-6" und "Veranda OG2-4", das Sonnensegel sitzt an der Pergola.
+ ["floor" => "AG", "mqtt" => "Carport", "id" => "carport", "werte" => []],
+ ["floor" => "AG", "mqtt" => "Veranda", "id" => "veranda", "werte" => []],
+ ["floor" => "AG", "mqtt" => "Pergola", "id" => "pergola", "werte" => []],
+ ["floor" => "AG", "mqtt" => "Terrasse", "id" => "terrasse", "werte" => []],
+ ["floor" => "AG", "mqtt" => "Garage", "id" => "garage", "werte" => []],
+ // Der Garten ist kein Bauteil wie die anderen, aber ein Ort: hier haengen
+ // der Regensensor und die beiden Ventilsteuerungen.
+ ["floor" => "AG", "mqtt" => "Garten", "id" => "garten", "werte" => []],
];
/**
@@ -91,6 +122,30 @@ function allRooms()
return $GLOBALS["rooms"];
}
+/** Ausgeschriebener Name einer Etage; unbekannte bleiben ihr Kürzel. */
+function floorLabel($floor)
+{
+ return $GLOBALS["floorLabels"][$floor] ?? $floor;
+}
+
+/**
+ * Etagen mit Grundriss - die, die im Menü und im SVG auftauchen.
+ *
+ * Das Außengelände ist eine Etage ohne Bild: seine Räume haben keine Kachel,
+ * und ein Menüpunkt dorthin führte auf eine leere Zeichenfläche. Sobald der
+ * erste AG-Raum ein x/y bekommt, erscheint die Etage von selbst auch dort -
+ * es gibt keine zweite Liste, die man nachziehen müsste.
+ *
+ * Für die Zuordnung und die Automatiken zählt dagegen $floors: dort ist das
+ * Außengelände von Anfang an dabei.
+ */
+function floorsWithPlan()
+{
+ return array_values(array_filter($GLOBALS["floors"], function ($floor) {
+ return count(roomsOnFloor($floor)) > 0;
+ }));
+}
+
/** Nur die Räume mit Kachel, für die Aktualisierungsschleife im Browser. */
function roomsWithTile()
{