Files
Smart-Dashboard/restricted/rooms.php
T
adminandClaude Opus 5 7c646c5aee Leistungen auf den Kacheln skalieren mit: unter 1 kW in W, darueber in kW
Nicht nachgebaut, sondern powerToString() aus common.js benutzt. Sie macht
genau das schon, und die Solarseite formatiert dieselben Zahlen damit - eine
eigene Regel hier haette bedeutet, dass dieselbe Wallbox an zwei Stellen
verschieden gerundet dasteht. common.js wird im Fuss vor den seitenweisen
Skripten geladen, steht homeMQTT.js also zur Verfuegung.

Ersetzt wird nur das Trennzeichen: auf der Kachel steht ein schmales
Leerzeichen zwischen Zahl und Einheit, im Rest der Anwendung ein
gewoehnliches.

Betroffen sind Garage, Carport OG und Carport EG. Der Regen im Garten bleibt
bei Einheit und Nachkommastelle - Millimeter skalieren nicht.

Nebenbei: mein Einschub von wechselrichterWert() hatte den Kommentarblock
von kachelWert() von seiner Funktion getrennt, er stand seitdem ueber der
falschen. Zurueckgesetzt und um die beiden neuen Schluessel ergaenzt.

Geprueft mit dem echten Code im Browser: 0 -> "0 W", 999 -> "999 W",
1234 -> "1.23 kW", 12340 -> "12.3 kW"; die Summe der vier Carport-Hoymiles
ergibt 1.23 kW und laesst einen fuenften Wechselrichter richtigerweise
draussen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 00:04:27 +02:00

291 lines
14 KiB
PHP

