Einstellungen -> Grundriss: Etagen, Raeume und Kacheln am Plan pflegen
Neuer Reiter mit Plan-Editor: Etagen anlegen, umbenennen, sortieren, loeschen und als Standard markieren; Grundrissbild hochladen; Raeume mit Name, Kuerzel und Thermostat-Zweig anlegen und bearbeiten; Kacheln im Plan ziehen oder einen Raum ohne Kachel mit "Platzieren" und einem Klick setzen. Loest tools/kachelpositionen.php ab - Raster, Geisterkachel und die Umrechnung Mitte -> linke obere Ecke sind von dort uebernommen. - restricted/grundriss.php: alle Pruefungen und Schreibzugriffe. Eine Etage mit Raeumen oder Automatiken laesst sich nicht loeschen, die Ablehnung nennt die Namen. Raeume loeschen laesst ihre Geraete ohne Raum. - Grundrissbilder nach tiles/grundriss/ (beschreibbar, nicht im Git), nur PNG/JPEG/WebP, am Inhalt geprueft; SVG bewusst nicht. Prüfsumme im Dateinamen, alte hochgeladene Bilder werden weggeraeumt. - ajax/settings.php: ?action=grundriss und sieben POST-Aktionen, jede antwortet mit dem ganzen neuen Stand. - "Werte" springt in den Reiter Home-Kacheln und oeffnet die Kachel; Home-Kacheln und Geraete laden nach einer Grundriss-Aenderung nach. - Startseite ohne Etage mit platziertem Raum: Hinweis auf die Einstellungen statt einer leeren Zeichenflaeche. - README: Grundriss als Inhalt statt Code, neue Dateien und Endpunkte, Reihenfolge der Schema-Dateien fuer ein neues Haus. Backend mit einer Probe-Etage durchgespielt (auch Fehlerfaelle, Upload, Aufraeumen); Oberflaeche auf einer Probeseite mit echten Daten und Zeiger-Ereignissen geprueft: Ziehen, Antippen, Platzieren, neuer Raum. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,447 @@
|
||||
<?php
|
||||
/**
|
||||
* Modell-Schicht für den Reiter "Grundriss": Etagen, Räume, Kachelpositionen,
|
||||
* Grundrissbilder.
|
||||
*
|
||||
* Gelesen wird der Grundriss in rooms.php - dort, wo ihn Startseite, Menü und
|
||||
* Editor ohnehin holen. Hier steht nur, was ihn ändert, und jede Prüfung
|
||||
* dazu. Die Maske (js/solar/grundriss.js) prüft vorab nichts, was hier nicht
|
||||
* auch geprüft würde; sie zeigt nur die Meldung, die von hier kommt.
|
||||
*
|
||||
* Tabellen: homeMesh.floors und homeMesh.rooms, siehe homeMesh_grundriss.sql.
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . "/rooms.php"); // grundriss(), meshDb()
|
||||
|
||||
/** Zeichenfläche und Kachel - dieselben Maße wie in restricted/home.php. */
|
||||
const PLAN_BREIT = 400;
|
||||
const PLAN_HOCH = 300;
|
||||
const KACHEL_BREIT = 24;
|
||||
const KACHEL_HOCH = 19;
|
||||
|
||||
/**
|
||||
* Wohin hochgeladene Grundrisse kommen, relativ zum Web-Root.
|
||||
*
|
||||
* tiles/ ist das eine Verzeichnis, in das der Webserver schreiben darf - dort
|
||||
* liegt schon der Zwischenspeicher der Kartenkacheln. Es steht nicht im Git;
|
||||
* ein Grundriss ist Inhalt dieses Hauses, nicht Teil der Anwendung.
|
||||
*/
|
||||
const GRUNDRISS_ORDNER = "tiles/grundriss";
|
||||
|
||||
/** Größte Bilddatei. Ein Rendering in 2048 x 1536 hat gut drei Megabyte. */
|
||||
const GRUNDRISS_MAX_BYTES = 16 * 1024 * 1024;
|
||||
|
||||
/** Web-Root als Dateipfad. */
|
||||
function webRoot()
|
||||
{
|
||||
return dirname(__DIR__);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alles, was der Reiter zum Zeichnen braucht, in einer Antwort.
|
||||
*
|
||||
* Je Etage steht dabei, wie viele Räume und Automatiken an ihr hängen, je
|
||||
* Raum, wie viele Geräte - danach richtet sich, ob und mit welcher Rückfrage
|
||||
* gelöscht werden darf. Die Maske soll die Namen nicht erst nachfragen müssen.
|
||||
*/
|
||||
function grundrissStand()
|
||||
{
|
||||
$db = meshDb();
|
||||
[$etagen, $raeume] = grundriss();
|
||||
|
||||
$automatiken = [];
|
||||
$res = $db->query("SELECT floor, name FROM automations ORDER BY name");
|
||||
while ($res && $z = $res->fetch_assoc()) {
|
||||
$automatiken[$z["floor"]][] = $z["name"];
|
||||
}
|
||||
$geraete = [];
|
||||
$res = $db->query("SELECT room_id, COUNT(*) AS n FROM actors
|
||||
WHERE room_id IS NOT NULL GROUP BY room_id");
|
||||
while ($res && $z = $res->fetch_assoc()) {
|
||||
$geraete[intval($z["room_id"])] = intval($z["n"]);
|
||||
}
|
||||
|
||||
$liste = [];
|
||||
foreach ($etagen as $code => $e) {
|
||||
$liste[] = [
|
||||
"code" => $code,
|
||||
"label" => $e["label"],
|
||||
"bild" => $e["bild"],
|
||||
"standard" => $e["standard"],
|
||||
"raeume" => count(array_filter($raeume, fn($r) => $r["floor"] === $code)),
|
||||
"automatiken" => $automatiken[$code] ?? [],
|
||||
];
|
||||
}
|
||||
$raumListe = [];
|
||||
foreach ($raeume as $r) {
|
||||
$raumListe[] = [
|
||||
"nr" => $r["nr"],
|
||||
"floor" => $r["floor"],
|
||||
"name" => $r["mqtt"],
|
||||
"kuerzel" => $r["id"],
|
||||
"x" => $r["x"] ?? null,
|
||||
"y" => $r["y"] ?? null,
|
||||
"thermostat" => $r["thermostat"],
|
||||
"geraete" => $geraete[$r["nr"]] ?? 0,
|
||||
];
|
||||
}
|
||||
return [
|
||||
"etagen" => $liste,
|
||||
"raeume" => $raumListe,
|
||||
"zweige" => thermostatZweige(),
|
||||
"standard" => standardEtage(),
|
||||
"mass" => ["planBreit" => PLAN_BREIT, "planHoch" => PLAN_HOCH,
|
||||
"kachelBreit" => KACHEL_BREIT, "kachelHoch" => KACHEL_HOCH],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Vorschläge für das Thermostat-Feld: die Zweige, unter denen gerade ein
|
||||
* Thermostat meldet.
|
||||
*
|
||||
* Erkannt an einem Messwert, dessen Topic auf "/Temp[degC]" endet - so
|
||||
* schreiben die ESP32-Thermostate. Ein anderes Haus mit anderen Thermostaten
|
||||
* tippt den Zweig von Hand; die Liste ist eine Abkürzung, keine Grenze.
|
||||
*/
|
||||
function thermostatZweige()
|
||||
{
|
||||
$zweige = [];
|
||||
$res = meshDb()->query("SELECT DISTINCT url FROM actor_states WHERE url LIKE '%/Temp[degC]'");
|
||||
while ($res && $z = $res->fetch_row()) {
|
||||
$zweige[] = substr($z[0], 0, -strlen("/Temp[degC]"));
|
||||
}
|
||||
sort($zweige, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
return $zweige;
|
||||
}
|
||||
|
||||
/* --- Etagen -------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Eine Etage anlegen oder ändern.
|
||||
*
|
||||
* Das Kürzel ist nach dem Anlegen fest: es steht in URLs, SVG-Ids und
|
||||
* automations.floor. Ändern lassen sich Name und "Standard".
|
||||
*
|
||||
* Kürzel aus bis zu vier Großbuchstaben oder Ziffern - so passt es in den
|
||||
* Etagenschalter im Grundriss und braucht in keiner URL eine Maskierung.
|
||||
*/
|
||||
function etageSpeichern($daten)
|
||||
{
|
||||
$db = meshDb();
|
||||
$code = strtoupper(trim(strval($daten["code"] ?? "")));
|
||||
$label = trim(strval($daten["label"] ?? ""));
|
||||
$neu = !empty($daten["neu"]);
|
||||
$standard = !empty($daten["standard"]);
|
||||
|
||||
if (!preg_match('/^[A-Z0-9]{1,4}$/', $code)) {
|
||||
throw new InvalidArgumentException(
|
||||
"Das Kürzel besteht aus 1 bis 4 Großbuchstaben oder Ziffern, z. B. EG.");
|
||||
}
|
||||
if ($label === "" || mb_strlen($label) > 40) {
|
||||
throw new InvalidArgumentException("Der Name der Etage braucht 1 bis 40 Zeichen.");
|
||||
}
|
||||
$da = isset(grundriss()[0][$code]);
|
||||
if ($neu && $da) {
|
||||
throw new InvalidArgumentException("Eine Etage " . $code . " gibt es schon.");
|
||||
}
|
||||
if (!$neu && !$da) {
|
||||
throw new InvalidArgumentException("Die Etage " . $code . " gibt es nicht (mehr).");
|
||||
}
|
||||
|
||||
$db->begin_transaction();
|
||||
try {
|
||||
if ($standard) {
|
||||
// Höchstens eine - der Schalter an der einen nimmt ihn allen anderen.
|
||||
$db->query("UPDATE floors SET standard = 0");
|
||||
}
|
||||
$std = $standard ? 1 : 0;
|
||||
if ($neu) {
|
||||
$pos = intval($db->query("SELECT COALESCE(MAX(position), -1) + 1 FROM floors")->fetch_row()[0]);
|
||||
$stmt = $db->prepare("INSERT INTO floors (code, label, position, standard) VALUES (?,?,?,?)");
|
||||
$stmt->bind_param("ssii", $code, $label, $pos, $std);
|
||||
} else {
|
||||
$stmt = $db->prepare("UPDATE floors SET label = ?, standard = ? WHERE code = ?");
|
||||
$stmt->bind_param("sis", $label, $std, $code);
|
||||
}
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
$db->commit();
|
||||
} catch (Throwable $e) {
|
||||
$db->rollback();
|
||||
throw $e;
|
||||
}
|
||||
return $code;
|
||||
}
|
||||
|
||||
/** Neue Reihenfolge der Etagen: die Kürzel in der gewünschten Folge. */
|
||||
function etagenReihenfolge($codes)
|
||||
{
|
||||
if (!is_array($codes)) {
|
||||
throw new InvalidArgumentException("Die Reihenfolge ist unlesbar.");
|
||||
}
|
||||
$vorhanden = array_keys(grundriss()[0]);
|
||||
$codes = array_values(array_map("strval", $codes));
|
||||
if (count($codes) !== count($vorhanden) || array_diff($vorhanden, $codes)) {
|
||||
throw new InvalidArgumentException(
|
||||
"Die Reihenfolge passt nicht zu den Etagen - bitte neu laden.");
|
||||
}
|
||||
$db = meshDb();
|
||||
$stmt = $db->prepare("UPDATE floors SET position = ? WHERE code = ?");
|
||||
foreach ($codes as $i => $code) {
|
||||
$stmt->bind_param("is", $i, $code);
|
||||
$stmt->execute();
|
||||
}
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Eine Etage löschen - nur, wenn nichts mehr an ihr hängt.
|
||||
*
|
||||
* Räume und Automatiken mitzulöschen wäre ein stiller Datenverlust. Die
|
||||
* Ablehnung nennt, was im Weg steht; aufräumen muss man bewusst.
|
||||
*/
|
||||
function etageLoeschen($code)
|
||||
{
|
||||
$code = strval($code);
|
||||
$etage = grundriss()[0][$code] ?? null;
|
||||
if (!$etage) {
|
||||
throw new InvalidArgumentException("Die Etage " . $code . " gibt es nicht (mehr).");
|
||||
}
|
||||
$raeume = array_column(array_filter(grundriss()[1], fn($r) => $r["floor"] === $code), "mqtt");
|
||||
if ($raeume) {
|
||||
throw new InvalidArgumentException(
|
||||
"Auf " . $etage["label"] . " liegen noch " . count($raeume) . " Räume: "
|
||||
. implode(", ", $raeume) . ". Erst die Räume löschen oder verschieben.");
|
||||
}
|
||||
$db = meshDb();
|
||||
$stmt = $db->prepare("SELECT name FROM automations WHERE floor = ? ORDER BY name");
|
||||
$stmt->bind_param("s", $code);
|
||||
$stmt->execute();
|
||||
$namen = array_column($stmt->get_result()->fetch_all(MYSQLI_ASSOC), "name");
|
||||
$stmt->close();
|
||||
if ($namen) {
|
||||
throw new InvalidArgumentException(
|
||||
"Unter " . $etage["label"] . " stehen noch " . count($namen) . " Automatiken: "
|
||||
. implode(", ", $namen) . ". Erst dort löschen oder einer anderen Etage zuordnen.");
|
||||
}
|
||||
$stmt = $db->prepare("DELETE FROM floors WHERE code = ?");
|
||||
$stmt->bind_param("s", $code);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
grundrissBildWegraeumen($etage["bild"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein hochgeladenes Grundrissbild übernehmen.
|
||||
*
|
||||
* Erlaubt sind PNG, JPEG und WebP - geprüft am Inhalt (getimagesize), nicht an
|
||||
* der Endung. SVG bewusst nicht: es kann Skript enthalten, und hier läge es
|
||||
* direkt abrufbar im Web-Root derselben Seite.
|
||||
*
|
||||
* Der Dateiname trägt einen Teil der Prüfsumme. Ein neues Bild hat damit eine
|
||||
* neue Adresse, und kein Browser zeigt nach dem Hochladen noch das alte aus
|
||||
* seinem Zwischenspeicher.
|
||||
*/
|
||||
function etageBildSpeichern($code, $datei)
|
||||
{
|
||||
$code = strval($code);
|
||||
$etage = grundriss()[0][$code] ?? null;
|
||||
if (!$etage) {
|
||||
throw new InvalidArgumentException("Die Etage " . $code . " gibt es nicht (mehr).");
|
||||
}
|
||||
if (!is_array($datei) || ($datei["error"] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
||||
$fehler = $datei["error"] ?? UPLOAD_ERR_NO_FILE;
|
||||
throw new InvalidArgumentException($fehler === UPLOAD_ERR_INI_SIZE || $fehler === UPLOAD_ERR_FORM_SIZE
|
||||
? "Das Bild ist zu groß." : "Es kam kein Bild an.");
|
||||
}
|
||||
if ($datei["size"] > GRUNDRISS_MAX_BYTES) {
|
||||
throw new InvalidArgumentException("Das Bild ist größer als 16 MB.");
|
||||
}
|
||||
$info = @getimagesize($datei["tmp_name"]);
|
||||
$endungen = [IMAGETYPE_PNG => "png", IMAGETYPE_JPEG => "jpg", IMAGETYPE_WEBP => "webp"];
|
||||
if (!$info || !isset($endungen[$info[2]])) {
|
||||
throw new InvalidArgumentException("Nur PNG, JPEG oder WebP.");
|
||||
}
|
||||
|
||||
$ordner = webRoot() . "/" . GRUNDRISS_ORDNER;
|
||||
if (!is_dir($ordner) && !@mkdir($ordner, 0775, true)) {
|
||||
throw new RuntimeException("Der Ordner " . GRUNDRISS_ORDNER . " lässt sich nicht anlegen.");
|
||||
}
|
||||
$name = $code . "-" . substr(sha1_file($datei["tmp_name"]), 0, 10) . "." . $endungen[$info[2]];
|
||||
$ziel = $ordner . "/" . $name;
|
||||
if (!move_uploaded_file($datei["tmp_name"], $ziel)) {
|
||||
throw new RuntimeException("Das Bild ließ sich nicht nach " . GRUNDRISS_ORDNER . " schreiben.");
|
||||
}
|
||||
@chmod($ziel, 0664);
|
||||
|
||||
$pfad = GRUNDRISS_ORDNER . "/" . $name;
|
||||
$db = meshDb();
|
||||
$stmt = $db->prepare("UPDATE floors SET bild = ? WHERE code = ?");
|
||||
$stmt->bind_param("ss", $pfad, $code);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
|
||||
if ($etage["bild"] !== $pfad) {
|
||||
grundrissBildWegraeumen($etage["bild"]);
|
||||
}
|
||||
return ["bild" => $pfad, "breit" => $info[0], "hoch" => $info[1]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein nicht mehr benutztes Bild löschen - aber nur ein hochgeladenes.
|
||||
*
|
||||
* Die mitgelieferten unter assets/img gehören zum Repository und bleiben.
|
||||
* Benutzt eine andere Etage dasselbe Bild, bleibt es ebenfalls.
|
||||
*/
|
||||
function grundrissBildWegraeumen($pfad)
|
||||
{
|
||||
if (!$pfad || strpos($pfad, GRUNDRISS_ORDNER . "/") !== 0 || strpos($pfad, "..") !== false) {
|
||||
return;
|
||||
}
|
||||
$stmt = meshDb()->prepare("SELECT COUNT(*) FROM floors WHERE bild = ?");
|
||||
$stmt->bind_param("s", $pfad);
|
||||
$stmt->execute();
|
||||
$benutzt = intval($stmt->get_result()->fetch_row()[0]);
|
||||
$stmt->close();
|
||||
if ($benutzt === 0) {
|
||||
@unlink(webRoot() . "/" . $pfad);
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Räume --------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Ein Kürzel aus einem Namen: klein, ohne Umlaute, nur a-z, 0-9 und _.
|
||||
*
|
||||
* Es steht in SVG-Element-Ids; Leerzeichen und Umlaute gehören dort nicht
|
||||
* hinein.
|
||||
*/
|
||||
function raumKuerzelAus($name)
|
||||
{
|
||||
$k = mb_strtolower($name);
|
||||
$k = strtr($k, ["ä" => "ae", "ö" => "oe", "ü" => "ue", "ß" => "ss"]);
|
||||
$k = preg_replace('/[^a-z0-9]+/', "_", $k);
|
||||
$k = trim($k, "_");
|
||||
return substr($k !== "" ? $k : "raum", 0, 36);
|
||||
}
|
||||
|
||||
/**
|
||||
* Einen Raum anlegen oder ändern.
|
||||
*
|
||||
* Fehlt das Kürzel, entsteht es aus dem Namen - und wird durchnummeriert,
|
||||
* falls es auf der Etage schon vergeben ist. Ein von Hand eingetragenes muss
|
||||
* dagegen passen: wer es tippt, meint genau dieses.
|
||||
*
|
||||
* Wechselt ein Raum die Etage, behält er seine Kachel an derselben Stelle der
|
||||
* Zeichenfläche - verschieben ist dann ein Zug im Plan.
|
||||
*/
|
||||
function raumSpeichern($daten)
|
||||
{
|
||||
$db = meshDb();
|
||||
$nr = intval($daten["nr"] ?? 0);
|
||||
$floor = strval($daten["floor"] ?? "");
|
||||
$name = trim(strval($daten["name"] ?? ""));
|
||||
$kuerzel = trim(strval($daten["kuerzel"] ?? ""));
|
||||
$thermostat = trim(strval($daten["thermostat"] ?? ""), " /");
|
||||
|
||||
if (!isset(grundriss()[0][$floor])) {
|
||||
throw new InvalidArgumentException("Bitte eine Etage wählen.");
|
||||
}
|
||||
if ($name === "" || mb_strlen($name) > 40) {
|
||||
throw new InvalidArgumentException("Der Name des Raums braucht 1 bis 40 Zeichen.");
|
||||
}
|
||||
if ($thermostat !== "" && (mb_strlen($thermostat) > 120 || preg_match('/[#+\s]/', $thermostat))) {
|
||||
throw new InvalidArgumentException(
|
||||
"Der Thermostat-Zweig ist ein MQTT-Topic ohne Leerzeichen, # oder +, z. B. Raumtemp/EG/Bad.");
|
||||
}
|
||||
|
||||
$andere = array_filter(grundriss()[1], fn($r) => $r["floor"] === $floor && $r["nr"] !== $nr);
|
||||
foreach ($andere as $r) {
|
||||
if (mb_strtolower($r["mqtt"]) === mb_strtolower($name)) {
|
||||
throw new InvalidArgumentException("Auf dieser Etage gibt es schon einen Raum „" . $name . "“.");
|
||||
}
|
||||
}
|
||||
$vergeben = array_column($andere, "id");
|
||||
if ($kuerzel === "") {
|
||||
$basis = raumKuerzelAus($name);
|
||||
$kuerzel = $basis;
|
||||
for ($i = 2; in_array($kuerzel, $vergeben, true); $i++) {
|
||||
$kuerzel = $basis . "_" . $i;
|
||||
}
|
||||
} elseif (!preg_match('/^[a-z0-9_]{1,40}$/', $kuerzel)) {
|
||||
throw new InvalidArgumentException("Das Kürzel besteht nur aus a-z, 0-9 und _.");
|
||||
} elseif (in_array($kuerzel, $vergeben, true)) {
|
||||
throw new InvalidArgumentException("Das Kürzel " . $kuerzel . " ist auf dieser Etage schon vergeben.");
|
||||
}
|
||||
|
||||
$th = $thermostat === "" ? null : $thermostat;
|
||||
if ($nr > 0) {
|
||||
$alt = null;
|
||||
foreach (grundriss()[1] as $r) {
|
||||
if ($r["nr"] === $nr) {
|
||||
$alt = $r;
|
||||
}
|
||||
}
|
||||
if (!$alt) {
|
||||
throw new InvalidArgumentException("Diesen Raum gibt es nicht (mehr).");
|
||||
}
|
||||
$stmt = $db->prepare("UPDATE rooms SET floor = ?, name = ?, kuerzel = ?, thermostat = ? WHERE id = ?");
|
||||
$stmt->bind_param("ssssi", $floor, $name, $kuerzel, $th, $nr);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
return $nr;
|
||||
}
|
||||
$pos = intval($db->query("SELECT COALESCE(MAX(position), -1) + 1 FROM rooms")->fetch_row()[0]);
|
||||
$stmt = $db->prepare("INSERT INTO rooms (floor, name, kuerzel, position, thermostat) VALUES (?,?,?,?,?)");
|
||||
$stmt->bind_param("sssis", $floor, $name, $kuerzel, $pos, $th);
|
||||
$stmt->execute();
|
||||
$nr = $db->insert_id;
|
||||
$stmt->close();
|
||||
return $nr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Kachel eines Raums setzen oder vom Plan nehmen ($x === null).
|
||||
*
|
||||
* x/y ist die linke obere Ecke, begrenzt auf die Zeichenfläche - eine Kachel,
|
||||
* die halb über den Rand hängt, ließe sich im Plan nicht mehr greifen.
|
||||
*/
|
||||
function raumPosition($nr, $x, $y)
|
||||
{
|
||||
$nr = intval($nr);
|
||||
$db = meshDb();
|
||||
if ($x === null || $y === null) {
|
||||
$stmt = $db->prepare("UPDATE rooms SET x = NULL, y = NULL WHERE id = ?");
|
||||
$stmt->bind_param("i", $nr);
|
||||
} else {
|
||||
$x = max(0, min(PLAN_BREIT - KACHEL_BREIT, intval(round(floatval($x)))));
|
||||
$y = max(0, min(PLAN_HOCH - KACHEL_HOCH, intval(round(floatval($y)))));
|
||||
$stmt = $db->prepare("UPDATE rooms SET x = ?, y = ? WHERE id = ?");
|
||||
$stmt->bind_param("iii", $x, $y, $nr);
|
||||
}
|
||||
$stmt->execute();
|
||||
$geaendert = $stmt->affected_rows;
|
||||
$stmt->close();
|
||||
if ($geaendert === 0 && !array_filter(grundriss()[1], fn($r) => $r["nr"] === $nr)) {
|
||||
throw new InvalidArgumentException("Diesen Raum gibt es nicht (mehr).");
|
||||
}
|
||||
return $x === null ? null : ["x" => $x, "y" => $y];
|
||||
}
|
||||
|
||||
/**
|
||||
* Einen Raum löschen. Seine Geräte bleiben und stehen danach ohne Raum da
|
||||
* (fk_actors_room: ON DELETE SET NULL) - die Rückfrage in der Maske nennt,
|
||||
* wie viele es sind.
|
||||
*/
|
||||
function raumLoeschen($nr)
|
||||
{
|
||||
$nr = intval($nr);
|
||||
$stmt = meshDb()->prepare("DELETE FROM rooms WHERE id = ?");
|
||||
$stmt->bind_param("i", $nr);
|
||||
$stmt->execute();
|
||||
$weg = $stmt->affected_rows;
|
||||
$stmt->close();
|
||||
if ($weg === 0) {
|
||||
throw new InvalidArgumentException("Diesen Raum gibt es nicht (mehr).");
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,23 @@ require_once(__DIR__ . "/reiter.php");
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body pb-1">
|
||||
<?php if (!floorsWithPlan()): ?>
|
||||
<!-- Eine neue Installation: noch keine Etage mit platzierten
|
||||
Raeumen. Statt einer leeren Zeichenflaeche der Weg dorthin,
|
||||
wo sich das aendert. -->
|
||||
<div class="alert alert-secondary mb-3">
|
||||
<?php if (!grundrissDa()): ?>
|
||||
Etagen und Räume lassen sich nicht lesen – die Datenbank
|
||||
<code>homeMesh</code> ist nicht erreichbar, oder
|
||||
<code>homeMesh_grundriss.sql</code> ist noch nicht eingespielt.
|
||||
<?php else: ?>
|
||||
Noch kein Grundriss. Unter
|
||||
<a href="?action=settings#grundriss">Einstellungen → Grundriss</a>
|
||||
werden Etagen angelegt, ihre Grundrissbilder hochgeladen und die
|
||||
Kacheln der Räume gesetzt.
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div lass="img-overlay-wrap">
|
||||
<svg viewBox="0 0 400 300" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
@@ -179,6 +196,7 @@ require_once(__DIR__ . "/reiter.php");
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.card -->
|
||||
|
||||
@@ -37,13 +37,17 @@ require_once(__DIR__ . "/meshdb.php");
|
||||
*
|
||||
* Gibt [etagen, raeume] zurück: etagen ist code => Zeile aus floors in der
|
||||
* eingestellten Reihenfolge, raeume die Liste im Format oben.
|
||||
*
|
||||
* $neuLaden nach einer Änderung in derselben Anfrage (restricted/grundriss.php)
|
||||
* - sonst antwortete der Endpunkt mit dem Stand von davor.
|
||||
*/
|
||||
function grundriss()
|
||||
function grundriss($neuLaden = false)
|
||||
{
|
||||
static $stand = null;
|
||||
if ($stand !== null) {
|
||||
if ($stand !== null && !$neuLaden) {
|
||||
return $stand;
|
||||
}
|
||||
unset($GLOBALS["grundrissFehler"]);
|
||||
$etagen = [];
|
||||
$raeume = [];
|
||||
try {
|
||||
|
||||
+38
-3
@@ -32,6 +32,7 @@ require_once(__DIR__ . "/skodaKeys.php");
|
||||
*/
|
||||
$reiter = [
|
||||
["id" => "preise", "titel" => "Preise", "symbol" => "bi-currency-euro"],
|
||||
["id" => "grundriss", "titel" => "Grundriss", "symbol" => "bi-house"],
|
||||
["id" => "kacheln", "titel" => "Home-Kacheln", "symbol" => "bi-grid-1x2"],
|
||||
["id" => "geraete", "titel" => "Geräte", "symbol" => "bi-house-gear"],
|
||||
["id" => "fahrzeug", "titel" => "Fahrzeug", "symbol" => "bi-ev-front"],
|
||||
@@ -128,6 +129,39 @@ try {
|
||||
<?php endforeach; ?>
|
||||
<?= reiterFlaecheZu() ?><!-- /Preise -->
|
||||
|
||||
<!-- ================================================== Grundriss ==== -->
|
||||
<!-- Etagen, Räume und wo ihre Kacheln sitzen. Stand bis September 2026
|
||||
als Tabelle in restricted/rooms.php; jetzt in homeMesh.floors und
|
||||
homeMesh.rooms, gepflegt hier. Den Inhalt zeichnet
|
||||
js/solar/grundriss.js aus ajax/settings.php?action=grundriss. -->
|
||||
<?= reiterFlaecheAuf("grundriss", $reiterOpt) ?>
|
||||
<div class="row">
|
||||
<div class="col-12 col-xxl-11">
|
||||
<div class="card mb-4" id="grundrissKarte">
|
||||
<div class="card-header pb-2 pt-2">
|
||||
<h3 class="card-title"><i class="bi bi-house me-2"></i>Etagen und Räume</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-tool" id="grundrissNeuLaden" title="Neu laden">
|
||||
<i class="bi bi-arrow-counterclockwise"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-secondary small mb-3">
|
||||
Jede Etage hat ein Grundrissbild, darauf sitzen die Kacheln ihrer Räume.
|
||||
Ein Raum ohne Kachel ist trotzdem ein Raum: Geräte lassen sich ihm
|
||||
zuordnen, er erscheint nur nicht im Plan. Was auf einer Kachel steht,
|
||||
wird unter „Home-Kacheln“ eingestellt.
|
||||
</p>
|
||||
<div id="grundrissFlaeche">
|
||||
<p class="text-secondary small mb-0">Wird geladen …</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?= reiterFlaecheZu() ?><!-- /Grundriss -->
|
||||
|
||||
<!-- ================================================ Home-Kacheln ==== -->
|
||||
<!-- Der Inhalt kommt aus js/solar/settings.js: eine Karte je Etage, darin
|
||||
je Raum eine Zeile mit ihren Werten. Gezeichnet wird aus dem Modell,
|
||||
@@ -149,9 +183,10 @@ try {
|
||||
<div class="card-body">
|
||||
<p class="text-secondary small mb-3">
|
||||
Jede Kachel im Grundriss zeigt bis zu drei Messwerte. Ohne eigene
|
||||
Angabe sind das die Werte aus <code>restricted/rooms.php</code> –
|
||||
bei Räumen mit Thermostat Soll, Ist und Feuchte. Was hier gesetzt
|
||||
wird, gilt stattdessen; „Standard“ nimmt es wieder zurück.
|
||||
Angabe sind das bei Räumen mit Thermostat Soll, Ist und Feuchte,
|
||||
sonst nur der Name. Was hier gesetzt wird, gilt stattdessen;
|
||||
„Standard“ nimmt es wieder zurück. Wo Kacheln sitzen und welcher
|
||||
Raum ein Thermostat hat, steht unter „Grundriss“.
|
||||
</p>
|
||||
<div id="kachelListe">
|
||||
<p class="text-secondary small mb-0">Wird geladen …</p>
|
||||
|
||||
Reference in New Issue
Block a user