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>
This commit is contained in:
@@ -0,0 +1,556 @@
|
||||
/* ==========================================================================
|
||||
Energiefluss-Übersicht: zeichnet den Katalog (energieflussKatalog.js) als
|
||||
animiertes SVG und hält es mit den MQTT-Werten aktuell.
|
||||
|
||||
const bild = new Energiefluss(behaelter, {
|
||||
einstellungen, // Abweichungen aus homeMesh.energiefluss
|
||||
bearbeiten: false, // true = Einstellungsvorschau, Kreise ziehbar
|
||||
onKlick(knoten), // Kreis mit klick-Ziel angetippt
|
||||
onVerschoben(id, x, y), // nur beim Bearbeiten
|
||||
onGezeichnet(), // nach jedem (Neu-)Zeichnen
|
||||
});
|
||||
bild.aktualisieren(mqttData);
|
||||
|
||||
Der Renderer kennt keinen Kreis beim Namen. Alles, was einen Kreis
|
||||
ausmacht, steht im Katalog; hier steht nur, wie Kreise, Flüsse, Symbole
|
||||
und Plaketten grundsätzlich aussehen.
|
||||
|
||||
Zwei Ansichten: quer (Lage aus dem Katalog) und hochkant für schmale
|
||||
Schirme. Hochkant sind x und y vertauscht - aus "Quellen links, Verbraucher
|
||||
rechts" wird "Quellen oben, Verbraucher unten". So braucht es keine zweite
|
||||
Lage je Kreis, und eine eigene Lage aus den Einstellungen gilt in beiden.
|
||||
|
||||
Zur Leistung: die Flüsse laufen über stroke-dashoffset in groben Stufen
|
||||
(steps), und alle Animationen stehen still, solange die Karte nicht zu
|
||||
sehen ist. Werte werden nur geschrieben, wenn sie sich geändert haben.
|
||||
========================================================================== */
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const NS = "http://www.w3.org/2000/svg";
|
||||
const RADIUS = 78;
|
||||
const RADIUS_GROSS = 92;
|
||||
const RAND = 60; // Luft um die äußersten Kreise
|
||||
const HOCHKANT_UNTER = 720; // px Breite, ab der hochkant gezeichnet wird
|
||||
|
||||
/* ------------------------------------------------------------------------
|
||||
Symbole: 24er Raster, Mitte 0/0, Linien in currentColor. Teile mit
|
||||
Klasse enf-s-* werden über CSS animiert oder über den Zustand gesetzt.
|
||||
------------------------------------------------------------------------ */
|
||||
const SYMBOLE = {
|
||||
sonne: () => `
|
||||
<g class="enf-s-strahlen">
|
||||
${[0, 45, 90, 135, 180, 225, 270, 315].map(w =>
|
||||
`<path d="M0-8.2V-11" transform="rotate(${w})"/>`).join("")}
|
||||
</g>
|
||||
<circle r="4.6" class="enf-s-kern"/>`,
|
||||
|
||||
netz: () => `
|
||||
<path d="M-5 11.5L-1.6-10.5h3.2L5 11.5"/>
|
||||
<path d="M-9.5-5.5h19M-7-1h14"/>
|
||||
<path d="M-8.5-5.5v2.4M8.5-5.5v2.4M-6-1v2.4M6-1v2.4"/>
|
||||
<path d="M-3.3 3.5l6.8 8M3.3 3.5l-6.8 8" class="enf-s-fein"/>`,
|
||||
|
||||
akku: () => `
|
||||
<rect x="-6.5" y="-8.5" width="13" height="20" rx="2.4"/>
|
||||
<path d="M-2.6-11h5.2"/>
|
||||
<rect x="-4" y="-6" width="8" height="15" rx="1" class="enf-s-stand"/>
|
||||
<path d="M1.2-4.2l-4 5.6h3l-1.4 4.6 4.2-6h-3z" class="enf-s-blitz"/>`,
|
||||
|
||||
wechselrichter: () => `
|
||||
<rect x="-10.5" y="-10.5" width="21" height="21" rx="3.5"/>
|
||||
<path d="M-10.5 10.5l21-21" class="enf-s-fein"/>
|
||||
<path d="M-7.5-5.2h5M-7.5-2.4h5"/>
|
||||
<path d="M1.8 4.2c1-1.7 2.1-1.7 3.1 0s2.1 1.7 3.1 0" class="enf-s-welle"/>`,
|
||||
|
||||
verbrauch: () => `
|
||||
<path d="M-11-1.5L0-10.5l11 9"/>
|
||||
<path d="M-8-4v14.5h16V-4"/>
|
||||
<path d="M1.3-3.8l-4.4 6.2h3.3l-1.6 5 4.6-6.6H-.1z" class="enf-s-blitz enf-s-an"/>`,
|
||||
|
||||
etageOben: () => etage(-4.5, 1.5),
|
||||
etageMitte: () => etage(1.5, 7.5),
|
||||
etageUnten: () => etage(7.5, 11.5),
|
||||
|
||||
wallbox: () => `
|
||||
<rect x="-10.5" y="-10.5" width="11" height="21" rx="2.2"/>
|
||||
<rect x="-8" y="-7.5" width="6" height="4" rx=".8" class="enf-s-fein"/>
|
||||
<path d="M-6.4 1.2l-2 3.5h2.2l-1 3.2 2.9-4.2h-2.2z" class="enf-s-blitz"/>
|
||||
<path d="M.5 2h2.8a2.3 2.3 0 0 1 2.3 2.3v3.9a1.9 1.9 0 0 0 3.8 0V-.5" class="enf-s-kabel"/>
|
||||
<path d="M7.2-4.6h3.6v2.3a1.8 1.8 0 0 1-3.6 0z"/>
|
||||
<path d="M8.2-4.6v-2M9.8-4.6v-2"/>`,
|
||||
|
||||
speicher: () => `
|
||||
<defs><clipPath id="{id}-tank"><rect x="-6.8" y="-10.8" width="13.6" height="19.6" rx="5.2"/></clipPath></defs>
|
||||
<g clip-path="url(#{id}-tank)" class="enf-s-temps">
|
||||
<rect x="-8" y="-12" width="16" height="7.6" class="enf-s-t0"/>
|
||||
<rect x="-8" y="-4.4" width="16" height="6.6" class="enf-s-t1"/>
|
||||
<rect x="-8" y="2.2" width="16" height="7.6" class="enf-s-t2"/>
|
||||
</g>
|
||||
<rect x="-7" y="-11" width="14" height="20" rx="5.4"/>
|
||||
<path d="M-3.4 5.6l1.1-1.8 1.2 1.8 1.1-1.8 1.2 1.8 1.1-1.8" class="enf-s-spirale"/>
|
||||
<path d="M-4 9v2.5M4 9v2.5"/>`,
|
||||
|
||||
tropfen: () => `
|
||||
<path d="M0-11c4.6 5.5 7 9 7 12.6a7 7 0 0 1-14 0C-7-2-4.6-5.5 0-11z"/>
|
||||
<path d="M-3.6 3.4c1.2 1 2.4 1 3.6 0s2.4-1 3.6 0" class="enf-s-welle"/>`,
|
||||
};
|
||||
|
||||
function etage(von, bis) {
|
||||
return `
|
||||
<rect x="-8" y="${von}" width="16" height="${bis - von}" class="enf-s-ebene"/>
|
||||
<path d="M-11.5-3L0-11l11.5 8"/>
|
||||
<path d="M-8-5v12.5h16V-5"/>
|
||||
<path d="M-8 1.5h16" class="enf-s-fein"/>
|
||||
<path d="M-12 7.5h24"/>
|
||||
<path d="M-8 7.5v4h16v-4" class="enf-s-fein enf-s-keller"/>`;
|
||||
}
|
||||
|
||||
/* Plaketten am Kreis: Zeichen aus Bootstrap Icons (liegt ohnehin im Dashboard). */
|
||||
const ZEICHEN = {
|
||||
eco: { glyph: "奈", farbe: "#6cbe58" }, // bi-leaf
|
||||
uhr: { glyph: "", farbe: "#e8a33a" }, // bi-clock-fill
|
||||
manuell: { glyph: "", farbe: "#c9a227" }, // bi-sliders
|
||||
stecker: { glyph: "", farbe: "#8fb8de" }, // bi-plug-fill
|
||||
schloss: { glyph: "", farbe: "#b9c0c8" }, // bi-lock-fill
|
||||
heizung: { glyph: "", farbe: "#e5624f" }, // bi-fire
|
||||
tank: { glyph: "", farbe: "#b9c0c8" }, // bi-fuel-pump-fill
|
||||
warnung: { glyph: "", farbe: "#e5624f" }, // bi-exclamation-triangle-fill
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------------
|
||||
Hilfen
|
||||
------------------------------------------------------------------------ */
|
||||
function el(name, attr, eltern) {
|
||||
const e = document.createElementNS(NS, name);
|
||||
if (attr) for (const k in attr) e.setAttribute(k, attr[k]);
|
||||
if (eltern) eltern.appendChild(e);
|
||||
return e;
|
||||
}
|
||||
|
||||
function leistungText(w) {
|
||||
const a = Math.abs(w);
|
||||
if (a < 1000) return Math.round(a) + " W";
|
||||
return (a / 1000).toFixed(a < 10000 ? 2 : 1).replace(".", ",") + " kW";
|
||||
}
|
||||
|
||||
/** Liest mqttData: Werte kommen als Buffer, Zahl und Text wie gehabt. */
|
||||
function leser(daten) {
|
||||
const cache = {};
|
||||
return {
|
||||
text(topic) {
|
||||
const v = daten[topic];
|
||||
return v === undefined ? undefined : String(v);
|
||||
},
|
||||
zahl(topic) {
|
||||
const v = Number(String(daten[topic]));
|
||||
return isFinite(v) ? v : 0;
|
||||
},
|
||||
json(topic) {
|
||||
if (topic in cache) return cache[topic];
|
||||
try { cache[topic] = JSON.parse(String(daten[topic])); } catch (e) { cache[topic] = null; }
|
||||
return cache[topic];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Farbe zwischen kalt und heiß, wie früher assignColor() fürs Pufferbild. */
|
||||
function tempFarbe(t) {
|
||||
const f = Math.max(0, Math.min(1, (t - 20) / 60));
|
||||
const kalt = [35, 137, 186], heiss = [186, 59, 35];
|
||||
return "rgb(" + kalt.map((k, i) => Math.round(k + (heiss[i] - k) * f)).join(",") + ")";
|
||||
}
|
||||
|
||||
let laufendeNummer = 0;
|
||||
|
||||
/* ------------------------------------------------------------------------
|
||||
Die Übersicht
|
||||
------------------------------------------------------------------------ */
|
||||
class Energiefluss {
|
||||
constructor(behaelter, opt = {}) {
|
||||
this.behaelter = behaelter;
|
||||
this.opt = opt;
|
||||
this.bearbeiten = !!opt.bearbeiten;
|
||||
this.nummer = ++laufendeNummer;
|
||||
this.letzte = null;
|
||||
this.setzeEinstellungen(opt.einstellungen || {});
|
||||
|
||||
// Hochkant oder quer - neu zeichnen nur, wenn die Ansicht wechselt.
|
||||
this.beobachter = new ResizeObserver(() => {
|
||||
const hoch = this.hochkantGewuenscht();
|
||||
if (hoch !== this.hochkant) this.zeichnen();
|
||||
});
|
||||
this.beobachter.observe(behaelter);
|
||||
|
||||
// Animationen anhalten, solange niemand hinsieht.
|
||||
this.sichtbar = true;
|
||||
this.sicht = new IntersectionObserver(eintraege => {
|
||||
this.sichtbar = eintraege[0].isIntersecting;
|
||||
this.pausieren();
|
||||
});
|
||||
this.sicht.observe(behaelter);
|
||||
document.addEventListener("visibilitychange", () => this.pausieren());
|
||||
}
|
||||
|
||||
/** Katalog und Abweichungen zu einer Liste zusammenführen. */
|
||||
setzeEinstellungen(einstellungen) {
|
||||
this.einstellungen = einstellungen;
|
||||
this.knoten = ENERGIEFLUSS_KATALOG.knoten.map(k => {
|
||||
const e = einstellungen[k.id] || {};
|
||||
return Object.assign({}, k, {
|
||||
aktiv: k.pflicht || e.aktiv !== false,
|
||||
anzeigeName: e.name || k.name,
|
||||
lage: [e.x ?? k.x, e.y ?? k.y],
|
||||
optionenWirksam: Object.assign({}, k.optionen || {}, e.optionen || {}),
|
||||
});
|
||||
});
|
||||
this.nachId = Object.fromEntries(this.knoten.map(k => [k.id, k]));
|
||||
// Wirksamer Elternkreis: der nächste eingeschaltete in der Kette.
|
||||
for (const k of this.knoten) {
|
||||
let e = k.eltern ? this.nachId[k.eltern] : null;
|
||||
while (e && !e.aktiv) e = e.eltern ? this.nachId[e.eltern] : null;
|
||||
k.elternWirksam = e;
|
||||
let tiefe = 0;
|
||||
for (let p = e; p; p = p.elternWirksam) tiefe++;
|
||||
k.tiefe = tiefe;
|
||||
}
|
||||
}
|
||||
|
||||
hochkantGewuenscht() {
|
||||
return !this.bearbeiten && this.behaelter.clientWidth > 0 && this.behaelter.clientWidth < HOCHKANT_UNTER;
|
||||
}
|
||||
|
||||
/** Lage eines Kreises in der aktuellen Ansicht. */
|
||||
punkt(k) {
|
||||
return this.hochkant ? [k.lage[1], k.lage[0]] : [k.lage[0], k.lage[1]];
|
||||
}
|
||||
|
||||
radius(k) {
|
||||
return k.gross ? RADIUS_GROSS : RADIUS;
|
||||
}
|
||||
|
||||
sichtbareKnoten() {
|
||||
return this.knoten.filter(k => k.aktiv || this.bearbeiten);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------- Zeichnen */
|
||||
zeichnen() {
|
||||
this.hochkant = this.hochkantGewuenscht();
|
||||
this.behaelter.innerHTML = "";
|
||||
const svg = this.svg = el("svg", {
|
||||
class: "enf" + (this.bearbeiten ? " enf-bearbeiten" : "") + (this.hochkant ? " enf-hoch" : ""),
|
||||
role: "img", "aria-label": "Energiefluss",
|
||||
}, this.behaelter);
|
||||
this.ebeneFluss = el("g", { class: "enf-fluesse" }, svg);
|
||||
this.ebeneKnoten = el("g", { class: "enf-knotenebene" }, svg);
|
||||
|
||||
this.elemente = {};
|
||||
for (const k of this.sichtbareKnoten()) this.knotenZeichnen(k);
|
||||
for (const k of this.sichtbareKnoten()) if (k.aktiv && k.elternWirksam) this.flussZeichnen(k);
|
||||
this.rahmenSetzen();
|
||||
this.pausieren();
|
||||
|
||||
if (this.letzte) {
|
||||
const d = this.letzte;
|
||||
this.letzte = null;
|
||||
this.aktualisieren(d);
|
||||
}
|
||||
if (this.opt.onGezeichnet) this.opt.onGezeichnet();
|
||||
}
|
||||
|
||||
rahmenSetzen() {
|
||||
const ks = this.sichtbareKnoten();
|
||||
if (!ks.length) return;
|
||||
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
||||
for (const k of ks) {
|
||||
const [x, y] = this.punkt(k), r = this.radius(k);
|
||||
x0 = Math.min(x0, x - r - 20); x1 = Math.max(x1, x + r + 20);
|
||||
y0 = Math.min(y0, y - r - 20); y1 = Math.max(y1, y + r + 56); // Name unter dem Kreis
|
||||
}
|
||||
this.svg.setAttribute("viewBox",
|
||||
[x0 - RAND / 2, y0 - RAND / 2, x1 - x0 + RAND, y1 - y0 + RAND].map(Math.round).join(" "));
|
||||
}
|
||||
|
||||
knotenZeichnen(k) {
|
||||
const [x, y] = this.punkt(k);
|
||||
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" : ""),
|
||||
transform: `translate(${x} ${y})`,
|
||||
style: `--farbe:${k.farbe};--verz:${k.tiefe * 110}ms`,
|
||||
"data-knoten": k.id,
|
||||
}, this.ebeneKnoten);
|
||||
|
||||
const koerper = el("g", { class: "enf-koerper" }, g);
|
||||
el("circle", { r: r + 17, class: "enf-hof" }, koerper);
|
||||
el("circle", { r, class: "enf-spur" }, koerper);
|
||||
const ring = el("g", { class: "enf-ring", transform: "rotate(-90)" }, koerper);
|
||||
el("circle", { r: r - 11, class: "enf-scheibe" }, koerper);
|
||||
|
||||
// Füllstand (Batterie, Auto): Welle, die mit dem Stand steigt.
|
||||
let fuellung = null;
|
||||
if (k.symbol === "akku" || k.symbol === "wallbox") {
|
||||
el("clipPath", { id: id + "-innen" }, el("defs", null, koerper))
|
||||
.appendChild(el("circle", { r: r - 11 }));
|
||||
const clip = el("g", { "clip-path": `url(#${id}-innen)` }, koerper);
|
||||
fuellung = el("g", { class: "enf-fuellung", style: `--stand:0;--r:${r - 11}` }, clip);
|
||||
const b = r * 4;
|
||||
el("path", {
|
||||
class: "enf-welle",
|
||||
d: `M${-b} 0 q${r / 2} -9 ${r} 0 t${r} 0 t${r} 0 t${r} 0 t${r} 0 t${r} 0 t${r} 0 t${r} 0 V${r * 2} H${-b} Z`,
|
||||
}, fuellung);
|
||||
}
|
||||
|
||||
const symbol = el("g", {
|
||||
class: "enf-symbol enf-sym-" + k.symbol,
|
||||
transform: `translate(0 ${-r * 0.43}) scale(${k.gross ? 1.95 : 1.75})`,
|
||||
}, koerper);
|
||||
symbol.innerHTML = SYMBOLE[k.symbol]().replace(/\{id\}/g, id);
|
||||
|
||||
const wert = el("text", { class: "enf-wert", y: r * 0.2 + 6 }, koerper);
|
||||
const neben = el("text", { class: "enf-neben", y: r * 0.2 + 33 }, koerper);
|
||||
const name = el("text", { class: "enf-name", y: r + 46 }, g);
|
||||
name.textContent = k.anzeigeName;
|
||||
|
||||
const zeichen = el("g", { class: "enf-zeichen" }, koerper);
|
||||
|
||||
if (k.klick && !this.bearbeiten) {
|
||||
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);
|
||||
g.setAttribute("tabindex", "0");
|
||||
g.setAttribute("role", "button");
|
||||
g.setAttribute("aria-label", k.anzeigeName + " – Details");
|
||||
const los = () => this.opt.onKlick && this.opt.onKlick(k);
|
||||
g.addEventListener("click", los);
|
||||
g.addEventListener("keydown", e => {
|
||||
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); los(); }
|
||||
});
|
||||
}
|
||||
|
||||
// Bewässerung: Zonen als Zeilen im Kreis. Die Ids gehören solarMQTT.js
|
||||
// und werden nur außerhalb der Einstellungsvorschau vergeben.
|
||||
if (k.zonen) {
|
||||
wert.remove(); neben.remove();
|
||||
symbol.setAttribute("transform", `translate(0 ${-r * 0.5}) scale(1.4)`);
|
||||
k.zonen.forEach((z, i) => {
|
||||
const zy = -r * 0.1 + i * 25;
|
||||
const glyph = el("text", { class: "enf-zone-glyph", x: -r * 0.52, y: zy }, koerper);
|
||||
glyph.textContent = z.zeichen;
|
||||
el("title", null, glyph).textContent = z.name;
|
||||
const nm = el("text", { class: "enf-zone-name", x: -r * 0.36, y: zy }, koerper);
|
||||
nm.textContent = z.name;
|
||||
const st = el("text", { class: "enf-zone-status", x: r * 0.6, y: zy }, koerper);
|
||||
st.textContent = "Aus";
|
||||
if (!this.bearbeiten) { glyph.id = z.symbol; st.id = z.status; }
|
||||
});
|
||||
}
|
||||
|
||||
if (this.bearbeiten) this.ziehbarMachen(g, k);
|
||||
|
||||
this.elemente[k.id] = { g, ring, wert, neben, zeichen, symbol, fuellung, fluss: null, segmente: [] };
|
||||
}
|
||||
|
||||
flussPfad(k) {
|
||||
const e = k.elternWirksam;
|
||||
const a = this.punkt(e), b = this.punkt(k);
|
||||
// Knick: quer vom Elternkreis senkrecht los, dann waagerecht in den Kreis
|
||||
// (Ecke x vom Eltern, y vom Kind); hochkant genau umgekehrt.
|
||||
let c;
|
||||
if (Math.abs(a[0] - b[0]) < 30 || Math.abs(a[1] - b[1]) < 30) {
|
||||
c = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
||||
} else {
|
||||
c = this.hochkant ? [b[0], a[1]] : [a[0], b[1]];
|
||||
}
|
||||
const rand = (p, q, r) => {
|
||||
const dx = q[0] - p[0], dy = q[1] - p[1], l = Math.hypot(dx, dy) || 1;
|
||||
return [p[0] + dx / l * (r + 20), p[1] + dy / l * (r + 20)];
|
||||
};
|
||||
const s = rand(a, c, this.radius(e)), z = rand(b, c, this.radius(k));
|
||||
return `M${s[0].toFixed(1)} ${s[1].toFixed(1)} Q${c[0]} ${c[1]} ${z[0].toFixed(1)} ${z[1].toFixed(1)}`;
|
||||
}
|
||||
|
||||
flussZeichnen(k) {
|
||||
const d = this.flussPfad(k);
|
||||
const g = el("g", { class: "enf-fluss enf-still", style: `--farbe:${k.farbe};--verz:${k.tiefe * 110 + 150}ms` }, this.ebeneFluss);
|
||||
const bett = el("path", { d, class: "enf-bett", pathLength: "1" }, g);
|
||||
const hof = el("path", { d, class: "enf-strom-hof" }, g);
|
||||
const strom = el("path", { d, class: "enf-strom" }, g);
|
||||
this.elemente[k.id].fluss = { g, bett, hof, strom };
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------ Aktualisieren */
|
||||
aktualisieren(daten) {
|
||||
this.letzte = daten;
|
||||
if (!this.svg) return;
|
||||
const m = leser(daten);
|
||||
for (const k of this.sichtbareKnoten()) {
|
||||
const e = this.elemente[k.id];
|
||||
if (!e) continue;
|
||||
let z;
|
||||
try {
|
||||
z = k.lesen(m, k.optionenWirksam) || {};
|
||||
} catch (fehler) {
|
||||
console.warn("Energiefluss: " + k.id, fehler);
|
||||
continue;
|
||||
}
|
||||
this.knotenSetzen(k, e, z);
|
||||
if (e.fluss) this.flussSetzen(e.fluss, z.fluss || 0);
|
||||
}
|
||||
}
|
||||
|
||||
knotenSetzen(k, e, z) {
|
||||
// Werte
|
||||
const wert = z.wert !== undefined ? z.wert : leistungText(z.leistung || 0);
|
||||
if (e.wert.isConnected && e.wert.textContent !== wert) e.wert.textContent = wert;
|
||||
const neben = z.neben || "";
|
||||
if (e.neben.isConnected && e.neben.textContent !== neben) e.neben.textContent = neben;
|
||||
e.g.classList.toggle("enf-ruhe", !!z.ruhe && k.aktiv);
|
||||
|
||||
// Ring: Segmente in Hundertsteln des Umfangs
|
||||
let segmente = z.segmente || (z.leistung !== undefined ? [{ w: Math.abs(z.leistung), farbe: k.farbe }] : []);
|
||||
segmente = this.segmenteZusammenfassen(segmente);
|
||||
const skala = k.skala || 15000;
|
||||
while (e.segmente.length < segmente.length) {
|
||||
e.segmente.push(el("circle", { r: this.radius(k), class: "enf-bogen", pathLength: "100" }, e.ring));
|
||||
}
|
||||
let start = 0;
|
||||
e.segmente.forEach((kreis, i) => {
|
||||
const s = segmente[i];
|
||||
let laenge = s ? Math.max(0, Math.min(100 - start, (s.w / skala) * 100)) : 0;
|
||||
if (laenge < 0.4) laenge = 0;
|
||||
const luecke = laenge > 0 && start > 0 ? 0.6 : 0;
|
||||
const a = (laenge - luecke).toFixed(2), off = (-(start + luecke)).toFixed(2);
|
||||
const schluessel = a + "|" + off + "|" + (s ? s.farbe : "");
|
||||
if (kreis._stand !== schluessel) {
|
||||
kreis._stand = schluessel;
|
||||
kreis.style.strokeDasharray = `${Math.max(0, a)} 100`;
|
||||
kreis.style.strokeDashoffset = off;
|
||||
if (s) kreis.style.stroke = s.farbe;
|
||||
}
|
||||
start += laenge;
|
||||
});
|
||||
|
||||
// Zustand des Symbols
|
||||
const zu = z.zustand || {};
|
||||
e.g.classList.toggle("enf-an", !!zu.an);
|
||||
e.g.classList.toggle("enf-laden", !!zu.laden);
|
||||
if (zu.stand !== undefined && isFinite(zu.stand)) {
|
||||
const s = Math.max(0, Math.min(1, zu.stand)).toFixed(3);
|
||||
if (e.g.style.getPropertyValue("--stand") !== s) {
|
||||
e.g.style.setProperty("--stand", s);
|
||||
if (e.fuellung) e.fuellung.style.setProperty("--stand", s);
|
||||
}
|
||||
}
|
||||
if (zu.temps) {
|
||||
zu.temps.forEach((t, i) => {
|
||||
const r = e.symbol.querySelector(".enf-s-t" + i);
|
||||
const f = tempFarbe(t);
|
||||
if (r && r._f !== f) { r._f = f; r.style.fill = f; }
|
||||
});
|
||||
}
|
||||
|
||||
// Plaketten
|
||||
const zeichen = z.zeichen || [];
|
||||
const sig = zeichen.map(x => x.art + ":" + (x.text || "") + ":" + (x.titel || "")).join("|");
|
||||
if (e.zeichen._sig !== sig) {
|
||||
e.zeichen._sig = sig;
|
||||
this.zeichenSetzen(k, e.zeichen, zeichen);
|
||||
}
|
||||
}
|
||||
|
||||
/** Anteile ausgeschalteter Kreise fallen ins letzte Segment. */
|
||||
segmenteZusammenfassen(segmente) {
|
||||
const aus = segmente.filter(s => s.knoten && this.nachId[s.knoten] && !this.nachId[s.knoten].aktiv);
|
||||
if (!aus.length) return segmente;
|
||||
const rest = segmente.filter(s => !aus.includes(s)).map(s => Object.assign({}, s));
|
||||
if (rest.length) rest[rest.length - 1].w += aus.reduce((a, s) => a + Math.max(0, s.w), 0);
|
||||
return rest;
|
||||
}
|
||||
|
||||
zeichenSetzen(k, gruppe, zeichen) {
|
||||
gruppe.innerHTML = "";
|
||||
const r = this.radius(k);
|
||||
zeichen.forEach((z, i) => {
|
||||
const art = ZEICHEN[z.art];
|
||||
if (!art) return;
|
||||
// Außen am Ring rechts unten, eine Plakette je 36 Grad - so bleiben
|
||||
// Wert und Zeile darunter frei.
|
||||
const winkel = (88 + i * 36) * Math.PI / 180;
|
||||
const px = Math.sin(winkel) * (r + 12), py = -Math.cos(winkel) * (r + 12);
|
||||
const p = el("g", { class: "enf-plakette", transform: `translate(${px.toFixed(1)} ${py.toFixed(1)})`, style: `--pfarbe:${art.farbe};--i:${i}` }, gruppe);
|
||||
if (z.titel) el("title", null, p).textContent = z.titel;
|
||||
if (z.text) {
|
||||
const breite = 26 + z.text.length * 10;
|
||||
el("rect", { x: -16, y: -16, width: breite, height: 32, rx: 16 }, p);
|
||||
el("text", { class: "enf-plakette-text", x: 12, y: 1 }, p).textContent = z.text;
|
||||
} else {
|
||||
el("circle", { r: 16 }, p);
|
||||
}
|
||||
el("text", { class: "enf-plakette-glyph", x: 0, y: 1 }, p).textContent = art.glyph;
|
||||
});
|
||||
}
|
||||
|
||||
flussSetzen(f, w) {
|
||||
const a = Math.abs(w);
|
||||
const still = a < 15;
|
||||
const breite = still ? 0 : 3.5 + 6.5 * (1 - Math.exp(-a / 3000));
|
||||
// Schneller bei mehr Leistung: 2,6 s je Abschnitt bei wenig, 0,7 s bei viel.
|
||||
const dauer = still ? 0 : 0.7 + 1.9 * Math.exp(-a / 3000);
|
||||
const sig = still ? "still" : breite.toFixed(1) + "|" + dauer.toFixed(2) + "|" + (w < 0);
|
||||
if (f._sig === sig) return;
|
||||
f._sig = sig;
|
||||
f.g.classList.toggle("enf-still", still);
|
||||
f.g.classList.toggle("enf-rueck", w < 0);
|
||||
f.g.style.setProperty("--breite", breite.toFixed(1));
|
||||
f.g.style.setProperty("--dauer", dauer.toFixed(2) + "s");
|
||||
}
|
||||
|
||||
pausieren() {
|
||||
if (!this.svg) return;
|
||||
this.svg.classList.toggle("enf-pause", !this.sichtbar || document.hidden);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------- Bearbeiten */
|
||||
ziehbarMachen(g, k) {
|
||||
g.classList.add("enf-ziehbar");
|
||||
g.addEventListener("pointerdown", ev => {
|
||||
ev.preventDefault();
|
||||
g.setPointerCapture(ev.pointerId);
|
||||
const pt = this.svg.createSVGPoint();
|
||||
const svgPunkt = e2 => {
|
||||
pt.x = e2.clientX; pt.y = e2.clientY;
|
||||
return pt.matrixTransform(this.svg.getScreenCTM().inverse());
|
||||
};
|
||||
const start = svgPunkt(ev);
|
||||
const [x0, y0] = k.lage;
|
||||
g.classList.add("enf-zieht");
|
||||
const bewegen = e2 => {
|
||||
const p = svgPunkt(e2);
|
||||
k.lage = [Math.round((x0 + p.x - start.x) / 10) * 10, Math.round((y0 + p.y - start.y) / 10) * 10];
|
||||
g.setAttribute("transform", `translate(${k.lage[0]} ${k.lage[1]})`);
|
||||
for (const kind of this.knoten) {
|
||||
const f = this.elemente[kind.id] && this.elemente[kind.id].fluss;
|
||||
if (f && (kind === k || kind.elternWirksam === k)) {
|
||||
const d = this.flussPfad(kind);
|
||||
f.bett.setAttribute("d", d); f.hof.setAttribute("d", d); f.strom.setAttribute("d", d);
|
||||
}
|
||||
}
|
||||
};
|
||||
const ende = () => {
|
||||
g.removeEventListener("pointermove", bewegen);
|
||||
g.classList.remove("enf-zieht");
|
||||
this.rahmenSetzen();
|
||||
if (this.opt.onVerschoben) this.opt.onVerschoben(k.id, k.lage[0], k.lage[1]);
|
||||
};
|
||||
g.addEventListener("pointermove", bewegen);
|
||||
g.addEventListener("pointerup", ende, { once: true });
|
||||
g.addEventListener("pointercancel", ende, { once: true });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.Energiefluss = Energiefluss;
|
||||
})();
|
||||
@@ -0,0 +1,279 @@
|
||||
/* ==========================================================================
|
||||
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);
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,290 @@
|
||||
/* ==========================================================================
|
||||
Katalog der Energiefluss-Übersicht (Solar-Seite, Karte "Realtime").
|
||||
|
||||
Jeder Kreis steht hier genau einmal: wie er heißt, wie er aussieht, woran
|
||||
er hängt, wo er steht und woher seine Werte kommen. Gezeichnet wird er von
|
||||
js/solar/energiefluss.js, ohne dass der Renderer einen Kreis beim Namen
|
||||
kennt. Ein neuer Kreis ist also ein neuer Eintrag hier - sonst nichts.
|
||||
|
||||
Was ein Haus davon abweichend einstellt (Kreis aus, eigener Name, eigene
|
||||
Lage, Optionen), steht in homeMesh.energiefluss und wird unter
|
||||
Einstellungen -> Übersicht gepflegt.
|
||||
|
||||
Felder eines Eintrags:
|
||||
id feste Kennung, steht in der Datenbank
|
||||
name Beschriftung unter dem Kreis
|
||||
titel Bezeichnung in den Einstellungen, falls name dort missverständlich ist
|
||||
symbol Schlüssel in ENERGIEFLUSS_SYMBOLE (energiefluss.js)
|
||||
farbe Hauptfarbe: Ring, Symbol, Fluss
|
||||
eltern Kennung des Kreises, mit dem er über einen Fluss verbunden
|
||||
ist; null = frei stehend. Ist der Elternkreis aus, hängt der
|
||||
Kreis am nächsten eingeschalteten darüber.
|
||||
pflicht lässt sich nicht ausschalten (die Mitte des Bildes)
|
||||
gross größerer Kreis
|
||||
x, y Mitte in der Queransicht (Zeichenfläche etwa 1400 x 850)
|
||||
skala Leistung in W, bei der der Ring voll ist
|
||||
klick {ansicht: "anlage"} oder {modal: "heater"} - Ziel beim Antippen
|
||||
optionen Vorgabe für die Optionen, die in den Einstellungen änderbar sind
|
||||
zonen nur Bewässerung: die Anzeigen, die solarMQTT.js beschreibt
|
||||
lesen(m, opt) liefert den Zustand aus den MQTT-Werten:
|
||||
leistung W, bestimmt den Hauptwert und den Ring
|
||||
wert Text statt der Leistung (optional)
|
||||
neben kleine Zeile unter dem Wert
|
||||
segmente [{w, farbe, knoten?}] statt eines einzigen Rings; knoten =
|
||||
der Anteil gehört zu diesem Kreis und fällt, ist der aus, in
|
||||
das letzte Segment
|
||||
fluss W vom Elternkreis zu diesem hin; negativ = in Gegenrichtung
|
||||
ruhe Kreis blass zeichnen (nichts los)
|
||||
zeichen kleine Plaketten: [{art, text?, titel?}] - art siehe
|
||||
ENERGIEFLUSS_ZEICHEN in energiefluss.js
|
||||
zustand was das Symbol braucht: {an, stand, laden, temps}
|
||||
|
||||
Vorzeichen wie im Manager: P_Grid > 0 Bezug, P_Akku > 0 Entladen,
|
||||
P_Load und og negativ.
|
||||
========================================================================== */
|
||||
|
||||
const ENERGIEFLUSS_KATALOG = (function () {
|
||||
"use strict";
|
||||
|
||||
const kw = w => (Math.abs(w) / 1000).toFixed(1).replace(".", ",");
|
||||
|
||||
const wallboxZeichen = (modus, stecker, gesperrt) => {
|
||||
const z = [];
|
||||
if (modus === "Eco") z.push({ art: "eco", titel: "Eco-Modus" });
|
||||
else if (modus === "Next Trip") z.push({ art: "uhr", titel: "Nächste Fahrt" });
|
||||
else if (modus !== undefined) z.push({ art: "manuell", titel: "Standard-Modus" });
|
||||
if (stecker) z.push({ art: "stecker", titel: "Fahrzeug angesteckt" });
|
||||
if (gesperrt) z.push({ art: "schloss", titel: "Fahrzeug verriegelt" });
|
||||
return z;
|
||||
};
|
||||
|
||||
const knoten = [
|
||||
// ---------------------------------------------------------------- Quellen
|
||||
{
|
||||
id: "pv", name: "Sonne", symbol: "sonne", farbe: "#f7c000",
|
||||
eltern: "wr", x: 180, y: 130, skala: 15000,
|
||||
klick: { ansicht: "anlage" },
|
||||
// Welche Einträge von solarManager/P_PVn zu welcher Fläche gehören.
|
||||
// Reihenfolge wie in solarManager.py.
|
||||
optionen: {
|
||||
flaechen: [
|
||||
{ name: "Dach", von: 0, bis: 1, farbe: "#f7c000" },
|
||||
{ name: "Veranda", von: 2, bis: 13, farbe: "#f78d00" },
|
||||
{ name: "Carport", von: 14, bis: 33, farbe: "#f75a1e" },
|
||||
],
|
||||
},
|
||||
lesen(m, opt) {
|
||||
const pvn = m.json("solarManager/P_PVn") || [];
|
||||
const pv = m.zahl("solarManager/P_PV");
|
||||
const flaechen = (opt.flaechen || []).map(f => {
|
||||
let w = 0;
|
||||
for (let i = f.von; i <= f.bis && i < pvn.length; i++) w += Number(pvn[i]) || 0;
|
||||
return { w, farbe: f.farbe, name: f.name };
|
||||
});
|
||||
return {
|
||||
leistung: pv,
|
||||
segmente: flaechen.length ? flaechen : null,
|
||||
neben: flaechen.map(f => kw(f.w)).join(" · "),
|
||||
fluss: -pv,
|
||||
ruhe: pv < 20,
|
||||
zustand: { an: pv >= 20, stand: Math.min(1, pv / 10000) },
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "netz", name: "Netz", symbol: "netz", farbe: "#9a9aab",
|
||||
eltern: "wr", x: 100, y: 380, skala: 15000,
|
||||
lesen(m) {
|
||||
const netz = m.zahl("solarManager/P_Grid");
|
||||
const a = n => (Math.round(Math.abs(m.zahl("solarManager/i_" + n + "evu")) * 10) / 10).toString().replace(".", ",");
|
||||
return {
|
||||
leistung: Math.abs(netz),
|
||||
neben: a("l1") + "·" + a("l2") + "·" + a("l3") + " A",
|
||||
fluss: -netz,
|
||||
ruhe: Math.abs(netz) < 30,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "akku", name: "Batterie", symbol: "akku", farbe: "#6cbe58",
|
||||
eltern: "wr", x: 180, y: 630, skala: 15000,
|
||||
lesen(m) {
|
||||
const p = m.zahl("solarManager/P_Akku");
|
||||
const soc = m.zahl("solarManager/SOC");
|
||||
const grenze = m.zahl("solarManager/crgMaxPct");
|
||||
const laden = p < -20;
|
||||
const zeichen = [];
|
||||
if (laden && grenze < 100) {
|
||||
zeichen.push({ art: "uhr", text: Math.round(grenze) + " %", titel: "Ladeleistung begrenzt" });
|
||||
}
|
||||
return {
|
||||
leistung: Math.abs(p),
|
||||
neben: Math.round(soc) + " %" + (laden ? " · lädt" : p > 20 ? " · entlädt" : ""),
|
||||
fluss: -p,
|
||||
ruhe: Math.abs(p) < 20,
|
||||
zeichen,
|
||||
zustand: { stand: soc / 100, laden, an: Math.abs(p) >= 20 },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------ Mitte
|
||||
{
|
||||
id: "wr", name: "Autarkie", titel: "Wechselrichter (Autarkie)", symbol: "wechselrichter", farbe: "#b9c0c8",
|
||||
eltern: null, pflicht: true, gross: true, x: 430, y: 380, skala: 100,
|
||||
lesen(m) {
|
||||
const aut = m.zahl("solarManager/autarky");
|
||||
return {
|
||||
leistung: aut,
|
||||
wert: Math.round(aut) + " %",
|
||||
neben: "Wirkungsgrad " + Math.round(m.zahl("solarManager/eff")) + " %",
|
||||
segmente: [{ w: aut, farbe: "#6cbe58" }],
|
||||
zustand: { an: m.zahl("solarManager/P_PV") >= 20 },
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "verbrauch", name: "Verbrauch", symbol: "verbrauch", farbe: "#71afcd",
|
||||
eltern: "wr", pflicht: true, gross: true, x: 770, y: 380, skala: 15000,
|
||||
lesen(m) {
|
||||
const last = -m.zahl("solarManager/P_Load");
|
||||
const og = -m.zahl("solarManager/og");
|
||||
const eg = m.zahl("solarManager/eg");
|
||||
const ug = m.zahl("solarManager/ug");
|
||||
const heiz = m.zahl("solarManager/pHeat");
|
||||
const gemein = Math.max(0, last - ug - eg - og);
|
||||
return {
|
||||
leistung: last,
|
||||
neben: "Gemein " + kw(gemein) + " kW",
|
||||
segmente: [
|
||||
{ w: og, farbe: "#70ccbb", knoten: "og" },
|
||||
{ w: eg, farbe: "#70cc9c", knoten: "eg" },
|
||||
{ w: ug, farbe: "#83a55b", knoten: "ug" },
|
||||
{ w: heiz, farbe: "#c2614e", knoten: "heizstab" },
|
||||
{ w: gemein, farbe: "#71afcd" },
|
||||
],
|
||||
fluss: last,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// --------------------------------------------------------------- Etagen
|
||||
{
|
||||
id: "og", name: "Obergeschoss", symbol: "etageOben", farbe: "#70ccbb",
|
||||
eltern: "verbrauch", x: 920, y: 70, skala: 15000,
|
||||
lesen(m) {
|
||||
const w = -m.zahl("solarManager/og");
|
||||
return {
|
||||
leistung: w, fluss: w, ruhe: w < 20,
|
||||
zeichen: m.zahl("solarManager/heatOG") ? [{ art: "heizung", titel: "Heizung an" }] : [],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "eg", name: "Erdgeschoss", symbol: "etageMitte", farbe: "#70cc9c",
|
||||
eltern: "verbrauch", x: 920, y: 670, skala: 15000,
|
||||
lesen(m) {
|
||||
const w = m.zahl("solarManager/eg");
|
||||
const wb = m.zahl("solarManager/evPower") * 1000;
|
||||
return {
|
||||
leistung: w, fluss: w, ruhe: w < 20,
|
||||
segmente: [
|
||||
{ w: Math.max(0, w - wb), farbe: "#70cc9c" },
|
||||
{ w: Math.min(w, wb), farbe: "#60afbf", knoten: "wallboxEG" },
|
||||
],
|
||||
zeichen: m.zahl("solarManager/heatEG") ? [{ art: "heizung", titel: "Heizung an" }] : [],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "ug", name: "Untergeschoss", symbol: "etageUnten", farbe: "#83a55b",
|
||||
eltern: "verbrauch", x: 600, y: 670, skala: 15000,
|
||||
lesen(m) {
|
||||
const w = m.zahl("solarManager/ug");
|
||||
return { leistung: w, fluss: w, ruhe: w < 20 };
|
||||
},
|
||||
},
|
||||
|
||||
// ----------------------------------------------------------- Verbraucher
|
||||
{
|
||||
id: "heizstab", name: "Heizstab", symbol: "speicher", farbe: "#c2614e",
|
||||
eltern: "verbrauch", x: 1120, y: 380, skala: 6000,
|
||||
klick: { modal: "heater" },
|
||||
lesen(m) {
|
||||
const w = m.zahl("solarManager/pHeat");
|
||||
const oben = m.zahl("solarManager/t_buffT");
|
||||
const mitte = m.zahl("solarManager/t_buffM");
|
||||
const unten = m.zahl("solarManager/t_buffB");
|
||||
const eco = m.text("solarManager/heatMode") === "eco";
|
||||
const grad = t => (Math.round(t * 10) / 10).toString().replace(".", ",");
|
||||
return {
|
||||
leistung: w, fluss: w, ruhe: w < 20,
|
||||
neben: grad(oben) + " · " + grad(mitte) + " °C",
|
||||
zeichen: [eco ? { art: "eco", titel: "Automatik (Überschuss)" } : { art: "manuell", titel: "Feste Leistung" }],
|
||||
zustand: { an: w >= 20, temps: [oben, mitte, unten] },
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "wallboxOG", name: "Wallbox OG", symbol: "wallbox", farbe: "#4b71a6",
|
||||
eltern: "og", x: 1220, y: 70, skala: 11000,
|
||||
klick: { modal: "CarOG" },
|
||||
lesen(m) {
|
||||
const w = m.zahl("solarManager/evPowerOG") * 1000;
|
||||
const stecker = m.zahl("solarManager/evPlugOG") > 0;
|
||||
return {
|
||||
leistung: w, fluss: w, ruhe: w < 50 && !stecker,
|
||||
neben: w >= 50 ? "lädt" : stecker ? "angesteckt" : "kein Auto",
|
||||
zeichen: wallboxZeichen(m.text("solarManager/evModeOG"), stecker, false),
|
||||
zustand: { an: w >= 50 },
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "wallboxEG", name: "Wallbox EG", symbol: "wallbox", farbe: "#60afbf",
|
||||
eltern: "eg", x: 1220, y: 670, skala: 11000,
|
||||
klick: { modal: "CarEG" },
|
||||
lesen(m) {
|
||||
const w = m.zahl("solarManager/evPower") * 1000;
|
||||
const soc = m.zahl("solarManager/evSOC");
|
||||
const tank = m.zahl("solarManager/evFuel");
|
||||
// solarManager/evPlug kommt als 1/0; aeltere Manager schickten den
|
||||
// Text der Wallbox ("no car", "charging", ...).
|
||||
const roh = m.text("solarManager/evPlug");
|
||||
const stecker = isNaN(Number(roh)) ? (roh !== undefined && roh !== "no car") : Number(roh) > 0;
|
||||
let neben = Math.round(soc) + " %";
|
||||
const rest = m.zahl("solarManager/carRemChrg");
|
||||
if (w >= 50 && rest > -1 && m.text("solarManager/carRemChrg") !== undefined) {
|
||||
const fertig = new Date(Date.now() + rest * 60000);
|
||||
neben = "fertig " + String(fertig.getHours()).padStart(2, "0") + ":" + String(fertig.getMinutes()).padStart(2, "0");
|
||||
}
|
||||
const zeichen = wallboxZeichen(m.text("solarManager/evMode"), stecker, m.zahl("solarManager/evLock") > 0);
|
||||
if (tank > 0) zeichen.push({ art: "tank", text: Math.round(tank) + " %", titel: "Tank" });
|
||||
return {
|
||||
leistung: w, fluss: w, ruhe: w < 50 && !stecker,
|
||||
neben, zeichen,
|
||||
zustand: { an: w >= 50, stand: soc / 100 },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------- frei
|
||||
{
|
||||
id: "bewaesserung", name: "Bewässerung", symbol: "tropfen", farbe: "#4b8fd6",
|
||||
eltern: null, x: 550, y: 70,
|
||||
klick: { modal: "watering" },
|
||||
// Ids wie in wateringZones (solarMQTT.js) - dort werden Farbe, Text und
|
||||
// Tooltip gesetzt, hier nur die Stellen dafür angelegt.
|
||||
zonen: [
|
||||
{ name: "Tröge", zeichen: "", status: "wateringTroughStatus", symbol: "wateringTroughIcon" },
|
||||
{ name: "Vorn", zeichen: "", status: "wateringFrontStatus", symbol: "wateringFrontIcon" },
|
||||
{ name: "Beete", zeichen: "", status: "wateringBedsStatus", symbol: "wateringBedsIcon" },
|
||||
],
|
||||
lesen() {
|
||||
return { wert: "" };
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return { knoten };
|
||||
})();
|
||||
+64
-235
@@ -142,255 +142,84 @@ function setFlowWidth(id, width) {
|
||||
flow.style.willChange = sichtbar ? "transform" : "auto";
|
||||
}
|
||||
|
||||
// Die Energiefluss-Uebersicht (js/solar/energiefluss.js) und der PV-Lageplan.
|
||||
// Bis September 2026 stand hier updateValuesMQTT fuer das feste realtime.svg -
|
||||
// jeder Kreis mit eigenen Zeilen. Was ein Kreis zeigt, steht jetzt im Katalog
|
||||
// (energieflussKatalog.js); hier bleibt nur der Lageplan hinter der Sonne.
|
||||
let energiefluss = null;
|
||||
|
||||
const solarSVG = {
|
||||
updateCnt: 99,
|
||||
/**
|
||||
* Beide Zwischenspeicher verwerfen - noetig, falls das SVG neu gezeichnet
|
||||
* wird. Nur die Knoten zu vergessen reicht nicht: letzterWert wuerde dann
|
||||
* jeden Schreibzugriff als "unveraendert" ueberspringen und das frische
|
||||
* SVG behielte seine Platzhalter.
|
||||
* Beide Zwischenspeicher verwerfen - noetig, wenn die Uebersicht neu
|
||||
* gezeichnet wird (Wechsel quer/hochkant). Nur die Knoten zu vergessen
|
||||
* reicht nicht: letzterWert wuerde dann jeden Schreibzugriff als
|
||||
* "unveraendert" ueberspringen und die frischen Elemente behielten ihre
|
||||
* Platzhalter.
|
||||
*/
|
||||
fillElementArray: function () {
|
||||
for (const id in svgElements) delete svgElements[id];
|
||||
letzterWert.clear();
|
||||
},
|
||||
updateValuesMQTT: function (mqttData) {
|
||||
if (energiefluss) energiefluss.aktualisieren(mqttData);
|
||||
|
||||
const angleFactor = 15000 / 288;
|
||||
const sixkWangleFactor = 6000 / 288;
|
||||
var cons = Number(mqttData["solarManager/P_Load"]);
|
||||
var pvn = JSON.parse(mqttData["solarManager/P_PVn"]);
|
||||
var pv = Number(mqttData["solarManager/P_PV"]);
|
||||
var pv2 = Number(pvn[1]);
|
||||
var pv1 = Number(pvn[0]);
|
||||
var pv3 = pv - (pv1 + pv2);
|
||||
var pbatt = Number(mqttData["solarManager/P_Akku"]);
|
||||
var limbatt = Number(mqttData["solarManager/crgMaxPct"]);
|
||||
var grid = Number(mqttData["solarManager/P_Grid"]);
|
||||
var soc = Number(mqttData["solarManager/SOC"]);
|
||||
var og = Number(mqttData["solarManager/og"]);
|
||||
var eg = Number(mqttData["solarManager/eg"]);
|
||||
var ug = Number(mqttData["solarManager/ug"]);
|
||||
var evsoc = Number(mqttData["solarManager/evSOC"]);
|
||||
var evlock = Number(mqttData["solarManager/evLock"]);
|
||||
var evMode = mqttData["solarManager/evMode"];
|
||||
var evPower = Number(mqttData["solarManager/evPower"]);
|
||||
var plugev = Number(mqttData["solarManager/evPlug"]);
|
||||
var fuelev = Number(mqttData["solarManager/evFuel"]);
|
||||
var heatOG = Number(mqttData["solarManager/heatOG"]);
|
||||
var heatEG = Number(mqttData["solarManager/heatEG"]);
|
||||
var puffO = Number(mqttData["solarManager/t_buffT"]);
|
||||
var puffM = Number(mqttData["solarManager/t_buffM"]);
|
||||
var puffU = Number(mqttData["solarManager/t_buffB"]);
|
||||
var heatMode = mqttData["solarManager/heatMode"];
|
||||
var carRemChrg = Number(mqttData["solarManager/carRemChrg"]);
|
||||
var aut = Number(mqttData["solarManager/autarky"]);
|
||||
var Pheat = Number(mqttData["solarManager/pHeat"]);
|
||||
var waterHeight = Number(mqttData["solarManager/waterHeight"]);
|
||||
var waterTemp = Number(mqttData["solarManager/waterTemp"]);
|
||||
var p_wr = Number(mqttData["solarManager/P_WR"]);
|
||||
var eff = Number(mqttData["solarManager/eff"]);
|
||||
var i_l1evu = Number(mqttData["solarManager/i_l1evu"]);
|
||||
var i_l2evu = Number(mqttData["solarManager/i_l2evu"]);
|
||||
var i_l3evu = Number(mqttData["solarManager/i_l3evu"]);
|
||||
var evPowerOG = Number(mqttData["solarManager/evPowerOG"]);
|
||||
var evPlugOG = Number(mqttData["solarManager/evPlugOG"]);
|
||||
var evModeOG = mqttData["solarManager/evModeOG"];
|
||||
|
||||
|
||||
var common = -cons - ug - eg + og ;
|
||||
if(common < 0){
|
||||
common = 0;
|
||||
// Lageplan (assets/img/pvplan.svg): Leistung je Eingang und die Liste
|
||||
// der Wechselrichter.
|
||||
let pvn = [];
|
||||
try { pvn = JSON.parse(mqttData["solarManager/P_PVn"]); } catch (e) { pvn = []; }
|
||||
for (let i = 0; i < pvn.length; i++) {
|
||||
if (svgElement("det_pv" + i + "P")) setText("det_pv" + i + "P", powerToString(pvn[i]));
|
||||
}
|
||||
let liste = "";
|
||||
for (let i = 0; mqttData["solarManager/inverters" + i + "/name"] !== undefined; i++) {
|
||||
const basis = "solarManager/inverters" + i;
|
||||
const gestoert = mqttData[basis + "/error"] > 0;
|
||||
liste += '<tspan x="0" dy="1.2em">' + mqttData[basis + "/name"] + ":</tspan>"
|
||||
+ '<tspan x="160">' + (gestoert ? "--" : Math.round(mqttData[basis + "/temp"])) + " °C</tspan>"
|
||||
+ '<tspan x="215">' + (gestoert ? "-- W" : powerToString(mqttData[basis + "/p_AC"])) + "</tspan>";
|
||||
}
|
||||
if (svgElement("invList")) setHTML("invList", liste);
|
||||
}
|
||||
};
|
||||
|
||||
setAttr('consumerArc', "d", describeArc(100, 100, 95, 0, Math.round(-cons / angleFactor)));
|
||||
/** Energiefluss aufbauen und den Umschalter Uebersicht/Anlage verdrahten. */
|
||||
function energieflussStarten() {
|
||||
const behaelter = document.getElementById("energiefluss");
|
||||
if (!behaelter || typeof Energiefluss === "undefined") return;
|
||||
|
||||
setAttr("ogArc", "d", describeArc(100, 100, 95, 0, Math.round(-og / angleFactor)));
|
||||
setText("consumerText", powerToString(-cons));
|
||||
|
||||
setText("consumerTextAllg", "Gemein: "+powerToString(common));
|
||||
|
||||
if (evPower > 0) {
|
||||
setDisplay("evCharge", "");
|
||||
if (carRemChrg > -1) {
|
||||
var today = new Date();
|
||||
today.setMinutes(today.getMinutes() + carRemChrg);
|
||||
var h = today.getHours();
|
||||
var m = today.getMinutes();
|
||||
if (h < 10)
|
||||
h = "0" + h;
|
||||
if (m < 10)
|
||||
m = "0" + m;
|
||||
setText("evRemText", h + ":" + m);
|
||||
const zeigeAnsicht = function (name) {
|
||||
document.querySelectorAll("[data-enf-ansicht]").forEach(function (knopf) {
|
||||
const an = knopf.dataset.enfAnsicht === name;
|
||||
knopf.classList.toggle("active", an);
|
||||
knopf.setAttribute("aria-selected", an ? "true" : "false");
|
||||
});
|
||||
document.querySelectorAll("[data-enf-ansicht-flaeche]").forEach(function (flaeche) {
|
||||
const an = flaeche.dataset.enfAnsichtFlaeche === name;
|
||||
flaeche.hidden = !an;
|
||||
if (an) {
|
||||
flaeche.classList.remove("enf-rein");
|
||||
void flaeche.offsetWidth;
|
||||
flaeche.classList.add("enf-rein");
|
||||
}
|
||||
} else {
|
||||
setText("evRemText", "");
|
||||
}
|
||||
setText("ogText", powerToString(-og));
|
||||
setText("egText", powerToString(eg));
|
||||
setText("ugText", powerToString(ug));
|
||||
setText("pvText", powerToString(pv));
|
||||
setText("pv1txt", powerToString(pv1));
|
||||
setText("pv2txt", powerToString(pv2));
|
||||
setText("pv3txt", powerToString(pv3));
|
||||
setText("il1txt", Math.round(i_l1evu * 10) / 10 + " A");
|
||||
setText("il2txt", Math.round(i_l2evu * 10) / 10 + " A");
|
||||
setText("il3txt", Math.round(i_l3evu * 10) / 10 + " A");
|
||||
setText("batText", powerToString(pbatt));
|
||||
setText("heatText", powerToString(Pheat));
|
||||
setText("evText", powerToString(evPower * 1000));
|
||||
setText("evTextOG", powerToString(evPowerOG * 1000));
|
||||
setText("genText", "Autarkie: " + Math.round(aut) + " %");
|
||||
setText("genInfoText", "Wirkungsgrad: " + Math.round(eff) + " %");// powerToString(p_wr);
|
||||
setText("gridText", powerToString(grid));
|
||||
|
||||
for (i = 0; i < pvn.length; i++) {
|
||||
setText("det_pv" + i + "P", powerToString(pvn[i]));
|
||||
}
|
||||
|
||||
tttext = ""
|
||||
var i = 0;
|
||||
while (mqttData["solarManager/inverters" + i]) {
|
||||
if (mqttData["solarManager/inverters" + i+"/error"] > 0) {
|
||||
tttext = tttext + '<tspan x="0" dy="1.2em">' + mqttData["solarManager/inverters" + i+"/name"] + ":</tspan>"
|
||||
tttext = tttext + '<tspan x="160">' + "--" + " °C</tspan>"
|
||||
tttext = tttext + '<tspan x="215">' + "-- W" + "</tspan>"
|
||||
} else {
|
||||
tttext = tttext + '<tspan x="0" dy="1.2em">' + mqttData["solarManager/inverters" + i+"/name"] + ":</tspan>"
|
||||
tttext = tttext + '<tspan x="160">' + Math.round(mqttData["solarManager/inverters" + i+"/temp"]) + " °C</tspan>"
|
||||
tttext = tttext + '<tspan x="215">' + powerToString(mqttData["solarManager/inverters" + i+"/p_AC"]) + "</tspan>"
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
setHTML("invList", tttext);
|
||||
setAttr("egArc", "d", describeArc(100, 100, 95, 0, Math.round(eg / angleFactor)));
|
||||
setAttr("ugArc", "d", describeArc(100, 100, 95, 0, Math.round(ug / angleFactor)));
|
||||
setAttr("pvArc", "d", describeArc(100, 100, 95, 0, Math.round(pv1 / angleFactor)));
|
||||
setAttr("pv2Arc", "d", describeArc(100, 100, 95, Math.round(pv1 / angleFactor), Math.round(pv1 / angleFactor) + Math.round(pv2 / angleFactor)));
|
||||
setAttr("pv3Arc", "d", describeArc(100, 100, 95, Math.round(pv1 / angleFactor) + Math.round(pv2 / angleFactor), Math.round(pv1 / angleFactor) + Math.round(pv2 / angleFactor) + Math.round(pv3 / angleFactor)));
|
||||
|
||||
if (pbatt < 0) {
|
||||
if (limbatt < 100) {
|
||||
setDisplay("charge", "none");
|
||||
setDisplay("batLim", "");
|
||||
setText("batLimText", Math.round(limbatt) + "%");
|
||||
} else {
|
||||
setDisplay("charge", "");
|
||||
setDisplay("batLim", "none");
|
||||
}
|
||||
setAttr("batArc", "d", describeArc(100, 100, 95, 0, Math.round(-pbatt / angleFactor)));
|
||||
setAttr("battani", "class", "stream-rev");
|
||||
setFlowWidth("battani", 55 * (1 - Math.exp(pbatt / 2000)));
|
||||
} else {
|
||||
setDisplay("charge", "none");
|
||||
setDisplay("batLim", "none");
|
||||
setAttr("batArc", "d", describeArc(100, 100, 95, 0, Math.round(pbatt / angleFactor)));
|
||||
setAttr("battani", "class", "stream");
|
||||
setFlowWidth("battani", 55 * (1 - Math.exp(-pbatt / 2000)));
|
||||
}
|
||||
setAttr("batChargeState", "y", (100 - soc) + "%");
|
||||
//setAttr("heatChargeState", "y", 100 - ((((puffO + puffM) / 2) - 30) * 1.66) + "%");
|
||||
setAttr('tmpH', "stop-color", assignColor("#2389BA", "#BA3B23", 20, 80, puffO));
|
||||
setAttr('tmpM', "stop-color", assignColor("#2389BA", "#BA3B23", 20, 80, puffM));
|
||||
setAttr('tmpL', "stop-color", assignColor("#2389BA", "#BA3B23", 20, 80, puffU));
|
||||
setText("batSOC", soc + " %");
|
||||
setAttr("evArc", "d", describeArc(100, 100, 95, 0, Math.round(evPower * 2000 / angleFactor)));
|
||||
setAttr("evChargeState", "y", (100 - evsoc) + "%");
|
||||
setText("evSOC", evsoc + " %");
|
||||
setText("evFuel", fuelev + " %");
|
||||
|
||||
setAttr("heatArc", "d", describeArc(100, 100, 95, 0, Math.round(Pheat / sixkWangleFactor)));
|
||||
setText("heatUp", Math.round(puffO * 10) / 10 + " °C");
|
||||
setText("heatDown", Math.round(puffM * 10) / 10 + " °C");
|
||||
|
||||
if (evlock == false) {
|
||||
setDisplay("evLock", "none");
|
||||
} else {
|
||||
setDisplay("evLock", "");
|
||||
}
|
||||
if (heatMode == "eco") {
|
||||
setDisplay("heatEco", "");
|
||||
setDisplay("heatDefault", "none");
|
||||
} else {
|
||||
setDisplay("heatEco", "none");
|
||||
setDisplay("heatDefault", "");
|
||||
}
|
||||
if (evMode == "Eco") {
|
||||
setDisplay("evEco", "");
|
||||
setDisplay("evNextTrip", "none");
|
||||
setDisplay("evDefault", "none");
|
||||
} else if (evMode == "Next Trip") {
|
||||
setDisplay("evEco", "none");
|
||||
setDisplay("evNextTrip", "");
|
||||
setDisplay("evDefault", "none");
|
||||
} else {
|
||||
setDisplay("evEco", "none");
|
||||
setDisplay("evNextTrip", "none");
|
||||
setDisplay("evDefault", "");
|
||||
}
|
||||
if (heatOG) {
|
||||
setDisplay("heatOG", "");
|
||||
} else {
|
||||
setDisplay("heatOG", "none");
|
||||
}
|
||||
if (heatEG) {
|
||||
setDisplay("heatEG", "");
|
||||
} else {
|
||||
setDisplay("heatEG", "none");
|
||||
}
|
||||
// solarManager/evPlug kommt als 1 oder 0, wie evPlugOG. Frueher kam
|
||||
// hier der Text der Wallbox an ("no car", "charging", ...); Number()
|
||||
// machte daraus NaN, und NaN != "no car" ist immer wahr - das
|
||||
// Steckersymbol stand dauerhaft da. Den Text weiter zu verstehen
|
||||
// kostet eine Zeile und haelt die Anzeige richtig, egal ob der
|
||||
// Manager schon neu gestartet ist oder nicht.
|
||||
const plugText = mqttData["solarManager/evPlug"];
|
||||
if (isNaN(plugev) ? (plugText !== undefined && plugText !== "no car") : plugev > 0) {
|
||||
setDisplay("evPlug", "");
|
||||
} else {
|
||||
setDisplay("evPlug", "none");
|
||||
}
|
||||
if (evModeOG == "Eco") {
|
||||
setDisplay("evEcoOG", "");
|
||||
setDisplay("evNextTripOG", "none");
|
||||
setDisplay("evDefaultOG", "none");
|
||||
} else if (evModeOG == "Next Trip") {
|
||||
setDisplay("evEcoOG", "none");
|
||||
setDisplay("evNextTripOG", "");
|
||||
setDisplay("evDefaultOG", "none");
|
||||
} else {
|
||||
setDisplay("evEcoOG", "none");
|
||||
setDisplay("evNextTripOG", "none");
|
||||
setDisplay("evDefaultOG", "");
|
||||
}
|
||||
if (evPlugOG > 0) {
|
||||
setDisplay("evPlugOG", "");
|
||||
} else {
|
||||
setDisplay("evPlugOG", "none");
|
||||
}
|
||||
if (grid < 0) {
|
||||
setAttr("gridArc", "d", describeArc(100, 100, 95, 0, -grid / angleFactor));
|
||||
setAttr("consGridani", "class", "stream-rev");
|
||||
setFlowWidth("consGridani", 55 * (1 - Math.exp(grid / 2000)));
|
||||
} else {
|
||||
setAttr("gridArc", "d", describeArc(100, 100, 95, 0, grid / angleFactor));
|
||||
setAttr("consGridani", "class", "stream");
|
||||
setFlowWidth("consGridani", 55 * (1 - Math.exp(-grid / 2000)));
|
||||
}
|
||||
|
||||
setFlowWidth("consEVani", 55 * (1 - Math.exp(-evPower)));
|
||||
setFlowWidth("consEVOGani", 55 * (1 - Math.exp(-evPowerOG)));
|
||||
setFlowWidth("consOGani", 55 * (1 - Math.exp(og / 2000)));
|
||||
|
||||
setFlowWidth("consUGani", 55 * (1 - Math.exp( -ug / 2000)));
|
||||
|
||||
setFlowWidth("consHeatAni", 55 * (1 - Math.exp(-Pheat / 2000)));
|
||||
setFlowWidth("consAni", 55 * (1 - Math.exp(cons / 2000)));
|
||||
setFlowWidth("consEGani", 55 * (1 - Math.exp(-eg / 2000)));
|
||||
setFlowWidth("pvani", 55 * (1 - Math.exp(-pv / 2000)));
|
||||
}
|
||||
});
|
||||
};
|
||||
document.querySelectorAll("[data-enf-ansicht]").forEach(function (knopf) {
|
||||
knopf.addEventListener("click", function () { zeigeAnsicht(knopf.dataset.enfAnsicht); });
|
||||
});
|
||||
|
||||
energiefluss = new Energiefluss(behaelter, {
|
||||
einstellungen: typeof energieflussEinstellungen !== "undefined" ? energieflussEinstellungen : {},
|
||||
onKlick: function (knoten) {
|
||||
if (knoten.klick.ansicht) zeigeAnsicht(knoten.klick.ansicht);
|
||||
else if (knoten.klick.modal) openModal(knoten.klick.modal);
|
||||
},
|
||||
onGezeichnet: function () {
|
||||
solarSVG.fillElementArray();
|
||||
if (typeof updateWateringStatus === "function") updateWateringStatus();
|
||||
},
|
||||
});
|
||||
energiefluss.zeichnen();
|
||||
}
|
||||
|
||||
var chartSettings = {
|
||||
@@ -604,7 +433,7 @@ var chartSettings = {
|
||||
// hier ist sie Beiwerk, dort der Zweck der Seite.
|
||||
baueJahresstatistik(document.getElementById("Stats-Year"),
|
||||
{ spalten: 1, werkzeuge: false, fussnoten: false });
|
||||
solarSVG.fillElementArray();
|
||||
energieflussStarten();
|
||||
mountMeteogram();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user