Skip to content

Instantly share code, notes, and snippets.

@andreaskueffel
Last active April 3, 2026 11:01
Show Gist options
  • Select an option

  • Save andreaskueffel/f6e8ee8b97204affb3aacaed9e655d69 to your computer and use it in GitHub Desktop.

Select an option

Save andreaskueffel/f6e8ee8b97204affb3aacaed9e655d69 to your computer and use it in GitHub Desktop.
Zero Export function Node for NodeRed to get target values for 2 independent batteries
/*****************************************************
* Marstek Dual‑Regler (1‑Sekunden Zyklus)
* Ziel: Netz = -5 Watt (leichte Einspeisung)
* Zwei Speicher werden dynamisch geregelt.
*
* Features:
* - PI‑Regler für Gesamtleistung
* - SOC‑Balancing
* - Anti‑Flattern: schneller Anstieg / langsamer Abfall
* - Glättung der Stellgrößen
*****************************************************/
// -----------------------
// KONFIGURATION
// -----------------------
const TARGET = -5; // Netzsollwert
const MAX_W = 800; // Max. Leistung pro Speicher
const MIN_W = 40;
const CYCLE = 1; // 1 Sekunde
// PI‑Parameter
const KP = 0.8;
const KI = 0.1;
// Glättungsfaktor
const SMOOTHING = 0.6;
// Boost für schnellen Anstieg
const BOOST_MULTIPLIER = 3; // >1 = schnelleres Hochregeln
const BOOST_THRESHOLD = 300; // Grid > 150W Bezug => sofort nach oben
// Verlangsamen des Abfalls
const DECAY_FACTOR = 0.8; // <1 = langsames Herunterregeln
// Mindest‑SOC Schutz
const SOC_MIN = 2; // Unter 10% kein Entladen
// -----------------------
// STATES (persistent)
// -----------------------
let integral = context.get("integral") || 0;
let lastPA = context.get("lastPA") || 0;
let lastPB = context.get("lastPB") || 0;
// -----------------------
// INPUTS
// -----------------------
const powerGrid = msg.payload.StatusSNS.eBZ.Power; // +W = Bezug, -W = Einspeisung
const socA = flow.get("soc1") || 50;
const socB = flow.get("soc2") || 50;
// -----------------------
// 1) PI‑REGELUNG
// -----------------------
let error = powerGrid - TARGET;
// Boost: schneller Anstieg bei hohem Netzbezug
if (powerGrid > BOOST_THRESHOLD) {
error *= BOOST_MULTIPLIER;
}
let rawOutput = error * KP + integral;
let saturated = Math.max(0, Math.min(rawOutput, MAX_W * 2));
// Anti-Windup
if (rawOutput === saturated) {
integral += error * KI;
}
let P_total = saturated;
// -----------------------
// 2) VERZÖGERTER ABFALL
// -----------------------
if (P_total < lastPA + lastPB) {
// herunterfahren nur langsam
P_total = (lastPA + lastPB) * DECAY_FACTOR + P_total * (1 - DECAY_FACTOR);
}
P_total = Math.round(P_total);
// -----------------------
// 3) SOC‑BALANCING
// -----------------------
const BALANCE_FACTOR = 2.0; // 1 = linear, 2 = stärker, 3 = aggressiv
let PA, PB;
if (socA < SOC_MIN && socB < SOC_MIN) {
PA = 0;
PB = 0;
}
else {
// Verstärkte SOC-Gewichtung
let wA = Math.pow(socA, BALANCE_FACTOR);
let wB = Math.pow(socB, BALANCE_FACTOR);
let sum = Math.max(wA + wB, 1);
let weightA = wA / sum;
let weightB = wB / sum;
PA = P_total * weightA;
PB = P_total * weightB;
// Maximalleistung beachten
PA = Math.min(PA, MAX_W);
PB = Math.min(PB, MAX_W);
//Wenn einer voll ist → Rest auf den anderen umschichten
let consumed = PA + PB;
let rest = P_total - consumed;
if (rest > 10) {
if (PA >= MAX_W) {
PB = Math.min(MAX_W, PB + rest);
}
else if (PB >= MAX_W) {
PA = Math.min(MAX_W, PA + rest);
}
}
// Not-Abschaltung
if (socA < SOC_MIN) PA = 0;
if (socB < SOC_MIN) PB = 0;
}
// -----------------------
// 4) GLÄTTUNG
// -----------------------
PA = lastPA * (1 - SMOOTHING) + PA * SMOOTHING;
PB = lastPB * (1 - SMOOTHING) + PB * SMOOTHING;
PA = Math.round(PA);
PB = Math.round(PB);
// Status anzeigen
node.status({
fill: "green",
shape: "ring",
text: "Power: " + powerGrid + "W, PTotal: " + P_total + "W (target1: " + PA + " | target2: " + PB + ")"
});
// -----------------------
// STATES SPEICHERN
// -----------------------
context.set("integral", integral);
context.set("lastPA", PA);
context.set("lastPB", PB);
// -----------------------
// OUTPUT
// -----------------------
return {
payload: {
setA: Math.round(PA),
setB: Math.round(PB),
debug: {
powerGrid,
socA,
socB,
error,
integral,
P_total
}
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment