AutoAction-Runner: Automatiken ausfuehren

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>
This commit is contained in:
2026-08-30 19:21:07 +02:00
co-authored by Claude Opus 5
parent eee3a89b4e
commit 5f643b9844
8 changed files with 1353 additions and 21 deletions
+44 -7
View File
@@ -33,6 +33,13 @@ function meshDb()
* Zeichen macht erst die Anzeige daraus. Frueher schickte der Editor sein
* innerHTML ("≠", "&gt;") 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)
{
@@ -40,6 +47,10 @@ function operatorsForType($type)
case "integer":
case "float":
return ["=", "!=", ">", "<"];
case "time":
case "date":
case "datetime":
return [">=", "<"];
case "deltatime": // Sonnenauf-/-untergang: Wert ist ein Versatz
return ["+", "-"];
case "bool":
@@ -49,6 +60,16 @@ function operatorsForType($type)
}
}
/** 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)
{
@@ -74,9 +95,21 @@ function inputForType($type, $hasOptions)
}
}
/** Anzeigeform eines Operators. */
function operatorLabel($op)
/**
* 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;
}
@@ -124,7 +157,7 @@ function deviceCatalog()
"name" => $row["state_name"],
"type" => $type,
"input" => inputForType($type, count($options) > 0),
"operators" => operatorsForType($type),
"operators" => operatorChoices($type),
"options" => array_values($options),
"unit" => $row["unit"],
"value" => $row["current_value"],
@@ -495,13 +528,16 @@ function listAutomations($floor)
function stateNames()
{
$db = meshDb();
$res = $db->query("SELECT s.id, s.state_name, s.unit, a.name AS actor_name
FROM actor_states s JOIN actors a ON a.id = s.actor_id");
$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;
@@ -530,8 +566,9 @@ function describeConditions($conditions, $names)
{
$groups = [];
foreach ($conditions as $c) {
$state = $names[$c["state_id"]] ?? ["text" => "Messwert " . $c["state_id"], "unit" => null];
$text = $state["text"] . " " . operatorLabel($c["operator"]) . " " . $c["value"];
$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"];
}