Die Uebersicht auf der Startseite zeigt die Automatiken jetzt als Zeitleiste: eine Bahn je Geraeteart, die Spur ist der Tag. Der Umschalter "Zeitleiste | Liste" oben in der Karte fuehrt zur bisherigen Tabelle zurueck und wird im Browser gemerkt. Jede Automatik erscheint genau einmal, nach festen Regeln (restricted/zeitleiste.php), auch kuenftige: - feste Uhrzeit oder Sonnenstand: Punkt; Zeitfenster mit Messwert: Balken; Kette: hinter ihrem Ausloeser, als gestrichelter Bogen verbunden - ohne jede Zeit: Band "Jederzeit"; pausiert: Band "Pausiert" - an diesem Tag nicht dran (Wochentag, Ferien, Feiertag, Vorabend, wie im Runner gerechnet): gestrichelt mit Grund, nie ausgeblendet - Bahn aus den geschalteten Geraeten ueber bedienform(), Mehrheit gewinnt; Bewaesserung am Geraetetyp - Zaehler "x von y" im Kopf Dazu: Tag vor/zurueck, Etagen einzeln einblendbar (gemerkt), Jetzt-Linie und Nachtschatten aus solarLog.daylight (spaetere Tage vom selben Kalendertag eines Vorjahres), gelaufene Eintraege mit Uhrzeit aus automation_log, Klick oeffnet den Editor. Schmal nur Symbole in den Bahnen und Start bei der aktuellen Uhrzeit. Auf einer Probeseite mit echten Daten geprueft: nichts ragt ueber, keine ueberlappenden Etiketten, alle 22 Automatiken genau einmal, Filter, Tageswechsel und Umschalter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
420 lines
17 KiB
PHP
420 lines
17 KiB
PHP
<?php
|
|
/**
|
|
* Die Automatiken eines Tages als Zeitleiste.
|
|
*
|
|
* Die Übersicht als Tabelle beantwortete nicht, was man eigentlich wissen
|
|
* will: läuft der Tag stimmig, und warum ist etwas noch nicht gefahren? Die
|
|
* Zeitleiste legt deshalb jede Automatik auf die Stunde, zu der sie greift -
|
|
* und zwar ausnahmslos jede, auch künftige, nach festen Regeln:
|
|
*
|
|
* feste Uhrzeit (um / ab 07:00) Punkt
|
|
* Sonnenauf-/-untergang ± Versatz Punkt, an diesem Tag ausgerechnet
|
|
* Kette (andere Automatik + 00:10) Punkt hinter dem Auslöser
|
|
* Zeitfenster und Messwerte Balken über das Fenster
|
|
* gar keine Zeit, nur Messwerte "jederzeit"
|
|
*
|
|
* Die Bahn - die Zeile, in der sie steht - ergibt sich aus den Geräten, die
|
|
* sie schaltet: dieselbe Einordnung wie im Raum-Modal (bedienform() in
|
|
* roomControls.php), die Mehrheit gewinnt. Bewässerung ist dort "sonstiges"
|
|
* und wird hier am Gerätetyp erkannt.
|
|
*
|
|
* Ob eine Automatik an diesem Tag überhaupt dran ist, wird genau so
|
|
* gerechnet wie im Runner (tag_passt und gemeinter_tag in
|
|
* autoaction_runner.py) - sonst hieße "heute nicht" hier etwas anderes als
|
|
* dort. Ausgeblendet wird trotzdem nichts: wer nicht dran ist, steht
|
|
* gestrichelt da, mit Grund.
|
|
*
|
|
* Gerechnet wird nur die Lage auf dem Tag, nicht, ob eine Bedingung gerade
|
|
* erfüllt ist. Das entscheidet der Runner; hier steht, ob sie gelaufen ist
|
|
* (automation_log) und wann sie frühestens könnte.
|
|
*/
|
|
|
|
require_once(__DIR__ . "/automations.php");
|
|
require_once(__DIR__ . "/roomControls.php");
|
|
require_once(__DIR__ . "/costs.php"); // solarDb() für die Sonnenzeiten
|
|
|
|
/** Reihenfolge der Bahnen und wie sie heißen. */
|
|
function zeitleisteBahnen()
|
|
{
|
|
return [
|
|
"licht" => ["titel" => "Licht", "symbol" => "bi-lightbulb"],
|
|
"beschattung" => ["titel" => "Beschattung", "symbol" => "bi-window-split"],
|
|
"heizung" => ["titel" => "Heizung", "symbol" => "bi-thermometer-half"],
|
|
"bewaesserung" => ["titel" => "Bewässerung", "symbol" => "bi-droplet"],
|
|
"schalter" => ["titel" => "Schalter", "symbol" => "bi-toggle-on"],
|
|
"sonstiges" => ["titel" => "Sonstiges", "symbol" => "bi-three-dots"],
|
|
];
|
|
}
|
|
|
|
/** "07:30" -> 450, "-00:30" wird von aufrufender Seite behandelt. */
|
|
function zlMinuten($text)
|
|
{
|
|
if (!preg_match('/^(\d{1,2}):(\d{2})/', trim(strval($text)), $t)) {
|
|
return null;
|
|
}
|
|
return intval($t[1]) * 60 + intval($t[2]);
|
|
}
|
|
|
|
/** 450 -> "07:30" */
|
|
function zlUhrzeit($m)
|
|
{
|
|
$m = (($m % 1440) + 1440) % 1440;
|
|
return sprintf("%02d:%02d", intdiv($m, 60), $m % 60);
|
|
}
|
|
|
|
/** Ferien und Feiertag an einem Datum aus calendar_days. */
|
|
function zlKalender($datum)
|
|
{
|
|
static $cache = [];
|
|
if (isset($cache[$datum])) {
|
|
return $cache[$datum];
|
|
}
|
|
$stand = ["feiertag" => null, "ferien" => null];
|
|
$stmt = meshDb()->prepare("SELECT holiday, vacation FROM calendar_days WHERE date = ?");
|
|
$stmt->bind_param("s", $datum);
|
|
$stmt->execute();
|
|
$z = $stmt->get_result()->fetch_assoc();
|
|
$stmt->close();
|
|
if ($z) {
|
|
$stand = ["feiertag" => $z["holiday"], "ferien" => $z["vacation"]];
|
|
}
|
|
return $cache[$datum] = $stand;
|
|
}
|
|
|
|
/**
|
|
* Ist die Automatik an diesem Datum dran? Gibt [ja/nein, Grund] zurück.
|
|
* Dieselbe Reihenfolge wie tag_passt(): erst öffnen, dann sperren.
|
|
*/
|
|
function zlTagPasst($a, $datum)
|
|
{
|
|
$tag = new DateTimeImmutable($datum);
|
|
if (!empty($a["next_day"])) {
|
|
$tag = $tag->modify("+1 day");
|
|
}
|
|
$kal = zlKalender($tag->format("Y-m-d"));
|
|
$bit = 1 << (intval($tag->format("N")) - 1); // Montag = Bit 0
|
|
$vorabend = !empty($a["next_day"]) ? "morgen " : "";
|
|
|
|
$passt = (bool)($a["weekdays"] & $bit);
|
|
if ($kal["feiertag"] && $a["on_holiday"] == 2) $passt = true;
|
|
if ($kal["ferien"] && $a["on_vacation"] == 2) $passt = true;
|
|
if (!$passt) {
|
|
$namen = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
|
$tage = [];
|
|
for ($i = 0; $i < 7; $i++) {
|
|
if ($a["weekdays"] & (1 << $i)) $tage[] = $namen[$i];
|
|
}
|
|
$extra = [];
|
|
if ($a["on_holiday"] == 2) $extra[] = "Feiertage";
|
|
if ($a["on_vacation"] == 2) $extra[] = "Ferien";
|
|
$liste = implode(", ", array_merge($tage, $extra));
|
|
return [false, ($vorabend ? "nur am Vorabend von " : "nur ") . ($liste ?: "an keinem Tag")];
|
|
}
|
|
if ($kal["feiertag"] && $a["on_holiday"] == 0) {
|
|
return [false, $vorabend . "Feiertag (" . $kal["feiertag"] . ")"];
|
|
}
|
|
if ($kal["ferien"] && $a["on_vacation"] == 0) {
|
|
return [false, $vorabend . "Ferien (" . $kal["ferien"] . ")"];
|
|
}
|
|
return [true, ""];
|
|
}
|
|
|
|
/**
|
|
* Sonnenauf- und -untergang eines Datums in Minuten, wie im Runner.
|
|
*
|
|
* solarLog.daylight kennt nur heute, morgen und die Vergangenheit. Für einen
|
|
* späteren Tag gilt derselbe Kalendertag eines Vorjahres - die Sonne
|
|
* verschiebt sich von Jahr zu Jahr um weniger als eine Minute, und so braucht
|
|
* es keinen Standort, der in einem anderen Haus wieder falsch wäre. "genau"
|
|
* sagt, ob es der Eintrag des Tages selbst war.
|
|
*/
|
|
function zlSonne($datum)
|
|
{
|
|
try {
|
|
$db = solarDb();
|
|
$stmt = $db->prepare("SELECT sunrise, sunset, date = ? AS genau FROM daylight
|
|
WHERE date = ? OR DATE_FORMAT(date, '%m-%d') = DATE_FORMAT(?, '%m-%d')
|
|
ORDER BY genau DESC, date DESC LIMIT 1");
|
|
$stmt->bind_param("sss", $datum, $datum, $datum);
|
|
$stmt->execute();
|
|
$z = $stmt->get_result()->fetch_assoc();
|
|
$stmt->close();
|
|
if ($z) {
|
|
return ["auf" => zlMinuten($z["sunrise"]), "unter" => zlMinuten($z["sunset"]),
|
|
"genau" => (bool)$z["genau"]];
|
|
}
|
|
} catch (Throwable $e) {
|
|
// ohne Sonnenzeiten eben ohne - die betroffenen stehen dann unter "jederzeit"
|
|
}
|
|
return ["auf" => null, "unter" => null, "genau" => false];
|
|
}
|
|
|
|
/**
|
|
* Die Bahn einer Automatik: welche Art Gerät sie überwiegend schaltet.
|
|
* $geraete ist command_id => Gerät aus deviceCatalog().
|
|
*/
|
|
function zlBahn($aktionen, $geraete)
|
|
{
|
|
$zaehler = [];
|
|
foreach ($aktionen as $akt) {
|
|
$d = $geraete[$akt["command_id"]] ?? null;
|
|
if (!$d) {
|
|
continue;
|
|
}
|
|
$form = bedienform($d);
|
|
if (stripos($d["type"], "bewässer") !== false || stripos($d["type"], "bewaesser") !== false
|
|
|| stripos($d["url"], "Gartenwasser") !== false) {
|
|
$form = "bewaesserung";
|
|
}
|
|
if (!isset(zeitleisteBahnen()[$form])) {
|
|
$form = "sonstiges";
|
|
}
|
|
$zaehler[$form] = ($zaehler[$form] ?? 0) + 1;
|
|
}
|
|
if (!$zaehler) {
|
|
return "sonstiges";
|
|
}
|
|
// Bei Gleichstand gewinnt die Bahn, die weiter oben steht.
|
|
$reihe = array_flip(array_keys(zeitleisteBahnen()));
|
|
uksort($zaehler, function ($x, $y) use ($zaehler, $reihe) {
|
|
return [$zaehler[$y], $reihe[$x]] <=> [$zaehler[$x], $reihe[$y]];
|
|
});
|
|
return array_key_first($zaehler);
|
|
}
|
|
|
|
/** Was die Automatik tut, kurz: "Wozi Fensterfront, Küche Tür +5: Auf". */
|
|
function zlSatz($aktionen, $geraete, $kommandos)
|
|
{
|
|
$namen = [];
|
|
$was = [];
|
|
foreach ($aktionen as $akt) {
|
|
$d = $geraete[$akt["command_id"]] ?? null;
|
|
$namen[] = $d ? $d["label"] : "?";
|
|
$voll = $kommandos[$akt["command_id"]] ?? "";
|
|
$was[] = strpos($voll, ": ") !== false ? substr($voll, strpos($voll, ": ") + 2) : $voll;
|
|
}
|
|
$namen = array_values(array_unique($namen));
|
|
$wer = implode(", ", array_slice($namen, 0, 2)) . (count($namen) > 2 ? " +" . (count($namen) - 2) : "");
|
|
$was = array_values(array_unique($was));
|
|
return $wer . ": " . (count($was) === 1 ? $was[0] : implode(" / ", array_slice($was, 0, 2)));
|
|
}
|
|
|
|
/**
|
|
* Die Zeitleiste eines Datums (YYYY-MM-DD).
|
|
*/
|
|
function zeitleiste($datum)
|
|
{
|
|
$db = meshDb();
|
|
$d = DateTimeImmutable::createFromFormat("Y-m-d", $datum) ?: new DateTimeImmutable("today");
|
|
$datum = $d->format("Y-m-d");
|
|
$sonne = zlSonne($datum);
|
|
|
|
// Geräte je Kommando, einmal für alle.
|
|
$geraete = [];
|
|
foreach (deviceCatalog() as $g) {
|
|
foreach ($g["commands"] as $c) {
|
|
$geraete[$c["id"]] = $g;
|
|
}
|
|
}
|
|
$kommandos = commandNames();
|
|
$namesState = stateNames();
|
|
|
|
// Messwerte, die eine Zeit tragen: Uhrzeit/Datum (Logic), Sonnenstand,
|
|
// Automatik-Auslöser. Alles andere ist ein Messwert, auf den gewartet wird.
|
|
$zeitStates = [];
|
|
$res = $db->query("SELECT s.id, s.url, a.url AS actor_url, t.type
|
|
FROM actor_states s JOIN actors a ON a.id = s.actor_id
|
|
LEFT JOIN state_types t ON t.id = s.state_type
|
|
WHERE a.url IN ('Logic', 'Automatik')");
|
|
while ($z = $res->fetch_assoc()) {
|
|
$zeitStates[intval($z["id"])] = $z;
|
|
}
|
|
|
|
// Läufe an diesem Tag.
|
|
$laeufe = [];
|
|
$stmt = $db->prepare("SELECT automation_id, ts, result FROM automation_log
|
|
WHERE ts >= ? AND ts < ? + INTERVAL 1 DAY AND result IN ('fired', 'forced')
|
|
ORDER BY ts");
|
|
$stmt->bind_param("ss", $datum, $datum);
|
|
$stmt->execute();
|
|
$r = $stmt->get_result();
|
|
while ($z = $r->fetch_assoc()) {
|
|
$laeufe[intval($z["automation_id"])][] = substr($z["ts"], 11, 5);
|
|
}
|
|
$stmt->close();
|
|
|
|
$ids = array_map("intval", array_column($db->query("SELECT id FROM automations")->fetch_all(MYSQLI_ASSOC), "id"));
|
|
$liste = [];
|
|
foreach ($ids as $id) {
|
|
$a = loadAutomation($id);
|
|
if (!$a) {
|
|
continue;
|
|
}
|
|
$von = zlMinuten($a["window_from"]) ?? 0;
|
|
$bis = zlMinuten($a["window_to"]) ?? 1439;
|
|
$ganzerTag = $von === 0 && $bis >= 1439;
|
|
|
|
// Je ODER-Gruppe: Zeitpunkt, frühester Beginn, spätestes Ende,
|
|
// Kette, wartet auf Messwert. Innerhalb der Gruppe UND.
|
|
$gruppen = [];
|
|
foreach ($a["conditions"] as $c) {
|
|
$g = &$gruppen[$c["group"]];
|
|
$g = $g ?? ["punkt" => null, "start" => null, "ende" => null, "kette" => null,
|
|
"versatz" => 0, "sensor" => false, "sonne" => false];
|
|
$st = $zeitStates[$c["state_id"]] ?? null;
|
|
$op = $c["operator"];
|
|
if ($st && $st["actor_url"] === "Logic" && $st["type"] === "time") {
|
|
$m = zlMinuten($c["value"]);
|
|
if ($op === "=") $g["punkt"] = $m;
|
|
elseif ($op === ">=") $g["start"] = max($g["start"] ?? 0, $m);
|
|
elseif ($op === "<") $g["ende"] = min($g["ende"] ?? 1440, $m);
|
|
} elseif ($st && $st["actor_url"] === "Logic" && $st["type"] === "deltatime") {
|
|
$basis = $st["url"] === "sunrise" ? $sonne["auf"] : $sonne["unter"];
|
|
$versatz = zlMinuten($c["value"]) ?? 0;
|
|
if ($basis === null) {
|
|
$g["sensor"] = true;
|
|
continue;
|
|
}
|
|
$m = $basis + (substr($op, -1) === "-" ? -$versatz : $versatz);
|
|
$g["sonne"] = true;
|
|
if ($op === "+" || $op === "-") $g["punkt"] = $m;
|
|
elseif (strpos($op, ">=") === 0) $g["start"] = max($g["start"] ?? 0, $m);
|
|
else $g["ende"] = min($g["ende"] ?? 1440, $m);
|
|
} elseif ($st && $st["actor_url"] === "Automatik" && preg_match('/^auto:(\d+)$/', $st["url"], $t)) {
|
|
$g["kette"] = intval($t[1]);
|
|
$g["versatz"] = zlMinuten($c["value"]) ?? 0;
|
|
} elseif ($st && $st["actor_url"] === "Logic") {
|
|
// Datum: ein Tagesfilter, keine Lage auf dem Tag.
|
|
} else {
|
|
$g["sensor"] = true;
|
|
}
|
|
unset($g);
|
|
}
|
|
|
|
[$dran, $grund] = zlTagPasst($a, $datum);
|
|
$eintrag = [
|
|
"id" => $id,
|
|
"name" => $a["name"],
|
|
"floor" => $a["floor"],
|
|
"enabled" => $a["enabled"],
|
|
"dran" => $dran,
|
|
"grund" => $grund,
|
|
"bahn" => zlBahn($a["actions"], $geraete),
|
|
"satz" => zlSatz($a["actions"], $geraete, $kommandos),
|
|
"wenn" => describeConditions($a["conditions"], $namesState),
|
|
"laeufe" => $laeufe[$id] ?? [],
|
|
"art" => "jederzeit",
|
|
"punkt" => null,
|
|
"von" => null,
|
|
"bis" => null,
|
|
"kette" => null,
|
|
"versatz" => 0,
|
|
"sonne" => false,
|
|
"wartet" => false,
|
|
];
|
|
|
|
$punkte = array_filter(array_column($gruppen, "punkt"), fn($m) => $m !== null);
|
|
$ketten = array_filter($gruppen, fn($g) => $g["kette"] !== null);
|
|
$eintrag["wartet"] = (bool)array_filter($gruppen, fn($g) => $g["sensor"]);
|
|
$eintrag["sonne"] = (bool)array_filter($gruppen, fn($g) => $g["sonne"]);
|
|
|
|
if ($punkte) {
|
|
$eintrag["art"] = "punkt";
|
|
$eintrag["punkt"] = min($punkte);
|
|
} elseif ($ketten) {
|
|
$k = reset($ketten);
|
|
$eintrag["art"] = "kette";
|
|
$eintrag["kette"] = $k["kette"];
|
|
$eintrag["versatz"] = $k["versatz"];
|
|
// Die eigenen Grenzen der Kette - aufgelöst wird unten, wenn der
|
|
// Auslöser bekannt ist.
|
|
$eintrag["_von"] = max($von, $k["start"] ?? $von);
|
|
$eintrag["_bis"] = min($bis, $k["ende"] ?? $bis);
|
|
} else {
|
|
$anfaenge = [];
|
|
$enden = [];
|
|
$zeitlich = false;
|
|
foreach ($gruppen as $g) {
|
|
if ($g["start"] !== null || $g["ende"] !== null) $zeitlich = true;
|
|
$anfaenge[] = max($von, $g["start"] ?? $von);
|
|
$enden[] = min($bis, $g["ende"] ?? $bis);
|
|
}
|
|
if ($zeitlich || !$ganzerTag) {
|
|
$eintrag["art"] = "fenster";
|
|
$eintrag["von"] = $anfaenge ? min($anfaenge) : $von;
|
|
$eintrag["bis"] = $enden ? max($enden) : $bis;
|
|
// Der früheste zeitlich bestimmte Beginn, falls es einen gibt -
|
|
// bei "Sonnenuntergang + 30 oder dunkel" die Sonnenmarke.
|
|
$marken = array_filter(array_column($gruppen, "start"), fn($m) => $m !== null);
|
|
if ($marken) {
|
|
$eintrag["punkt"] = min($marken);
|
|
}
|
|
}
|
|
}
|
|
$liste[$id] = $eintrag;
|
|
}
|
|
|
|
/*
|
|
* Ketten auflösen. Die Lage hängt am Auslöser:
|
|
*
|
|
* Auslöser lief schon Punkt bei dessen Zeit + Versatz
|
|
* Auslöser hat einen Punkt Punkt dort + Versatz
|
|
* Auslöser ist ein Fenster Fenster ab dessen Beginn + Versatz
|
|
*
|
|
* Jeweils nicht vor dem eigenen Fensterbeginn und nicht nach dem eigenen
|
|
* Ende. Mehrstufig (Wecker -> Rollos -> Schlafzimmer), deshalb reihum.
|
|
*/
|
|
$aufgeloest = [];
|
|
for ($runde = 0; $runde < 6; $runde++) {
|
|
foreach ($liste as $id => $e) {
|
|
if ($e["art"] !== "kette" || isset($aufgeloest[$id])) {
|
|
continue;
|
|
}
|
|
$vater = $liste[$e["kette"]] ?? null;
|
|
if (!$vater || ($vater["art"] === "kette" && !isset($aufgeloest[$e["kette"]]))) {
|
|
continue;
|
|
}
|
|
$aufgeloest[$id] = true;
|
|
$eigenVon = $e["_von"];
|
|
$eigenBis = $e["_bis"];
|
|
if ($vater["laeufe"]) {
|
|
$liste[$id]["punkt"] = max($eigenVon, zlMinuten(end($vater["laeufe"])) + $e["versatz"]);
|
|
} elseif ($vater["punkt"] !== null && $vater["art"] !== "fenster") {
|
|
$liste[$id]["punkt"] = max($eigenVon, $vater["punkt"] + $e["versatz"]);
|
|
} elseif ($vater["art"] === "fenster") {
|
|
$liste[$id]["art"] = "fenster";
|
|
$liste[$id]["von"] = max($eigenVon, $vater["von"] + $e["versatz"]);
|
|
$liste[$id]["bis"] = max($liste[$id]["von"], $eigenBis);
|
|
} else {
|
|
$liste[$id]["art"] = "jederzeit"; // Auslöser ohne Lage: dann eben ohne
|
|
}
|
|
// Eine Kette erbt, ob der Tag passt: läuft der Wecker nicht, kommt auch der Nachfolger nicht.
|
|
if (!$vater["dran"] && $liste[$id]["dran"]) {
|
|
$liste[$id]["dran"] = false;
|
|
$liste[$id]["grund"] = "Auslöser „" . $vater["name"] . "“ ist nicht dran";
|
|
}
|
|
}
|
|
}
|
|
foreach ($liste as $id => $e) {
|
|
if ($e["art"] === "kette" && $e["punkt"] === null) {
|
|
$liste[$id]["art"] = "jederzeit"; // Kreis oder fehlender Auslöser
|
|
}
|
|
unset($liste[$id]["_von"], $liste[$id]["_bis"]);
|
|
}
|
|
|
|
$etagen = [];
|
|
foreach (grundriss()[0] as $code => $e) {
|
|
$etagen[] = ["code" => $code, "label" => $e["label"]];
|
|
}
|
|
$heute = (new DateTimeImmutable("today"))->format("Y-m-d");
|
|
return [
|
|
"datum" => $datum,
|
|
"heute" => $datum === $heute,
|
|
"jetzt" => $datum === $heute ? intval(date("G")) * 60 + intval(date("i")) : null,
|
|
"sonne" => $sonne,
|
|
"bahnen" => zeitleisteBahnen(),
|
|
"etagen" => $etagen,
|
|
"automatiken" => array_values($liste),
|
|
];
|
|
}
|