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:
2026-09-15 19:15:46 +02:00
co-authored by Claude Opus 5
parent 377aa03637
commit b8b2e66fd0
10 changed files with 503 additions and 180 deletions
-169
View File
@@ -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);