<?php
/*
* Einzige Quelle für die Räume der Home-Ansicht.
*
* Aus dieser Tabelle entstehen sowohl die SVG-Kacheln in home.php als auch
* die Aktualisierungsschleife in js/solar/homeMQTT.js (die Tabelle wird dafür
* als JSON in die Seite geschrieben). Ein neuer oder umbenannter Raum ist
* damit eine Zeile hier statt drei Stellen im Code.
*
* floor Etage; bestimmt zugleich den Zweig Raumtemp/<floor>/
* mqtt Name des Raums im MQTT-Baum: Raumtemp/<floor>/<mqtt>/...
* id Kürzel in den SVG-Element-IDs: <floor>_w0_<id>, <floor>_<id>_heater
* x, y Position der Kachel im SVG (viewBox 400x300) - fehlt bei Räumen
* ohne Kachel
* werte Was auf der Kachel steht: bis zu drei Messwerte, jeder mit
* topic (oder wechselrichter, siehe Carport), optional pfad
* (Schlüssel in einer JSON-Nachricht) und entweder format
* ("leistung" skaliert selbst zwischen W und kW) oder dem Paar
* einheit und stellen (Nachkommastellen). Fehlt der Eintrag, sind
* es die drei Thermostatwerte - Soll, Ist, Feuchte - wie bisher.
* Eine leere Liste heisst: kein Messwert, die Kachel trägt nur
* ihren Namen und öffnet den Raum.
*
* Ein Raum ohne x/y ist trotzdem ein Raum: Geräte lassen sich ihm zuordnen
* (siehe die Spalte actors.room und den Zuordnungsdialog in der Karte
* "Automatismen"), er erscheint nur nicht als Kachel im Grundriss. Derzeit
* hat jeder Raum eine Kachel; die Unterscheidung bleibt, weil ein neuer Raum
* zuerst als Zeile ohne Position entsteht und erst dann seinen Platz im
* Grundriss bekommt.
*
* Wo eine Kachel hingehört, misst tools/kachelpositionen.php aus: Grundriss
* mit Raster, Geisterkachel am Zeiger, fertige Zeile zum Abschreiben. x/y
* ist die linke obere Ecke der 24 x 19 grossen Kachel, nicht ihre Mitte.
*/
// Reihenfolge im Menü und in der Anzeige; OG ist die Standardetage.
//
// 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],
["floor" => "OG", "mqtt" => "KiZi", "id" => "kizi", "x" => 130, "y" => 80],
["floor" => "OG", "mqtt" => "Schlafzimmer", "id" => "schlafen", "x" => 235, "y" => 70],
["floor" => "OG", "mqtt" => "Bad", "id" => "bad", "x" => 260, "y" => 120],
["floor" => "OG", "mqtt" => "Buero", "id" => "buero", "x" => 265, "y" => 175],
// Kein Thermostat, und noch kein Gerät zugeordnet - die Kachel trägt
// vorerst nur ihren Namen.
["floor" => "OG", "mqtt" => "Kueche", "id" => "kueche", "x" => 122, "y" => 138,
"werte" => []],
["floor" => "OG", "mqtt" => "Technik", "id" => "technik", "x" => 173, "y" => 67,
"werte" => []],
["floor" => "EG", "mqtt" => "Wohnzimmer", "id" => "wozi", "x" => 185, "y" => 150],
["floor" => "EG", "mqtt" => "Florian", "id" => "kizi", "x" => 130, "y" => 90],
["floor" => "EG", "mqtt" => "Schlafen", "id" => "schlafen", "x" => 235, "y" => 80],
["floor" => "EG", "mqtt" => "Bad", "id" => "bad", "x" => 253, "y" => 130],
["floor" => "EG", "mqtt" => "Magdalena", "id" => "buero", "x" => 258, "y" => 180],
["floor" => "EG", "mqtt" => "Kueche", "id" => "kueche", "x" => 126, "y" => 146,
"werte" => []],
// Der Shelly-Zähler des EG meldet seine Werte als JSON in einer einzigen
// Nachricht; "pfad" greift den Schlüssel daraus heraus.
["floor" => "EG", "mqtt" => "Technik", "id" => "technik", "x" => 172, "y" => 78,
"werte" => [
["topic" => "Power_EG/status/em:0", "pfad" => "total_act_power",
"einheit" => "W", "stellen" => 0, "gross" => true],
]],
["floor" => "UG", "mqtt" => "Buero", "id" => "buero", "x" => 140, "y" => 140],
["floor" => "UG", "mqtt" => "Bad", "id" => "bad", "x" => 210, "y" => 80],
["floor" => "UG", "mqtt" => "Kueche", "id" => "kueche", "x" => 190, "y" => 195],
["floor" => "UG", "mqtt" => "Technik", "id" => "technik", "x" => 238, "y" => 93,
"werte" => [
["topic" => "Power_UG/status/em:0", "pfad" => "total_act_power",
"einheit" => "W", "stellen" => 0, "gross" => true],
["topic" => "wasser/main/value", "einheit" => "m³", "stellen" => 1],
]],
// Außengelände: der Grundriss ist ein freigestelltes Rendering des
// Grundstücks (assets/img/AG.png). Die Kachelpositionen sind darauf
// eingemessen; nachklicken geht mit tools/kachelpositionen.php.
//
// Messwerte nur, wo es etwas zu messen gibt: die beiden Wallboxen und
// der Regen. Veranda, Terrasse und Pergola tragen ihren Namen - dort
// haengen Licht und Rollladen, und die zeigt das Raum-Modal.
//
// 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.
// Zwei Carports, einer je Wohnpartei - deshalb dieselben Kuerzel wie die
// Etagen. Die Wattpilot der EG-Partei steht dagegen in der Garage, siehe
// restricted/config.php; am Carport OG haengt der go-e Charger.
// Die PV des Carports haengt an mehreren Hoymiles. Sie werden ueber
// ihren Namen gesucht und aufsummiert, nicht ueber ihre Nummer: die
// Topics heissen solarManager/invertersN, und N ist eine Position in
// einer Liste. Aendert der SolarManager die Reihenfolge, zeigte eine
// fest eingetragene Nummer stillschweigend das falsche Dach an.
//
// Die Namen muessen genau so lauten wie unter invertersN/name. Passt
// einer nicht, schreibt homeMQTT.js die vorhandenen Namen in die
// Browser-Konsole - dann steht dort, was hier hingehoert.
["floor" => "AG", "mqtt" => "Carport EG", "id" => "carport_eg", "x" => 179, "y" => 207,
"werte" => [
["wechselrichter" => ["Carport 1-6", "Carport 7-12", "Carport 13-16",
"Carport 17-19"],
"feld" => "p_AC", "format" => "leistung", "icon" => "sun"],
]],
// Am Carport OG haengt der go-e Charger. Die Ladeleistung ist das
// Einzige, was sich hier messen laesst - und sie beantwortet die Frage,
// die man sich vor dem Carport stellt: laedt gerade jemand?
["floor" => "AG", "mqtt" => "Carport OG", "id" => "carport_og", "x" => 72, "y" => 154,
"werte" => [
["topic" => "solarManager/evPowerOG", "format" => "leistung",
"icon" => "ev-station"],
]],
// Veranda und Terrasse gibt es zweimal, je Wohnpartei eine. In der
// Draufsicht laege das obere Paar genau auf dem unteren - deshalb die
// Schraegsicht, siehe restricted/aussenplan.php.
//
// Die beiden Veranden haben bewusst kein x/y: ihre Kacheln lagen auf der
// Westfassade des Hauses so dicht beieinander, dass sie mehr gestiftet
// als erklaert haben. Als Raeume bleiben sie bestehen - Geraete sind
// ihnen zugeordnet und Automatiken laufen darauf, das haengt allein
// daran, dass sie hier stehen.
//
// Kommen die Kacheln zurueck, sind es zwei Zahlen je Zeile (gemessen mit
// tools/kachelpositionen.php, zuletzt 156/132 und 150/92) und fuer die
// Veranda OG diese Wechselrichter:
//
// ["wechselrichter" => ["Veranda OG2-4", "Veranda UG/OG1"],
// "feld" => "p_AC", "format" => "leistung", "icon" => "sun"]
["floor" => "AG", "mqtt" => "Veranda EG", "id" => "veranda_eg", "werte" => []],
["floor" => "AG", "mqtt" => "Veranda OG", "id" => "veranda_og", "werte" => []],
["floor" => "AG", "mqtt" => "Terrasse EG", "id" => "terrasse_eg", "x" => 229, "y" => 158, "werte" => []],
["floor" => "AG", "mqtt" => "Terrasse OG", "id" => "terrasse_og", "x" => 223, "y" => 124, "werte" => []],
["floor" => "AG", "mqtt" => "Pergola", "id" => "pergola", "x" => 344, "y" => 111, "werte" => []],
// In der Garage steht die Wattpilot der EG-Partei, siehe fahrzeuge() in
// restricted/config.php. Ohne Kuerzel im Topic ist das die EG-Wallbox.
["floor" => "AG", "mqtt" => "Garage", "id" => "garage", "x" => 131, "y" => 135,
"werte" => [
["topic" => "solarManager/evPower", "format" => "leistung",
"icon" => "ev-station"],
]],
// 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", "x" => 298, "y" => 176,
"werte" => [
// gatherRainData.py im SolarManager legt auf Wetter/Regen ein JSON
// mit tage1 bis tage7 und dem Stand des Sammlers. "tage1" ist der
// laufende Tag - die Zahl, die man im Garten wissen will. Ein
// laengeres Fenster ist ein geaenderter Pfad und sonst nichts.
["topic" => "Wetter/Regen", "pfad" => "tage1", "einheit" => "mm",
"stellen" => 1, "icon" => "cloud-rain"],
]],
];
/**
* Räume einer Etage, die eine Kachel im Grundriss haben.
*
* Alles, was zeichnet oder aktualisiert, geht hierüber - ein Raum ohne
* Position hätte keine Stelle im SVG und keine Elemente, die sich füllen
* liessen.
*/
function roomsOnFloor($floor)
{
return array_values(array_filter($GLOBALS["rooms"], function ($room) use ($floor) {
return $room["floor"] == $floor && isset($room["x"]);
}));
}
/** Alle Räume, auch die ohne Kachel - für die Geräte-Zuordnung. */
function allRooms()
{
return $GLOBALS["rooms"];
}
/** Ausgeschriebener Name einer Etage; unbekannte bleiben ihr Kürzel. */
function floorLabel($floor)
{
return $GLOBALS["floorLabels"][$floor] ?? $floor;
}
/**
* Codepunkt eines Bootstrap-Icons für die Kacheln.
*
* Die Icons kommen als Schrift, nicht als Pfad: css/bootstrap-icons.min.css
* ist ohnehin eingebunden (restricted/header.php), und SVG-Text darf dieselbe
* Schriftfamilie benutzen wie HTML. Ein eingebettetes <path> je Symbol wäre
* dieselbe Wirkung mit mehr Zeilen.
*
* Die Zahlen stehen in genau dieser CSS unter .bi-<name>::before und sind von
* dort abgeschrieben - deshalb steht der Name daneben, sonst prüft das nie
* wieder jemand nach.
*/
function kachelIcon($name)
{
$icons = [
"ev-station" => "F83A", // Wallbox, Ladeleistung
"cloud-rain" => "F2B6", // Regenmenge
"sun" => "F5A2", // PV-Erzeugung
];
return isset($icons[$name]) ? mb_chr(hexdec($icons[$name]), "UTF-8") : "";
}
/**
* 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()
{
return array_values(array_map("mitWerten", array_filter($GLOBALS["rooms"], function ($room) {
return isset($room["x"]);
})));
}
/**
* Die Messwerte einer Kachel, fertig ausgeschrieben.
*
* Ohne eigene Angabe sind es die drei Thermostatwerte, die dort schon immer
* standen: Solltemperatur klein darüber, Isttemperatur gross, Feuchte klein
* darunter. Nur diese Räume bekommen auch die Symbole für "heizt" und
* "Pufferbetrieb" - anderswo gibt es nichts zu heizen.
*/
function mitWerten($room)
{
if (isset($room["werte"])) {
$room["heizung"] = false;
$room["werte"] = array_slice($room["werte"], 0, 3);
return $room;
}
$zweig = "Raumtemp/" . $room["floor"] . "/" . $room["mqtt"] . "/";
$room["heizung"] = true;
$room["werte"] = [
["topic" => $zweig . "Set Temp[degC]", "einheit" => "°C", "stellen" => 0],
["topic" => $zweig . "Temp[degC]", "einheit" => "°C", "gross" => true],
["topic" => $zweig . "rHum[%]", "einheit" => "%rF"],
];
return $room;
}
/**
* Was der Browser beim Broker anmelden muss, damit die Kacheln laufen.
*
* Nicht die einzelnen Topics, sondern ihre Zweige: aus vierzig Anmeldungen
* für Raumtemp werden so vier insgesamt. Der Zweig bringt ausserdem mit,
* was die Kachel nebenbei braucht - "Heating" und "mode" stehen in keiner
* Werteliste, das Heizsymbol lebt aber davon.
*/
function tileTopics()
{
$zweige = [];
foreach (roomsWithTile() as $room) {
foreach ($room["werte"] as $wert) {
// Ein Wert nennt entweder sein Topic oder einen Wechselrichter,
// den homeMQTT.js unter solarManager/invertersN sucht. Ohne diese
// Unterscheidung entstand aus dem fehlenden Topic ein "/#" - eine
// Anmeldung auf einen leeren Zweig.
$zweig = isset($wert["topic"])
? explode("/", $wert["topic"])[0]
: "solarManager";
$zweige[$zweig . "/#"] = true;
}
}
return array_keys($zweige);
}