Die Einstellungen bestanden aus einer einzigen langen Spalte mit den Preistabellen. Sie bekommt Reiter, und darin zwei neue Flaechen: was die Kacheln der Startseite anzeigen, und welche Geraete es ueberhaupt gibt. Reiter gab es vorher dreimal in drei Ausfuehrungen - die Etagen ueber den Automatismen mit eigenem switchTab(), die Geraetegruppen im Raum-Modal mit eigener CSS-Klasse, und die Einstellungsseite noch einmal anders. Jetzt baut restricted/reiter.php alle drei; umgeschaltet wird von Bootstrap, den offenen Reiter merkt reiterMerken() in der Adresse. Kachelwerte: was auf einer Kachel steht, stand fest in rooms.php. Die neue Tabelle kachelWerte (solarLog_kacheln.sql) ueberschreibt das je Raum, die Maske waehlt die Messwerte aus demselben Katalog wie der Automatik-Editor - nach Etage sortiert, die Geraete des eigenen Raums oben. Fehlt die Tabelle, zeigt jede Kachel weiter ihre Vorgabe. Geraete: die Zuordnung Geraet -> Raum war ein Modal, erreichbar ueber ein Zahnrad neben den Automatismen. Sie ist eine Einstellung und steht jetzt dort, zusammen mit dem, was fehlte: seit wann ein Geraet nichts mehr meldet, in welchen Automatiken es steckt, und ein Loeschknopf. Letzterer ist noetig, weil der Suchlauf nichts loescht - er legt an und frischt auf, damit die Automatiken ihre Zustands-Ids behalten; ein abgebautes Geraet bliebe sonst fuer immer stehen. Geloescht wird nur, was in keiner Automatik vorkommt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
372 lines
16 KiB
PHP
372 lines
16 KiB
PHP
<?php
|
|
/*
|
|
* Einzige Quelle für die Räume der Home-Ansicht.
|
|
*
|
|
* Die Werte auf den Kacheln sind hier die Vorgabe; überschrieben werden sie
|
|
* auf der Einstellungsseite (Reiter "Home-Kacheln"). Was dort gesetzt wurde,
|
|
* steht in der Tabelle kachelWerte und hat Vorrang - alles Übrige zeigt
|
|
* weiterhin, was hier steht.
|
|
*
|
|
* 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, sofern auf der Einstellungsseite nichts
|
|
* anderes gewählt wurde (Tabelle kachelWerte, siehe
|
|
* restricted/kacheln.php): 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.
|
|
*/
|
|
|
|
// solarDb() für die Kachelwerte. costs.php definiert nur Funktionen und
|
|
// verbindet sich erst, wenn eine davon gerufen wird.
|
|
require_once(__DIR__ . "/costs.php");
|
|
|
|
// 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"]);
|
|
})));
|
|
}
|
|
|
|
/**
|
|
* Was eine Kachel ohne Zutun zeigt.
|
|
*
|
|
* Ohne eigene Angabe sind es die drei Thermostatwerte, die dort schon immer
|
|
* standen: Solltemperatur klein darüber, Isttemperatur gross, Feuchte klein
|
|
* darunter.
|
|
*/
|
|
function kachelVorgabe($room)
|
|
{
|
|
if (isset($room["werte"])) {
|
|
return array_slice($room["werte"], 0, 3);
|
|
}
|
|
$zweig = "Raumtemp/" . $room["floor"] . "/" . $room["mqtt"] . "/";
|
|
return [
|
|
["topic" => $zweig . "Set Temp[degC]", "einheit" => "°C", "stellen" => 0],
|
|
["topic" => $zweig . "Temp[degC]", "einheit" => "°C", "gross" => true],
|
|
["topic" => $zweig . "rHum[%]", "einheit" => "%rF"],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Gibt es die Tabelle mit den selbst gewählten Kachelwerten?
|
|
*
|
|
* Sie darf fehlen (solarLog_kacheln.sql legt sie an). Dann gilt überall die
|
|
* Vorgabe - die Startseite bleibt nicht mit einem SQL-Fehler stehen, nur weil
|
|
* eine Einstellung noch nie benutzt wurde.
|
|
*/
|
|
function kachelTabelleDa()
|
|
{
|
|
static $da = null;
|
|
if ($da === null) {
|
|
/*
|
|
* Auch der Ausfall der Datenbank fuehrt hier nur zur Vorgabe. Die
|
|
* Kacheln holen ihre Zahlen vom Broker, nicht aus MySQL - die
|
|
* Startseite lief bisher ohne Datenbank, und das soll so bleiben.
|
|
* Ohne dieses try stuende dort im Ernstfall ein Fatal Error statt des
|
|
* Hauses.
|
|
*/
|
|
try {
|
|
$res = solarDb()->query("SHOW TABLES LIKE 'kachelWerte'");
|
|
$da = $res && $res->num_rows > 0;
|
|
} catch (Throwable $e) {
|
|
$da = false;
|
|
}
|
|
}
|
|
return $da;
|
|
}
|
|
|
|
/**
|
|
* Die von Hand gesetzten Werte, nach "Etage|Raum" abgelegt.
|
|
*
|
|
* Eine Abfrage je Seitenaufbau, nicht eine je Kachel - die Startseite zeichnet
|
|
* zwanzig davon.
|
|
*/
|
|
function kachelUeberschreibungen()
|
|
{
|
|
static $werte = null;
|
|
if ($werte !== null) {
|
|
return $werte;
|
|
}
|
|
$werte = [];
|
|
if (!kachelTabelleDa()) {
|
|
return $werte;
|
|
}
|
|
try {
|
|
$res = solarDb()->query("SELECT floor, room, werte FROM kachelWerte");
|
|
while ($res && $row = $res->fetch_assoc()) {
|
|
$liste = json_decode($row["werte"], true);
|
|
if (is_array($liste)) {
|
|
$werte[$row["floor"] . "|" . $row["room"]] = array_slice($liste, 0, 3);
|
|
}
|
|
}
|
|
} catch (Throwable $e) {
|
|
$werte = []; // siehe kachelTabelleDa(): dann eben die Vorgabe
|
|
}
|
|
return $werte;
|
|
}
|
|
|
|
/**
|
|
* Die Messwerte einer Kachel, fertig ausgeschrieben.
|
|
*
|
|
* Zuerst das, was auf der Einstellungsseite gewählt wurde; sonst die Vorgabe.
|
|
*
|
|
* Die Symbole für "heizt" und "Pufferbetrieb" hängen am Raum, nicht an der
|
|
* Anzeige: ein Thermostatraum bleibt einer, auch wenn auf seiner Kachel
|
|
* inzwischen etwas anderes steht. Anderswo gibt es nichts zu heizen.
|
|
*/
|
|
function mitWerten($room)
|
|
{
|
|
$room["heizung"] = !isset($room["werte"]);
|
|
$eigen = kachelUeberschreibungen()[$room["floor"] . "|" . $room["id"]] ?? null;
|
|
$room["werte"] = $eigen !== null ? $eigen : kachelVorgabe($room);
|
|
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);
|
|
}
|