Bisher konnte man Automatiken nur anlegen - ausgefuehrt hat sie niemand. restricted/autoActions/autoaction_runner.py holt das nach. Dauerlaeufer statt Cronjob, aus zwei Gruenden: Schwellwert-Ausloeser sollen greifen, wenn die MQTT-Nachricht hereinkommt, und actor_states.current_value wird sonst von niemandem fortgeschrieben - beim Discovery einmal gesetzt und danach nie wieder. Ein zustandsloser Lauf haette gar nichts, womit er vergleichen koennte. Der Runner pflegt den Wert nebenbei mit, wovon auch der Editor profitiert: er zeigt neben jedem Messwert den aktuellen Stand. Ausgeloest wird nur auf der steigenden Flanke (automations.cond_met), sonst wuerde "Temperatur ueber 22 Grad" bei jedem Takt erneut feuern. Aus demselben Grund heissen Zeit-Ausloeser jetzt "ab 16:30" statt "gleich 16:30": ein Gleichheitsvergleich waere nur in einer einzigen Minute wahr, und ein Ausfall in genau dieser Minute kostet den ganzen Tag. Dieselbe Ueberlegung steht hinter den breiten Zeitfenstern in auto_watering.py. Der Weg zum Geraet haengt an der URL des Aktors: mqtt:// abonniert und publiziert, http:// pollt und haengt Parameter an, io:// spricht mit der Tahoma-Box, Logic rechnet Uhrzeit, Datum und Sonnenzeiten. Alle vier stehen in transports.py; eine fuenfte Geraeteart ist eine weitere Klasse mit passt(), zustaende_lesen() und senden(). fetch_calendar.py fuellt calendar_days aus openholidaysapi.org, damit "in den Ferien" und "an Feiertagen" eine Grundlage haben. Einmal jaehrlich per Cron. Die Uebersicht blendet auf schmalen Schirmen Spalten aus, statt sie wegzuschieben - auf dem Handy waren die Knoepfe zum Pausieren und Loeschen sonst nur per Seitwaertsscrollen erreichbar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
604 lines
21 KiB
PHP
604 lines
21 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");
|
|
|
|
/** 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 gibt es bewusst kein "gleich", sondern "ab" und "vor".
|
|
* "Uhrzeit = 16:30" waere nur in genau der einen Minute wahr - faellt der
|
|
* Runner in dieser Minute aus, ist die Automatik fuer den Tag verloren. "ab
|
|
* 16:30" bleibt bis Mitternacht wahr; ausgeloest wird trotzdem nur einmal,
|
|
* weil nur die steigende Flanke zaehlt. Dieselbe Ueberlegung steht hinter den
|
|
* breiten Zeitfenstern in auto_watering.py.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/** 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 "ab"/"vor"
|
|
* verstaendlicher als ">="/"<", und bei Sonnenauf-/-untergang steht der
|
|
* Versatz dahinter: "ab + 00:30" heisst eine halbe Stunde danach.
|
|
*/
|
|
function operatorLabel($op, $type = "")
|
|
{
|
|
if ($type === "time" || $type === "date" || $type === "datetime") {
|
|
if ($op === ">=") return "ab";
|
|
if ($op === "<") return "vor";
|
|
}
|
|
if ($type === "deltatime") {
|
|
if ($op === "+") return "ab +";
|
|
if ($op === "-") return "ab -";
|
|
}
|
|
return $op === "!=" ? "≠" : $op;
|
|
}
|
|
|
|
/**
|
|
* 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 = [];
|
|
|
|
$res = $db->query("SELECT id, name, type, url FROM actors ORDER BY name");
|
|
while ($row = $res->fetch_assoc()) {
|
|
$devices[intval($row["id"])] = [
|
|
"id" => intval($row["id"]),
|
|
"name" => $row["name"],
|
|
"type" => $row["type"],
|
|
"url" => $row["url"],
|
|
"states" => [],
|
|
"commands" => [],
|
|
];
|
|
}
|
|
|
|
$res = $db->query("SELECT s.id, s.actor_id, s.state_name, s.unit, s.current_value,
|
|
s.url, 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 = json_decode($row["possible_values"], true);
|
|
if (!is_array($options)) {
|
|
$options = [];
|
|
}
|
|
$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" => array_values($options),
|
|
"unit" => $row["unit"],
|
|
"value" => $row["current_value"],
|
|
"url" => $row["url"],
|
|
];
|
|
}
|
|
|
|
$res = $db->query("SELECT id, actor_id, command_name 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"],
|
|
"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 = json_decode($row["possible_values"], true);
|
|
if (!is_array($options)) {
|
|
$options = [];
|
|
}
|
|
$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" => array_values($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" => "",
|
|
"floor" => $floor,
|
|
"enabled" => true,
|
|
"window_from" => "00:00",
|
|
"window_to" => "23:59",
|
|
"weekdays" => 127,
|
|
"on_vacation" => true,
|
|
"on_holiday" => true,
|
|
"force_once" => false,
|
|
"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"],
|
|
"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, ["", "UG", "EG", "OG"], true)) {
|
|
$floor = "";
|
|
}
|
|
$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;
|
|
|
|
$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.");
|
|
}
|
|
|
|
$stmt = $db->prepare("UPDATE automations SET name = ?, floor = ?, enabled = ?,
|
|
window_from = ?, window_to = ?, weekdays = ?,
|
|
on_vacation = ?, on_holiday = ?, force_once = ?
|
|
WHERE id = ?");
|
|
$stmt->bind_param("ssissiiiii", $name, $floor, $enabled, $windowFrom, $windowTo,
|
|
$weekdays, $onVacation, $onHoliday, $forceOnce, $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)
|
|
VALUES (?,?,?,?,?,?,?,?,?)");
|
|
$stmt->bind_param("ssissiiii", $name, $floor, $enabled, $windowFrom, $windowTo,
|
|
$weekdays, $onVacation, $onHoliday, $forceOnce);
|
|
$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();
|
|
|
|
$list = [];
|
|
foreach ($ids as $id) {
|
|
$auto = loadAutomation($id);
|
|
if (!$auto) {
|
|
continue;
|
|
}
|
|
$auto["conditionText"] = describeConditions($auto["conditions"], $namesState);
|
|
$auto["actionText"] = describeActions($auto["actions"], $namesCommand);
|
|
$list[] = $auto;
|
|
}
|
|
return $list;
|
|
}
|
|
|
|
/** id => "Geraet: Messwert" fuer alle Messwerte. */
|
|
function stateNames()
|
|
{
|
|
$db = meshDb();
|
|
$res = $db->query("SELECT s.id, s.state_name, s.unit, a.name AS actor_name, t.type
|
|
FROM actor_states s
|
|
JOIN actors a ON a.id = s.actor_id
|
|
LEFT JOIN state_types t ON s.state_type = t.id");
|
|
$names = [];
|
|
while ($row = $res->fetch_assoc()) {
|
|
$names[intval($row["id"])] = [
|
|
"text" => $row["actor_name"] . ": " . $row["state_name"],
|
|
"unit" => $row["unit"],
|
|
"type" => $row["type"] ?: "string",
|
|
];
|
|
}
|
|
return $names;
|
|
}
|
|
|
|
/** id => "Geraet: Kommando" fuer alle Kommandos. */
|
|
function commandNames()
|
|
{
|
|
$db = meshDb();
|
|
$res = $db->query("SELECT c.id, c.command_name, a.name AS actor_name
|
|
FROM actor_commands c JOIN actors a ON a.id = c.actor_id");
|
|
$names = [];
|
|
while ($row = $res->fetch_assoc()) {
|
|
$names[intval($row["id"])] = $row["actor_name"] . ": " . $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)
|
|
{
|
|
$parts = [];
|
|
foreach ($actions as $a) {
|
|
$text = $names[$a["command_id"]] ?? ("Kommando " . $a["command_id"]);
|
|
$values = array_values($a["params"]);
|
|
if ($values) {
|
|
$text .= " (" . implode(", ", $values) . ")";
|
|
}
|
|
$parts[] = $text;
|
|
}
|
|
return implode(", ", $parts);
|
|
}
|