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>
262 lines
12 KiB
PHP
262 lines
12 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"] ?? "", $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> 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-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> 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>
|