Automatiken als Ausloeser: Editor, Uebersicht, Loeschen
Gegenstueck zur Runner-Seite. Der Editor selbst braucht nichts: das
gerechnete Geraet "Automatiken" fuehrt jede Automatik als Messwert, und
der Geraetewaehler listet ohnehin Geraete und deren Messwerte. Neu sind
nur die Operatoren des Datentyps `elapsed` ("+", "ab +", "vor +") und
sein Eingabefeld.
Drei Stellen wissen trotzdem davon:
pruefeKreis() lehnt beim Speichern ab, was sich mittelbar selbst
ausloesen wuerde. Im Betrieb waere ein Kreis kaum zu bemerken - die
Sperrzeit begrenzt ihn auf eine Ausloesung je lockout_secs, und im
Protokoll sieht das aus wie eine Automatik, die halt oft laeuft.
deleteAutomation() fragt vorher. fk_cond_state steht auf ON DELETE
CASCADE, mit dem Messwert verschwaende also still die Bedingung des
Nachfolgers: aus "zehn Minuten nach dem Wecker UND es ist hell" wuerde
ein blosses "es ist hell", und der Rollladen fuehre ab morgen jeden Tag
bei Sonnenaufgang hoch. Bei der letzten Bedingung faengt der Runner das
ab, bei einer von zweien niemand. Zur Wahl stehen Abhaengen (Bedingung
raus, Nachfolger pausiert) und Mitloeschen; automatisch mitzuloeschen
waere die falsche Vorgabe.
Die Uebersicht rueckt Nachfolger unter ihren Ausloeser ein und zeigt sie
eingeklappt. Das ist nur die Darstellung - jeder Nachfolger bleibt eine
vollwertige Automatik mit eigener Pause, eigenen Rahmenbedingungen und
eigener Zeile im Protokoll. Sobald "gruppiert" auch "gehoert dazu"
hiesse, waere zu klaeren, wem die Pause gehoert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+202
-1
@@ -54,6 +54,11 @@ function meshDb()
|
||||
* 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.
|
||||
*
|
||||
* Dieselbe Schreibweise traegt die Verkettung. Das gerechnete Geraet
|
||||
* "Automatiken" fuehrt jede Automatik als Messwert, ihr Wert ist der
|
||||
* Zeitpunkt der letzten Ausloesung (Datentyp `elapsed`) - "Wecker Magdalena
|
||||
* + 00:10" heisst zehn Minuten danach. Ein Minus gibt es dort nicht.
|
||||
*/
|
||||
function operatorsForType($type)
|
||||
{
|
||||
@@ -67,6 +72,10 @@ function operatorsForType($type)
|
||||
return ["=", ">=", "<"];
|
||||
case "deltatime": // Sonnenauf-/-untergang: Wert ist ein Versatz
|
||||
return ["+", "-", ">=+", ">=-", "<+", "<-"];
|
||||
case "elapsed": // andere Automatik: Wert ist ein Versatz danach
|
||||
// Nur die drei Formen "danach". Ein Minus gibt es nicht: vor dem
|
||||
// Auslöser kann nichts liegen, das erst der Auslöser anstößt.
|
||||
return ["+", ">=+", "<+"];
|
||||
case "bool":
|
||||
return ["="]; // JA/NEIN steht im Wertfeld, nicht im Operator
|
||||
default:
|
||||
@@ -157,6 +166,7 @@ function inputForType($type, $hasOptions)
|
||||
return "number";
|
||||
case "time":
|
||||
case "deltatime":
|
||||
case "elapsed":
|
||||
return "time";
|
||||
case "date":
|
||||
return "date";
|
||||
@@ -192,6 +202,14 @@ function operatorLabel($op, $type = "")
|
||||
return $beschriftung[$op];
|
||||
}
|
||||
}
|
||||
if ($type === "elapsed") {
|
||||
// Dieselben Wörter wie beim Sonnenstand, damit ein Versatz überall
|
||||
// gleich gelesen wird: "Wecker + 00:10" sind zehn Minuten danach.
|
||||
$beschriftung = ["+" => "+", ">=+" => "ab +", "<+" => "vor +"];
|
||||
if (isset($beschriftung[$op])) {
|
||||
return $beschriftung[$op];
|
||||
}
|
||||
}
|
||||
return $op === "!=" ? "≠" : $op;
|
||||
}
|
||||
|
||||
@@ -785,6 +803,7 @@ function saveAutomation($data)
|
||||
if (!$actions) {
|
||||
throw new InvalidArgumentException("Ohne Aktion haette die Automatik nichts zu tun.");
|
||||
}
|
||||
pruefeKreis($data["id"] ?? 0, $conditions);
|
||||
|
||||
$id = intval($data["id"] ?? 0);
|
||||
$enabled = !empty($data["enabled"]) ? 1 : 0;
|
||||
@@ -925,9 +944,186 @@ function normalizeTime($value, $fallback)
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
function deleteAutomation($id)
|
||||
/**
|
||||
* Welcher Messwert des gerechneten Geraets "Automatiken" auf welche Automatik
|
||||
* zeigt: state_id => automation_id.
|
||||
*
|
||||
* Die Zeilen legt der Runner an und fuehrt sie nach (ausloeser_nachfuehren()),
|
||||
* hier werden sie nur gelesen. Gefuehrt wird nach der Kennung in der URL
|
||||
* ("auto:15") und nicht nach dem Namen - wer umbenennt, soll die abhaengigen
|
||||
* Bedingungen behalten.
|
||||
*/
|
||||
function ausloeserStates()
|
||||
{
|
||||
static $karte = null;
|
||||
if ($karte !== null) {
|
||||
return $karte;
|
||||
}
|
||||
$karte = [];
|
||||
$db = meshDb();
|
||||
$res = $db->query("SELECT s.id, s.url FROM actor_states s
|
||||
JOIN actors a ON a.id = s.actor_id
|
||||
WHERE a.url = 'Automatik'");
|
||||
while ($row = $res->fetch_assoc()) {
|
||||
if (preg_match('/^auto:(\d+)$/', strval($row["url"]), $t)) {
|
||||
$karte[intval($row["id"])] = intval($t[1]);
|
||||
}
|
||||
}
|
||||
return $karte;
|
||||
}
|
||||
|
||||
/** automation_id => Liste der Automatiken, die sie ausloesen. */
|
||||
function automationTriggers()
|
||||
{
|
||||
$states = ausloeserStates();
|
||||
if (!$states) {
|
||||
return [];
|
||||
}
|
||||
$db = meshDb();
|
||||
$res = $db->query("SELECT automation_id, state_id FROM automation_conditions");
|
||||
$karte = [];
|
||||
while ($row = $res->fetch_assoc()) {
|
||||
$wer = intval($row["automation_id"]);
|
||||
$davor = $states[intval($row["state_id"])] ?? null;
|
||||
if ($davor !== null && $davor !== $wer) {
|
||||
$karte[$wer][$davor] = true;
|
||||
}
|
||||
}
|
||||
return array_map("array_keys", $karte);
|
||||
}
|
||||
|
||||
/** Die Automatiken, die diese hier als Ausloeser benutzen. */
|
||||
function automationFollowers($id)
|
||||
{
|
||||
$id = intval($id);
|
||||
$liste = [];
|
||||
foreach (automationTriggers() as $wer => $davor) {
|
||||
if (in_array($id, $davor, true)) {
|
||||
$liste[] = $wer;
|
||||
}
|
||||
}
|
||||
if (!$liste) {
|
||||
return [];
|
||||
}
|
||||
$db = meshDb();
|
||||
$platz = implode(",", array_fill(0, count($liste), "?"));
|
||||
$stmt = $db->prepare("SELECT id, name, floor, enabled FROM automations
|
||||
WHERE id IN ($platz) ORDER BY name");
|
||||
$stmt->bind_param(str_repeat("i", count($liste)), ...$liste);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
$nachfolger = [];
|
||||
while ($row = $res->fetch_assoc()) {
|
||||
$nachfolger[] = [
|
||||
"id" => intval($row["id"]),
|
||||
"name" => $row["name"],
|
||||
"floor" => $row["floor"],
|
||||
"enabled" => (bool)$row["enabled"],
|
||||
];
|
||||
}
|
||||
$stmt->close();
|
||||
return $nachfolger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wuerde diese Automatik sich mittelbar selbst ausloesen?
|
||||
*
|
||||
* Ein Kreis (A loest B loest A) ist kein Fehler, den man im Betrieb bemerkt:
|
||||
* die Sperrzeit begrenzt ihn auf eine Ausloesung je lockout_secs, und im
|
||||
* Protokoll sieht das aus wie eine Automatik, die halt oft laeuft. Deshalb
|
||||
* wird er hier abgelehnt und nicht dort geheilt. Der Runner meldet einen
|
||||
* Kreis trotzdem - falls doch einer an der Datenbank vorbei entsteht.
|
||||
*/
|
||||
function pruefeKreis($id, $conditions)
|
||||
{
|
||||
$id = intval($id);
|
||||
if ($id <= 0) {
|
||||
return; // eine neue Automatik kennt noch niemand
|
||||
}
|
||||
$states = ausloeserStates();
|
||||
$davor = [];
|
||||
foreach ($conditions as $c) {
|
||||
$wer = $states[intval($c["state_id"] ?? 0)] ?? null;
|
||||
if ($wer !== null) {
|
||||
$davor[] = $wer;
|
||||
}
|
||||
}
|
||||
if (!$davor) {
|
||||
return;
|
||||
}
|
||||
if (in_array($id, $davor, true)) {
|
||||
throw new InvalidArgumentException(
|
||||
"Eine Automatik kann sich nicht selbst auslösen.");
|
||||
}
|
||||
$kanten = automationTriggers();
|
||||
$kanten[$id] = $davor;
|
||||
$offen = $davor;
|
||||
$gesehen = [];
|
||||
while ($offen) {
|
||||
$aktuell = array_pop($offen);
|
||||
if ($aktuell === $id) {
|
||||
throw new InvalidArgumentException(
|
||||
"Das ergäbe einen Kreis: die Automatik würde sich mittelbar "
|
||||
. "selbst auslösen.");
|
||||
}
|
||||
if (isset($gesehen[$aktuell])) {
|
||||
continue;
|
||||
}
|
||||
$gesehen[$aktuell] = true;
|
||||
foreach ($kanten[$aktuell] ?? [] as $weiter) {
|
||||
$offen[] = $weiter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loeschen - und was mit den Automatiken geschieht, die davon abhaengen.
|
||||
*
|
||||
* Nichts zu tun waere die schlechteste Wahl: `fk_cond_state` steht auf
|
||||
* ON DELETE CASCADE, mit dem Messwert verschwaende also still die Bedingung
|
||||
* des Nachfolgers. Bei der LETZTEN Bedingung faengt der Runner das ab (eine
|
||||
* Automatik ohne Bedingung loest nie aus), bei einer von zweien niemand: aus
|
||||
* "zehn Minuten nach dem Wecker UND es ist hell" wuerde ein blosses "es ist
|
||||
* hell", und der Rollladen fuehre ab morgen jeden Tag bei Sonnenaufgang hoch.
|
||||
*
|
||||
* abhaengen die Bedingung wird entfernt und der Nachfolger pausiert.
|
||||
* Eine pausierte Automatik mit sichtbarer Luecke ist besser
|
||||
* als eine stille Regelaenderung.
|
||||
* mitloeschen der Nachfolger geht mit, und dessen Nachfolger auch.
|
||||
*/
|
||||
function deleteAutomation($id, $modus = "abhaengen", &$gesehen = [])
|
||||
{
|
||||
$id = intval($id);
|
||||
if (isset($gesehen[$id])) {
|
||||
return; // Schutz gegen einen Kreis, den es nicht geben sollte
|
||||
}
|
||||
$gesehen[$id] = true;
|
||||
|
||||
$db = meshDb();
|
||||
$nachfolger = automationFollowers($id);
|
||||
$eigeneStates = array_keys(ausloeserStates(), $id, true);
|
||||
|
||||
if ($nachfolger && $modus === "mitloeschen") {
|
||||
foreach ($nachfolger as $n) {
|
||||
deleteAutomation($n["id"], "mitloeschen", $gesehen);
|
||||
}
|
||||
} elseif ($nachfolger && $eigeneStates) {
|
||||
$platz = implode(",", array_fill(0, count($eigeneStates), "?"));
|
||||
foreach ($nachfolger as $n) {
|
||||
$args = array_merge([$n["id"]], $eigeneStates);
|
||||
$stmt = $db->prepare("DELETE FROM automation_conditions
|
||||
WHERE automation_id = ? AND state_id IN ($platz)");
|
||||
$stmt->bind_param(str_repeat("i", count($args)), ...$args);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
$stmt = $db->prepare("UPDATE automations SET enabled = 0, changed = NOW()
|
||||
WHERE id = ?");
|
||||
$stmt->bind_param("i", $n["id"]);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("DELETE FROM automations WHERE id = ?");
|
||||
$stmt->bind_param("i", $id);
|
||||
$stmt->execute();
|
||||
@@ -970,6 +1166,7 @@ function listAutomations($floor)
|
||||
$namesState = stateNames();
|
||||
$namesCommand = commandNames();
|
||||
$labelsParam = parameterLabels();
|
||||
$kanten = automationTriggers();
|
||||
|
||||
$list = [];
|
||||
foreach ($ids as $id) {
|
||||
@@ -979,6 +1176,10 @@ function listAutomations($floor)
|
||||
}
|
||||
$auto["conditionText"] = describeConditions($auto["conditions"], $namesState);
|
||||
$auto["actionText"] = describeActions($auto["actions"], $namesCommand, $labelsParam);
|
||||
// Woran diese Automatik haengt - die Uebersicht rueckt Nachfolger
|
||||
// unter ihren Ausloeser ein, statt sie alphabetisch dazwischen zu
|
||||
// streuen.
|
||||
$auto["triggers"] = $kanten[$id] ?? [];
|
||||
$list[] = $auto;
|
||||
}
|
||||
return $list;
|
||||
|
||||
Reference in New Issue
Block a user