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:
2026-09-08 22:45:24 +02:00
co-authored by Claude Opus 5
parent 1b58579c22
commit 07bca5104e
15 changed files with 2058 additions and 226 deletions
+164 -12
View File
@@ -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"],
];
}