Einstellungsseite: Reiter, Kachelwerte und Geraeteliste
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>
This commit is contained in:
+164
-12
@@ -316,17 +316,6 @@ function raumVorschlaege()
|
||||
return $vorschlag;
|
||||
}
|
||||
|
||||
/** Raum je Geraet, wie gespeichert. */
|
||||
function geraeteRaeume()
|
||||
{
|
||||
$res = meshDb()->query("SELECT id, room FROM actors");
|
||||
$raeume = [];
|
||||
while ($row = $res->fetch_assoc()) {
|
||||
$raeume[intval($row["id"])] = $row["room"];
|
||||
}
|
||||
return $raeume;
|
||||
}
|
||||
|
||||
/** Zuordnungen speichern. $zuordnung ist [actor_id => Raumschluessel|""]. */
|
||||
function saveRooms($zuordnung)
|
||||
{
|
||||
@@ -352,6 +341,161 @@ function saveRooms($zuordnung)
|
||||
return $anzahl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Geraeteliste fuer die Einstellungsseite.
|
||||
*
|
||||
* Enthaelt alles, was man ueber ein Geraet wissen will, ohne es anzuklicken:
|
||||
* Beschriftung, Art, Raum, ein Vorschlag fuer den Raum (aus dem MQTT-Topic),
|
||||
* wie viele Messwerte und Kommandos daran haengen, wann zuletzt etwas davon
|
||||
* gemeldet wurde - und in welchen Automatiken es steckt.
|
||||
*
|
||||
* Das Letzte ist der Grund, warum die Liste hier steht und nicht in der Maske:
|
||||
* ein Geraet, auf das eine Automatik zeigt, darf nicht verschwinden, und wer
|
||||
* das entscheidet, muss die Verweise kennen.
|
||||
*/
|
||||
function geraeteListe()
|
||||
{
|
||||
$db = meshDb();
|
||||
$labels = geraeteBeschriftungen();
|
||||
$tipps = raumVorschlaege();
|
||||
|
||||
$geraete = [];
|
||||
$res = $db->query("SELECT id, name, type, url, room FROM actors");
|
||||
while ($row = $res->fetch_assoc()) {
|
||||
$id = intval($row["id"]);
|
||||
$wo = raumEinordnung($row["room"]);
|
||||
$geraete[$id] = [
|
||||
"id" => $id,
|
||||
"name" => $row["name"],
|
||||
"label" => $labels[$id] ?? $row["name"],
|
||||
"type" => $row["type"],
|
||||
"url" => $row["url"],
|
||||
"room" => $row["room"],
|
||||
"roomLabel" => raumBeschriftung($row["room"]),
|
||||
"floor" => $wo["floor"],
|
||||
"tipp" => $tipps[$id] ?? null,
|
||||
"messwerte" => 0,
|
||||
"kommandos" => 0,
|
||||
"gesehen" => null,
|
||||
"benutzt" => [],
|
||||
];
|
||||
}
|
||||
|
||||
// Messwerte: Anzahl und juengste Meldung. Letztere sagt, ob ein Geraet
|
||||
// noch lebt - ein Eintrag von vor einem halben Jahr ist eine Karteileiche.
|
||||
$res = $db->query("SELECT actor_id, COUNT(*) AS n, MAX(last_updated) AS gesehen
|
||||
FROM actor_states GROUP BY actor_id");
|
||||
while ($row = $res->fetch_assoc()) {
|
||||
$id = intval($row["actor_id"]);
|
||||
if (isset($geraete[$id])) {
|
||||
$geraete[$id]["messwerte"] = intval($row["n"]);
|
||||
$geraete[$id]["gesehen"] = $row["gesehen"];
|
||||
}
|
||||
}
|
||||
|
||||
$res = $db->query("SELECT actor_id, COUNT(*) AS n FROM actor_commands GROUP BY actor_id");
|
||||
while ($row = $res->fetch_assoc()) {
|
||||
$id = intval($row["actor_id"]);
|
||||
if (isset($geraete[$id])) {
|
||||
$geraete[$id]["kommandos"] = intval($row["n"]);
|
||||
}
|
||||
}
|
||||
|
||||
// Verweise aus den Automatiken - ueber Messwerte (Bedingungen) und ueber
|
||||
// Kommandos (Aktionen).
|
||||
$sql = "SELECT s.actor_id AS actor_id, a.id AS aid, a.name AS aname
|
||||
FROM automation_conditions c
|
||||
JOIN actor_states s ON s.id = c.state_id
|
||||
JOIN automations a ON a.id = c.automation_id
|
||||
UNION
|
||||
SELECT k.actor_id AS actor_id, a.id AS aid, a.name AS aname
|
||||
FROM automation_actions x
|
||||
JOIN actor_commands k ON k.id = x.command_id
|
||||
JOIN automations a ON a.id = x.automation_id";
|
||||
$res = $db->query($sql);
|
||||
while ($res && $row = $res->fetch_assoc()) {
|
||||
$id = intval($row["actor_id"]);
|
||||
if (isset($geraete[$id])) {
|
||||
$geraete[$id]["benutzt"][intval($row["aid"])] = $row["aname"];
|
||||
}
|
||||
}
|
||||
foreach ($geraete as $id => $g) {
|
||||
$geraete[$id]["benutzt"] = array_values($g["benutzt"]);
|
||||
}
|
||||
|
||||
// Nach Etage, Raum und Beschriftung - dieselbe Ordnung wie ueberall.
|
||||
$reihenfolge = array_flip($GLOBALS["floors"]);
|
||||
$liste = array_values($geraete);
|
||||
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.
|
||||
$ra = ($a["room"] === null || $a["room"] === "") ? -1 : ($reihenfolge[$wa[0]] ?? 98);
|
||||
$rb = ($b["room"] === null || $b["room"] === "") ? -1 : ($reihenfolge[$wb[0]] ?? 98);
|
||||
if ($ra !== $rb) {
|
||||
return $ra <=> $rb;
|
||||
}
|
||||
$raum = strcoll($wa[1] ?? "", $wb[1] ?? "");
|
||||
return $raum !== 0 ? $raum : strcoll($a["label"], $b["label"]);
|
||||
});
|
||||
return $liste;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein Geraet samt Messwerten, Kommandos und Parametern loeschen.
|
||||
*
|
||||
* Gedacht fuer Karteileichen: der Suchlauf loescht nichts (clear_tables steht
|
||||
* bewusst auf false, weil die Automatiken auf die Ids zeigen), also bleibt ein
|
||||
* abgebautes Geraet fuer immer in der Liste stehen.
|
||||
*
|
||||
* Zeigt eine Automatik darauf, wird nicht geloescht, sondern abgelehnt - mit
|
||||
* den Namen der Automatiken. Sie stillschweigend mitzuloeschen waere ein
|
||||
* stiller Datenverlust, sie kaputt zurueckzulassen ebenfalls. Wer das Geraet
|
||||
* wirklich loswerden will, raeumt erst die Automatik auf.
|
||||
*
|
||||
* Steht das Geraet noch im Netz, findet der naechste Suchlauf es wieder - das
|
||||
* ist kein Fehler, sondern der Zweck des Suchlaufs.
|
||||
*/
|
||||
function geraetLoeschen($id)
|
||||
{
|
||||
$id = intval($id);
|
||||
if ($id <= 0) {
|
||||
throw new InvalidArgumentException("Kein Gerät angegeben.");
|
||||
}
|
||||
$db = meshDb();
|
||||
|
||||
$res = $db->query("SELECT name FROM actors WHERE id = " . $id);
|
||||
$geraet = $res ? $res->fetch_assoc() : null;
|
||||
if (!$geraet) {
|
||||
throw new InvalidArgumentException("Dieses Gerät gibt es nicht (mehr).");
|
||||
}
|
||||
|
||||
foreach (geraeteListe() as $g) {
|
||||
if ($g["id"] === $id && $g["benutzt"]) {
|
||||
throw new InvalidArgumentException(
|
||||
"Das Gerät steckt in " . count($g["benutzt"]) . " Automatik(en): "
|
||||
. implode(", ", $g["benutzt"]) . ". Erst dort entfernen.");
|
||||
}
|
||||
}
|
||||
|
||||
$db->begin_transaction();
|
||||
try {
|
||||
// Von innen nach aussen: Parameter haengen an Kommandos, Kommandos und
|
||||
// Messwerte am Geraet.
|
||||
$db->query("DELETE p FROM command_parameters p
|
||||
JOIN actor_commands k ON k.id = p.command_id
|
||||
WHERE k.actor_id = " . $id);
|
||||
$db->query("DELETE FROM actor_commands WHERE actor_id = " . $id);
|
||||
$db->query("DELETE FROM actor_states WHERE actor_id = " . $id);
|
||||
$db->query("DELETE FROM actors WHERE id = " . $id);
|
||||
$db->commit();
|
||||
} catch (Throwable $e) {
|
||||
$db->rollback();
|
||||
throw $e;
|
||||
}
|
||||
return $geraet["name"];
|
||||
}
|
||||
|
||||
/**
|
||||
* id => Beschriftung fuer alle Geraete, geputzt und eindeutig.
|
||||
*
|
||||
@@ -425,8 +569,14 @@ function deviceCatalog()
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
* value_path kommt mit, seit die Einstellungsseite denselben Katalog fuer
|
||||
* die Home-Kacheln benutzt: bei Geraeten, die ihre Werte als JSON in einer
|
||||
* einzigen Nachricht schicken (der Regensensor etwa), unterscheidet erst
|
||||
* dieser Schluessel die sechs Messwerte unter demselben Topic.
|
||||
*/
|
||||
$res = $db->query("SELECT s.id, s.actor_id, s.state_name, s.unit, s.current_value,
|
||||
s.url, s.possible_values, t.type
|
||||
s.url, s.value_path, s.possible_values, t.type
|
||||
FROM actor_states s
|
||||
LEFT JOIN state_types t ON s.state_type = t.id
|
||||
ORDER BY s.actor_id, s.id");
|
||||
@@ -447,6 +597,8 @@ function deviceCatalog()
|
||||
"unit" => $row["unit"],
|
||||
"value" => $row["current_value"],
|
||||
"url" => $row["url"],
|
||||
// Schluessel in einer JSON-Nachricht, sonst leer - siehe oben.
|
||||
"pfad" => $row["value_path"],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+26
-22
@@ -1,4 +1,7 @@
|
||||
<?php require_once(__DIR__ . "/rooms.php"); ?>
|
||||
<?php
|
||||
require_once(__DIR__ . "/rooms.php");
|
||||
require_once(__DIR__ . "/reiter.php");
|
||||
?>
|
||||
<!--begin::App Main-->
|
||||
<main class="app-main">
|
||||
<!--begin::App Content Header-->
|
||||
@@ -185,12 +188,6 @@
|
||||
<div class="card-header pb-0 pt-1">
|
||||
<h3 class="card-title">Automatismen</h3>
|
||||
<div class="card-tools">
|
||||
<!-- Zuordnung Geraet -> Raum: sie ordnet die Geraeteliste im
|
||||
Editor nach Raeumen statt nach Geraeteart. -->
|
||||
<button type="button" class="btn btn-tool" title="Geräte den Räumen zuordnen"
|
||||
onclick="openRoomModal()">
|
||||
<i class="bi bi-house-gear"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-tool" data-lte-toggle="card-remove">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
@@ -198,28 +195,35 @@
|
||||
|
||||
</div>
|
||||
<div class="card-body pb-1">
|
||||
<div class="card-header p-0 pt-1">
|
||||
<ul class="nav nav-tabs" id="actions-tab" role="tablist">
|
||||
<?php foreach ($floors as $etage): $offen = $_GET["floor"] == $etage; ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?php if ($offen) echo "active"; ?>" id="actions-<?= $etage ?>-tab"
|
||||
href="#actions-<?= $etage ?>" onclick="switchTab('actions-<?= $etage ?>')"
|
||||
title="<?= htmlspecialchars(floorLabel($etage)) ?>"
|
||||
aria-controls="actions-<?= $etage ?>" aria-selected="<?= $offen ? "true" : "false" ?>"><?= $etage ?></a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php
|
||||
/*
|
||||
* Eine Etage, ein Reiter. Gezeichnet von reiterLeiste() aus
|
||||
* restricted/reiter.php - dieselbe Bauart wie im Raum-Modal und
|
||||
* auf der Einstellungsseite. Frueher stand hier ein eigenes
|
||||
* Markup samt eigenem switchTab() in homeMQTT.js; umgeschaltet
|
||||
* wird jetzt von Bootstrap wie ueberall sonst.
|
||||
*
|
||||
* Der Name "actions" haelt die Element-Ids, an denen
|
||||
* autoActionFuncs.js haengt: actions-OG, actions-OG-list.
|
||||
*/
|
||||
$autoReiter = array_map(function ($etage) {
|
||||
return ["id" => $etage, "titel" => $etage,
|
||||
"titel2" => floorLabel($etage)];
|
||||
}, $floors);
|
||||
$autoOpt = ["name" => "actions", "aktiv" => $_GET["floor"],
|
||||
"klasse" => "mb-2"];
|
||||
$autoOpt["aktiv"] = reiterAktiv($autoReiter, $autoOpt);
|
||||
?>
|
||||
<?= reiterLeiste($autoReiter, $autoOpt) ?>
|
||||
<div class="tab-content" id="actions-tabContent">
|
||||
<?php foreach ($floors as $etage): ?>
|
||||
<div class="tab-pane fade <?php if ($_GET["floor"] == $etage) echo "active show"; ?>"
|
||||
id="actions-<?= $etage ?>" role="tabpanel" aria-labelledby="actions-<?= $etage ?>-tab">
|
||||
<?= reiterFlaecheAuf($etage, $autoOpt) ?>
|
||||
<!-- Inhalt kommt aus ajax/AutoAction.php?action=list, gefuellt von
|
||||
refreshAutomations() in js/solar/autoActionFuncs.js -->
|
||||
<div id="actions-<?= $etage ?>-list">Wird geladen...</div>
|
||||
<button type="button" class="btn btn-success" title="Neue Automatik"
|
||||
onclick="openAutoActionModal('?action=editor&floor=<?= $etage ?>')"><i class="bi bi-plus-square"></i></button>
|
||||
</div>
|
||||
<?= reiterFlaecheZu() ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
<?php
|
||||
/**
|
||||
* Modell-Schicht für die Anzeigewerte der Home-Kacheln.
|
||||
*
|
||||
* Was auf einer Kachel steht, stand bisher fest in restricted/rooms.php. Das
|
||||
* ist weiterhin die Vorgabe - hier kommt nur die Möglichkeit dazu, sie zu
|
||||
* überschreiben, ohne die Datei anzufassen. Eine Kachel ohne Zeile in
|
||||
* kachelWerte zeigt also genau das, was sie immer gezeigt hat.
|
||||
*
|
||||
* Die Form einer Wertdefinition ist dieselbe wie in rooms.php, damit zwischen
|
||||
* Maske, Datenbank und Anzeige nichts übersetzt werden muss:
|
||||
*
|
||||
* topic MQTT-Topic, aus dem der Wert kommt
|
||||
* pfad optional: Schlüssel in einer JSON-Nachricht
|
||||
* wechselrichter Liste von Namen unter solarManager/invertersN (statt topic)
|
||||
* feld bei Wechselrichtern das Feld, meist p_AC
|
||||
* format "leistung" skaliert selbst zwischen W und kW
|
||||
* einheit sonst die Einheit, die hinter der Zahl steht
|
||||
* stellen Nachkommastellen (0-3)
|
||||
* gross die Zahl steht gross auf der Kachel
|
||||
* icon Symbol vor der Zahl, siehe kachelIcon() in rooms.php
|
||||
*
|
||||
* Die Tabelle darf fehlen. Ist sie nicht angelegt (solarLog_kacheln.sql), so
|
||||
* verhält sich alles wie vorher, statt dass die Startseite mit einem
|
||||
* SQL-Fehler stehenbleibt - die Kacheln sind keine Einstellung, ohne die man
|
||||
* das Haus nicht mehr sieht.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Geladen wird nicht hier, sondern in rooms.php: kachelVorgabe() und
|
||||
* kachelUeberschreibungen() stehen dort, weil die Startseite sie braucht und
|
||||
* diese Datei nicht. So bleibt der Weg zur Anzeige kurz - und es gibt keine
|
||||
* Ringabhängigkeit zwischen Raum-Modell und Maske.
|
||||
*/
|
||||
require_once(__DIR__ . "/rooms.php"); // Räume, Vorgaben, Überschreibungen
|
||||
require_once(__DIR__ . "/costs.php"); // solarDb()
|
||||
|
||||
/** Höchstens drei Werte je Kachel - mehr passt nicht auf 24 x 19 Einheiten. */
|
||||
const KACHEL_MAX = 3;
|
||||
|
||||
/** Symbole, die vor einer Zahl stehen dürfen. Schlüssel wie in kachelIcon(). */
|
||||
function kachelSymbole()
|
||||
{
|
||||
return [
|
||||
"" => "ohne",
|
||||
"ev-station" => "Ladesäule",
|
||||
"cloud-rain" => "Regen",
|
||||
"sun" => "Sonne",
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Kacheln mit ihrem aktuellen Stand, für die Einstellungsseite.
|
||||
*
|
||||
* Je Kachel steht dabei, ob die Werte die Vorgabe sind oder von Hand gesetzt -
|
||||
* die Maske zeigt "Standard wiederherstellen" nur dort, wo es etwas
|
||||
* herzustellen gibt.
|
||||
*/
|
||||
function kachelListe()
|
||||
{
|
||||
$ueber = kachelUeberschreibungen();
|
||||
$liste = [];
|
||||
foreach (allRooms() as $room) {
|
||||
if (!isset($room["x"])) {
|
||||
continue; // kein Platz im Grundriss, keine Kachel
|
||||
}
|
||||
$key = $room["floor"] . "|" . $room["id"];
|
||||
$liste[] = [
|
||||
"floor" => $room["floor"],
|
||||
"floorLabel" => floorLabel($room["floor"]),
|
||||
"room" => $room["id"],
|
||||
"name" => $room["mqtt"],
|
||||
"eigen" => isset($ueber[$key]),
|
||||
"werte" => $ueber[$key] ?? kachelVorgabe($room),
|
||||
"vorgabe" => kachelVorgabe($room),
|
||||
];
|
||||
}
|
||||
return $liste;
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Messwerte, die sich auswählen lassen - nach Gerät gruppiert.
|
||||
*
|
||||
* Quelle ist derselbe Katalog, aus dem der Automatik-Editor seine Bedingungen
|
||||
* baut (deviceCatalog() in restricted/automations.php). Damit steht in beiden
|
||||
* Masken dasselbe zur Auswahl, und ein neu erkanntes Gerät taucht ohne Zutun
|
||||
* auch hier auf.
|
||||
*
|
||||
* Nicht alles daraus taugt für eine Kachel: die Tahoma-Geräte melden ihre
|
||||
* Zustände über die Box, nicht über den Broker (ihre "url" ist dann ein
|
||||
* Zustandsname wie core:OpenClosedState). Eine Kachel liest aber ausschließlich
|
||||
* MQTT - solche Werte blieben ewig auf "–" stehen und sind deshalb hier nicht
|
||||
* dabei. Übrig bleibt, was ein Topic hat.
|
||||
*
|
||||
* Der Katalog darf fehlen: ohne Mesh-Datenbank gibt es eben keine Auswahl,
|
||||
* und die Maske fällt auf die Eingabe von Hand zurück.
|
||||
*/
|
||||
function kachelKatalog()
|
||||
{
|
||||
$datei = __DIR__ . "/automations.php";
|
||||
if (!file_exists($datei)) {
|
||||
return [];
|
||||
}
|
||||
require_once($datei);
|
||||
|
||||
try {
|
||||
$katalog = deviceCatalog();
|
||||
} catch (Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$geraete = [];
|
||||
foreach ($katalog as $g) {
|
||||
$werte = [];
|
||||
foreach ($g["states"] as $s) {
|
||||
$topic = (string)($s["url"] ?? "");
|
||||
// Ein Topic hat Schrägstriche und kein Schema davor; "core:..."
|
||||
// und "io://..." kommen von der Tahoma-Box.
|
||||
if ($topic === "" || strpos($topic, "/") === false
|
||||
|| preg_match('/^[a-z]+:/', $topic)) {
|
||||
continue;
|
||||
}
|
||||
$werte[] = [
|
||||
"name" => $s["name"],
|
||||
"einheit" => (string)($s["unit"] ?? ""),
|
||||
"topic" => $topic,
|
||||
"pfad" => (string)($s["pfad"] ?? ""),
|
||||
"wert" => $s["value"],
|
||||
];
|
||||
}
|
||||
if (!$werte) {
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* Etage und Raum auch als Kuerzel, nicht nur ausgeschrieben: die
|
||||
* Maske stellt die Geraete des eigenen Raums nach oben und muss dafuer
|
||||
* genau vergleichen koennen. actors.room steht als "EG/Technik" da -
|
||||
* dieselben Kuerzel, die auch rooms.php benutzt.
|
||||
*/
|
||||
$wo = explode("/", (string)($g["room"] ?? ""), 2);
|
||||
$geraete[] = [
|
||||
"label" => $g["label"],
|
||||
"floor" => $g["floorLabel"],
|
||||
"floorKey" => $wo[0] ?? "",
|
||||
"raum" => $g["roomName"],
|
||||
"raumKey" => $wo[1] ?? "",
|
||||
"werte" => $werte,
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
* Nach Etage sortieren, darin nach Raum und Geraet. deviceCatalog()
|
||||
* liefert alphabetisch nach Geraetenamen - in einer nach Etagen
|
||||
* gruppierten Liste stuende dieselbe Etage dann mehrfach da, jedes Mal
|
||||
* mit ein, zwei Geraeten darin.
|
||||
*/
|
||||
$reihenfolge = array_flip($GLOBALS["floors"]);
|
||||
usort($geraete, function ($a, $b) use ($reihenfolge) {
|
||||
$fa = $reihenfolge[$a["floorKey"]] ?? 99;
|
||||
$fb = $reihenfolge[$b["floorKey"]] ?? 99;
|
||||
if ($fa !== $fb) {
|
||||
return $fa <=> $fb;
|
||||
}
|
||||
$raum = strcoll($a["raum"], $b["raum"]);
|
||||
return $raum !== 0 ? $raum : strcoll($a["label"], $b["label"]);
|
||||
});
|
||||
return $geraete;
|
||||
}
|
||||
|
||||
/**
|
||||
* Topics für die Vorschlagsliste der Eingabe von Hand.
|
||||
*
|
||||
* Alles aus dem Katalog plus das, was die Kacheln heute zeigen - denn nicht
|
||||
* jedes Topic am Broker ist irgendwo registriert. Die beiden Shelly-Zähler in
|
||||
* den Technikräumen etwa stehen in keinem Katalog, sind aber auf zwei Kacheln
|
||||
* in Gebrauch.
|
||||
*/
|
||||
function kachelTopicVorschlaege()
|
||||
{
|
||||
$topics = [];
|
||||
|
||||
foreach (allRooms() as $room) {
|
||||
foreach (kachelVorgabe($room) as $wert) {
|
||||
if (!empty($wert["topic"])) {
|
||||
$topics[$wert["topic"]] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (kachelUeberschreibungen() as $werte) {
|
||||
foreach ($werte as $wert) {
|
||||
if (!empty($wert["topic"])) {
|
||||
$topics[$wert["topic"]] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (kachelKatalog() as $g) {
|
||||
foreach ($g["werte"] as $w) {
|
||||
$topics[$w["topic"]] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$liste = array_keys($topics);
|
||||
sort($liste, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
return $liste;
|
||||
}
|
||||
|
||||
/**
|
||||
* Eine Wertdefinition prüfen und auf das Nötige eindampfen.
|
||||
*
|
||||
* Geworfen wird bei allem, was die Anzeige nicht darstellen könnte. Was nur
|
||||
* überflüssig ist - eine Einheit neben format "leistung" etwa - fällt still
|
||||
* weg, damit in der Datenbank keine Angaben stehen, die niemand liest.
|
||||
*/
|
||||
function kachelWertPruefen($wert, $nummer)
|
||||
{
|
||||
if (!is_array($wert)) {
|
||||
throw new InvalidArgumentException("Wert " . $nummer . " ist unlesbar.");
|
||||
}
|
||||
$sauber = [];
|
||||
|
||||
$wechselrichter = array_values(array_filter(array_map(
|
||||
"trim", (array)($wert["wechselrichter"] ?? []))));
|
||||
$topic = trim((string)($wert["topic"] ?? ""));
|
||||
|
||||
if ($wechselrichter) {
|
||||
$sauber["wechselrichter"] = $wechselrichter;
|
||||
$feld = trim((string)($wert["feld"] ?? "p_AC"));
|
||||
$sauber["feld"] = $feld !== "" ? $feld : "p_AC";
|
||||
} elseif ($topic !== "") {
|
||||
if (mb_strlen($topic) > 120) {
|
||||
throw new InvalidArgumentException("Wert " . $nummer . ": das Topic ist zu lang.");
|
||||
}
|
||||
$sauber["topic"] = $topic;
|
||||
$pfad = trim((string)($wert["pfad"] ?? ""));
|
||||
if ($pfad !== "") {
|
||||
$sauber["pfad"] = $pfad;
|
||||
}
|
||||
} else {
|
||||
throw new InvalidArgumentException(
|
||||
"Wert " . $nummer . " braucht ein Topic oder einen Wechselrichter.");
|
||||
}
|
||||
|
||||
// Entweder das Format rechnet die Einheit selbst aus, oder sie steht da.
|
||||
if (($wert["format"] ?? "") === "leistung") {
|
||||
$sauber["format"] = "leistung";
|
||||
} else {
|
||||
$einheit = trim((string)($wert["einheit"] ?? ""));
|
||||
if (mb_strlen($einheit) > 8) {
|
||||
throw new InvalidArgumentException("Wert " . $nummer . ": die Einheit ist zu lang.");
|
||||
}
|
||||
if ($einheit !== "") {
|
||||
$sauber["einheit"] = $einheit;
|
||||
}
|
||||
$stellen = $wert["stellen"] ?? null;
|
||||
if ($stellen !== null && $stellen !== "") {
|
||||
$stellen = intval($stellen);
|
||||
if ($stellen < 0 || $stellen > 3) {
|
||||
throw new InvalidArgumentException(
|
||||
"Wert " . $nummer . ": Nachkommastellen gehen von 0 bis 3.");
|
||||
}
|
||||
$sauber["stellen"] = $stellen;
|
||||
}
|
||||
}
|
||||
|
||||
$icon = (string)($wert["icon"] ?? "");
|
||||
if ($icon !== "") {
|
||||
if (!array_key_exists($icon, kachelSymbole())) {
|
||||
throw new InvalidArgumentException("Wert " . $nummer . ": unbekanntes Symbol.");
|
||||
}
|
||||
$sauber["icon"] = $icon;
|
||||
}
|
||||
if (!empty($wert["gross"])) {
|
||||
$sauber["gross"] = true;
|
||||
}
|
||||
return $sauber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Werte einer Kachel speichern.
|
||||
*
|
||||
* $werte === null setzt auf die Vorgabe zurück - die Zeile verschwindet, statt
|
||||
* die Vorgabe hineinzukopieren. Sonst stünde in der Datenbank eine Kopie, die
|
||||
* nicht mitzöge, wenn sich rooms.php ändert.
|
||||
*
|
||||
* Eine leere Liste ist etwas anderes: die Kachel zeigt dann bewusst keinen
|
||||
* Messwert, sondern nur ihren Namen.
|
||||
*/
|
||||
function kachelWerteSpeichern($floor, $room, $werte)
|
||||
{
|
||||
if (!kachelTabelleDa()) {
|
||||
throw new RuntimeException(
|
||||
"Die Tabelle kachelWerte fehlt. Sie steht in solarLog_kacheln.sql.");
|
||||
}
|
||||
$treffer = null;
|
||||
foreach (allRooms() as $r) {
|
||||
if ($r["floor"] === $floor && $r["id"] === $room && isset($r["x"])) {
|
||||
$treffer = $r;
|
||||
}
|
||||
}
|
||||
if (!$treffer) {
|
||||
throw new InvalidArgumentException("Diese Kachel gibt es nicht.");
|
||||
}
|
||||
|
||||
$db = solarDb();
|
||||
if ($werte === null) {
|
||||
$stmt = $db->prepare("DELETE FROM kachelWerte WHERE floor = ? AND room = ?");
|
||||
$stmt->bind_param("ss", $floor, $room);
|
||||
$stmt->execute();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!is_array($werte)) {
|
||||
throw new InvalidArgumentException("Die Werteliste ist unlesbar.");
|
||||
}
|
||||
if (count($werte) > KACHEL_MAX) {
|
||||
throw new InvalidArgumentException(
|
||||
"Mehr als " . KACHEL_MAX . " Werte passen nicht auf eine Kachel.");
|
||||
}
|
||||
$sauber = [];
|
||||
foreach (array_values($werte) as $i => $wert) {
|
||||
$sauber[] = kachelWertPruefen($wert, $i + 1);
|
||||
}
|
||||
|
||||
// Zwei grosse Zahlen nebeneinander gibt es nicht: die Kachel hat genau
|
||||
// eine Zeile, die aus der Entfernung lesbar ist.
|
||||
$grosse = count(array_filter($sauber, function ($w) { return !empty($w["gross"]); }));
|
||||
if ($grosse > 1) {
|
||||
throw new InvalidArgumentException("Nur ein Wert darf gross stehen.");
|
||||
}
|
||||
|
||||
$json = json_encode($sauber, JSON_UNESCAPED_UNICODE);
|
||||
$stmt = $db->prepare("INSERT INTO kachelWerte (floor, room, werte) VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE werte = VALUES(werte)");
|
||||
$stmt->bind_param("sss", $floor, $room, $json);
|
||||
if (!$stmt->execute()) {
|
||||
throw new RuntimeException($stmt->error);
|
||||
}
|
||||
return $sauber;
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
/**
|
||||
* Reiter - eine Bauart für die ganze Seite.
|
||||
*
|
||||
* Vorher gab es drei: die Etagenreiter über den Automatiken (Home) mit einem
|
||||
* eigenen switchTab() in JavaScript, die Gerätegruppen im Raum-Modal als
|
||||
* Bootstrap-Reiter mit eigener CSS-Klasse, und die Einstellungsseite noch
|
||||
* einmal anders. Drei Markups, zwei Verhaltensweisen, ein Sonderaussehen -
|
||||
* und jede Änderung am Erscheinungsbild wäre dreimal zu machen gewesen.
|
||||
*
|
||||
* Hier steht nur noch, wie ein Reiter aussieht und heißt. Umgeschaltet wird
|
||||
* von Bootstrap (data-bs-toggle="tab"); eigenes JavaScript dafür gibt es
|
||||
* nicht mehr. Wer sich den offenen Reiter merken will, setzt "merken" - das
|
||||
* übernimmt reiterMerken() in js/solar/common.js für alle Leisten der Seite.
|
||||
*
|
||||
* Ein Reiter ist:
|
||||
*
|
||||
* id Kürzel, taucht in den Element-Ids und in der Adresse auf
|
||||
* titel Beschriftung
|
||||
* symbol optional ein Bootstrap-Icon ("bi-currency-euro")
|
||||
* titel2 optional der lange Name als Sprechblase (Etagenkürzel!)
|
||||
* inhalt nur für reiterBlock(): der fertige Inhalt der Fläche
|
||||
*
|
||||
* Aufrufarten - je nachdem, ob der Inhalt schon als Text vorliegt:
|
||||
*
|
||||
* reiterBlock($reiter, $opt) Leiste und Flächen am Stück
|
||||
* reiterLeiste($reiter, $opt) nur die Leiste ...
|
||||
* reiterFlaecheAuf($id, $opt) ... und die Flächen einzeln, wenn der
|
||||
* reiterFlaecheZu() Inhalt im Template steht
|
||||
*/
|
||||
|
||||
/**
|
||||
* Die Kennung eines Reiters. Der Name der Leiste steht davor, damit zwei
|
||||
* Leisten auf derselben Seite sich nicht dieselben Ids teilen.
|
||||
*/
|
||||
function reiterId($opt, $id, $anhang = "")
|
||||
{
|
||||
return ($opt["name"] ?? "reiter") . "-" . $id . $anhang;
|
||||
}
|
||||
|
||||
/**
|
||||
* Welcher Reiter offen ist: der ausdrücklich genannte, sonst der erste.
|
||||
*
|
||||
* Als eigene Funktion, weil Leiste und Flächen dieselbe Antwort brauchen -
|
||||
* und weil "der erste" bei den Flächen sonst noch einmal ausgerechnet würde.
|
||||
*/
|
||||
function reiterAktiv($reiter, $opt)
|
||||
{
|
||||
$ids = array_column($reiter, "id");
|
||||
$aktiv = $opt["aktiv"] ?? null;
|
||||
return ($aktiv !== null && in_array($aktiv, $ids, true)) ? $aktiv : ($ids[0] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Leiste.
|
||||
*
|
||||
* $opt:
|
||||
* name Namensraum der Ids; sollte gesetzt sein, sonst "reiter"
|
||||
* aktiv id des offenen Reiters (Vorgabe: der erste)
|
||||
* merken true: der offene Reiter steht in der Adresse und übersteht ein
|
||||
* Neuladen. Nur eine Leiste je Seite sollte das tun.
|
||||
* schmal true: kleinere Reiter, die waagerecht scrollen statt umzubrechen
|
||||
* - für enge Stellen wie das Raum-Modal
|
||||
* klasse zusätzliche Klassen für die Leiste
|
||||
*/
|
||||
function reiterLeiste($reiter, $opt = [])
|
||||
{
|
||||
if (count($reiter) < 2 && empty($opt["immer"])) {
|
||||
// Ein einzelner Reiter beschriftet nur sich selbst. Das Raum-Modal
|
||||
// hat oft nur eine Gerätegruppe - dort ist die Leiste dann weg.
|
||||
return "";
|
||||
}
|
||||
$aktiv = reiterAktiv($reiter, $opt);
|
||||
$klassen = "nav nav-tabs reiter"
|
||||
. (!empty($opt["schmal"]) ? " reiter-schmal" : "")
|
||||
. (isset($opt["klasse"]) ? " " . $opt["klasse"] : "");
|
||||
|
||||
$html = "<ul class='" . $klassen . "' role='tablist'"
|
||||
. (!empty($opt["merken"]) ? " data-reiter-merken='" . htmlspecialchars($opt["name"] ?? "reiter") . "'" : "")
|
||||
. ">";
|
||||
foreach ($reiter as $r) {
|
||||
$offen = $r["id"] === $aktiv;
|
||||
$html .= "<li class='nav-item' role='presentation'>"
|
||||
. "<button class='nav-link" . ($offen ? " active" : "") . "' type='button' role='tab'"
|
||||
. " id='" . reiterId($opt, $r["id"], "-tab") . "'"
|
||||
. " data-bs-toggle='tab' data-bs-target='#" . reiterId($opt, $r["id"]) . "'"
|
||||
. " data-reiter='" . htmlspecialchars($r["id"]) . "'"
|
||||
. " aria-controls='" . reiterId($opt, $r["id"]) . "'"
|
||||
. " aria-selected='" . ($offen ? "true" : "false") . "'"
|
||||
. (isset($r["titel2"]) ? " title='" . htmlspecialchars($r["titel2"]) . "'" : "")
|
||||
. ">"
|
||||
// Der Abstand zum Text kommt aus dem Stylesheet, nicht aus
|
||||
// einer Hilfsklasse: in der schmalen Fassung ist er kleiner.
|
||||
. (isset($r["symbol"]) ? "<i class='bi " . htmlspecialchars($r["symbol"]) . "'></i>" : "")
|
||||
. htmlspecialchars($r["titel"])
|
||||
. "</button></li>";
|
||||
}
|
||||
return $html . "</ul>";
|
||||
}
|
||||
|
||||
/** Anfang einer Fläche - der Inhalt steht danach im Template. */
|
||||
function reiterFlaecheAuf($id, $opt = [])
|
||||
{
|
||||
$offen = ($opt["aktiv"] ?? null) === $id;
|
||||
return "<div class='tab-pane fade" . ($offen ? " show active" : "") . "'"
|
||||
. " id='" . reiterId($opt, $id) . "' role='tabpanel'"
|
||||
. " aria-labelledby='" . reiterId($opt, $id, "-tab") . "'>";
|
||||
}
|
||||
|
||||
/** Ende einer Fläche. Eigene Funktion, damit im Template kein nacktes </div> steht. */
|
||||
function reiterFlaecheZu()
|
||||
{
|
||||
return "</div>";
|
||||
}
|
||||
|
||||
/**
|
||||
* Leiste und Flächen am Stück - für Aufrufer, die den Inhalt schon als Text
|
||||
* haben (siehe zeichneGruppen() im Raum-Modal).
|
||||
*/
|
||||
function reiterBlock($reiter, $opt = [])
|
||||
{
|
||||
$opt["aktiv"] = reiterAktiv($reiter, $opt);
|
||||
$html = reiterLeiste($reiter, $opt) . "<div class='tab-content'>";
|
||||
foreach ($reiter as $r) {
|
||||
$html .= reiterFlaecheAuf($r["id"], $opt) . ($r["inhalt"] ?? "") . reiterFlaecheZu();
|
||||
}
|
||||
return $html . "</div>";
|
||||
}
|
||||
+20
-25
@@ -19,6 +19,10 @@
|
||||
* die hier gesetzt werden.
|
||||
*/
|
||||
|
||||
// Die Geraetegruppen werden als Reiter gezeichnet - eine Bauart fuer die
|
||||
// ganze Seite, siehe restricted/reiter.php.
|
||||
require_once(__DIR__ . "/reiter.php");
|
||||
|
||||
/**
|
||||
* Was das Kommando schaltet: "ein", "aus", "um" - oder null, wenn es kein
|
||||
* Schaltkommando ist.
|
||||
@@ -209,38 +213,29 @@ function zeichneGruppen($gruppen, $fertig = [])
|
||||
return "";
|
||||
}
|
||||
|
||||
$html = "";
|
||||
if (count($vorhanden) > 1) {
|
||||
$html .= "<ul class='nav nav-tabs flex-nowrap mb-2 raum-tabs' role='tablist'>";
|
||||
$erster = true;
|
||||
foreach ($vorhanden as $form => $titel) {
|
||||
$html .= "<li class='nav-item' role='presentation'>"
|
||||
. "<button class='nav-link" . ($erster ? " active" : "") . "' type='button'"
|
||||
. " data-bs-toggle='tab' data-bs-target='#raumtab-" . $form . "' role='tab'>"
|
||||
. "<i class='bi " . $titel[1] . "'></i>"
|
||||
. "<span class='ms-1'>" . htmlspecialchars($titel[0]) . "</span>"
|
||||
. "</button></li>";
|
||||
$erster = false;
|
||||
}
|
||||
$html .= "</ul>";
|
||||
}
|
||||
|
||||
$html .= "<div class='tab-content'>";
|
||||
$erster = true;
|
||||
// Aus den Gruppen werden Reiter. Wie sie aussehen und wie umgeschaltet
|
||||
// wird, steht in restricted/reiter.php - hier nur, was drinsteht.
|
||||
$reiter = [];
|
||||
foreach ($vorhanden as $form => $titel) {
|
||||
$html .= "<div class='tab-pane fade" . ($erster ? " show active" : "") . "'"
|
||||
. " id='raumtab-" . $form . "' role='tabpanel'>";
|
||||
$inhalt = "";
|
||||
if (isset($fertig[$form])) {
|
||||
$html .= $fertig[$form];
|
||||
$inhalt = $fertig[$form];
|
||||
} else {
|
||||
foreach ($gruppen[$form] as $d) {
|
||||
$html .= zeichneGeraet($d, $form);
|
||||
$inhalt .= zeichneGeraet($d, $form);
|
||||
}
|
||||
}
|
||||
$html .= "</div>";
|
||||
$erster = false;
|
||||
$reiter[] = [
|
||||
"id" => $form,
|
||||
"titel" => $titel[0],
|
||||
"symbol" => $titel[1],
|
||||
"inhalt" => $inhalt,
|
||||
];
|
||||
}
|
||||
return $html . "</div>";
|
||||
// "schmal", weil das Modal eng ist: kleinere Reiter, die waagerecht
|
||||
// scrollen statt umzubrechen.
|
||||
return reiterBlock($reiter, ["name" => "raumtab", "schmal" => true,
|
||||
"klasse" => "mb-2"]);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
|
||||
+91
-10
@@ -2,6 +2,11 @@
|
||||
/*
|
||||
* 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
|
||||
@@ -12,7 +17,9 @@
|
||||
* 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
|
||||
* 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
|
||||
@@ -33,6 +40,10 @@
|
||||
* 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,
|
||||
@@ -239,27 +250,97 @@ function roomsWithTile()
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Messwerte einer Kachel, fertig ausgeschrieben.
|
||||
* 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. Nur diese Räume bekommen auch die Symbole für "heizt" und
|
||||
* "Pufferbetrieb" - anderswo gibt es nichts zu heizen.
|
||||
* darunter.
|
||||
*/
|
||||
function mitWerten($room)
|
||||
function kachelVorgabe($room)
|
||||
{
|
||||
if (isset($room["werte"])) {
|
||||
$room["heizung"] = false;
|
||||
$room["werte"] = array_slice($room["werte"], 0, 3);
|
||||
return $room;
|
||||
return array_slice($room["werte"], 0, 3);
|
||||
}
|
||||
$zweig = "Raumtemp/" . $room["floor"] . "/" . $room["mqtt"] . "/";
|
||||
$room["heizung"] = true;
|
||||
$room["werte"] = [
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
+104
-4
@@ -2,10 +2,15 @@
|
||||
/**
|
||||
* Einstellungsseite: Parameter, die von Hand gepflegt werden.
|
||||
*
|
||||
* Bisher sind das die Preistabellen aus restricted/costs.php. Die Seite ist
|
||||
* als Sammlung von Karten angelegt - eine Karte je Thema. Kommt spaeter ein
|
||||
* weiterer Parameter dazu, kommt er als eigene Karte hinzu, ohne dass die
|
||||
* vorhandenen etwas davon merken.
|
||||
* Gegliedert in Reiter, einer je Thema - Preise und die Anzeigewerte der
|
||||
* Home-Kacheln. Die Liste $reiter unten ist die einzige Stelle, die davon
|
||||
* weiss: ein neues Thema ist ein Eintrag dort plus der Inhalt, alles Uebrige
|
||||
* (Navigation, Umschalten, Merken des Reiters in der Adresse) ergibt sich
|
||||
* daraus. Vorher stand jedes Thema als Karte untereinander; bei drei Themen
|
||||
* waere die Seite ein Scrollband geworden.
|
||||
*
|
||||
* Innerhalb eines Reiters bleibt es bei Karten - eine je Tabelle bzw. je
|
||||
* Etage.
|
||||
*
|
||||
* Die Zeilen zeichnet js/solar/settings.js aus dem Modell, das hier
|
||||
* eingebettet wird - dieselbe Arbeitsteilung wie im Automatik-Editor. Sonst
|
||||
@@ -15,6 +20,22 @@
|
||||
* die Felder dort muessen dieselben sein.
|
||||
*/
|
||||
require_once(__DIR__ . "/costs.php");
|
||||
require_once(__DIR__ . "/kacheln.php");
|
||||
require_once(__DIR__ . "/reiter.php");
|
||||
|
||||
/*
|
||||
* Die Reiter. Reihenfolge ist Anzeigereihenfolge; der erste ist der offene,
|
||||
* solange die Adresse nichts anderes sagt (#kacheln).
|
||||
*/
|
||||
$reiter = [
|
||||
["id" => "preise", "titel" => "Preise", "symbol" => "bi-currency-euro"],
|
||||
["id" => "kacheln", "titel" => "Home-Kacheln", "symbol" => "bi-grid-1x2"],
|
||||
["id" => "geraete", "titel" => "Geräte", "symbol" => "bi-house-gear"],
|
||||
];
|
||||
// "merken": der offene Reiter steht in der Adresse und uebersteht ein
|
||||
// Neuladen - siehe reiterMerken() in js/solar/common.js.
|
||||
$reiterOpt = ["name" => "einstellung", "merken" => true, "klasse" => "mb-3"];
|
||||
$reiterOpt["aktiv"] = reiterAktiv($reiter, $reiterOpt);
|
||||
|
||||
$reihen = preisReihen();
|
||||
$daten = [];
|
||||
@@ -50,6 +71,13 @@ try {
|
||||
<?php if ($fehler): ?>
|
||||
<div class="alert alert-danger">Die Einstellungen sind nicht erreichbar: <?= htmlspecialchars($fehler) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Reiter. Welcher offen ist, merkt sich die Adresse (#kacheln) -
|
||||
ein Neuladen nach dem Speichern landet damit wieder dort. -->
|
||||
<?= reiterLeiste($reiter, $reiterOpt) ?>
|
||||
|
||||
<div class="tab-content">
|
||||
<?= reiterFlaecheAuf("preise", $reiterOpt) ?>
|
||||
<?php foreach ($reihen as $key => $reihe): ?>
|
||||
<?php $breiten = preisSpalten($reihe["felder"]); ?>
|
||||
<div class="row">
|
||||
@@ -94,6 +122,78 @@ try {
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?= reiterFlaecheZu() ?><!-- /Preise -->
|
||||
|
||||
<!-- ================================================ Home-Kacheln ==== -->
|
||||
<!-- Der Inhalt kommt aus js/solar/settings.js: eine Karte je Etage, darin
|
||||
je Raum eine Zeile mit ihren Werten. Gezeichnet wird aus dem Modell,
|
||||
das ajax/settings.php?action=kacheln liefert - dieselbe Arbeits-
|
||||
teilung wie bei den Preisen. -->
|
||||
<?= reiterFlaecheAuf("kacheln", $reiterOpt) ?>
|
||||
<div class="row">
|
||||
<div class="col-12 col-xxl-9">
|
||||
<div class="card mb-4" id="kachelKarte">
|
||||
<div class="card-header pb-2 pt-2">
|
||||
<h3 class="card-title"><i class="bi bi-grid-1x2 me-2"></i>Werte auf den Kacheln</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-tool" id="kachelNeuLaden"
|
||||
title="Neu laden">
|
||||
<i class="bi bi-arrow-counterclockwise"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-secondary small mb-3">
|
||||
Jede Kachel im Grundriss zeigt bis zu drei Messwerte. Ohne eigene
|
||||
Angabe sind das die Werte aus <code>restricted/rooms.php</code> –
|
||||
bei Räumen mit Thermostat Soll, Ist und Feuchte. Was hier gesetzt
|
||||
wird, gilt stattdessen; „Standard“ nimmt es wieder zurück.
|
||||
</p>
|
||||
<div id="kachelListe">
|
||||
<p class="text-secondary small mb-0">Wird geladen …</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?= reiterFlaecheZu() ?><!-- /Home-Kacheln -->
|
||||
|
||||
<!-- ===================================================== Geräte ==== -->
|
||||
<!-- Die Zuordnung Gerät -> Raum lag vorher als Modal auf der Home-Seite.
|
||||
Sie gehört hierher: sie wird selten geändert, sie ist eine
|
||||
Einstellung, und hier ist Platz für das, was daneben gehört - wie
|
||||
lange ein Gerät nichts mehr gemeldet hat, in welchen Automatiken es
|
||||
steckt, und der Weg, eine Karteileiche loszuwerden. -->
|
||||
<?= reiterFlaecheAuf("geraete", $reiterOpt) ?>
|
||||
<div class="row">
|
||||
<div class="col-12 col-xxl-10">
|
||||
<div class="card mb-4" id="geraeteKarte">
|
||||
<div class="card-header pb-2 pt-2">
|
||||
<h3 class="card-title"><i class="bi bi-house-gear me-2"></i>Geräte und Räume</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-tool" id="geraeteNeuLaden" title="Neu laden">
|
||||
<i class="bi bi-arrow-counterclockwise"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-secondary small mb-3">
|
||||
Der Raum ordnet die Geräteliste im Automatik-Editor und bestimmt,
|
||||
welche Messwerte bei einer Home-Kachel oben stehen. Er wird von Hand
|
||||
vergeben und bleibt bei einem neuen Suchlauf erhalten – der Suchlauf
|
||||
legt nur an und aktualisiert, er löscht nichts. Abgebaute Geräte
|
||||
bleiben deshalb stehen, bis sie hier entfernt werden.
|
||||
</p>
|
||||
<div id="geraeteListe">
|
||||
<p class="text-secondary small mb-0">Wird geladen …</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?= reiterFlaecheZu() ?><!-- /Geräte -->
|
||||
</div><!-- /tab-content -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!--end::App Content-->
|
||||
|
||||
Reference in New Issue
Block a user