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>
1089 lines
40 KiB
PHP
1089 lines
40 KiB
PHP
<?php
|
||
/**
|
||
* Modell-Schicht fuer die Automatiken (AutoActions).
|
||
*
|
||
* Hier liegt alles, was Editor und Uebersicht gemeinsam brauchen: die
|
||
* Verbindung zur homeMesh-Datenbank, der Geraetekatalog, Laden und Speichern
|
||
* einer Automatik und die Beschreibung in Alltagssprache. Die Ajax-Endpunkte
|
||
* sind damit reine Verteiler.
|
||
*
|
||
* Tabellen siehe homeMesh_automations.sql. Kurzfassung: eine Automatik hat
|
||
* Bedingungen (Verweis auf actor_states) und Aktionen (Verweis auf
|
||
* actor_commands, Werte je command_parameters). Bedingungen mit derselben
|
||
* group_no sind mit UND verknuepft, verschiedene Gruppen mit ODER -
|
||
* ausgewertet wird any(all(gruppe)).
|
||
*/
|
||
|
||
require_once(__DIR__ . "/mysql.php");
|
||
require_once(__DIR__ . "/rooms.php");
|
||
|
||
/** Verbindung zur Geraete- und Automatik-Datenbank. */
|
||
function meshDb()
|
||
{
|
||
static $db = null;
|
||
if ($db === null) {
|
||
$db = new mysqli($GLOBALS["mysql_server"], $GLOBALS["mysql_MeshUser"],
|
||
$GLOBALS["mysql_MeshPass"], $GLOBALS["mysql_MeshDB"]);
|
||
$db->set_charset("utf8mb4");
|
||
}
|
||
return $db;
|
||
}
|
||
|
||
/**
|
||
* Operatoren, die zu einem Datentyp passen. Immer ASCII - die huebschen
|
||
* Zeichen macht erst die Anzeige daraus. Frueher schickte der Editor sein
|
||
* innerHTML ("≠", ">") an den Server, wo eine Whitelist mit "!=" stand:
|
||
* jede Bedingung "ungleich" wurde still zu "gleich".
|
||
*
|
||
* Bei Zeit und Datum heissen die Operatoren "um", "ab" und "vor":
|
||
*
|
||
* um 16:30 ab dieser Minute, und noch ein paar Minuten danach
|
||
* (catchup_minutes im Runner, Vorgabe fuenf). Der uebliche
|
||
* Fall - einmal taeglich zu einem festen Zeitpunkt. Das
|
||
* Fenster kostet nichts, weil ohnehin nur die steigende
|
||
* Flanke zaehlt; es sorgt nur dafuer, dass ein Neustart oder
|
||
* ein langsamer Durchlauf den Termin nicht verschluckt.
|
||
* ab 16:30 ab da bis Mitternacht wahr. Ausgeloest wird trotzdem nur
|
||
* einmal, weil nur die steigende Flanke zaehlt; dafuer kostet
|
||
* ein verpasster Takt nicht den ganzen Tag. Fuer alles, was
|
||
* laufen muss, auch wenn der Termin selbst verpasst wurde,
|
||
* ist das die richtige Wahl - etwa die Bewaesserung.
|
||
* vor 16:30 bis dahin wahr, danach nicht mehr.
|
||
*
|
||
* Beim Sonnenauf- und -untergang ist der Wert ein Versatz, der davor oder
|
||
* danach liegen kann - deshalb dieselben drei Faelle noch einmal mal zwei:
|
||
* "+ 00:30" eine halbe Stunde nach Sonnenaufgang, "ab - 00:30" ab einer
|
||
* halben Stunde davor, "vor + 00:30" bis eine halbe Stunde danach.
|
||
*/
|
||
function operatorsForType($type)
|
||
{
|
||
switch ($type) {
|
||
case "integer":
|
||
case "float":
|
||
return ["=", "!=", ">", "<"];
|
||
case "time":
|
||
case "date":
|
||
case "datetime":
|
||
return ["=", ">=", "<"];
|
||
case "deltatime": // Sonnenauf-/-untergang: Wert ist ein Versatz
|
||
return ["+", "-", ">=+", ">=-", "<+", "<-"];
|
||
case "bool":
|
||
return ["="]; // JA/NEIN steht im Wertfeld, nicht im Operator
|
||
default:
|
||
return ["=", "!="];
|
||
}
|
||
}
|
||
|
||
/** Operatoren mit ihrer Beschriftung, so wie der Editor sie anbietet. */
|
||
function operatorChoices($type)
|
||
{
|
||
$liste = [];
|
||
foreach (operatorsForType($type) as $op) {
|
||
$liste[] = ["value" => $op, "label" => operatorLabel($op, $type)];
|
||
}
|
||
return $liste;
|
||
}
|
||
|
||
/**
|
||
* Sperrzeit: so lange nach einer Ausloesung wird nicht erneut geschaltet.
|
||
*
|
||
* Gedacht gegen Messwerte, die um die Schwelle pendeln - "Temperatur > 22"
|
||
* bei 22,1 / 21,9 / 22,1 Grad ist jedes Mal eine echte steigende Flanke, und
|
||
* ohne Sperre ginge jedes Mal ein Kommando raus. Wie schlimm das ist, haengt
|
||
* am Geraet: ein Rollladen soll nicht alle zwanzig Sekunden losfahren, eine
|
||
* Lichtfarbe darf das.
|
||
*
|
||
* Gespeichert werden Sekunden, angeboten werden nur diese Stufen - eine
|
||
* vierte ist damit eine Zeile und keine Wanderung durch die Datenbank.
|
||
*/
|
||
function lockoutChoices()
|
||
{
|
||
return [
|
||
["value" => 0, "label" => "Ohne – jede Flanke schaltet"],
|
||
["value" => 60, "label" => "1 Minute – z.B. Licht und Farbe"],
|
||
["value" => 900, "label" => "15 Minuten – z.B. Rollläden und Ventile"],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Auswahlwerte aus possible_values in eine einheitliche Form bringen.
|
||
*
|
||
* In der Datenbank stehen zwei Schreibweisen. Eine schlichte Liste
|
||
* (["open","close"]) und eine Liste aus Ein-Schluessel-Objekten
|
||
* ([{"0":"Unknown"},{"1":"Idle"}]) - letztere ueberall dort, wo das Geraet
|
||
* eine Zahl schickt und einen Zustand meint.
|
||
*
|
||
* Was davon der gespeicherte Wert ist, haengt an der Seite:
|
||
*
|
||
* Messwert: der Runner uebersetzt die Zahl schon beim Lesen in den
|
||
* Klartext, eine Bedingung vergleicht also "Charging".
|
||
* Parameter: das Geraet will die Zahl - gespeichert wird der Schluessel,
|
||
* angezeigt der Klartext.
|
||
*/
|
||
function optionList($json, $klartextAlsWert)
|
||
{
|
||
$roh = json_decode($json, true);
|
||
if (!is_array($roh)) {
|
||
return [];
|
||
}
|
||
$liste = [];
|
||
foreach ($roh as $eintrag) {
|
||
if (is_array($eintrag)) {
|
||
foreach ($eintrag as $wert => $bezeichnung) {
|
||
$liste[] = [
|
||
"value" => $klartextAlsWert ? strval($bezeichnung) : strval($wert),
|
||
"label" => strval($bezeichnung),
|
||
];
|
||
}
|
||
} else {
|
||
$liste[] = ["value" => strval($eintrag), "label" => strval($eintrag)];
|
||
}
|
||
}
|
||
return $liste;
|
||
}
|
||
|
||
/** Wie das Wertfeld im Editor aussieht. */
|
||
function inputForType($type, $hasOptions)
|
||
{
|
||
if ($type === "bool") {
|
||
return "bool";
|
||
}
|
||
if ($hasOptions) {
|
||
return "select";
|
||
}
|
||
switch ($type) {
|
||
case "integer":
|
||
case "float":
|
||
return "number";
|
||
case "time":
|
||
case "deltatime":
|
||
return "time";
|
||
case "date":
|
||
return "date";
|
||
case "datetime":
|
||
return "datetime-local";
|
||
default:
|
||
return "text";
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Anzeigeform eines Operators. Bei Zeitangaben liest sich "um"/"ab"/"vor"
|
||
* verstaendlicher als "="/">="/"<", und beim Sonnenstand steht das
|
||
* Vorzeichen des Versatzes dahinter: "ab + 00:30" heisst ab einer halben
|
||
* Stunde nach Sonnenaufgang.
|
||
*/
|
||
function operatorLabel($op, $type = "")
|
||
{
|
||
if ($type === "time" || $type === "datetime") {
|
||
if ($op === "=") return "um";
|
||
if ($op === ">=") return "ab";
|
||
if ($op === "<") return "vor";
|
||
}
|
||
if ($type === "date") {
|
||
if ($op === "=") return "am";
|
||
if ($op === ">=") return "ab";
|
||
if ($op === "<") return "vor";
|
||
}
|
||
if ($type === "deltatime") {
|
||
$beschriftung = ["+" => "+", "-" => "-", ">=+" => "ab +", ">=-" => "ab -",
|
||
"<+" => "vor +", "<-" => "vor -"];
|
||
if (isset($beschriftung[$op])) {
|
||
return $beschriftung[$op];
|
||
}
|
||
}
|
||
return $op === "!=" ? "≠" : $op;
|
||
}
|
||
|
||
/**
|
||
* Technische Zutaten aus einem Geraetenamen nehmen, ohne ihn in der
|
||
* Datenbank anzufassen.
|
||
*
|
||
* Die Discovery vergibt Namen, wie das Geraet sie meldet: "u_Wozi
|
||
* Terrassentuer" (das u_ stammt aus der Tahoma-Beschriftung),
|
||
* "Power_EG_EM_0" oder "Handtuchtrockner_OG_Relay_0" (die angehaengte Ziffer
|
||
* ist der Kanal, und es gibt nur einen). In der Auswahlliste stoert das nur.
|
||
*
|
||
* Bewusst nur die einstellige Endung: "go-eCharger_270003" traegt seine
|
||
* Seriennummer, die bleibt stehen.
|
||
*/
|
||
function anzeigeName($name)
|
||
{
|
||
$name = preg_replace('/^u_/', '', $name);
|
||
$name = preg_replace('/_\d$/', '', $name);
|
||
$name = str_replace('_', ' ', $name);
|
||
return trim(preg_replace('/\s+/', ' ', $name));
|
||
}
|
||
|
||
/**
|
||
* Waehlbare Raeume: die Raeume der Home-Ansicht.
|
||
*
|
||
* Einzige Quelle bleibt restricted/rooms.php - ein neuer Raum ist dort eine
|
||
* Zeile und taucht hier automatisch auf. Der Schluessel ist "Etage/Raum",
|
||
* weil "Bad" allein auf drei Etagen vorkommt.
|
||
*
|
||
* Frueher stand hier ein Pseudo-Raum "aussen" fuer alles, was zu keinem Raum
|
||
* der Wohnung gehoert - Carport, Pergola, Garage, die Wechselrichter. Das war
|
||
* ein einziger Topf mit sechzehn Geraeten. Inzwischen ist das Aussengelaende
|
||
* die Etage AG mit eigenen Raeumen, und dieser Sonderfall ist keiner mehr.
|
||
*
|
||
* Etage und Raum stehen zusaetzlich einzeln daneben: die Geraeteauswahl im
|
||
* Editor gruppiert nach der einen und sortiert nach dem anderen, und der
|
||
* Schluessel soll dafuer nicht erst im JavaScript wieder auseinandergenommen
|
||
* werden. Die Reihenfolge dieser Liste ist zugleich die Reihenfolge der
|
||
* Ueberschriften dort - erst OG, dann EG, dann UG, wie in rooms.php notiert.
|
||
*/
|
||
function raumListe()
|
||
{
|
||
$liste = [];
|
||
foreach (allRooms() as $raum) {
|
||
$liste[] = [
|
||
"key" => $raum["floor"] . "/" . $raum["mqtt"],
|
||
"label" => $raum["floor"] . " · " . $raum["mqtt"],
|
||
"floor" => $raum["floor"],
|
||
"room" => $raum["mqtt"],
|
||
];
|
||
}
|
||
return $liste;
|
||
}
|
||
|
||
/** Anzeigename eines gespeicherten Raumschluessels. */
|
||
function raumBeschriftung($key)
|
||
{
|
||
if (!$key) {
|
||
return "Nicht zugeordnet";
|
||
}
|
||
foreach (raumListe() as $raum) {
|
||
if ($raum["key"] === $key) {
|
||
return $raum["label"];
|
||
}
|
||
}
|
||
// Raum aus rooms.php entfernt - der Eintrag am Geraet bleibt stehen,
|
||
// damit eine Umbenennung dort keine Zuordnung stillschweigend loescht.
|
||
return $key . " (unbekannt)";
|
||
}
|
||
|
||
/**
|
||
* Etage und Raumname zu einem gespeicherten Raumschluessel.
|
||
*
|
||
* Die Geraeteauswahl im Editor braucht beides getrennt: die Etage wird zur
|
||
* Ueberschrift, der Raum zur Sortierung darunter. Geraete ohne Zuordnung
|
||
* bekommen ihre eigene Ueberschrift ganz am Ende; ein Raum, den es in
|
||
* rooms.php nicht mehr gibt, ebenso - er verschwindet nicht still zwischen
|
||
* den Unzugeordneten, sondern faellt auf.
|
||
*/
|
||
function raumEinordnung($key)
|
||
{
|
||
if (!$key) {
|
||
return ["floor" => "Ohne Raum", "room" => ""];
|
||
}
|
||
foreach (raumListe() as $raum) {
|
||
if ($raum["key"] === $key) {
|
||
return ["floor" => $raum["floor"], "room" => $raum["room"]];
|
||
}
|
||
}
|
||
return ["floor" => "Unbekannter Raum", "room" => $key];
|
||
}
|
||
|
||
/**
|
||
* Raumvorschlaege aus den MQTT-Topics.
|
||
*
|
||
* Die Thermostate tragen ihren Raum im Topic: "Raumtemp/OG/Bad/Temp[degC]".
|
||
* Das ist keine Rateaufgabe, sondern dieselbe Schreibweise wie in rooms.php,
|
||
* und deckt dreizehn Geraete ohne Zutun ab. Bei allen anderen wird bewusst
|
||
* nicht geraten: "Bad Links" sagt nichts darueber, auf welcher der drei
|
||
* Etagen dieses Bad liegt.
|
||
*/
|
||
function raumVorschlaege()
|
||
{
|
||
$bekannt = [];
|
||
foreach (raumListe() as $raum) {
|
||
$bekannt[strtolower($raum["key"])] = $raum["key"];
|
||
}
|
||
$res = meshDb()->query("SELECT actor_id, url FROM actor_states
|
||
WHERE url LIKE 'Raumtemp/%/%/%'");
|
||
$vorschlag = [];
|
||
while ($row = $res->fetch_assoc()) {
|
||
$teile = explode("/", $row["url"]);
|
||
if (count($teile) < 3) {
|
||
continue;
|
||
}
|
||
$key = strtolower($teile[1] . "/" . $teile[2]);
|
||
if (isset($bekannt[$key])) {
|
||
$vorschlag[intval($row["actor_id"])] = $bekannt[$key];
|
||
}
|
||
}
|
||
return $vorschlag;
|
||
}
|
||
|
||
/** Zuordnungen speichern. $zuordnung ist [actor_id => Raumschluessel|""]. */
|
||
function saveRooms($zuordnung)
|
||
{
|
||
$db = meshDb();
|
||
$erlaubt = array_column(raumListe(), "key");
|
||
$stmt = $db->prepare("UPDATE actors SET room = ? WHERE id = ?");
|
||
$anzahl = 0;
|
||
foreach ($zuordnung as $actorId => $key) {
|
||
$actorId = intval($actorId);
|
||
$key = strval($key);
|
||
if ($actorId <= 0) {
|
||
continue;
|
||
}
|
||
if ($key !== "" && !in_array($key, $erlaubt, true)) {
|
||
throw new InvalidArgumentException("Unbekannter Raum: " . $key);
|
||
}
|
||
$wert = $key === "" ? null : $key;
|
||
$stmt->bind_param("si", $wert, $actorId);
|
||
$stmt->execute();
|
||
$anzahl++;
|
||
}
|
||
$stmt->close();
|
||
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.
|
||
*
|
||
* Eine Stelle fuer Editor und Uebersicht, damit in der Auswahlliste nicht
|
||
* "Wozi Terrassentuer" steht und im Satz darunter "u_Wozi Terrassentuer".
|
||
*/
|
||
function geraeteBeschriftungen()
|
||
{
|
||
static $labels = null;
|
||
if ($labels !== null) {
|
||
return $labels;
|
||
}
|
||
$res = meshDb()->query("SELECT id, name, type FROM actors");
|
||
$zeilen = [];
|
||
$haeufigkeit = [];
|
||
while ($row = $res->fetch_assoc()) {
|
||
$row["label"] = anzeigeName($row["name"]);
|
||
$haeufigkeit[$row["label"]] = ($haeufigkeit[$row["label"]] ?? 0) + 1;
|
||
$zeilen[] = $row;
|
||
}
|
||
// Die Tahoma-Box vergibt Beschriftungen doppelt: "Terasse" ist einmal der
|
||
// Rollladen und einmal der Sonnenenergie-Sensor. Untereinander in der
|
||
// Liste waeren sie nicht auseinanderzuhalten, deshalb kommt bei
|
||
// Doppelungen die Geraeteart dazu.
|
||
$labels = [];
|
||
foreach ($zeilen as $row) {
|
||
$labels[intval($row["id"])] = $haeufigkeit[$row["label"]] > 1
|
||
? $row["label"] . " (" . $row["type"] . ")"
|
||
: $row["label"];
|
||
}
|
||
return $labels;
|
||
}
|
||
|
||
/**
|
||
* Alle Geraete mit ihren Messwerten und Kommandos in einem Rutsch.
|
||
*
|
||
* Der Editor holt den Katalog einmal und baut daraus jede Zeile - frueher
|
||
* kostete jede neue Bedingung zwei zusaetzliche Anfragen (Geraeteliste und
|
||
* Details), und die Dropdowns fuellten sich asynchron nach.
|
||
*/
|
||
function deviceCatalog()
|
||
{
|
||
$db = meshDb();
|
||
$devices = [];
|
||
|
||
$labels = geraeteBeschriftungen();
|
||
$res = $db->query("SELECT id, name, type, url, room FROM actors");
|
||
$zeilen = [];
|
||
while ($row = $res->fetch_assoc()) {
|
||
$zeilen[] = $row;
|
||
}
|
||
usort($zeilen, function ($a, $b) use ($labels) {
|
||
return strcoll($labels[intval($a["id"])], $labels[intval($b["id"])]);
|
||
});
|
||
foreach ($zeilen as $row) {
|
||
$wo = raumEinordnung($row["room"]);
|
||
$devices[intval($row["id"])] = [
|
||
"id" => intval($row["id"]),
|
||
"name" => $row["name"],
|
||
"label" => $labels[intval($row["id"])],
|
||
"type" => $row["type"],
|
||
"url" => $row["url"],
|
||
"room" => $row["room"],
|
||
"roomLabel" => raumBeschriftung($row["room"]),
|
||
// Etage und Raum getrennt: die Auswahlliste im Editor gruppiert
|
||
// nach der Etage und sortiert darin nach dem Raum.
|
||
"floorLabel" => $wo["floor"],
|
||
"roomName" => $wo["room"],
|
||
"states" => [],
|
||
"commands" => [],
|
||
];
|
||
}
|
||
|
||
/*
|
||
* 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.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");
|
||
while ($row = $res->fetch_assoc()) {
|
||
$id = intval($row["actor_id"]);
|
||
if (!isset($devices[$id])) {
|
||
continue;
|
||
}
|
||
$options = optionList($row["possible_values"], true);
|
||
$type = $row["type"] ?: "string";
|
||
$devices[$id]["states"][] = [
|
||
"id" => intval($row["id"]),
|
||
"name" => $row["state_name"],
|
||
"type" => $type,
|
||
"input" => inputForType($type, count($options) > 0),
|
||
"operators" => operatorChoices($type),
|
||
"options" => $options,
|
||
"unit" => $row["unit"],
|
||
"value" => $row["current_value"],
|
||
"url" => $row["url"],
|
||
// Schluessel in einer JSON-Nachricht, sonst leer - siehe oben.
|
||
"pfad" => $row["value_path"],
|
||
];
|
||
}
|
||
|
||
$res = $db->query("SELECT id, actor_id, command_name, command_url FROM actor_commands
|
||
ORDER BY actor_id, id");
|
||
$commands = [];
|
||
while ($row = $res->fetch_assoc()) {
|
||
$id = intval($row["actor_id"]);
|
||
if (!isset($devices[$id])) {
|
||
continue;
|
||
}
|
||
$commands[intval($row["id"])] = $id;
|
||
$devices[$id]["commands"][] = [
|
||
"id" => intval($row["id"]),
|
||
"name" => $row["command_name"],
|
||
// Das Raum-Modal erkennt an der URL, was ein Kommando tut:
|
||
// "setClosure" ist eine Position, "up"/"down" faehrt, ein
|
||
// WLED-"{"on":true}" schaltet ein. Am Namen ginge das nicht -
|
||
// der kommt aus dem Geraetesuchlauf und heisst mal "An", mal
|
||
// "turn_on".
|
||
"url" => $row["command_url"],
|
||
"params" => [],
|
||
];
|
||
}
|
||
|
||
if ($commands) {
|
||
$res = $db->query("SELECT p.id, p.command_id, p.parameter_name, p.min_value,
|
||
p.max_value, p.possible_values, p.url, t.type
|
||
FROM command_parameters p
|
||
LEFT JOIN state_types t ON p.parameter_type = t.id
|
||
ORDER BY p.command_id, p.id");
|
||
while ($row = $res->fetch_assoc()) {
|
||
$cmdId = intval($row["command_id"]);
|
||
if (!isset($commands[$cmdId])) {
|
||
continue;
|
||
}
|
||
$options = optionList($row["possible_values"], false);
|
||
$type = $row["type"] ?: "string";
|
||
// Das passende Kommando im Geraet suchen. Wenige Kommandos je
|
||
// Geraet, deshalb reicht die lineare Suche.
|
||
foreach ($devices[$commands[$cmdId]]["commands"] as &$cmd) {
|
||
if ($cmd["id"] === $cmdId) {
|
||
$cmd["params"][] = [
|
||
"id" => intval($row["id"]),
|
||
"name" => $row["parameter_name"],
|
||
"type" => $type,
|
||
"input" => inputForType($type, count($options) > 0),
|
||
"options" => $options,
|
||
"min" => $row["min_value"] === null ? null : floatval($row["min_value"]),
|
||
"max" => $row["max_value"] === null ? null : floatval($row["max_value"]),
|
||
"url" => $row["url"],
|
||
];
|
||
break;
|
||
}
|
||
}
|
||
unset($cmd);
|
||
}
|
||
}
|
||
|
||
return array_values($devices);
|
||
}
|
||
|
||
/** Leere Automatik, wie sie der Editor fuer "Neu anlegen" bekommt. */
|
||
function emptyAutomation($floor)
|
||
{
|
||
return [
|
||
"id" => 0,
|
||
"name" => "",
|
||
// Aufgerufen wird der Editor immer aus einem Etagenreiter. Fehlt die
|
||
// Angabe trotzdem, gilt dieselbe Etage wie auf der Startseite.
|
||
"floor" => in_array($floor, $GLOBALS["floors"], true) ? $floor : "OG",
|
||
"enabled" => true,
|
||
"window_from" => "00:00",
|
||
"window_to" => "23:59",
|
||
"weekdays" => 127,
|
||
"on_vacation" => true,
|
||
"on_holiday" => true,
|
||
"force_once" => false,
|
||
"lockout_secs" => 60,
|
||
"last_run" => null,
|
||
"conditions" => [],
|
||
"actions" => [],
|
||
];
|
||
}
|
||
|
||
/** Eine Automatik mit Bedingungen und Aktionen laden. */
|
||
function loadAutomation($id)
|
||
{
|
||
$db = meshDb();
|
||
$stmt = $db->prepare("SELECT * FROM automations WHERE id = ?");
|
||
$stmt->bind_param("i", $id);
|
||
$stmt->execute();
|
||
$row = $stmt->get_result()->fetch_assoc();
|
||
$stmt->close();
|
||
if (!$row) {
|
||
return null;
|
||
}
|
||
|
||
$auto = [
|
||
"id" => intval($row["id"]),
|
||
"name" => $row["name"],
|
||
"floor" => $row["floor"],
|
||
"enabled" => (bool)$row["enabled"],
|
||
"window_from" => substr($row["window_from"], 0, 5),
|
||
"window_to" => substr($row["window_to"], 0, 5),
|
||
"weekdays" => intval($row["weekdays"]),
|
||
"on_vacation" => (bool)$row["on_vacation"],
|
||
"on_holiday" => (bool)$row["on_holiday"],
|
||
"force_once" => (bool)$row["force_once"],
|
||
"lockout_secs" => intval($row["lockout_secs"]),
|
||
"last_run" => $row["last_run"],
|
||
"conditions" => [],
|
||
"actions" => [],
|
||
];
|
||
|
||
$stmt = $db->prepare("SELECT id, group_no, state_id, operator, value
|
||
FROM automation_conditions WHERE automation_id = ?
|
||
ORDER BY group_no, position, id");
|
||
$stmt->bind_param("i", $id);
|
||
$stmt->execute();
|
||
$res = $stmt->get_result();
|
||
while ($c = $res->fetch_assoc()) {
|
||
$auto["conditions"][] = [
|
||
"group" => intval($c["group_no"]),
|
||
"state_id" => intval($c["state_id"]),
|
||
"operator" => $c["operator"],
|
||
"value" => $c["value"],
|
||
];
|
||
}
|
||
$stmt->close();
|
||
|
||
$stmt = $db->prepare("SELECT a.id, a.command_id, p.parameter_id, p.value
|
||
FROM automation_actions a
|
||
LEFT JOIN automation_action_params p ON p.action_id = a.id
|
||
WHERE a.automation_id = ?
|
||
ORDER BY a.position, a.id, p.parameter_id");
|
||
$stmt->bind_param("i", $id);
|
||
$stmt->execute();
|
||
$res = $stmt->get_result();
|
||
$actions = [];
|
||
while ($a = $res->fetch_assoc()) {
|
||
$aid = intval($a["id"]);
|
||
if (!isset($actions[$aid])) {
|
||
$actions[$aid] = ["command_id" => intval($a["command_id"]), "params" => []];
|
||
}
|
||
if ($a["parameter_id"] !== null) {
|
||
$actions[$aid]["params"][strval($a["parameter_id"])] = $a["value"];
|
||
}
|
||
}
|
||
$stmt->close();
|
||
$auto["actions"] = array_values($actions);
|
||
|
||
return $auto;
|
||
}
|
||
|
||
/**
|
||
* Eine Automatik anlegen oder ueberschreiben. Gibt die ID zurueck.
|
||
*
|
||
* Bedingungen und Aktionen werden komplett neu geschrieben statt einzeln
|
||
* abgeglichen: sie haben keine Identitaet, die der Benutzer kennt, und der
|
||
* Editor schickt ohnehin immer den vollstaendigen Stand. Alles laeuft in
|
||
* einer Transaktion, damit eine halb ersetzte Regel nie sichtbar wird - und
|
||
* schon gar nicht ausgefuehrt.
|
||
*/
|
||
function saveAutomation($data)
|
||
{
|
||
$db = meshDb();
|
||
|
||
$name = trim(strval($data["name"] ?? ""));
|
||
if ($name === "") {
|
||
throw new InvalidArgumentException("Bitte einen Namen vergeben.");
|
||
}
|
||
$floor = strval($data["floor"] ?? "");
|
||
if (!in_array($floor, $GLOBALS["floors"], true)) {
|
||
// Nicht stillschweigend auf "" zurueckfallen: die Automatik wuerde
|
||
// dann in keinem der drei Reiter mehr auftauchen.
|
||
throw new InvalidArgumentException("Bitte eine Etage wählen.");
|
||
}
|
||
$conditions = is_array($data["conditions"] ?? null) ? $data["conditions"] : [];
|
||
$actions = is_array($data["actions"] ?? null) ? $data["actions"] : [];
|
||
if (!$conditions) {
|
||
throw new InvalidArgumentException("Ohne Ausloeser wuerde die Automatik nie starten.");
|
||
}
|
||
if (!$actions) {
|
||
throw new InvalidArgumentException("Ohne Aktion haette die Automatik nichts zu tun.");
|
||
}
|
||
|
||
$id = intval($data["id"] ?? 0);
|
||
$enabled = !empty($data["enabled"]) ? 1 : 0;
|
||
$windowFrom = normalizeTime($data["window_from"] ?? "00:00", "00:00:00");
|
||
$windowTo = normalizeTime($data["window_to"] ?? "23:59", "23:59:00");
|
||
$weekdays = intval($data["weekdays"] ?? 127) & 127;
|
||
$onVacation = !empty($data["on_vacation"]) ? 1 : 0;
|
||
$onHoliday = !empty($data["on_holiday"]) ? 1 : 0;
|
||
$forceOnce = !empty($data["force_once"]) ? 1 : 0;
|
||
// Nur die angebotenen Stufen, sonst die Vorgabe. Eine krumme Zahl kaeme
|
||
// hier nur aus einer selbstgebauten Anfrage.
|
||
$lockout = intval($data["lockout_secs"] ?? 60);
|
||
if (!in_array($lockout, array_column(lockoutChoices(), "value"), true)) {
|
||
$lockout = 60;
|
||
}
|
||
|
||
$db->begin_transaction();
|
||
try {
|
||
if ($id > 0) {
|
||
// Erst nachsehen, ob es die Automatik noch gibt. Ohne das liefe
|
||
// das UPDATE ins Leere und erst der Fremdschluessel der
|
||
// Bedingungen wuerde meckern - mit einer Meldung, die niemandem
|
||
// weiterhilft.
|
||
$stmt = $db->prepare("SELECT id FROM automations WHERE id = ?");
|
||
$stmt->bind_param("i", $id);
|
||
$stmt->execute();
|
||
$exists = $stmt->get_result()->fetch_assoc();
|
||
$stmt->close();
|
||
if (!$exists) {
|
||
throw new RuntimeException("Automatik " . $id . " gibt es nicht mehr.");
|
||
}
|
||
|
||
// changed wird ausdruecklich mitgeschrieben. Es haengt zwar an
|
||
// ON UPDATE, das greift aber nur, wenn sich hier oben wirklich
|
||
// eine Spalte aendert - wer nur eine Bedingung verstellt, laesst
|
||
// Name, Stockwerk und Fenster ja unberuehrt. Der Runner sieht am
|
||
// Zeitstempel, dass er sein Regelwerk neu laden muss.
|
||
$stmt = $db->prepare("UPDATE automations SET name = ?, floor = ?, enabled = ?,
|
||
window_from = ?, window_to = ?, weekdays = ?,
|
||
on_vacation = ?, on_holiday = ?, force_once = ?,
|
||
lockout_secs = ?, changed = NOW()
|
||
WHERE id = ?");
|
||
$stmt->bind_param("ssissiiiiii", $name, $floor, $enabled, $windowFrom, $windowTo,
|
||
$weekdays, $onVacation, $onHoliday, $forceOnce, $lockout, $id);
|
||
$stmt->execute();
|
||
$stmt->close();
|
||
$db->query("DELETE FROM automation_conditions WHERE automation_id = " . $id);
|
||
$db->query("DELETE FROM automation_actions WHERE automation_id = " . $id);
|
||
} else {
|
||
$stmt = $db->prepare("INSERT INTO automations
|
||
(name, floor, enabled, window_from, window_to, weekdays,
|
||
on_vacation, on_holiday, force_once, lockout_secs)
|
||
VALUES (?,?,?,?,?,?,?,?,?,?)");
|
||
$stmt->bind_param("ssissiiiii", $name, $floor, $enabled, $windowFrom, $windowTo,
|
||
$weekdays, $onVacation, $onHoliday, $forceOnce, $lockout);
|
||
$stmt->execute();
|
||
$id = $db->insert_id;
|
||
$stmt->close();
|
||
}
|
||
|
||
// Gruppennummern luecken lassen waere erlaubt, aber die Uebersicht
|
||
// liest sich besser, wenn sie bei 0 anfangen und dicht sind.
|
||
$groupMap = [];
|
||
foreach ($conditions as $c) {
|
||
$g = intval($c["group"] ?? 0);
|
||
if (!isset($groupMap[$g])) {
|
||
$groupMap[$g] = count($groupMap);
|
||
}
|
||
}
|
||
|
||
$stmt = $db->prepare("INSERT INTO automation_conditions
|
||
(automation_id, group_no, position, state_id, operator, value)
|
||
VALUES (?,?,?,?,?,?)");
|
||
$positions = [];
|
||
foreach ($conditions as $c) {
|
||
$group = $groupMap[intval($c["group"] ?? 0)];
|
||
$stateId = intval($c["state_id"] ?? 0);
|
||
$operator = strval($c["operator"] ?? "=");
|
||
$value = strval($c["value"] ?? "");
|
||
if ($stateId <= 0) {
|
||
throw new InvalidArgumentException("Eine Bedingung hat keinen Messwert.");
|
||
}
|
||
if (!in_array($operator, ["=", "!=", ">", "<", ">=", "<=",
|
||
"+", "-", ">=+", ">=-", "<+", "<-"], true)) {
|
||
throw new InvalidArgumentException("Unbekannter Operator: " . $operator);
|
||
}
|
||
$positions[$group] = ($positions[$group] ?? -1) + 1;
|
||
$position = $positions[$group];
|
||
$stmt->bind_param("iiiiss", $id, $group, $position, $stateId, $operator, $value);
|
||
$stmt->execute();
|
||
}
|
||
$stmt->close();
|
||
|
||
$stmtAction = $db->prepare("INSERT INTO automation_actions
|
||
(automation_id, position, command_id) VALUES (?,?,?)");
|
||
$stmtParam = $db->prepare("INSERT INTO automation_action_params
|
||
(action_id, parameter_id, value) VALUES (?,?,?)");
|
||
$position = 0;
|
||
foreach ($actions as $a) {
|
||
$commandId = intval($a["command_id"] ?? 0);
|
||
if ($commandId <= 0) {
|
||
throw new InvalidArgumentException("Eine Aktion hat kein Kommando.");
|
||
}
|
||
$stmtAction->bind_param("iii", $id, $position, $commandId);
|
||
$stmtAction->execute();
|
||
$actionId = $db->insert_id;
|
||
$position++;
|
||
|
||
$params = is_array($a["params"] ?? null) ? $a["params"] : [];
|
||
foreach ($params as $paramId => $value) {
|
||
$paramId = intval($paramId);
|
||
$value = strval($value);
|
||
if ($paramId <= 0) {
|
||
continue;
|
||
}
|
||
$stmtParam->bind_param("iis", $actionId, $paramId, $value);
|
||
$stmtParam->execute();
|
||
}
|
||
}
|
||
$stmtAction->close();
|
||
$stmtParam->close();
|
||
|
||
$db->commit();
|
||
} catch (Throwable $e) {
|
||
$db->rollback();
|
||
throw $e;
|
||
}
|
||
|
||
return $id;
|
||
}
|
||
|
||
/** "16:30" oder "16:30:00" auf das Spaltenformat bringen. */
|
||
function normalizeTime($value, $fallback)
|
||
{
|
||
if (preg_match('/^([01]\d|2[0-3]):([0-5]\d)(:([0-5]\d))?$/', trim(strval($value)), $t)) {
|
||
return $t[1] . ":" . $t[2] . ":" . (isset($t[4]) ? $t[4] : "00");
|
||
}
|
||
return $fallback;
|
||
}
|
||
|
||
function deleteAutomation($id)
|
||
{
|
||
$db = meshDb();
|
||
$stmt = $db->prepare("DELETE FROM automations WHERE id = ?");
|
||
$stmt->bind_param("i", $id);
|
||
$stmt->execute();
|
||
$stmt->close();
|
||
}
|
||
|
||
/** Pausieren oder wieder aufnehmen. Gibt den neuen Zustand zurueck. */
|
||
function toggleAutomation($id)
|
||
{
|
||
$db = meshDb();
|
||
$stmt = $db->prepare("UPDATE automations SET enabled = 1 - enabled WHERE id = ?");
|
||
$stmt->bind_param("i", $id);
|
||
$stmt->execute();
|
||
$stmt->close();
|
||
$stmt = $db->prepare("SELECT enabled FROM automations WHERE id = ?");
|
||
$stmt->bind_param("i", $id);
|
||
$stmt->execute();
|
||
$row = $stmt->get_result()->fetch_assoc();
|
||
$stmt->close();
|
||
return $row ? (bool)$row["enabled"] : false;
|
||
}
|
||
|
||
/**
|
||
* Alle Automatiken einer Etage, angereichert um lesbare Beschreibungen fuer
|
||
* die Uebersichtstabelle.
|
||
*/
|
||
function listAutomations($floor)
|
||
{
|
||
$db = meshDb();
|
||
$stmt = $db->prepare("SELECT id FROM automations WHERE floor = ? ORDER BY name");
|
||
$stmt->bind_param("s", $floor);
|
||
$stmt->execute();
|
||
$res = $stmt->get_result();
|
||
$ids = [];
|
||
while ($row = $res->fetch_assoc()) {
|
||
$ids[] = intval($row["id"]);
|
||
}
|
||
$stmt->close();
|
||
|
||
$namesState = stateNames();
|
||
$namesCommand = commandNames();
|
||
$labelsParam = parameterLabels();
|
||
|
||
$list = [];
|
||
foreach ($ids as $id) {
|
||
$auto = loadAutomation($id);
|
||
if (!$auto) {
|
||
continue;
|
||
}
|
||
$auto["conditionText"] = describeConditions($auto["conditions"], $namesState);
|
||
$auto["actionText"] = describeActions($auto["actions"], $namesCommand, $labelsParam);
|
||
$list[] = $auto;
|
||
}
|
||
return $list;
|
||
}
|
||
|
||
/** id => "Geraet: Messwert" fuer alle Messwerte. */
|
||
function stateNames()
|
||
{
|
||
$db = meshDb();
|
||
$labels = geraeteBeschriftungen();
|
||
$res = $db->query("SELECT s.id, s.state_name, s.unit, s.actor_id, t.type
|
||
FROM actor_states s
|
||
LEFT JOIN state_types t ON s.state_type = t.id");
|
||
$names = [];
|
||
while ($row = $res->fetch_assoc()) {
|
||
$names[intval($row["id"])] = [
|
||
"text" => ($labels[intval($row["actor_id"])] ?? "?") . ": " . $row["state_name"],
|
||
"unit" => $row["unit"],
|
||
"type" => $row["type"] ?: "string",
|
||
];
|
||
}
|
||
return $names;
|
||
}
|
||
|
||
/**
|
||
* id => [gespeicherter Wert => Bezeichnung] fuer alle Kommando-Parameter,
|
||
* die eine Auswahl haben. Gespeichert wird bei Parametern die Zahl, die das
|
||
* Geraet erwartet - in der Uebersicht soll aber "Blink" stehen und nicht "1".
|
||
*/
|
||
function parameterLabels()
|
||
{
|
||
$db = meshDb();
|
||
$res = $db->query("SELECT id, possible_values FROM command_parameters
|
||
WHERE possible_values <> '' AND possible_values <> '[]'");
|
||
$labels = [];
|
||
while ($row = $res->fetch_assoc()) {
|
||
foreach (optionList($row["possible_values"], false) as $option) {
|
||
$labels[intval($row["id"])][$option["value"]] = $option["label"];
|
||
}
|
||
}
|
||
return $labels;
|
||
}
|
||
|
||
/** id => "Geraet: Kommando" fuer alle Kommandos. */
|
||
function commandNames()
|
||
{
|
||
$db = meshDb();
|
||
$labels = geraeteBeschriftungen();
|
||
$res = $db->query("SELECT id, command_name, actor_id FROM actor_commands");
|
||
$names = [];
|
||
while ($row = $res->fetch_assoc()) {
|
||
$names[intval($row["id"])] = ($labels[intval($row["actor_id"])] ?? "?")
|
||
. ": " . $row["command_name"];
|
||
}
|
||
return $names;
|
||
}
|
||
|
||
/**
|
||
* Die Bedingungen als Satz, so geklammert wie sie im Editor gezeichnet sind:
|
||
* "(A und B) oder C". Denselben Text zeigt der Editor unter den Bloecken und
|
||
* die Uebersicht in der Spalte "Ausloeser" - was man liest, ist damit genau
|
||
* das, was der Runner rechnet.
|
||
*/
|
||
function describeConditions($conditions, $names)
|
||
{
|
||
$groups = [];
|
||
foreach ($conditions as $c) {
|
||
$state = $names[$c["state_id"]]
|
||
?? ["text" => "Messwert " . $c["state_id"], "unit" => null, "type" => ""];
|
||
$text = $state["text"] . " " . operatorLabel($c["operator"], $state["type"]) . " " . $c["value"];
|
||
if ($state["unit"]) {
|
||
$text .= " " . $state["unit"];
|
||
}
|
||
$groups[$c["group"]][] = $text;
|
||
}
|
||
if (!$groups) {
|
||
return "";
|
||
}
|
||
$parts = [];
|
||
foreach ($groups as $group) {
|
||
$text = implode(" und ", $group);
|
||
if (count($groups) > 1 && count($group) > 1) {
|
||
$text = "(" . $text . ")";
|
||
}
|
||
$parts[] = $text;
|
||
}
|
||
return implode(" oder ", $parts);
|
||
}
|
||
|
||
function describeActions($actions, $names, $labels = [])
|
||
{
|
||
$parts = [];
|
||
foreach ($actions as $a) {
|
||
$text = $names[$a["command_id"]] ?? ("Kommando " . $a["command_id"]);
|
||
$values = [];
|
||
foreach ($a["params"] as $parameterId => $wert) {
|
||
// Wo es eine Auswahl gibt, die Bezeichnung nennen: "Blink" sagt
|
||
// mehr als die 1, die das Geraet tatsaechlich bekommt.
|
||
$values[] = $labels[intval($parameterId)][$wert] ?? $wert;
|
||
}
|
||
if ($values) {
|
||
$text .= " (" . implode(", ", $values) . ")";
|
||
}
|
||
$parts[] = $text;
|
||
}
|
||
return implode(", ", $parts);
|
||
}
|