diff --git a/ajax/room.php b/ajax/room.php index 518d77c..beb5f95 100644 --- a/ajax/room.php +++ b/ajax/room.php @@ -133,19 +133,7 @@ if (!$gruppen && !$hatThermostat) { . "Das geht über das Haus-Symbol in der Karte „Automatismen“.

"; } -foreach (raumGruppenTitel() as $form => $titel) { - if (empty($gruppen[$form])) { - continue; - } - // Eine einzelne Lampe braucht keine Ueberschrift "Licht" - der Name der - // Lampe steht direkt darunter. Ueberschriften ordnen erst ab zwei. - if (count($gruppen[$form]) > 1) { - echo gruppenTitel($titel[0], $titel[1]); - } - foreach ($gruppen[$form] as $d) { - echo zeichneGeraet($d, $form); - } -} +echo zeichneGruppen($gruppen); echo "
"; echo ""; diff --git a/css/solar.css b/css/solar.css index 1bbac26..577b901 100644 --- a/css/solar.css +++ b/css/solar.css @@ -1,15 +1,15 @@ -.winddir { - position:absolute; - left:60%; - width:25%; - height:40%; - padding: 0; - background-image: url('../assets/img/arrow.svg'); - background-size:contain; - background-repeat: no-repeat; - background-position: center; - } - +.winddir { + position:absolute; + left:60%; + width:25%; + height:40%; + padding: 0; + background-image: url('../assets/img/arrow.svg'); + background-size:contain; + background-repeat: no-repeat; + background-position: center; + } + /* Schieberegler in den Modals (Autoladung, Heizstab, Thermostat). Der gefuellte Anteil kommt aus --background-size, gesetzt von @@ -111,91 +111,176 @@ input[type='range']::-webkit-slider-runnable-track { .input-group.raum-cmd > .form-select { min-width: 5rem; } - - -/* -------------------------------------------------------------------------- - Raum-Modal: Bedienelemente - Erzeugt von restricted/roomControls.php, verdrahtet in js/solar/homeMQTT.js. - -------------------------------------------------------------------------- */ - -/* Ueberschrift einer Geraetegruppe (Beschattung, Licht, ...). Klein und - ruhig - sie ordnet, sie soll nicht mit den Geraeten um Aufmerksamkeit - ringen. */ -.raum-gruppe { - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.05em; - margin: 0.75rem 0 0.35rem; -} -.raum-gruppe:first-child { - margin-top: 0; -} - -/* Waehrend ein Kommando unterwegs ist. Ein "Zu" an einer Jalousie dauert - ueber eine Minute; ohne sichtbare Sperre tippt man in der Zeit nach. */ -.raum-geraet.raum-warte { - opacity: 0.55; - pointer-events: none; -} -.raum-geraet.raum-warte::after { - content: ""; - position: absolute; - top: 0.5rem; - right: 0.5rem; - width: 0.9rem; - height: 0.9rem; - border: 2px solid currentColor; - border-right-color: transparent; - border-radius: 50%; - animation: raum-dreht 0.8s linear infinite; -} -.raum-geraet { - position: relative; -} -@keyframes raum-dreht { - to { transform: rotate(360deg); } -} - -/* Schieberegler stehen dicht untereinander (Position und Neigung). Ohne den - Abstand kleben Beschriftung und Bahn aneinander. */ -.raum-regler + .raum-regler { - margin-top: 0.35rem; -} -.raum-regler .form-range { - margin-bottom: 0; -} - -/* Messwerte als Gitter statt als Textwurst: ein Stromzaehler hat sechzehn - Werte, die in einer Zeile niemand findet. auto-fill statt fester Spalten, - damit auf dem Handy zwei und auf dem Schirm vier nebeneinander passen. */ -.raum-werte { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr)); - gap: 0.4rem; -} -.raum-wert { - background: rgba(128, 128, 128, 0.08); - border-radius: 0.35rem; - padding: 0.3rem 0.45rem; - overflow: hidden; -} -.raum-wert-zahl { - font-size: 0.95rem; - font-weight: 600; - line-height: 1.2; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.raum-wert-einheit { - font-size: 0.7rem; - font-weight: 400; - opacity: 0.7; -} -.raum-wert-name { - font-size: 0.68rem; - opacity: 0.7; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} + + +/* -------------------------------------------------------------------------- + Raum-Modal: Bedienelemente + Erzeugt von restricted/roomControls.php, verdrahtet in js/solar/homeMQTT.js. + -------------------------------------------------------------------------- */ + +/* Die Geraetegruppen als Tabs. Auf einem Handy passen fuenf Beschriftungen + nicht nebeneinander - dann scrollt die Leiste seitwaerts, statt die Namen + umzubrechen. */ +.raum-tabs { + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: none; +} +.raum-tabs::-webkit-scrollbar { + display: none; +} +.raum-tabs .nav-link { + padding: 0.25rem 0.6rem; + font-size: 0.8rem; + white-space: nowrap; +} + +/* Waehrend ein Kommando unterwegs ist. Ein "Zu" an einer Jalousie dauert + ueber eine Minute; ohne sichtbare Sperre tippt man in der Zeit nach. */ +.raum-geraet.raum-warte { + opacity: 0.55; + pointer-events: none; +} +.raum-geraet.raum-warte::after { + content: ""; + position: absolute; + top: 0.5rem; + right: 0.5rem; + width: 0.9rem; + height: 0.9rem; + border: 2px solid currentColor; + border-right-color: transparent; + border-radius: 50%; + animation: raum-dreht 0.8s linear infinite; +} +.raum-geraet { + position: relative; +} +@keyframes raum-dreht { + to { transform: rotate(360deg); } +} + +/* Schieberegler stehen dicht untereinander (Position und Neigung). Ohne den + Abstand kleben Beschriftung und Bahn aneinander. */ +.raum-regler + .raum-regler { + margin-top: 0.35rem; +} +.raum-regler .form-range { + margin-bottom: 0; +} + +/* Messwerte als Gitter statt als Textwurst: ein Stromzaehler hat sechzehn + Werte, die in einer Zeile niemand findet. auto-fill statt fester Spalten, + damit auf dem Handy zwei und auf dem Schirm vier nebeneinander passen. */ +.raum-werte { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr)); + gap: 0.4rem; +} +.raum-wert { + background: rgba(128, 128, 128, 0.08); + border-radius: 0.35rem; + padding: 0.3rem 0.45rem; + overflow: hidden; +} +.raum-wert-zahl { + font-size: 0.95rem; + font-weight: 600; + line-height: 1.2; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.raum-wert-einheit { + font-size: 0.7rem; + font-weight: 400; + opacity: 0.6; +} +.raum-wert-name { + font-size: 0.68rem; + opacity: 0.6; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + + +/* -------------------------------------------------------------------------- + Raum-Modal: Jalousien + -------------------------------------------------------------------------- */ + +/* Fahrknoepfe, Hoehe und Neigung nebeneinander. Die Neigung nimmt den Rest - + sie ist die einzige der drei Spalten, die von Breite profitiert. */ +.raum-jalousie { + display: flex; + align-items: stretch; + gap: 0.6rem; +} +.raum-jalousie .raum-neigung-block { + flex: 1 1 auto; + min-width: 0; + align-self: center; +} +.raum-jalousie .dropdown { + align-self: flex-start; +} + +/* Auf, Stop, Zu untereinander - die Knoepfe zeigen in die Richtung, in die + sie fahren. Schmal, weil nur Symbole darin stehen. */ +.raum-fahrt { + flex: 0 0 auto; +} +.raum-fahrt .btn { + padding: 0.2rem 0.55rem; + line-height: 1.4; +} + +/* Die Hoehe als senkrechter Regler: der Griff steht dort, wo der Rollladen + steht. writing-mode ist der Weg, den Chrome und Firefox heute gehen - + slider-vertical war nie standardisiert und ist inzwischen entfernt. */ +.raum-position { + flex: 0 0 auto; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.1rem; +} +.raum-position input[type='range'] { + writing-mode: vertical-rl; + width: 1.2rem; + height: 4.5rem; + padding: 0; + margin: 0; +} +/* Die gefuellte Bahn laeuft hier von oben nach unten mit - "voll" heisst + heruntergefahren. */ +.raum-position input[type='range']::-webkit-slider-runnable-track { + background: linear-gradient(to bottom, #00788F, #00788F), #D7D7D7; + background-size: 100% var(--background-size, 0%); +} + +/* Die Endstellungen in Worten statt in Prozent. */ +.raum-ende { + font-size: 0.68rem; + opacity: 0.65; + line-height: 1.1; +} + +/* Die Lamellen zur Neigung. Ihre Hoehe setzt lamellenZeichnen() in + js/solar/homeMQTT.js - schmal mit Luft dazwischen heisst offen, + aneinanderliegend heisst geschlossen. */ +.raum-lamellen { + display: block; + width: 100%; + height: 1.8rem; + margin: 0.1rem 0; + /* Der Rahmen macht aus dem Streifen ein Fenster - ohne ihn liest sich das + geschlossene Bild wie eine zweite Reglerbahn. */ + background: rgba(0, 120, 143, 0.14); + border: 1px solid rgba(128, 128, 128, 0.35); + border-radius: 0.2rem; +} +.raum-lamelle { + fill: currentColor; + opacity: 0.6; +} diff --git a/js/solar/homeMQTT.js b/js/solar/homeMQTT.js index 915dd79..e95c463 100644 --- a/js/solar/homeMQTT.js +++ b/js/solar/homeMQTT.js @@ -226,14 +226,13 @@ function switchFloor(floor){ k.addEventListener("click", () => schalte(k)); }); - // Schieberegler: waehrend des Ziehens nur die Beschriftung, geschickt - // wird erst beim Loslassen. Sonst bekaeme eine Jalousie bei jedem Pixel - // ein neues Ziel und kaeme aus dem Anfahren nicht mehr heraus. + // Schieberegler: waehrend des Ziehens nur die Anzeige, geschickt wird + // erst beim Loslassen. Sonst bekaeme eine Jalousie bei jedem Pixel ein + // neues Ziel und kaeme aus dem Anfahren nicht mehr heraus. koerper.querySelectorAll(".raum-regler").forEach(regler => { const schieber = regler.querySelector("input[type=range]"); - const anzeige = regler.querySelector(".raum-regler-wert"); - const einheit = schieber.dataset.einheit || ""; - schieber.addEventListener("input", () => { anzeige.textContent = schieber.value + einheit; }); + reglerAnzeigen(regler); + schieber.addEventListener("input", () => reglerAnzeigen(regler)); schieber.addEventListener("change", () => { const params = {}; params[regler.dataset.param] = schieber.value; @@ -280,6 +279,54 @@ function switchFloor(floor){ }); } + /** + * Anzeige, gefuellte Bahn und Lamellenbild eines Reglers nachziehen. + * + * An den Endstellungen steht ein Wort statt der Zahl: "unten" liest sich + * ohne Umweg, "100 %" muss man erst uebersetzen. Dazwischen bleibt es bei + * der Prozentzahl - fuer halb heruntergefahren gibt es kein Wort. + */ + function reglerAnzeigen(regler) { + const schieber = regler.querySelector("input[type=range]"); + const anzeige = regler.querySelector(".raum-regler-wert"); + const wert = Number(schieber.value); + + if (anzeige) { + const worte = regler.dataset.worte ? JSON.parse(regler.dataset.worte) : null; + anzeige.textContent = (worte && worte[wert] !== undefined) + ? worte[wert] + : wert + (schieber.dataset.einheit || ""); + } + + // Der gefuellte Anteil der Bahn - dieselbe Eigenschaft, aus der auch + // bindModalSlider() in common.js den Temperaturregler faerbt. + const min = Number(schieber.min); + const max = Number(schieber.max); + schieber.style.setProperty("--background-size", + ((wert - min) * 100 / (max - min)) + "%"); + + lamellenZeichnen(regler.closest(".raum-neigung-block"), wert, min, max); + } + + /** + * Die Lamellen zur Neigung zeichnen. + * + * Gezeichnet wird die Hoehe, die eine gekippte Lamelle von vorn zeigt: + * w * sin(Winkel). Bei 0 % steht sie waagerecht und man sieht nur ihre + * Kante, bei 100 % steht sie quer und deckt den Abstand zur naechsten + * vollstaendig ab - genau das, was das Licht abhaelt. + */ + function lamellenZeichnen(block, wert, min, max) { + if (!block) return; + const anteil = (wert - min) / (max - min); + const hoehe = Math.max(1.6, 11 * Math.sin(anteil * Math.PI / 2)); + block.querySelectorAll(".raum-lamelle").forEach(lamelle => { + const mitte = Number(lamelle.dataset.mitte); + lamelle.setAttribute("height", hoehe); + lamelle.setAttribute("y", mitte - hoehe / 2); + }); + } + /** * Die Ist-Werte im Heizungsteil aus dem laufenden MQTT-Strom fuellen. * diff --git a/restricted/roomControls.php b/restricted/roomControls.php index 10f0907..cdd4b64 100644 --- a/restricted/roomControls.php +++ b/restricted/roomControls.php @@ -137,18 +137,72 @@ function bedienform($d) return "sonstiges"; } -/** Ueberschriften der Gruppen, in der Reihenfolge, in der sie erscheinen. */ +/** + * Die Gruppen in der Reihenfolge, in der ihre Tabs erscheinen. + * + * Beschattung zuerst: das ist es, was man in einem Raum am haeufigsten + * anfasst. Messwerte zuletzt - die schaut man an, man bedient sie nicht. + */ function raumGruppenTitel() { return [ "beschattung" => ["Beschattung", "bi-blinds"], "licht" => ["Licht", "bi-lightbulb"], "schalter" => ["Schalten", "bi-toggle-on"], - "sonstiges" => ["Weitere Geräte", "bi-sliders"], + "sonstiges" => ["Geräte", "bi-sliders"], "messwerte" => ["Messwerte", "bi-speedometer2"], ]; } +/** + * Die Geraetegruppen als Tabs. + * + * Vorher standen sie untereinander, und man scrollte an drei Rollladen + * vorbei, um zum Licht zu kommen. Bei nur einer Gruppe bleibt die Leiste + * weg - ein einzelner Tab beschriftet nur sich selbst. + */ +function zeichneGruppen($gruppen) +{ + $vorhanden = []; + foreach (raumGruppenTitel() as $form => $titel) { + if (!empty($gruppen[$form])) { + $vorhanden[$form] = $titel; + } + } + if (!$vorhanden) { + return ""; + } + + $html = ""; + if (count($vorhanden) > 1) { + $html .= ""; + } + + $html .= "
"; + $erster = true; + foreach ($vorhanden as $form => $titel) { + $html .= "
"; + foreach ($gruppen[$form] as $d) { + $html .= zeichneGeraet($d, $form); + } + $html .= "
"; + $erster = false; + } + return $html . "
"; +} + // =========================================================================== // Bausteine // =========================================================================== @@ -178,20 +232,109 @@ function karteZu() */ function reglerZeile($titel, $cmd, $param, $wert, $einheit = "%") { - $min = $param["min"] === null ? 0 : intval($param["min"]); - $max = $param["max"] === null ? 100 : intval($param["max"]); - $wert = $wert === null || $wert === "" ? $min : intval($wert); - $wert = max($min, min($max, $wert)); + $min = reglerMin($param); + $max = reglerMax($param); + $wert = reglerWert($wert, $min, $max); return "
" . "
" . "" . htmlspecialchars($titel) . "" . "" . $wert . $einheit . "
" - . "" + . reglerFeld($min, $max, $wert, $einheit) . "
"; } +function reglerMin($param) +{ + return $param["min"] === null ? 0 : intval($param["min"]); +} + +function reglerMax($param) +{ + return $param["max"] === null ? 100 : intval($param["max"]); +} + +function reglerWert($wert, $min, $max) +{ + $wert = $wert === null || $wert === "" ? $min : intval($wert); + return max($min, min($max, $wert)); +} + +function reglerFeld($min, $max, $wert, $einheit) +{ + return ""; +} + +/** + * Die beiden Endstellungen in Worten. + * + * "Position 100 %" muss man uebersetzen, "unten" nicht. Dazwischen bleibt es + * bei der Zahl - fuer halb heruntergefahren gibt es kein Wort. Ausgewertet + * wird das Attribut in raumBedienungAktivieren(), js/solar/homeMQTT.js. + */ +function worteAttribut($beiNull, $beiHundert) +{ + return " data-worte='" . htmlspecialchars( + json_encode([0 => $beiNull, 100 => $beiHundert], JSON_UNESCAPED_UNICODE), ENT_QUOTES) . "'"; +} + +/** + * Die Hoehe als senkrechter Regler - oben ist oben. + * + * Einen waagerechten Schieber fuer eine Bewegung, die hoch und runter geht, + * muss man erst uebersetzen; senkrecht steht der Griff dort, wo der Rollladen + * steht. + */ +function positionsRegler($cmd, $param, $wert) +{ + $min = reglerMin($param); + $max = reglerMax($param); + $wert = reglerWert($wert, $min, $max); + return "
" + . "oben" + . reglerFeld($min, $max, $wert, "%") + . "unten" + . "
"; +} + +/** + * Die Lamellenstellung mit einem Bild daneben. + * + * Bei 30 % oder 70 % sagt die Zahl nicht, ob man noch hinaussieht. Die vier + * Streifen zeigen es: schmal mit Luft dazwischen heisst offen, geschlossen + * liegen sie aneinander. Gezeichnet wird die Hoehe, die eine gekippte + * Lamelle von vorn zeigt - genau das, was das Licht abhaelt. + */ +function neigungsRegler($cmd, $param, $wert) +{ + $min = reglerMin($param); + $max = reglerMax($param); + $wert = reglerWert($wert, $min, $max); + return "
" + . "
" + . "Neigung" + . "" . $wert . "%
" + . lamellenBild() + . reglerFeld($min, $max, $wert, "%") + . "
aufzu
" + . "
"; +} + +/** Die vier Lamellen. Ihre Hoehe setzt der Browser, siehe homeMQTT.js. */ +function lamellenBild() +{ + $html = ""; +} + /** Kippschalter mit dem Stand, den das Geraet zuletzt gemeldet hat. */ function kippSchalter($d, $schalter, $an) { @@ -291,28 +434,32 @@ function zeichneBeschattung($d) })); $html = karteAuf($d, $status); + $html .= "
"; + + // Auf, Stop, Zu untereinander: so zeigen die Knoepfe in die Richtung, in + // die sie fahren. if ($auf || $stop || $zu) { - $html .= "
"; - $html .= "
"; + $html .= "
"; foreach ([[$auf, "bi-chevron-up", "Auf"], [$stop, "bi-stop-fill", "Stop"], [$zu, "bi-chevron-down", "Zu"]] as $k) { if (!$k[0]) { continue; } $html .= ""; + . " title='" . htmlspecialchars($k[2], ENT_QUOTES) . "'" + . " aria-label='" . htmlspecialchars($k[2], ENT_QUOTES) . "'>" + . ""; } - $html .= "
" . mehrMenue($rest) . "
"; + $html .= "
"; } if ($position) { - $html .= reglerZeile("Position", $position, $position["params"][0], $standPos); + $html .= positionsRegler($position, $position["params"][0], $standPos); } if ($neigung) { - $html .= reglerZeile("Neigung", $neigung, $neigung["params"][0], $standNeigung); + $html .= neigungsRegler($neigung, $neigung["params"][0], $standNeigung); } - return $html . karteZu(); + $html .= mehrMenue($rest); + return $html . "
" . karteZu(); } /** @@ -537,10 +684,3 @@ function zeichneGeraet($d, $form) default: return zeichneSonstiges($d); } } - -/** Ueberschrift einer Gruppe. */ -function gruppenTitel($titel, $symbol) -{ - return "
" - . "" . htmlspecialchars($titel) . "
"; -}