Heizstab- und Bewaesserungsfenster im neuen Look, Bewaesserungszeilen enger

Beide Fenster lesen den Zustand live aus MQTT und uebernehmen jede Wahl
sofort (bedienfenster.js). submitFormAjax entfaellt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 21:26:16 +02:00
co-authored by Claude Opus 5
parent bf5fdfefa4
commit 6e805678d4
9 changed files with 466 additions and 311 deletions
+11 -157
View File
@@ -443,9 +443,9 @@ var chartSettings = {
function openModal(type) {
// Das Ladefenster und die Bewaesserung uebernehmen jede Aenderung sofort -
// "Save changes" braucht nur noch der Heizstab.
document.getElementById("modalSaveBtn").style.display = type === "heater" ? "" : "none";
// Alle Fenster der Solar-Seite uebernehmen jede Aenderung sofort -
// "Save changes" braucht hier keines mehr.
document.getElementById("modalSaveBtn").style.display = "none";
switch(type){
case "CarEG":
document.getElementById("modal-title").innerHTML = "Autoladung EG";
@@ -471,8 +471,6 @@ var chartSettings = {
modalBodyElement = document.getElementById('modal-body');
modalBodyElement.innerHTML = loadingHTML("Wird geladen...");
document.getElementById("modalSaveBtn").addEventListener("click", submitFormAjax);
document.getElementById("modalSaveBtn").contentURL = contentURL;
modalEV.show();
fetch(contentURL, {
method: 'GET',
@@ -485,25 +483,17 @@ var chartSettings = {
.then(html => {
modalBodyElement.innerHTML = html;
var slider = document.getElementById("modal-slider");
var span = document.getElementById("modal-slider-label");
// Aufbau und Logik der Fenster: ladefenster.js und bedienfenster.js.
switch(type){
case "CarEG":
case "CarOG":
// Alles Weitere in js/solar/ladefenster.js.
ladefensterStarten(modalBodyElement.querySelector(".lade"), contentURL);
break;
case "heater":
bindModalSlider(slider, span, value => value == 0 ? "Automatik" : (value / 10) + " kW");
if(mqttData["solarManager/heatMode"] == "eco"){
document.getElementById("modal-slider").value = 0;
}else{
document.getElementById("modal-slider").value = Math.round(mqttData["solarManager/pHeat"]/100);
}
document.getElementById("modal-slider").dispatchEvent(new Event('input'));
break;
case "heater":
heizfensterStarten(modalBodyElement.querySelector(".heiz"), contentURL);
break;
case "watering":
initializeWateringModal();
wasserfensterStarten(modalBodyElement.querySelector(".wasser"), contentURL);
break;
}
@@ -513,146 +503,10 @@ var chartSettings = {
});
}
function wateringCommand(topic, command, directTopic = false) {
const feedback = document.getElementById("watering-feedback");
const targetTopic = directTopic ? topic : topic + "/set";
const payload = typeof command === "string" ? command : JSON.stringify(command);
const endpoint = window.location.pathname.replace(/\/index\.php$/, "").replace(/\/$/, "") + "/ajax/watering.php";
const showFeedback = (message, type) => {
if (!feedback) return;
feedback.textContent = message;
feedback.className = "alert alert-" + type + " mb-3";
feedback.style.whiteSpace = "pre-line";
};
fetch(endpoint, {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ topic: targetTopic, payload: payload })
}).then(async (response) => {
const result = await response.json();
if (!response.ok || !result.success) throw new Error(result.message || "Unbekannter MQTT-Fehler.");
showFeedback("Befehl gesendet.\nTopic: " + result.topic + "\nInhalt: " + result.payload, "success");
}).catch((error) => {
showFeedback("Befehl konnte nicht gesendet werden: " + error.message + "\nTopic: " + targetTopic + "\nInhalt: " + payload, "danger");
});
}
function initializeWateringModal() {
document.querySelectorAll("[data-watering-toggle]").forEach((button) => {
const updateButton = () => {
const zone = wateringZones[button.dataset.wateringZone];
setzeZonenButton(button, wateringLabel(zone.topic, zone.valve));
};
button.addEventListener("click", () => {
const zone = wateringZones[button.dataset.wateringZone];
if (wateringLabel(zone.topic, zone.valve) === "aus") {
const timerInput = document.querySelector(`[data-watering-timer='${button.dataset.wateringZone}']`);
const timer = Math.min(120, Math.max(1, Number(timerInput.value) || 30));
wateringCommand(zone.topic, { valve: zone.valve, set: "open", timer: timer });
} else {
wateringCommand(zone.topic, { valve: zone.valve, set: "close" });
}
});
updateButton();
});
document.querySelectorAll("[data-watering-auto-toggle]").forEach((button) => {
button.addEventListener("click", () => {
const topics = (button.dataset.wateringTopics || button.dataset.wateringTopic).split(",");
const modes = button.dataset.wateringAuto.split(",");
const active = topics.every((topic, index) => wateringAutoActive(topic, modes[index]));
topics.forEach((topic, index) => {
wateringCommand(topic + "/auto", active ? "Stop" : modes[index], true);
});
});
updateWateringAutoButton(button);
});
updateWateringStatus();
}
// Die Zonensymbole im Bewaesserungskreis; das Fenster aktualisiert sich
// selbst (bedienfenster.js).
function updateWateringStatus() {
wateringZoneList.forEach(([key, 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);
// Die Regenautomatik laeuft zum Sonnenaufgang; wenn jemand auf die
// Anzeige schaut, ist die Entscheidung laengst gefallen. Angezeigt wird
// deshalb nicht mehr, was geplant ist, sondern was passiert ist. Der
// Zaehler unterscheidet nicht, wer ausgeloest hat - eine Bewaesserung
// von Hand zaehlt genauso, und das ist so gewollt.
const bewaessert = state === "aus" && heute.sekunden > 0;
// Die Anzeigen liegen im Realtime-SVG und bleiben bestehen -> svgElement.
const status = svgElement(zone.statusId);
const icon = svgElement(zone.iconId);
if (status && icon) updateWateringIndicator(status, icon, zone.label, state, paused, bewaessert);
// Die Modal-Elemente werden bei jedem Oeffnen neu erzeugt -> nicht cachen.
setzeZonenButton(document.querySelector(`[data-watering-toggle][data-watering-zone='${key}']`), state);
setzeSchild(`[data-watering-state='${key}']`, state, paused, bewaessert);
const history = wateringHistory(zone.topic, zone.valve);
setzeZelle(`[data-watering-last='${key}']`, history.dateTime);
setzeZelle(`[data-watering-duration='${key}']`, history.duration);
const vortag = wateringDayTotal(zone.topic, "LastWateringDay", "wateringDurationDaySecs", zone.valve, false);
setzeZelle(`[data-watering-today='${key}']`, heute.label, heute.day && "Stand: " + heute.day);
setzeZelle(`[data-watering-prevday='${key}']`, vortag.label, vortag.day && "Bewässerungstag: " + vortag.day);
});
document.querySelectorAll("[data-watering-auto-toggle]").forEach(updateWateringAutoButton);
}
// Der Knopf zeigt die naechste Aktion, nicht den Zustand: laeuft die Zone,
// steht "Aus" darauf.
function setzeZonenButton(button, state) {
if (!button) return;
const beschriftung = state === "aus" ? "Ein" : "Aus";
if (button.textContent !== beschriftung) button.textContent = beschriftung;
button.classList.toggle("btn-success", state === "aus");
button.classList.toggle("btn-danger", state !== "aus");
}
// Zustandsschild der Zonenkarte, Farben wie im SVG: laufend gruen,
// pausiert gelb, heute schon bewaessert blau, sonst grau.
function setzeSchild(selektor, state, paused, bewaessert) {
const schild = document.querySelector(selektor);
if (!schild) return;
let text = "unbekannt";
let farbe = "bg-secondary";
if (state !== "--" && state !== "aus") {
text = paused ? "pausiert " + state : "läuft " + state;
farbe = paused ? "bg-warning text-dark" : "bg-success";
} else if (state === "aus") {
text = bewaessert ? "heute bewässert" : "aus";
farbe = bewaessert ? "bg-primary" : "bg-secondary";
}
if (schild.textContent !== text) schild.textContent = text;
const klasse = "badge " + farbe;
if (schild.className !== klasse) schild.className = klasse;
}
// Schreibt nur bei echter Aenderung - die Tabelle wird im Sekundentakt
// aktualisiert, ihre Werte aendern sich aber hoechstens minuetlich.
function setzeZelle(selektor, text, titel) {
const element = document.querySelector(selektor);
if (!element) return;
if (element.textContent !== text) element.textContent = text;
if (titel) {
if (element.getAttribute("title") !== titel) element.setAttribute("title", titel);
} else if (element.hasAttribute("title")) {
element.removeAttribute("title");
}
}
function updateWateringAutoButton(button) {
const topics = (button.dataset.wateringTopics || button.dataset.wateringTopic).split(",");
const modes = button.dataset.wateringAuto.split(",");
const active = topics.every((topic, index) => wateringAutoActive(topic, modes[index]));
if (!button.dataset.wateringLabel) {
button.dataset.wateringLabel = button.textContent.replace(/^Automatik\s+/, "");
}
button.textContent = (active ? "Aus: " : "Ein: ") + button.dataset.wateringLabel;
button.classList.toggle("btn-outline-primary", !active);
button.classList.toggle("btn-outline-danger", active);
wateringSymboleSetzen(svgElement);
}
window.setInterval(updateWateringStatus, 1000);