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>
245 lines
10 KiB
PHP
245 lines
10 KiB
PHP
<?php
|
||
/**
|
||
* Endpunkt fuer die Automatiken: Editor, Uebersicht, Speichern, Loeschen.
|
||
*
|
||
* Alles laeuft ueber ?action=... , damit Formular und Uebersicht dieselbe
|
||
* Modell-Schicht (restricted/automations.php) benutzen und nicht zwei
|
||
* Wahrheiten ueber denselben Datensatz entstehen.
|
||
*
|
||
* GET ?action=editor&id=N Editor fuer eine vorhandene Automatik
|
||
* GET ?action=editor&floor=OG Editor fuer eine neue Automatik
|
||
* GET ?action=list&floor=OG Tabelle fuer die Karte "Automatismen"
|
||
* POST ?action=save JSON-Rumpf, legt an oder ueberschreibt
|
||
* POST ?action=delete {"id": N}
|
||
* POST ?action=toggle {"id": N} - pausieren / fortsetzen
|
||
*/
|
||
|
||
require_once("../helper.php");
|
||
require_once("../restricted/automations.php");
|
||
|
||
if (!checkLogin()) {
|
||
http_response_code(403);
|
||
exit;
|
||
}
|
||
|
||
function jsonAntwort($data, $code = 200)
|
||
{
|
||
http_response_code($code);
|
||
header("Content-Type: application/json; charset=utf-8");
|
||
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
||
exit;
|
||
}
|
||
|
||
$action = $_GET["action"] ?? "editor";
|
||
$floor = in_array($_GET["floor"] ?? "", ["UG", "EG", "OG"], true) ? $_GET["floor"] : "";
|
||
|
||
// --- Schreibende Aufrufe -------------------------------------------------
|
||
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||
$body = json_decode(file_get_contents("php://input"), true);
|
||
if (!is_array($body)) {
|
||
jsonAntwort(["error" => "Konnte die Anfrage nicht lesen."], 400);
|
||
}
|
||
try {
|
||
switch ($action) {
|
||
case "save":
|
||
$id = saveAutomation($body);
|
||
jsonAntwort(["id" => $id]);
|
||
case "delete":
|
||
deleteAutomation(intval($body["id"] ?? 0));
|
||
jsonAntwort(["ok" => true]);
|
||
case "toggle":
|
||
jsonAntwort(["enabled" => toggleAutomation(intval($body["id"] ?? 0))]);
|
||
default:
|
||
jsonAntwort(["error" => "Unbekannte Aktion."], 400);
|
||
}
|
||
} catch (InvalidArgumentException $e) {
|
||
jsonAntwort(["error" => $e->getMessage()], 400);
|
||
} catch (Throwable $e) {
|
||
jsonAntwort(["error" => "Speichern fehlgeschlagen: " . $e->getMessage()], 500);
|
||
}
|
||
}
|
||
|
||
// --- Uebersichtstabelle --------------------------------------------------
|
||
if ($action === "list") {
|
||
$tage = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
||
$list = listAutomations($floor);
|
||
if (!$list) {
|
||
echo "<p class='text-body-secondary mb-2'>Für diese Etage ist noch nichts eingerichtet.</p>";
|
||
} else {
|
||
echo "<div class='table-responsive'><table class='table table-hover align-middle'>";
|
||
// Je schmaler der Schirm, desto weniger Spalten: auf dem Handy bleiben
|
||
// Name, Ausloeser und die Knoepfe. Sonst muss man seitwaerts scrollen,
|
||
// um eine Automatik ueberhaupt pausieren zu koennen.
|
||
echo "<thead><tr><th>Name</th><th>Auslöser</th>"
|
||
. "<th class='d-none d-sm-table-cell'>Aktion</th>"
|
||
. "<th class='d-none d-md-table-cell'>Wann</th>"
|
||
. "<th class='d-none d-md-table-cell'>Zuletzt</th><th></th></tr></thead><tbody>";
|
||
foreach ($list as $a) {
|
||
// Nur die aktiven Tage nennen. Sieben Haekchen untereinander
|
||
// haben die Zeile frueher unnoetig hoch gemacht.
|
||
$aktiveTage = [];
|
||
for ($i = 0; $i < 7; $i++) {
|
||
if ($a["weekdays"] & (1 << $i)) {
|
||
$aktiveTage[] = $tage[$i];
|
||
}
|
||
}
|
||
$wann = count($aktiveTage) === 7 ? "täglich" : implode(" ", $aktiveTage);
|
||
if ($a["window_from"] !== "00:00" || $a["window_to"] !== "23:59") {
|
||
$wann .= ", " . $a["window_from"] . "–" . $a["window_to"];
|
||
}
|
||
if (!$a["on_holiday"]) {
|
||
$wann .= ", nicht an Feiertagen";
|
||
}
|
||
if (!$a["on_vacation"]) {
|
||
$wann .= ", nicht in den Ferien";
|
||
}
|
||
|
||
$zuletzt = $a["last_run"] ? date("d.m. H:i", strtotime($a["last_run"])) : "–";
|
||
$klasse = $a["enabled"] ? "" : " class='opacity-50'";
|
||
$pause = $a["enabled"] ? "bi-pause-fill" : "bi-play-fill";
|
||
$titel = $a["enabled"] ? "Pausieren" : "Fortsetzen";
|
||
$id = intval($a["id"]);
|
||
|
||
echo "<tr" . $klasse . ">";
|
||
echo "<td>" . htmlspecialchars($a["name"]) . "</td>";
|
||
echo "<td>" . htmlspecialchars($a["conditionText"]) . "</td>";
|
||
echo "<td class='d-none d-sm-table-cell'>" . htmlspecialchars($a["actionText"]) . "</td>";
|
||
echo "<td class='text-nowrap d-none d-md-table-cell'>" . htmlspecialchars($wann) . "</td>";
|
||
echo "<td class='text-nowrap d-none d-md-table-cell'>" . $zuletzt . "</td>";
|
||
echo "<td class='text-nowrap'>"
|
||
. "<button type='button' class='btn btn-info btn-sm' title='" . $titel . "'"
|
||
. " onclick='toggleAutomation(" . $id . ")'><i class='bi " . $pause . "'></i></button> "
|
||
. "<button type='button' class='btn btn-warning btn-sm' title='Bearbeiten'"
|
||
. " onclick=\"openAutoActionModal('?action=editor&id=" . $id . "')\"><i class='bi bi-pencil-square'></i></button> "
|
||
. "<button type='button' class='btn btn-danger btn-sm' title='Löschen'"
|
||
. " onclick='deleteAutomation(" . $id . ", \"" . htmlspecialchars($a["name"], ENT_QUOTES) . "\")'><i class='bi bi-trash3'></i></button>"
|
||
. "</td></tr>";
|
||
}
|
||
echo "</tbody></table></div>";
|
||
}
|
||
exit;
|
||
}
|
||
|
||
// --- Editor --------------------------------------------------------------
|
||
$id = intval($_GET["id"] ?? 0);
|
||
$auto = $id > 0 ? loadAutomation($id) : null;
|
||
if (!$auto) {
|
||
$auto = emptyAutomation($floor);
|
||
}
|
||
|
||
// Geraetekatalog und Datensatz reisen im selben Dokument mit. Frueher holte
|
||
// der Editor die Geraeteliste und je Zeile noch einmal deren Details nach -
|
||
// zwei zusaetzliche Anfragen pro Bedingung, und die Dropdowns fuellten sich
|
||
// asynchron, weshalb ein gespeicherter Wert nicht zuverlaessig ankam.
|
||
$nutzdaten = json_encode([
|
||
"automation" => $auto,
|
||
"devices" => deviceCatalog(),
|
||
], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
|
||
|
||
?>
|
||
<script type="application/json" id="autoActionData"><?= $nutzdaten ?></script>
|
||
<form id="autoActionForm" class="p-2">
|
||
<div class="row g-2 mb-2">
|
||
<div class="col-8">
|
||
<div class="form-floating">
|
||
<input type="text" class="form-control" id="autoName" placeholder="Name" maxlength="100">
|
||
<label for="autoName">Name der Automatik</label>
|
||
</div>
|
||
</div>
|
||
<div class="col-4">
|
||
<div class="form-floating">
|
||
<select class="form-select" id="autoFloor">
|
||
<option value="">–</option>
|
||
<option value="UG">UG</option>
|
||
<option value="EG">EG</option>
|
||
<option value="OG">OG</option>
|
||
</select>
|
||
<label for="autoFloor">Etage</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="accordion" id="actionAccordion">
|
||
<div class="accordion-item">
|
||
<h2 class="accordion-header">
|
||
<button class="accordion-button" type="button" data-bs-toggle="collapse"
|
||
data-bs-target="#collapseOne" aria-expanded="true">
|
||
<i class="bi bi-lightning-fill"></i> Auslöser
|
||
</button>
|
||
</h2>
|
||
<div id="collapseOne" class="accordion-collapse collapse show" data-bs-parent="#actionAccordion">
|
||
<div class="accordion-body">
|
||
<!-- Ein Block ist eine UND-Gruppe, zwischen den Bloecken steht ODER.
|
||
Die Klammerung ist damit gezeichnet und nicht bloss vereinbart. -->
|
||
<div id="condBlocks"></div>
|
||
<button class="btn btn-outline-secondary btn-sm mt-2" type="button" id="btnAddGroup">
|
||
<i class="bi bi-plus-lg"></i> Alternative (oder)
|
||
</button>
|
||
<div class="alert alert-secondary mt-3 mb-0 py-2 small" id="condSummary"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="accordion-item">
|
||
<h2 class="accordion-header">
|
||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||
data-bs-target="#collapseTwo" aria-expanded="false">
|
||
<i class="bi bi-calendar-check-fill"></i> Bedingungen
|
||
</button>
|
||
</h2>
|
||
<div id="collapseTwo" class="accordion-collapse collapse" data-bs-parent="#actionAccordion">
|
||
<div class="accordion-body">
|
||
<div class="input-group mb-3">
|
||
<span class="input-group-text">Aktiver Zeitraum</span>
|
||
<div class="form-floating">
|
||
<input type="time" class="form-control" id="tSpanFrom" value="00:00">
|
||
<label for="tSpanFrom">Von</label>
|
||
</div>
|
||
<div class="form-floating">
|
||
<input type="time" class="form-control" id="tSpanTo" value="23:59">
|
||
<label for="tSpanTo">Bis</label>
|
||
</div>
|
||
</div>
|
||
Aktive Wochentage:
|
||
<div class="input-group mb-3" id="weekdayGroup"></div>
|
||
<div class="input-group mb-3">
|
||
<div class="input-group-text">
|
||
<input class="form-check-input mt-0 me-2" type="checkbox" id="actFerien">
|
||
<label class="form-check-label" for="actFerien">In den Ferien ausführen</label>
|
||
</div>
|
||
<div class="input-group-text">
|
||
<input class="form-check-input mt-0 me-2" type="checkbox" id="actFeier">
|
||
<label class="form-check-label" for="actFeier">An Feiertagen ausführen</label>
|
||
</div>
|
||
</div>
|
||
<div class="input-group mb-0">
|
||
<div class="input-group-text">
|
||
<input class="form-check-input mt-0 me-2" type="checkbox" id="runOnce">
|
||
<label class="form-check-label" for="runOnce">
|
||
Am Ende des Zeitraums auf jeden Fall ausführen, falls der Auslöser nie zutraf
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="accordion-item">
|
||
<h2 class="accordion-header">
|
||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||
data-bs-target="#collapseThree" aria-expanded="false">
|
||
<i class="bi bi-play-fill"></i> Aktionen
|
||
</button>
|
||
</h2>
|
||
<div id="collapseThree" class="accordion-collapse collapse" data-bs-parent="#actionAccordion">
|
||
<div class="accordion-body">
|
||
<div id="actionList"></div>
|
||
<button class="btn btn-outline-success btn-sm mt-2" type="button" id="btnAddActor">
|
||
<i class="bi bi-plus-lg"></i> Aktion
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</form>
|