Files
Smart-Dashboard/js/solar/energieflussEinstellungen.js
T
adminandClaude Opus 5 a59277e33e Solar: neue Energiefluss-Uebersicht, anpassbar je Haus
Das feste realtime.svg ist ersetzt durch einen Katalog
(js/solar/energieflussKatalog.js: jeder Kreis einmal, samt Werten aus MQTT)
und einen Renderer (js/solar/energiefluss.js): Ringe mit Segmenten,
Fluesse als Boegen mit laufenden Punkten, Fuellstandswelle fuer Batterie
und Auto, animierte Symbole, Plaketten, Hochkant-Ansicht auf schmalen
Schirmen. Hierarchie und Lage wie bisher: Quellen -> Wechselrichter ->
Verbrauch -> OG/EG/UG -> Wallboxen, Heizstab.

Einstellungen -> Uebersicht: Kreise ein/aus (Kinder ruecken eine Ebene
hoeher), Namen, Lage per Ziehen in der Vorschau, PV-Flaechen. Gespeichert
werden nur Abweichungen in homeMesh.energiefluss.

Der PV-Lageplan hinter der Sonne liegt unveraendert als assets/img/pvplan.svg
und ist ueber den Umschalter Uebersicht/Anlage erreichbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 16:42:52 +02:00

