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"],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user