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
+206
View File
@@ -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);
});
}