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:
+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