Vollbildanzeige fuer den Raspberry Pi (800 x 480)
index.php?action=anzeige: Uebersicht, Anlage und Speicher ohne Kopf und
Seitenleiste, schmale Leiste links, immer quer. Nach zwei Minuten ohne
Beruehrung zurueck zur Uebersicht, MQTT verbindet still neu (Punkt in der
Leiste), alle sechs Stunden neu laden. Standard "ruhig" fuer den Pi 3B+
(nur Fluesse, grobe Stufen, kein Mauszeiger); &animation=1 und &maus=1.
Dafuer:
- Seitentabelle kennt "eigen" (Vorlage ist eine ganze Seite)
- Energiefluss: Optionen hochkantUnter und klickFilter; Anlage: hochkantUnter
- Bewaesserungszustand aus solarMQTT.js nach bewaesserungAnzeige.js, von
Solar-Seite und Anzeige gemeinsam genutzt
- Speicher: "voll/leer" mit Tag ("morgen 21:52", "in 3 Tagen")
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -192,6 +192,15 @@ genau die Topics, die auf den Kacheln stehen (`tileTopics()` in `rooms.php`).
|
||||
| `settings` | `settings.php` | `settings.js`, `grundriss.js`, `energieflussKatalog.js`, `energiefluss.js`, `energieflussEinstellungen.js` | `settings.php?action=…` | — |
|
||||
| `logs` | `logs.php` | `logs.js` | `logs.php?action=…` | — |
|
||||
| `einfuehrung` | `einfuehrung.php` | `einfuehrung.js` | nichts — Folien und Bilder stehen in der Seite | — |
|
||||
| `anzeige` | `anzeige.php` (ganze Seite, ohne `header.php`/`footer.php`) | `anlageKatalog.js`, `anlage.js`, `speicher.js`, `energieflussKatalog.js`, `energiefluss.js`, `bewaesserungAnzeige.js`, `anzeige.js` | `speicher` | `solarManager/#`, `Gartenwasser/#` |
|
||||
|
||||
**Vollbildanzeige** (`?action=anzeige`) — Übersicht, Anlage und Speicher für ein
|
||||
festes Display, gebaut für den Raspberry Pi 3B+ mit 800 × 480 im Kioskmodus:
|
||||
schmale Leiste links, immer quer, nach zwei Minuten ohne Berührung zurück zur
|
||||
Übersicht, MQTT verbindet still neu (Punkt unten: grün/gelb/rot), alle sechs
|
||||
Stunden lädt die Seite neu. Ohne weitere Parameter läuft sie „ruhig“ — nur die
|
||||
Flüsse bewegen sich, in groben Stufen — und ohne Mauszeiger;
|
||||
`&animation=1` schaltet alle Animationen ein, `&maus=1` zeigt den Zeiger.
|
||||
|
||||
Und dasselbe als Abhängigkeitsbild — von der Seite bis in den Speicher:
|
||||
|
||||
|
||||
@@ -3868,6 +3868,73 @@ svg.anl-plan.anl-hoch { max-height: none; }
|
||||
.spk .spk-verlauf.neu .spk-linie { stroke-dashoffset: 0; }
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Vollbildanzeige (restricted/anzeige.php, js/solar/anzeige.js), z.B. am
|
||||
Raspberry Pi mit 800 x 480: schmale Leiste links, Ansicht auf voller Höhe.
|
||||
========================================================================== */
|
||||
body.anzeige {
|
||||
margin: 0; height: 100vh; overflow: hidden;
|
||||
display: grid; grid-template-columns: 3.2rem minmax(0, 1fr);
|
||||
background: var(--ton-grund, #191d20); color: var(--ton-schrift, #e8ede9);
|
||||
font-family: "Poppins", system-ui, sans-serif; font-size: 14px;
|
||||
-webkit-tap-highlight-color: transparent; user-select: none;
|
||||
}
|
||||
body.anzeige-ohne-maus,
|
||||
body.anzeige-ohne-maus * { cursor: none !important; }
|
||||
/* Die Seite lädt AdminLTE nicht - dessen Grundregeln fehlen hier. */
|
||||
body.anzeige [hidden] { display: none !important; }
|
||||
body.anzeige *, body.anzeige *::before, body.anzeige *::after { box-sizing: border-box; }
|
||||
.anzeige .anzeige-leiste {
|
||||
display: flex; flex-direction: column; align-items: center; gap: .4rem; padding: .5rem 0;
|
||||
background: var(--ton-flaeche, #23282c); border-right: 1px solid var(--ton-linie, rgba(138, 148, 158, .2));
|
||||
}
|
||||
.anzeige .anzeige-leiste button {
|
||||
all: unset; box-sizing: border-box; width: 2.6rem; height: 2.6rem; border-radius: .7rem;
|
||||
display: grid; place-items: center; font-size: 1.3rem; color: var(--ton-schrift-2, #9aa6ad);
|
||||
}
|
||||
.anzeige .anzeige-leiste button[aria-pressed="true"] { background: var(--ton-flaeche-2, #2b3237); color: #f7c000; }
|
||||
.anzeige .anzeige-fuss { margin-top: auto; display: grid; justify-items: center; gap: .3rem; }
|
||||
.anzeige .anzeige-punkt { width: .6rem; height: .6rem; border-radius: 50%; background: #e5624f; }
|
||||
.anzeige .anzeige-punkt.warn { background: #e8a33a; }
|
||||
.anzeige .anzeige-punkt.gut { background: #6cbe58; }
|
||||
.anzeige .anzeige-uhr { font-size: .72rem; font-weight: 600; color: var(--ton-schrift-2, #9aa6ad); font-variant-numeric: tabular-nums; }
|
||||
.anzeige .anzeige-inhalt { min-width: 0; height: 100vh; }
|
||||
.anzeige .anzeige-flaeche { box-sizing: border-box; height: 100vh; padding: .35rem; overflow-y: auto; scrollbar-width: none; }
|
||||
.anzeige .anzeige-flaeche::-webkit-scrollbar { display: none; }
|
||||
.anzeige .anzeige-flaeche[hidden] { display: none; }
|
||||
.anzeige #anzeige-fluss { overflow: hidden; padding: .2rem; }
|
||||
.anzeige #anzeige-fluss svg.enf { width: 100%; height: 100%; max-height: none; }
|
||||
/* Anlage und Speicher: die Umbrüche der Solar-Seite richten sich nach der
|
||||
Fensterbreite und würden am 800er-Display zu früh greifen. */
|
||||
.anzeige .anl svg.anl-plan { max-height: calc(100vh - 7.5rem); }
|
||||
.anzeige .spk .spk-kennzahlen { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: .35rem; }
|
||||
.anzeige .spk .spk-kachel { padding: .4rem .55rem .45rem; }
|
||||
.anzeige .spk .spk-kachel b { font-size: 1.05rem; }
|
||||
.anzeige .spk .spk-kachel small,
|
||||
.anzeige .spk .spk-kachel-titel { font-size: .66rem; }
|
||||
.anzeige .spk .spk-mitte,
|
||||
.anzeige .spk .spk-diagramme { grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: .5rem; }
|
||||
.anzeige .spk .spk-fach { padding: .6rem .7rem; gap: .5rem; }
|
||||
.anzeige .spk .spk-modul { grid-template-columns: 1.7rem minmax(0, 1fr) 5.4rem; gap: .4rem; padding: .3rem .45rem; }
|
||||
.anzeige .spk .spk-zellen { height: 1.4rem; }
|
||||
.anzeige .spk .spk-werte b { font-size: .68rem; }
|
||||
.anzeige .spk .spk-werte small { font-size: .62rem; }
|
||||
.anzeige .spk .spk-gross b { font-size: 1.4rem; }
|
||||
|
||||
/* Ruhig (Vorgabe am Pi): alles Zierwerk aus, nur die Flüsse laufen - in
|
||||
groben Stufen, damit ein Raspberry Pi 3 nicht dauernd neu zeichnet. */
|
||||
.anzeige-ruhig *,
|
||||
.anzeige-ruhig *::after { animation: none !important; }
|
||||
.anzeige-ruhig .enf .enf-bett,
|
||||
.anzeige-ruhig .spk .spk-verlauf.neu .spk-linie { stroke-dashoffset: 0; }
|
||||
.anzeige-ruhig .enf .enf-strom-hof { display: none; }
|
||||
.anzeige-ruhig .enf .enf-strom { animation: enf-fliessen var(--dauer, 1.5s) steps(8) infinite !important; }
|
||||
.anzeige-ruhig .enf .enf-rueck .enf-strom { animation-direction: reverse !important; }
|
||||
.anzeige-ruhig .enf .enf-still .enf-strom,
|
||||
.anzeige-ruhig .enf.enf-pause .enf-strom { animation: none !important; }
|
||||
.anzeige-ruhig .enf .enf-strahlen,
|
||||
.anzeige-ruhig .enf .enf-koerper { transition: none; }
|
||||
|
||||
/* --- Karte: Umschalter Übersicht / Anlage -------------------------------- */
|
||||
.enf-umschalter { display: inline-flex; gap: 2px; padding: 3px; border-radius: var(--ton-radius-klein, .5rem); background: var(--ton-grund, #191d20); }
|
||||
.enf-umschalter .btn.btn-sm { border: 0; color: var(--ton-schrift-2, #9aa6ad); font-weight: 600; padding: .2rem .8rem; }
|
||||
|
||||
@@ -101,7 +101,7 @@ if (isset($_GET["addUser"])) {
|
||||
$pages = [
|
||||
"solar" => ["template" => "solar.php", "mqtt" => true, "charts" => true, "meteogram" => true, "leaflet" => false,
|
||||
"scripts" => ["js/solar/jahresstatistik.js", "js/solar/anlageKatalog.js", "js/solar/anlage.js", "js/solar/speicher.js",
|
||||
"js/solar/energieflussKatalog.js", "js/solar/energiefluss.js", "js/solar/solarMQTT.js"]],
|
||||
"js/solar/energieflussKatalog.js", "js/solar/energiefluss.js", "js/solar/bewaesserungAnzeige.js", "js/solar/solarMQTT.js"]],
|
||||
"home" => ["template" => "home.php", "mqtt" => true, "charts" => false, "meteogram" => false, "leaflet" => false,
|
||||
"scripts" => ["js/solar/autoActionFuncs.js", "js/solar/zeitleiste.js", "js/solar/homeMQTT.js"]],
|
||||
"heat" => ["template" => "heat.php", "mqtt" => true, "charts" => true, "meteogram" => false, "leaflet" => false,
|
||||
@@ -120,6 +120,12 @@ if (isset($_GET["addUser"])) {
|
||||
"scripts" => ["js/solar/logs.js"]],
|
||||
"einfuehrung" => ["template" => "einfuehrung.php", "mqtt" => false, "charts" => false, "meteogram" => false, "leaflet" => false,
|
||||
"scripts" => ["js/solar/einfuehrung.js"]],
|
||||
// Vollbild für ein festes Display (Raspberry Pi). "eigen": die Vorlage
|
||||
// ist eine ganze Seite, ohne header.php und footer.php.
|
||||
"anzeige" => ["template" => "anzeige.php", "eigen" => true, "mqtt" => true, "charts" => false, "meteogram" => false, "leaflet" => false,
|
||||
"scripts" => ["js/solar/anlageKatalog.js", "js/solar/anlage.js", "js/solar/speicher.js",
|
||||
"js/solar/energieflussKatalog.js", "js/solar/energiefluss.js",
|
||||
"js/solar/bewaesserungAnzeige.js", "js/solar/anzeige.js"]],
|
||||
];
|
||||
|
||||
// Unbekannte oder fehlende Aktion faellt auf die Solar-Seite zurueck.
|
||||
@@ -128,9 +134,13 @@ if (isset($_GET["addUser"])) {
|
||||
}
|
||||
$page = $pages[$_GET["action"]];
|
||||
|
||||
include "restricted/header.php";
|
||||
include "restricted/" . $page["template"];
|
||||
include "restricted/footer.php";
|
||||
if (!empty($page["eigen"])) {
|
||||
include "restricted/" . $page["template"];
|
||||
} else {
|
||||
include "restricted/header.php";
|
||||
include "restricted/" . $page["template"];
|
||||
include "restricted/footer.php";
|
||||
}
|
||||
} else {
|
||||
echo <<<ENDE
|
||||
<html>
|
||||
|
||||
+3
-2
@@ -74,6 +74,7 @@
|
||||
constructor(behaelter, opt = {}) {
|
||||
this.behaelter = behaelter;
|
||||
this.katalog = opt.katalog || ANLAGE_KATALOG;
|
||||
this.hochkantUnter = opt.hochkantUnter ?? HOCHKANT_UNTER; // px; 0 = nie hochkant
|
||||
this.nummer = ++laufendeNummer;
|
||||
this.auswahl = null; // {art: "platte"|"string"|"wr", schluessel}
|
||||
this.verlauf = {}; // Eingang -> [[zeit, W], ...]
|
||||
@@ -87,7 +88,7 @@
|
||||
|
||||
this.beobachter = new ResizeObserver(() => {
|
||||
if (!this.behaelter.clientWidth) return;
|
||||
if ((this.behaelter.clientWidth < HOCHKANT_UNTER) !== this.hochkant) this.zeichnen();
|
||||
if ((this.behaelter.clientWidth < this.hochkantUnter) !== this.hochkant) this.zeichnen();
|
||||
else this.masstab();
|
||||
});
|
||||
this.beobachter.observe(behaelter);
|
||||
@@ -108,7 +109,7 @@
|
||||
/* --------------------------------------------------------- Zeichnen */
|
||||
zeichnen() {
|
||||
const breite = this.behaelter.clientWidth;
|
||||
this.hochkant = breite > 0 && breite < HOCHKANT_UNTER;
|
||||
this.hochkant = breite > 0 && breite < this.hochkantUnter;
|
||||
const id = "anl" + this.nummer;
|
||||
const k = this.katalog;
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/* ==========================================================================
|
||||
Vollbildanzeige (restricted/anzeige.php): Übersicht, Anlage und Speicher
|
||||
für ein festes Display, z.B. den Raspberry Pi mit 800 x 480.
|
||||
|
||||
Dieselben Bausteine wie auf der Solar-Seite, nur anders verdrahtet:
|
||||
- immer quer (hochkantUnter: 0), das Display steht quer
|
||||
- antippbar sind nur Kreise, die eine andere Ansicht öffnen; Bedienfenster
|
||||
(Heizstab, Wallbox) gibt es hier nicht
|
||||
- nach LEERLAUF ohne Berührung zurück zur Übersicht
|
||||
- MQTT verbindet nach einem Abbruch still neu; der Punkt unten in der
|
||||
Leiste zeigt den Zustand (grün verbunden, gelb keine frischen Werte,
|
||||
rot getrennt). Kein alert() - am Display ist niemand, der ihn wegklickt.
|
||||
- alle NEU_LADEN Stunden lädt die Seite neu, damit ein Browser, der
|
||||
wochenlang läuft, keinen Speicher ansammelt und Änderungen am Code
|
||||
ankommen
|
||||
========================================================================== */
|
||||
|
||||
var mqttData = {};
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const ADRESSE = "wss://mqtt.nas.el-wa.org:443"; // wie in solarMQTT.js
|
||||
const LEERLAUF = 2 * 60000;
|
||||
const NEU_LADEN = 6 * 3600000;
|
||||
const TAKT = typeof anzeigeRuhig !== "undefined" && anzeigeRuhig ? 2000 : 1000;
|
||||
const VERALTET = 30000;
|
||||
const THEMEN = [
|
||||
"solarManager/#",
|
||||
"Gartenwasser/vorn/Timers", "Gartenwasser/vorn/status", "Gartenwasser/vorn/WateringToday",
|
||||
"Gartenwasser/hinten/Timers", "Gartenwasser/hinten/status", "Gartenwasser/hinten/WateringToday",
|
||||
];
|
||||
|
||||
const $ = id => document.getElementById(id);
|
||||
let bild = null, anlage = null, speicher = null;
|
||||
let leerlaufUhr = null, geplant = false, verbunden = false, letzteDaten = 0;
|
||||
|
||||
function zeige(name) {
|
||||
document.querySelectorAll("[data-anzeige]").forEach(b => b.setAttribute("aria-pressed", b.dataset.anzeige === name ? "true" : "false"));
|
||||
document.querySelectorAll("[data-anzeige-flaeche]").forEach(f => { f.hidden = f.dataset.anzeigeFlaeche !== name; });
|
||||
wachHalten();
|
||||
}
|
||||
|
||||
function wachHalten() {
|
||||
clearTimeout(leerlaufUhr);
|
||||
leerlaufUhr = setTimeout(() => zeige("fluss"), LEERLAUF);
|
||||
}
|
||||
|
||||
function zustand() {
|
||||
const punkt = $("anzeigeVerbindung");
|
||||
const frisch = Date.now() - letzteDaten < VERALTET;
|
||||
const klasse = "anzeige-punkt" + (verbunden && frisch ? " gut" : verbunden ? " warn" : "");
|
||||
if (punkt.className !== klasse) {
|
||||
punkt.className = klasse;
|
||||
punkt.title = verbunden && frisch ? "verbunden" : verbunden ? "verbunden, aber keine frischen Werte" : "Verbindung getrennt – wird neu aufgebaut";
|
||||
}
|
||||
}
|
||||
|
||||
function uhr() {
|
||||
const d = new Date();
|
||||
const text = String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
|
||||
if ($("anzeigeUhr").textContent !== text) $("anzeigeUhr").textContent = text;
|
||||
}
|
||||
|
||||
function aktualisieren() {
|
||||
if (geplant) return;
|
||||
geplant = true;
|
||||
setTimeout(() => {
|
||||
geplant = false;
|
||||
bild.aktualisieren(mqttData);
|
||||
anlage.aktualisieren(mqttData);
|
||||
speicher.aktualisieren(mqttData);
|
||||
}, TAKT);
|
||||
}
|
||||
|
||||
function verbinden() {
|
||||
const client = mqtt.connect(ADRESSE, { rejectUnauthorized: false, reconnectPeriod: 5000, connectTimeout: 15000 });
|
||||
client.on("connect", () => {
|
||||
verbunden = true;
|
||||
client.subscribe(THEMEN);
|
||||
zustand();
|
||||
});
|
||||
const weg = () => { verbunden = false; zustand(); };
|
||||
client.on("close", weg);
|
||||
client.on("offline", weg);
|
||||
client.on("error", () => {});
|
||||
client.on("message", (topic, nachricht) => {
|
||||
mqttData[topic] = nachricht;
|
||||
if (topic.endsWith("/Timers")) wateringTimersLastSeen[topic] = Date.now();
|
||||
if (topic === "solarManager/P_Load") {
|
||||
letzteDaten = Date.now();
|
||||
aktualisieren();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const bewaesserung = () => wateringSymboleSetzen(id => document.getElementById(id));
|
||||
|
||||
bild = new Energiefluss($("anzeige-fluss"), {
|
||||
einstellungen: typeof energieflussEinstellungen !== "undefined" ? energieflussEinstellungen : {},
|
||||
hochkantUnter: 0,
|
||||
klickFilter: k => !!k.klick.ansicht,
|
||||
onKlick: k => zeige(k.klick.ansicht),
|
||||
onGezeichnet: bewaesserung,
|
||||
});
|
||||
bild.zeichnen();
|
||||
anlage = new Anlage($("anzeige-anlage"), { hochkantUnter: 0 });
|
||||
anlage.zeichnen();
|
||||
speicher = new Speicher($("anzeige-speicher"));
|
||||
speicher.zeichnen();
|
||||
|
||||
document.querySelectorAll("[data-anzeige]").forEach(b => b.addEventListener("click", () => zeige(b.dataset.anzeige)));
|
||||
document.addEventListener("pointerdown", wachHalten, { passive: true });
|
||||
|
||||
setInterval(() => { uhr(); zustand(); bewaesserung(); }, 1000);
|
||||
uhr();
|
||||
zustand();
|
||||
verbinden();
|
||||
setTimeout(() => location.reload(), NEU_LADEN);
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,206 @@
|
||||
/* ==========================================================================
|
||||
Zustand der Bewässerungszonen - geteilt von der Solar-Seite (solarMQTT.js,
|
||||
dort zusätzlich das Modal) und der Vollbildanzeige (anzeige.js).
|
||||
|
||||
Liest aus dem globalen mqttData, das die jeweilige Seite füllt:
|
||||
Gartenwasser/<vorn|hinten>/Timers, /status, /WateringToday. Wer die
|
||||
Timers-Nachricht empfängt, trägt die Uhrzeit in wateringTimersLastSeen ein
|
||||
- daran hängt, ob eine Zone als laufend gilt.
|
||||
|
||||
Bis September 2026 stand das alles in solarMQTT.js.
|
||||
========================================================================== */
|
||||
|
||||
const wateringTimersLastSeen = {};
|
||||
// Eine Tabelle fuer alles: Ventil, Beschriftung und die Anzeige-Ids im
|
||||
// Realtime-SVG. Vorher standen dieselben Zuordnungen in drei getrennten
|
||||
// Tabellen.
|
||||
const wateringZones = {
|
||||
troughs: {
|
||||
topic: "Gartenwasser/vorn", valve: 1, label: "Tröge",
|
||||
statusId: "wateringTroughStatus", iconId: "wateringTroughIcon"
|
||||
},
|
||||
frontGarden: {
|
||||
topic: "Gartenwasser/vorn", valve: 6, label: "Vorn",
|
||||
statusId: "wateringFrontStatus", iconId: "wateringFrontIcon"
|
||||
},
|
||||
raisedBeds: {
|
||||
topic: "Gartenwasser/hinten", valve: 6, label: "Beete",
|
||||
statusId: "wateringBedsStatus", iconId: "wateringBedsIcon"
|
||||
}
|
||||
};
|
||||
// Feste Liste - der Sekundentakt soll sie nicht jedes Mal neu aufbauen.
|
||||
const wateringZoneList = Object.entries(wateringZones);
|
||||
|
||||
// Die Anzeige laeuft im Sekundentakt, die Nutzlasten aendern sich aber nur,
|
||||
// wenn eine neue MQTT-Nachricht kommt. messageReceived legt dabei jedes Mal
|
||||
// ein neues Buffer-Objekt ab - der Identitaetsvergleich reicht also als
|
||||
// Gueltigkeitspruefung, ein Invalidieren von Hand braucht es nicht.
|
||||
const wateringJsonCache = {};
|
||||
const wateringDecoder = new TextDecoder();
|
||||
|
||||
function wateringJson(topic) {
|
||||
const value = mqttData[topic];
|
||||
if (!value) return null;
|
||||
const cached = wateringJsonCache[topic];
|
||||
if (cached && cached.raw === value) return cached.parsed;
|
||||
|
||||
let parsed = null;
|
||||
try {
|
||||
parsed = JSON.parse(typeof value === "string" ? value : wateringDecoder.decode(value));
|
||||
} catch (error) {
|
||||
parsed = null;
|
||||
}
|
||||
wateringJsonCache[topic] = { raw: value, parsed: parsed };
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function wateringHistory(topic, valve) {
|
||||
const data = wateringJson(topic + "/LastWatering");
|
||||
if (!data) return { dateTime: "--", duration: "--" };
|
||||
return {
|
||||
dateTime: data["wateringDateTime" + valve] || "--",
|
||||
duration: data["wateringDurationSecs" + valve] !== undefined
|
||||
? wateringTimeLabel(Number(data["wateringDurationSecs" + valve]))
|
||||
: "--"
|
||||
};
|
||||
}
|
||||
|
||||
// Tagesdatum im Format der Ventilsteuerung ("29.08.26").
|
||||
function wateringDayStamp(date) {
|
||||
const zwei = (zahl) => String(zahl).padStart(2, "0");
|
||||
return zwei(date.getDate()) + "." + zwei(date.getMonth() + 1) + "." + zwei(date.getFullYear() % 100);
|
||||
}
|
||||
|
||||
// Tagessummen der Ventilsteuerung: WateringToday fuer den laufenden Tag,
|
||||
// LastWateringDay fuer den letzten Tag, an dem ueberhaupt bewaessert wurde.
|
||||
// Beide Nutzlasten tragen den zugehoerigen Tag als "wateringDay" mit. Bei
|
||||
// WateringToday wird der geprueft - die Nachricht ist retained, nach einem
|
||||
// Tageswechsel ohne Bewaesserung stuende sonst die Summe von gestern als
|
||||
// "heute" in der Karte.
|
||||
function wateringDayTotal(topic, subTopic, feld, valve, mussHeuteSein) {
|
||||
const data = wateringJson(topic + "/" + subTopic);
|
||||
if (!data) return { label: "--", day: "", sekunden: null };
|
||||
const tag = data.wateringDay || "";
|
||||
const sekunden = data[feld + valve];
|
||||
if (sekunden === undefined) return { label: "--", day: tag, sekunden: null };
|
||||
if (mussHeuteSein && tag !== wateringDayStamp(new Date())) {
|
||||
return { label: "--", day: tag, sekunden: null };
|
||||
}
|
||||
return { label: wateringTimeLabel(Number(sekunden)), day: tag, sekunden: Number(sekunden) };
|
||||
}
|
||||
|
||||
function wateringAutoActive(topic, mode) {
|
||||
const timers = wateringJson(topic + "/Timers");
|
||||
return !!(timers && timers.auto === mode);
|
||||
}
|
||||
|
||||
function wateringValue(data, valve) {
|
||||
if (!data) return null;
|
||||
const candidates = [
|
||||
data.timers && data.timers[valve],
|
||||
data.timers && data.timers[String(valve)],
|
||||
data[valve],
|
||||
data[String(valve)],
|
||||
data.valves && data.valves[valve],
|
||||
data.valves && data.valves[String(valve)],
|
||||
data.status && data.status[valve],
|
||||
data.status && data.status[String(valve)],
|
||||
data.valveState && data.valveState[valve],
|
||||
data.valveState && data.valveState[String(valve)],
|
||||
data.setValveState && data.setValveState[valve],
|
||||
data.setValveState && data.setValveState[String(valve)]
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (candidate !== undefined) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function wateringState(value) {
|
||||
if (value && typeof value === "object") {
|
||||
return value.state ?? value.status ?? value.valveState ?? value.set;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function wateringSeconds(value) {
|
||||
if (value && typeof value === "object") {
|
||||
value = value.remaining ?? value.timer ?? value.seconds ?? value.time;
|
||||
}
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
function wateringLabel(topic, valve) {
|
||||
const timers = wateringJson(topic + "/Timers");
|
||||
const status = wateringJson(topic + "/status");
|
||||
const lastSeen = wateringTimersLastSeen[topic + "/Timers"];
|
||||
if (!lastSeen || Date.now() - lastSeen > 3000) return "aus";
|
||||
const remaining = wateringValue(timers, valve);
|
||||
if (remaining !== null) {
|
||||
const seconds = wateringSeconds(remaining);
|
||||
if (seconds !== 0) return wateringTimeLabel(Math.abs(seconds));
|
||||
return "aus";
|
||||
}
|
||||
const state = wateringState(wateringValue(status, valve));
|
||||
if (state === true || state === 1 || state === "1" || state === "open" || state === "OPEN") return "an";
|
||||
if (state === false || state === 0 || state === "0" || state === "close" || state === "closed" || state === "CLOSED") return "aus";
|
||||
return "--";
|
||||
}
|
||||
|
||||
function wateringTimeLabel(seconds) {
|
||||
if (seconds < 60) return seconds + "s";
|
||||
return Math.ceil(seconds / 60) + "m";
|
||||
}
|
||||
|
||||
function wateringIsPaused(topic, valve) {
|
||||
const timers = wateringJson(topic + "/Timers");
|
||||
const remaining = wateringValue(timers, valve);
|
||||
return remaining !== null && wateringSeconds(remaining) < 0;
|
||||
}
|
||||
|
||||
function updateWateringIndicator(status, icon, name, state, paused, bewaessert) {
|
||||
// "Aus" bleibt stehen, solange die Zone heute noch kein Wasser hatte;
|
||||
// danach ersetzt das Haekchen (U+2713, ohne Emoji-Variationsselektor ->
|
||||
// einfarbig, folgt der Icon-Farbe) den Text, um Platz auf der textPath zu
|
||||
// sparen. Laeuft im Sekundentakt, aendert sich aber selten - ohne Pruefung
|
||||
// wuerde jede Sekunde derselbe Wert geschrieben und das Icon samt
|
||||
// Filter neu gezeichnet.
|
||||
const text = state === "aus" ? (bewaessert ? "✓" : "Aus") : state;
|
||||
if (status.textContent !== text) status.textContent = text;
|
||||
// Das Blau ("#3A6292") entspricht dem Wassertropfen-Icon (id="water").
|
||||
const color = state !== "aus" ? (paused ? "#ffc107" : "#20c997") : bewaessert ? "#3A6292" : "#6c757d";
|
||||
if (icon.getAttribute("color") !== color) {
|
||||
icon.setAttribute("color", color);
|
||||
icon.setAttribute("fill", color);
|
||||
icon.style.color = color;
|
||||
}
|
||||
const titel = name + ": " + (state === "aus" && bewaessert ? "Aus (heute bereits bewässert)" : state);
|
||||
const titelKnoten = icon.querySelector("title");
|
||||
if (titelKnoten.textContent !== titel) titelKnoten.textContent = titel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zustand einer Zone aus den MQTT-Werten.
|
||||
*
|
||||
* Die Regenautomatik laeuft zum Sonnenaufgang; wenn jemand auf die Anzeige
|
||||
* schaut, ist die Entscheidung laengst gefallen. Angezeigt wird deshalb nicht,
|
||||
* was geplant ist, sondern was passiert ist. Der Zaehler unterscheidet nicht,
|
||||
* wer ausgeloest hat - eine Bewaesserung von Hand zaehlt genauso.
|
||||
*/
|
||||
function wateringZonenZustand(zone) {
|
||||
const state = wateringLabel(zone.topic, zone.valve);
|
||||
const paused = wateringIsPaused(zone.topic, zone.valve);
|
||||
const heute = wateringDayTotal(zone.topic, "WateringToday", "wateringDurationTodaySecs", zone.valve, true);
|
||||
return { state, paused, heute, bewaessert: state === "aus" && heute.sekunden > 0 };
|
||||
}
|
||||
|
||||
/** Symbol und Text jeder Zone setzen; finde(id) liefert das Element. */
|
||||
function wateringSymboleSetzen(finde) {
|
||||
wateringZoneList.forEach(([, zone]) => {
|
||||
const status = finde(zone.statusId);
|
||||
const icon = finde(zone.iconId);
|
||||
if (!status || !icon) return;
|
||||
const z = wateringZonenZustand(zone);
|
||||
updateWateringIndicator(status, icon, zone.label, z.state, z.paused, z.bewaessert);
|
||||
});
|
||||
}
|
||||
@@ -8,6 +8,8 @@
|
||||
onKlick(knoten), // Kreis mit klick-Ziel angetippt
|
||||
onVerschoben(id, x, y), // nur beim Bearbeiten
|
||||
onGezeichnet(), // nach jedem (Neu-)Zeichnen
|
||||
klickFilter(knoten), // optional: welche Kreise antippbar sind
|
||||
hochkantUnter: 720, // optional: px Breite, darunter hochkant (0 = nie)
|
||||
});
|
||||
bild.aktualisieren(mqttData);
|
||||
|
||||
@@ -220,8 +222,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
/** Antippbar: hat ein Ziel, nicht im Bearbeiten, und opt.klickFilter lässt es zu. */
|
||||
klickbar(k) {
|
||||
return !!k.klick && !this.bearbeiten && (!this.opt.klickFilter || this.opt.klickFilter(k));
|
||||
}
|
||||
|
||||
hochkantGewuenscht() {
|
||||
return !this.bearbeiten && this.behaelter.clientWidth > 0 && this.behaelter.clientWidth < HOCHKANT_UNTER;
|
||||
const grenze = this.opt.hochkantUnter ?? HOCHKANT_UNTER;
|
||||
return !this.bearbeiten && this.behaelter.clientWidth > 0 && this.behaelter.clientWidth < grenze;
|
||||
}
|
||||
|
||||
/** Lage eines Kreises in der aktuellen Ansicht. */
|
||||
@@ -280,7 +288,7 @@
|
||||
const r = this.radius(k);
|
||||
const id = "enf" + this.nummer + "-" + k.id;
|
||||
const g = el("g", {
|
||||
class: "enf-knoten" + (k.aktiv ? "" : " enf-aus") + ((k.klick && !this.bearbeiten) ? " enf-klickbar" : ""),
|
||||
class: "enf-knoten" + (k.aktiv ? "" : " enf-aus") + (this.klickbar(k) ? " enf-klickbar" : ""),
|
||||
transform: `translate(${x} ${y})`,
|
||||
style: `--farbe:${k.farbe};--verz:${k.tiefe * 110}ms`,
|
||||
"data-knoten": k.id,
|
||||
@@ -319,7 +327,7 @@
|
||||
|
||||
const zeichen = el("g", { class: "enf-zeichen" }, koerper);
|
||||
|
||||
if (k.klick && !this.bearbeiten) {
|
||||
if (this.klickbar(k)) {
|
||||
const t = el("g", { class: "enf-tipp", transform: `translate(${r * 0.72} ${-r * 0.72})` }, koerper);
|
||||
el("circle", { r: 15 }, t);
|
||||
el("path", { d: "M-3.5-6l6 6-6 6" }, t);
|
||||
|
||||
@@ -565,27 +565,6 @@ var chartSettings = {
|
||||
});
|
||||
}
|
||||
|
||||
const wateringTimersLastSeen = {};
|
||||
// Eine Tabelle fuer alles: Ventil, Beschriftung und die Anzeige-Ids im
|
||||
// Realtime-SVG. Vorher standen dieselben Zuordnungen in drei getrennten
|
||||
// Tabellen.
|
||||
const wateringZones = {
|
||||
troughs: {
|
||||
topic: "Gartenwasser/vorn", valve: 1, label: "Tröge",
|
||||
statusId: "wateringTroughStatus", iconId: "wateringTroughIcon"
|
||||
},
|
||||
frontGarden: {
|
||||
topic: "Gartenwasser/vorn", valve: 6, label: "Vorn",
|
||||
statusId: "wateringFrontStatus", iconId: "wateringFrontIcon"
|
||||
},
|
||||
raisedBeds: {
|
||||
topic: "Gartenwasser/hinten", valve: 6, label: "Beete",
|
||||
statusId: "wateringBedsStatus", iconId: "wateringBedsIcon"
|
||||
}
|
||||
};
|
||||
// Feste Liste - der Sekundentakt soll sie nicht jedes Mal neu aufbauen.
|
||||
const wateringZoneList = Object.entries(wateringZones);
|
||||
|
||||
function wateringCommand(topic, command, directTopic = false) {
|
||||
const feedback = document.getElementById("watering-feedback");
|
||||
const targetTopic = directTopic ? topic : topic + "/set";
|
||||
@@ -643,133 +622,6 @@ var chartSettings = {
|
||||
updateWateringStatus();
|
||||
}
|
||||
|
||||
// Die Anzeige laeuft im Sekundentakt, die Nutzlasten aendern sich aber nur,
|
||||
// wenn eine neue MQTT-Nachricht kommt. messageReceived legt dabei jedes Mal
|
||||
// ein neues Buffer-Objekt ab - der Identitaetsvergleich reicht also als
|
||||
// Gueltigkeitspruefung, ein Invalidieren von Hand braucht es nicht.
|
||||
const wateringJsonCache = {};
|
||||
const wateringDecoder = new TextDecoder();
|
||||
|
||||
function wateringJson(topic) {
|
||||
const value = mqttData[topic];
|
||||
if (!value) return null;
|
||||
const cached = wateringJsonCache[topic];
|
||||
if (cached && cached.raw === value) return cached.parsed;
|
||||
|
||||
let parsed = null;
|
||||
try {
|
||||
parsed = JSON.parse(typeof value === "string" ? value : wateringDecoder.decode(value));
|
||||
} catch (error) {
|
||||
parsed = null;
|
||||
}
|
||||
wateringJsonCache[topic] = { raw: value, parsed: parsed };
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function wateringHistory(topic, valve) {
|
||||
const data = wateringJson(topic + "/LastWatering");
|
||||
if (!data) return { dateTime: "--", duration: "--" };
|
||||
return {
|
||||
dateTime: data["wateringDateTime" + valve] || "--",
|
||||
duration: data["wateringDurationSecs" + valve] !== undefined
|
||||
? wateringTimeLabel(Number(data["wateringDurationSecs" + valve]))
|
||||
: "--"
|
||||
};
|
||||
}
|
||||
|
||||
// Tagesdatum im Format der Ventilsteuerung ("29.08.26").
|
||||
function wateringDayStamp(date) {
|
||||
const zwei = (zahl) => String(zahl).padStart(2, "0");
|
||||
return zwei(date.getDate()) + "." + zwei(date.getMonth() + 1) + "." + zwei(date.getFullYear() % 100);
|
||||
}
|
||||
|
||||
// Tagessummen der Ventilsteuerung: WateringToday fuer den laufenden Tag,
|
||||
// LastWateringDay fuer den letzten Tag, an dem ueberhaupt bewaessert wurde.
|
||||
// Beide Nutzlasten tragen den zugehoerigen Tag als "wateringDay" mit. Bei
|
||||
// WateringToday wird der geprueft - die Nachricht ist retained, nach einem
|
||||
// Tageswechsel ohne Bewaesserung stuende sonst die Summe von gestern als
|
||||
// "heute" in der Karte.
|
||||
function wateringDayTotal(topic, subTopic, feld, valve, mussHeuteSein) {
|
||||
const data = wateringJson(topic + "/" + subTopic);
|
||||
if (!data) return { label: "--", day: "", sekunden: null };
|
||||
const tag = data.wateringDay || "";
|
||||
const sekunden = data[feld + valve];
|
||||
if (sekunden === undefined) return { label: "--", day: tag, sekunden: null };
|
||||
if (mussHeuteSein && tag !== wateringDayStamp(new Date())) {
|
||||
return { label: "--", day: tag, sekunden: null };
|
||||
}
|
||||
return { label: wateringTimeLabel(Number(sekunden)), day: tag, sekunden: Number(sekunden) };
|
||||
}
|
||||
|
||||
function wateringAutoActive(topic, mode) {
|
||||
const timers = wateringJson(topic + "/Timers");
|
||||
return !!(timers && timers.auto === mode);
|
||||
}
|
||||
|
||||
function wateringValue(data, valve) {
|
||||
if (!data) return null;
|
||||
const candidates = [
|
||||
data.timers && data.timers[valve],
|
||||
data.timers && data.timers[String(valve)],
|
||||
data[valve],
|
||||
data[String(valve)],
|
||||
data.valves && data.valves[valve],
|
||||
data.valves && data.valves[String(valve)],
|
||||
data.status && data.status[valve],
|
||||
data.status && data.status[String(valve)],
|
||||
data.valveState && data.valveState[valve],
|
||||
data.valveState && data.valveState[String(valve)],
|
||||
data.setValveState && data.setValveState[valve],
|
||||
data.setValveState && data.setValveState[String(valve)]
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (candidate !== undefined) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function wateringState(value) {
|
||||
if (value && typeof value === "object") {
|
||||
return value.state ?? value.status ?? value.valveState ?? value.set;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function wateringSeconds(value) {
|
||||
if (value && typeof value === "object") {
|
||||
value = value.remaining ?? value.timer ?? value.seconds ?? value.time;
|
||||
}
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
function wateringLabel(topic, valve) {
|
||||
const timers = wateringJson(topic + "/Timers");
|
||||
const status = wateringJson(topic + "/status");
|
||||
const lastSeen = wateringTimersLastSeen[topic + "/Timers"];
|
||||
if (!lastSeen || Date.now() - lastSeen > 3000) return "aus";
|
||||
const remaining = wateringValue(timers, valve);
|
||||
if (remaining !== null) {
|
||||
const seconds = wateringSeconds(remaining);
|
||||
if (seconds !== 0) return wateringTimeLabel(Math.abs(seconds));
|
||||
return "aus";
|
||||
}
|
||||
const state = wateringState(wateringValue(status, valve));
|
||||
if (state === true || state === 1 || state === "1" || state === "open" || state === "OPEN") return "an";
|
||||
if (state === false || state === 0 || state === "0" || state === "close" || state === "closed" || state === "CLOSED") return "aus";
|
||||
return "--";
|
||||
}
|
||||
|
||||
function wateringTimeLabel(seconds) {
|
||||
if (seconds < 60) return seconds + "s";
|
||||
return Math.ceil(seconds / 60) + "m";
|
||||
}
|
||||
|
||||
function wateringIsPaused(topic, valve) {
|
||||
const timers = wateringJson(topic + "/Timers");
|
||||
const remaining = wateringValue(timers, valve);
|
||||
return remaining !== null && wateringSeconds(remaining) < 0;
|
||||
}
|
||||
|
||||
function updateWateringStatus() {
|
||||
wateringZoneList.forEach(([key, zone]) => {
|
||||
const state = wateringLabel(zone.topic, zone.valve);
|
||||
@@ -855,27 +707,6 @@ var chartSettings = {
|
||||
button.classList.toggle("btn-outline-danger", active);
|
||||
}
|
||||
|
||||
function updateWateringIndicator(status, icon, name, state, paused, bewaessert) {
|
||||
// "Aus" bleibt stehen, solange die Zone heute noch kein Wasser hatte;
|
||||
// danach ersetzt das Haekchen (U+2713, ohne Emoji-Variationsselektor ->
|
||||
// einfarbig, folgt der Icon-Farbe) den Text, um Platz auf der textPath zu
|
||||
// sparen. Laeuft im Sekundentakt, aendert sich aber selten - ohne Pruefung
|
||||
// wuerde jede Sekunde derselbe Wert geschrieben und das Icon samt
|
||||
// Filter neu gezeichnet.
|
||||
const text = state === "aus" ? (bewaessert ? "✓" : "Aus") : state;
|
||||
if (status.textContent !== text) status.textContent = text;
|
||||
// Das Blau ("#3A6292") entspricht dem Wassertropfen-Icon (id="water").
|
||||
const color = state !== "aus" ? (paused ? "#ffc107" : "#20c997") : bewaessert ? "#3A6292" : "#6c757d";
|
||||
if (icon.getAttribute("color") !== color) {
|
||||
icon.setAttribute("color", color);
|
||||
icon.setAttribute("fill", color);
|
||||
icon.style.color = color;
|
||||
}
|
||||
const titel = name + ": " + (state === "aus" && bewaessert ? "Aus (heute bereits bewässert)" : state);
|
||||
const titelKnoten = icon.querySelector("title");
|
||||
if (titelKnoten.textContent !== titel) titelKnoten.textContent = titel;
|
||||
}
|
||||
|
||||
window.setInterval(updateWateringStatus, 1000);
|
||||
|
||||
|
||||
|
||||
+11
-2
@@ -34,6 +34,15 @@
|
||||
const uhrzeit = d => String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
|
||||
const schutz = s => String(s).replace(/[&<>"]/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
||||
|
||||
/** Zeitpunkt in h Stunden: "um 17:40", "morgen 06:10" oder "in 3 Tagen". */
|
||||
function wann(h) {
|
||||
const ziel = new Date(Date.now() + h * 3600000);
|
||||
const tage = Math.round((new Date(ziel).setHours(0, 0, 0, 0) - new Date().setHours(0, 0, 0, 0)) / 86400000);
|
||||
if (tage <= 0) return "um " + uhrzeit(ziel);
|
||||
if (tage === 1) return "morgen " + uhrzeit(ziel);
|
||||
return "in " + tage + " Tagen";
|
||||
}
|
||||
|
||||
function leser(daten) {
|
||||
return {
|
||||
da(t) { return daten[t] !== undefined; },
|
||||
@@ -219,10 +228,10 @@
|
||||
let prognose = "in Ruhe";
|
||||
if (p > 50 && kwh) {
|
||||
const h = kwh * (100 - soc) / 100 / (p / 1000);
|
||||
prognose = soc >= 99.5 ? "voll" : "voll um " + uhrzeit(new Date(Date.now() + h * 3600000));
|
||||
prognose = soc >= 99.5 ? "voll" : "voll " + wann(h);
|
||||
} else if (p < -50 && kwh) {
|
||||
const h = kwh * soc / 100 / (-p / 1000);
|
||||
prognose = "leer um " + uhrzeit(new Date(Date.now() + h * 3600000));
|
||||
prognose = "leer " + wann(h);
|
||||
}
|
||||
if (p > 50 && s.grenze > 0 && s.grenze < 100) prognose += " · begrenzt auf " + Math.round(s.grenze) + " %";
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
/**
|
||||
* Vollbildanzeige für ein festes Display, gedacht für den Raspberry Pi mit
|
||||
* dem 7-Zoll-Display (800 x 480) im Kioskmodus:
|
||||
*
|
||||
* index.php?action=anzeige ruhige Animationen, kein Mauszeiger
|
||||
* index.php?action=anzeige&animation=1 alle Animationen (schnellere Geräte)
|
||||
* index.php?action=anzeige&maus=1 Mauszeiger sichtbar (zum Einrichten)
|
||||
*
|
||||
* Eine eigene Seite ohne Kopf, Seitenleiste und die Karten der Solar-Seite:
|
||||
* links eine schmale Leiste mit Übersicht, Anlage und Speicher, rechts die
|
||||
* Ansicht auf voller Höhe. Gezeichnet wird mit denselben Bausteinen wie auf
|
||||
* der Solar-Seite (energiefluss.js, anlage.js, speicher.js); verdrahtet
|
||||
* werden sie von js/solar/anzeige.js.
|
||||
*
|
||||
* index.php bindet für diese Seite weder header.php noch footer.php ein
|
||||
* ("eigen" in der Seitentabelle) - deshalb steht der Kopf hier selbst.
|
||||
*/
|
||||
require_once(__DIR__ . "/energiefluss.php");
|
||||
|
||||
$ruhig = empty($_GET["animation"]);
|
||||
$maus = !empty($_GET["maus"]);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||||
<title>Energiefluss</title>
|
||||
<link rel="icon" type="image/png" href="assets/img/favicon.png" />
|
||||
<link rel="stylesheet" href="assets/fonts/font_poppins.css" />
|
||||
<link rel="stylesheet" href="css/bootstrap-icons.min.css" />
|
||||
<link rel="stylesheet" href="./css/solar.css?v=<?= filemtime("css/solar.css") ?>" />
|
||||
</head>
|
||||
<body class="anzeige<?= $ruhig ? " anzeige-ruhig" : "" ?><?= $maus ? "" : " anzeige-ohne-maus" ?>">
|
||||
<nav class="anzeige-leiste" aria-label="Ansicht">
|
||||
<button type="button" data-anzeige="fluss" aria-pressed="true" title="Übersicht"><i class="bi bi-diagram-3"></i></button>
|
||||
<button type="button" data-anzeige="anlage" aria-pressed="false" title="Anlage"><i class="bi bi-grid-3x2-gap"></i></button>
|
||||
<button type="button" data-anzeige="speicher" aria-pressed="false" title="Speicher"><i class="bi bi-battery-half"></i></button>
|
||||
<div class="anzeige-fuss">
|
||||
<span class="anzeige-punkt" id="anzeigeVerbindung" title="Verbindung wird aufgebaut"></span>
|
||||
<span class="anzeige-uhr" id="anzeigeUhr"></span>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="anzeige-inhalt">
|
||||
<div class="anzeige-flaeche" data-anzeige-flaeche="fluss" id="anzeige-fluss"></div>
|
||||
<div class="anzeige-flaeche" data-anzeige-flaeche="anlage" id="anzeige-anlage" hidden></div>
|
||||
<div class="anzeige-flaeche" data-anzeige-flaeche="speicher" id="anzeige-speicher" hidden></div>
|
||||
</main>
|
||||
<script>
|
||||
// Abweichungen dieses Hauses vom Katalog, siehe restricted/energiefluss.php.
|
||||
const energieflussEinstellungen = <?= json_encode((object)energieflussLaden(), JSON_UNESCAPED_UNICODE) ?>;
|
||||
const anzeigeRuhig = <?= $ruhig ? "true" : "false" ?>;
|
||||
</script>
|
||||
<script src="js/mqtt.min.js"></script>
|
||||
<?php foreach ($page["scripts"] as $script): ?>
|
||||
<script src="<?= $script ?>?v=<?= filemtime($script) ?>"></script>
|
||||
<?php endforeach; ?>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user