280 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ==========================================================================
Reiter "Übersicht": die Energiefluss-Übersicht an dieses Haus anpassen.
Links die Übersicht selbst im Bearbeitungsmodus (energiefluss.js) mit
Beispielwerten, rechts je Kreis Schalter und Name, beim PV-Kreis die
Flächen. Verschoben wird in der Vorschau. Gespeichert wird der ganze Stand
auf einmal (restricted/energiefluss.php hält davon nur die Abweichungen).
Nutzt textSchuetzen() aus settings.js.
========================================================================== */
(function () {
"use strict";
// Beispielwerte: ein sonniger Nachmittag, Auto lädt im EG, Heizstab läuft.
const BEISPIEL = {
"solarManager/P_PV": "8400", "solarManager/P_PVn": JSON.stringify([2600, 1300, 130, 130, 140, 140, 130, 130, 135, 135, 130, 130, 120, 120,
155, 150, 152, 158, 150, 149, 150, 154, 151, 150, 153, 155, 150, 92, 150, 152, 151, 150, 149, 0]),
"solarManager/P_Grid": "-900", "solarManager/i_l1evu": "-1.3", "solarManager/i_l2evu": "-1.4", "solarManager/i_l3evu": "-1.2",
"solarManager/P_Akku": "-2100", "solarManager/SOC": "64", "solarManager/crgMaxPct": "100",
"solarManager/autarky": "100", "solarManager/eff": "96",
"solarManager/P_Load": "-5400", "solarManager/og": "-350", "solarManager/eg": "3720", "solarManager/ug": "270",
"solarManager/pHeat": "1000", "solarManager/t_buffT": "61", "solarManager/t_buffM": "48", "solarManager/t_buffB": "35",
"solarManager/heatMode": "eco", "solarManager/heatOG": "1", "solarManager/heatEG": "0",
"solarManager/evPower": "3.2", "solarManager/evSOC": "78", "solarManager/evFuel": "0", "solarManager/evMode": "Eco",
"solarManager/evPlug": "1", "solarManager/evLock": "1", "solarManager/carRemChrg": "95",
"solarManager/evPowerOG": "0", "solarManager/evPlugOG": "0", "solarManager/evModeOG": "Eco",
};
let bild = null;
let gespeichert = {}; // Stand der Datenbank
let stand = {}; // Stand der Maske
let geladen = false;
const $ = id => document.getElementById(id);
const katalog = () => ENERGIEFLUSS_KATALOG.knoten;
const kopie = o => JSON.parse(JSON.stringify(o));
function hinweis(text, klasse) {
const ziel = $("energieflussHinweis");
if (!ziel) return;
ziel.className = "small " + (klasse || "text-body-secondary");
ziel.textContent = text;
}
function geaendert() {
const sauber = s => JSON.stringify(Object.keys(s).sort().map(k => [k, s[k]]));
const anders = sauber(normalisiert(stand)) !== sauber(normalisiert(gespeichert));
hinweis(anders ? "Nicht gespeichert." : "", anders ? "text-warning" : "");
}
/** Nur echte Abweichungen, in fester Form - zum Vergleichen und Schicken. */
function normalisiert(s) {
const aus = {};
for (const k of katalog()) {
const e = s[k.id] || {};
const eintrag = {
aktiv: k.pflicht ? true : e.aktiv !== false,
name: (e.name || "").trim() || null,
x: e.x ?? null, y: e.y ?? null,
optionen: e.optionen || null,
};
if (eintrag.x === k.x && eintrag.y === k.y) { eintrag.x = null; eintrag.y = null; }
if (!eintrag.aktiv || eintrag.name || eintrag.x !== null || eintrag.optionen) aus[k.id] = eintrag;
}
return aus;
}
function vorschauZeichnen() {
if (!bild) {
bild = new Energiefluss($("energieflussVorschau"), {
bearbeiten: true,
einstellungen: stand,
onVerschoben(id, x, y) {
stand[id] = Object.assign({}, stand[id], { x, y });
listeZeichnen();
geaendert();
},
});
}
bild.setzeEinstellungen(normalisiert(stand));
bild.zeichnen();
bild.aktualisieren(BEISPIEL);
}
/* ------------------------------------------------------------ Liste */
function listeZeichnen() {
const ziel = $("energieflussListe");
const wirksam = bild ? bild.nachId : {};
ziel.innerHTML = katalog().map(k => {
const e = stand[k.id] || {};
const aktiv = k.pflicht || e.aktiv !== false;
const w = wirksam[k.id];
const eltern = w && w.elternWirksam;
const elternName = eltern ? (stand[eltern.id] && stand[eltern.id].name) || eltern.titel || eltern.name : null;
const verschoben = e.x != null && (e.x !== k.x || e.y !== k.y);
const flaechen = k.id === "pv" && aktiv ? flaechenZeichnen(k, e) : "";
return `
<div class="enf-e-zeile${aktiv ? "" : " enf-e-aus"}" data-id="${k.id}">
<div class="form-check form-switch m-0" title="${k.pflicht ? "Gehört immer dazu" : "Ein- oder ausschalten"}">
<input class="form-check-input" type="checkbox" id="enf-aktiv-${k.id}" data-feld="aktiv"
${aktiv ? "checked" : ""} ${k.pflicht ? "disabled" : ""}>
</div>
<label class="enf-e-name" for="enf-name-${k.id}">
<span class="enf-e-punkt" style="--farbe:${k.farbe}"></span>
<input class="form-control form-control-sm" id="enf-name-${k.id}" data-feld="name" maxlength="40"
placeholder="${textSchuetzen(k.name)}" title="${textSchuetzen(k.titel || k.name)}" value="${textSchuetzen(e.name || "")}" ${aktiv ? "" : "disabled"}>
</label>
<span class="enf-e-eltern small text-body-secondary">${elternName ? "an " + textSchuetzen(elternName) : k.eltern ? "" : "frei"}</span>
<button type="button" class="btn btn-sm btn-link text-body-secondary p-0" data-aktion="lage"
title="Lage aus dem Katalog" ${verschoben ? "" : "hidden"}><i class="bi bi-arrow-counterclockwise"></i></button>
${flaechen}
</div>`;
}).join("");
}
function flaechenZeichnen(k, e) {
const liste = (e.optionen && e.optionen.flaechen) || k.optionen.flaechen;
const eigen = !!(e.optionen && e.optionen.flaechen);
return `
<div class="enf-e-flaechen">
<div class="small text-body-secondary mb-1">
Flächen im Sonnenring – welche Einträge von <code>P_PVn</code> dazugehören
${eigen ? `<button type="button" class="btn btn-sm btn-link p-0 ms-2" data-aktion="flaechen-zurueck">Katalog</button>` : ""}
</div>
${liste.map((f, i) => `
<div class="enf-e-flaeche" data-index="${i}">
<input type="color" class="form-control form-control-sm form-control-color" data-flaeche="farbe" value="${f.farbe}" title="Farbe">
<input class="form-control form-control-sm" data-flaeche="name" value="${textSchuetzen(f.name)}" placeholder="Name" maxlength="20">
<input class="form-control form-control-sm" type="number" min="0" max="99" data-flaeche="von" value="${f.von}" title="erster Eintrag">
<input class="form-control form-control-sm" type="number" min="0" max="99" data-flaeche="bis" value="${f.bis}" title="letzter Eintrag">
<button type="button" class="btn btn-sm btn-link text-body-secondary p-0" data-aktion="flaeche-weg" title="Fläche entfernen"><i class="bi bi-x-lg"></i></button>
</div>`).join("")}
<button type="button" class="btn btn-sm btn-outline-secondary mt-1" data-aktion="flaeche-neu"><i class="bi bi-plus-lg me-1"></i>Fläche</button>
</div>`;
}
function flaechenLesen(zeile) {
return [...zeile.querySelectorAll(".enf-e-flaeche")].map(r => ({
name: r.querySelector('[data-flaeche="name"]').value.trim(),
von: Math.max(0, parseInt(r.querySelector('[data-flaeche="von"]').value, 10) || 0),
bis: Math.max(0, parseInt(r.querySelector('[data-flaeche="bis"]').value, 10) || 0),
farbe: r.querySelector('[data-flaeche="farbe"]').value,
}));
}
function flaechenSetzen(id, flaechen) {
const e = stand[id] = Object.assign({}, stand[id]);
e.optionen = Object.assign({}, e.optionen, { flaechen });
}
/* --------------------------------------------------------- Ereignisse */
function binden() {
const liste = $("energieflussListe");
liste.addEventListener("change", ev => {
const zeile = ev.target.closest(".enf-e-zeile");
if (!zeile) return;
const id = zeile.dataset.id;
if (ev.target.dataset.feld === "aktiv") {
stand[id] = Object.assign({}, stand[id], { aktiv: ev.target.checked });
vorschauZeichnen();
listeZeichnen();
} else if (ev.target.dataset.flaeche) {
flaechenSetzen(id, flaechenLesen(zeile));
vorschauZeichnen();
}
geaendert();
});
liste.addEventListener("input", ev => {
const zeile = ev.target.closest(".enf-e-zeile");
if (!zeile || ev.target.dataset.feld !== "name") return;
stand[zeile.dataset.id] = Object.assign({}, stand[zeile.dataset.id], { name: ev.target.value });
clearTimeout(binden._uhr);
binden._uhr = setTimeout(vorschauZeichnen, 300);
geaendert();
});
liste.addEventListener("click", ev => {
const knopf = ev.target.closest("[data-aktion]");
if (!knopf) return;
const zeile = knopf.closest(".enf-e-zeile");
const id = zeile.dataset.id;
switch (knopf.dataset.aktion) {
case "lage":
stand[id] = Object.assign({}, stand[id], { x: null, y: null });
break;
case "flaeche-neu": {
const f = flaechenLesen(zeile);
const bis = f.length ? f[f.length - 1].bis + 1 : 0;
f.push({ name: "Fläche " + (f.length + 1), von: bis, bis, farbe: "#f7c000" });
flaechenSetzen(id, f);
break;
}
case "flaeche-weg": {
const f = flaechenLesen(zeile);
f.splice(Number(knopf.closest(".enf-e-flaeche").dataset.index), 1);
flaechenSetzen(id, f);
break;
}
case "flaechen-zurueck":
if (stand[id] && stand[id].optionen) {
const o = Object.assign({}, stand[id].optionen);
delete o.flaechen;
stand[id].optionen = Object.keys(o).length ? o : null;
}
break;
default:
return;
}
vorschauZeichnen();
listeZeichnen();
geaendert();
});
$("energieflussSpeichern").addEventListener("click", speichern);
$("energieflussNeuLaden").addEventListener("click", () => {
stand = kopie(gespeichert);
vorschauZeichnen(); listeZeichnen(); geaendert();
});
$("energieflussZuruecksetzen").addEventListener("click", () => {
stand = {};
vorschauZeichnen(); listeZeichnen(); geaendert();
});
}
async function speichern() {
const knopf = $("energieflussSpeichern");
knopf.disabled = true;
try {
const n = normalisiert(stand);
const knoten = Object.keys(n).map(id => Object.assign({ id }, n[id]));
const antwort = await fetch("ajax/settings.php?action=energiefluss-save", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ knoten }),
});
const d = await antwort.json();
if (!antwort.ok || d.error) throw new Error(d.error || "HTTP " + antwort.status);
gespeichert = d.einstellungen || {};
stand = kopie(gespeichert);
vorschauZeichnen(); listeZeichnen();
hinweis("Gespeichert – die Solar-Seite zeigt es beim nächsten Laden.", "text-success");
} catch (e) {
hinweis(e.message, "text-danger");
} finally {
knopf.disabled = false;
}
}
async function laden() {
if (geladen) return;
geladen = true;
try {
const antwort = await fetch("ajax/settings.php?action=energiefluss", { cache: "no-store" });
const d = await antwort.json();
if (!antwort.ok || d.error) throw new Error(d.error || "HTTP " + antwort.status);
gespeichert = d.einstellungen || {};
stand = kopie(gespeichert);
binden();
vorschauZeichnen();
listeZeichnen();
} catch (e) {
geladen = false;
$("energieflussListe").innerHTML = '<div class="alert alert-danger mb-0">Übersicht nicht abrufbar: '
+ textSchuetzen(e.message) + "</div>";
}
}
document.addEventListener("DOMContentLoaded", () => {
if (!$("energieflussVorschau") || typeof Energiefluss === "undefined") return;
// Erst zeichnen, wenn der Reiter offen ist - verborgen hat die Vorschau keine Größe.
const knopf = $("einstellung-energiefluss-tab");
if (knopf) knopf.addEventListener("shown.bs.tab", laden);
if ($("einstellung-energiefluss").classList.contains("active")) laden();
// Ein Anker in der Adresse öffnet den Reiter erst nach DOMContentLoaded.
setTimeout(() => { if ($("einstellung-energiefluss").classList.contains("active")) laden(); }, 0);
});
})();