["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), ]; }