Files
Smart-Dashboard/ajax/AutoAction.php
T
adminandClaude Opus 5 07bca5104e Einstellungsseite: Reiter, Kachelwerte und Geraeteliste
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>
2026-09-08 22:45:24 +02:00

262 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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"] ?? "", $floors, 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";
}
if ($a["lockout_secs"] > 0) {
$wann .= ", frühestens alle "
. ($a["lockout_secs"] >= 60
? intdiv($a["lockout_secs"], 60) . " Min."
: $a["lockout_secs"] . " Sek.");
}
$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(),
"lockouts" => lockoutChoices(),
// Nur fuer die Reihenfolge der Raum-Gruppen: rooms.php gibt sie vor, und
// Etage fuer Etage liest sich besser als alphabetisch.
"rooms" => raumListe(),
], 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">
<!-- Ohne Etage waere die Automatik in keinem der drei Reiter zu
sehen und nur noch in der Datenbank auffindbar. Reihenfolge wie
die Reiter, damit der Rueckfall auf den ersten Eintrag dieselbe
Etage trifft wie die Startseite. -->
<select class="form-select" id="autoFloor">
<?php foreach ($floors as $etage): ?>
<option value="<?= $etage ?>" title="<?= htmlspecialchars(floorLabel($etage)) ?>"><?= $etage ?></option>
<?php endforeach; ?>
</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>&nbsp;&nbsp;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>&nbsp;&nbsp;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-3">
<span class="input-group-text">Sperrzeit nach dem Auslösen</span>
<select class="form-select" id="lockoutSecs"></select>
</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>&nbsp;&nbsp;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>