Files
Smart-Dashboard/js/solar/energieflussKatalog.js
T
adminandClaude Opus 5 ba5622f9b2 Solar: Speicher-Ansicht mit Modulen und Zellen der BYD-Batterie
Dritte Ansicht der Realtime-Karte (auch per Klick auf die Batterie):
Kennzahlen (Ladestand mit kWh und voll/leer um, Leistung, SOH,
Zellspreizung, Temperaturen, Spannung, Durchsatz, Zustand), der Turm aus
BMU und acht Modulen mit je 16 Zellbalken, das gewaehlte Modul mit
Zellspannungen und Temperaturfuehlern, dazu Verlaeufe fuer Ladestand und
Leistung (EnergyFlow) sowie Spreizung und Temperatur (byd) mit Zeitraum-
wahl und Anzeige beim Ueberfahren. Neu: ajax/speicher.php.

Nebenbei: Icon-Zeichen in energiefluss.js und im Katalog stehen als
Escape (\uF...) statt als unsichtbare Zeichen im Quelltext.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 18:11:22 +02:00

305 lines
13 KiB
JavaScript

/* ==========================================================================
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(".", ",");
/** Flächen des Sonnenrings aus anlageKatalog.js - fehlt der, feste Vorgabe. */
function flaechenAusAnlage() {
if (typeof ANLAGE_KATALOG === "undefined") {
return [
{ name: "Dach", von: 0, bis: 1, farbe: "#f7c000" },
{ name: "Veranda", von: 2, bis: 13, farbe: "#f78d00" },
{ name: "Carport", von: 14, bis: 33, farbe: "#f75a1e" },
];
}
return ANLAGE_KATALOG.flaechen.map(f => {
const eingaenge = [
...ANLAGE_KATALOG.platten.filter(p => p.flaeche === f.id).map(p => p.eingang),
...ANLAGE_KATALOG.strings.filter(s => s.flaeche === f.id).map(s => s.eingang),
];
return { name: f.name, von: Math.min(...eingaenge), bis: Math.max(...eingaenge), farbe: f.farbe };
}).filter(f => isFinite(f.von));
}
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.
// Vorgabe aus dem Anlagenkatalog, damit die Zuordnung nur einmal
// gepflegt wird; in den Einstellungen überschreibbar.
optionen: { flaechen: flaechenAusAnlage() },
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,
klick: { ansicht: "speicher" },
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: "\uF180", status: "wateringTroughStatus", symbol: "wateringTroughIcon" },
{ name: "Vorn", zeichen: "\uF3CE", status: "wateringFrontStatus", symbol: "wateringFrontIcon" },
{ name: "Beete", zeichen: "\uF3CD", status: "wateringBedsStatus", symbol: "wateringBedsIcon" },
],
lesen() {
return { wert: "" };
},
},
];
return { knoten };
})();