NEWS
Venus V3.0 API Token
-
Habe nun auch komplett umgeschwenkt auf die Api Steuerung.
Mit der neuen Firmware hatte ich zwar auch die Api Funktion in der App, aber der Speicher hatte nicht geantwortet.
Der Support hatte mir dann neuere Firmware freigeschaltet, und ja hat geklappt, die Speicher kann ich nun via API regeln.
Das Laden hatte via Skript schon mal geklappt, mal sehen, wie es sich nachher verhält, beim endladen. Das neue Skript ist noch "frisch". -
Habe nun auch komplett umgeschwenkt auf die Api Steuerung.
Mit der neuen Firmware hatte ich zwar auch die Api Funktion in der App, aber der Speicher hatte nicht geantwortet.
Der Support hatte mir dann neuere Firmware freigeschaltet, und ja hat geklappt, die Speicher kann ich nun via API regeln.
Das Laden hatte via Skript schon mal geklappt, mal sehen, wie es sich nachher verhält, beim endladen. Das neue Skript ist noch "frisch".@Gismoh sagte in Venus V3.0 API Token:
Habe nun auch komplett umgeschwenkt auf die Api Steuerung.
Mit der neuen Firmware hatte ich zwar auch die Api Funktion in der App, aber der Speicher hatte nicht geantwortet.
Der Support hatte mir dann neuere Firmware freigeschaltet, und ja hat geklappt, die Speicher kann ich nun via API regeln.
Das Laden hatte via Skript schon mal geklappt, mal sehen, wie es sich nachher verhält, beim endladen. Das neue Skript ist noch "frisch".Kannst du deinen Stand hier aktuell halten? Mein Script ist überhaupt noch nicht ausgereift und mit meiner Shelly Emulation hab ich nur Probleme
-
@Gismoh sagte in Venus V3.0 API Token:
Habe nun auch komplett umgeschwenkt auf die Api Steuerung.
Mit der neuen Firmware hatte ich zwar auch die Api Funktion in der App, aber der Speicher hatte nicht geantwortet.
Der Support hatte mir dann neuere Firmware freigeschaltet, und ja hat geklappt, die Speicher kann ich nun via API regeln.
Das Laden hatte via Skript schon mal geklappt, mal sehen, wie es sich nachher verhält, beim endladen. Das neue Skript ist noch "frisch".Kannst du deinen Stand hier aktuell halten? Mein Script ist überhaupt noch nicht ausgereift und mit meiner Shelly Emulation hab ich nur Probleme
@Hansi1234 gerne.
Wie gesagt kann die steuern, das das Skript zwischen laden und entladen wechselt.
Aber das Timing stimmt aktuell nicht, bin gerade aktiv am testen und quasi am "debuggen". -
@Hansi1234 gerne.
Wie gesagt kann die steuern, das das Skript zwischen laden und entladen wechselt.
Aber das Timing stimmt aktuell nicht, bin gerade aktiv am testen und quasi am "debuggen". -
@Gismoh ja, Stichwort Hysterese. Aber gab's da nicht ein Adapter Nulleinspeisung oä?
Hast du es zufällig geschafft, den Akku via uni-meter zu betreiben?@Hansi1234 ne, unimeter habe ich ja gar nicht versucht. Wollte es ja ohne zusätzlichen Code machen, bei mir war ja alles nur im IOBroker als JS. Hatte dort die Regelung nicht hinbekommen, steuern konnte ich ja.
Hatte dies nun komplett verworfen und habe gestern mit der Steuerung über Api angefangen.
Ein Adapter "Nulleinspeisung" kenne ich nicht. -
@Gismoh ja, Stichwort Hysterese. Aber gab's da nicht ein Adapter Nulleinspeisung oä?
Hast du es zufällig geschafft, den Akku via uni-meter zu betreiben?@Hansi1234 habe das Regelskript mal sehr verkürzt und viel Funktionen rausgenommen (von ca. 1300 Zeilen auf ca. 200) und teste dies gerade.
Neuberechnung auch nur 1x je Minute.
Sieht bisher sauber aus, evtl. wollte ich auch direkt zu viel auf einmal.Dies ist mein aktuelles kleines Testscript: (aktuell nur für das endladen)
Aber nur für Testzwecke, denn es sind keine Fallbacks etc. eingebaut - lasse ich gerade auch nur unter Aufsicht laufen)// ============================================================ // MARSTEK_3 Hauptskript // v1.0.0 // // Datenpfad: 0_userdata.0.Akku_PV.Marstek_3. // // Logik: // - Jede Minute: Tibber lesen // - Formel: plug_real + tibber = neues Ziel // - 50/50 auf beide Speicher // - Keine Suppression, keine Verteilung, keine Sonderfaelle // ============================================================ const dgram = require('dgram'); const DP = '0_userdata.0.Akku_PV.Marstek_3.'; const TIBBER = 'tibberlink.0.LocalPulse.0.Power'; const PLUG_28 = 'shelly.1.shellyplugsg3#3528#1.Relay0.Power'; const PLUG_29 = 'shelly.1.shellyplugsg3#3529#1.Relay0.Power'; const IP_28 = '192.168.145.179'; const IP_29 = '192.168.145.155'; const PORT = 30000; const INTERVALL_MS = 60000; // 1 Minute const MAX_W = 800; // Hartgrenze pro Speicher const MIN_W = 50; // unter MIN_W -> 0W senden const EV_W = 10; // Eigenverbrauch Speicher (Elektronik) let udpClient = null; let ivMain = null; // ------------------------------------------------------------ // Datenpunkte anlegen // ------------------------------------------------------------ function initDP() { createState(DP + 'einstellungen.aktiv', true, { name: 'Steuerung aktiv', type: 'boolean', read: true, write: true }); createState(DP + 'einstellungen.max_w_pro_speicher', MAX_W, { name: 'Max W pro Speicher', type: 'number', unit: 'W', read: true, write: true }); createState(DP + 'einstellungen.min_w', MIN_W, { name: 'Min W (unter diesem Wert -> 0W)', type: 'number', unit: 'W', read: true, write: true }); createState(DP + 'status.tibber_w', 0, { name: 'Tibber aktuell', type: 'number', unit: 'W', read: true, write: false }); createState(DP + 'status.plug_28_w', 0, { name: 'Plug 3528', type: 'number', unit: 'W', read: true, write: false }); createState(DP + 'status.plug_29_w', 0, { name: 'Plug 3529', type: 'number', unit: 'W', read: true, write: false }); createState(DP + 'status.ziel_gesamt_w', 0, { name: 'Ziel gesamt', type: 'number', unit: 'W', read: true, write: false }); createState(DP + 'status.gesendet_28_w', 0, { name: 'Gesendet 3528', type: 'number', unit: 'W', read: true, write: false }); createState(DP + 'status.gesendet_29_w', 0, { name: 'Gesendet 3529', type: 'number', unit: 'W', read: true, write: false }); createState(DP + 'status.letzte_regelung', '', { name: 'Letzte Regelung', type: 'string', read: true, write: false }); } // ------------------------------------------------------------ // UDP // ------------------------------------------------------------ function initUDP() { udpClient = dgram.createSocket('udp4'); udpClient.on('error', function(e) { log('UDP Fehler: ' + e.message, 'warn'); }); udpClient.bind(30002, function() { log('✅ UDP bereit (Port 30002)'); }); } function sendManual(ip, watt) { if (!udpClient) return; const power = Math.round(watt); // positiv = entladen, negativ = laden const msg = JSON.stringify({ id: 1, method: 'ES.SetMode', params: { id: 0, config: { mode: 'Manual', manual_cfg: { time_num: 1, start_time: '00:00:00', end_time: '23:59:59', week_set: 127, power: power, enable: 1 } }} }); udpClient.send(Buffer.from(msg), PORT, ip, function(err) { if (err) log('UDP Send Fehler (' + ip + '): ' + err.message, 'warn'); }); } // ------------------------------------------------------------ // Hilfsfunktionen // ------------------------------------------------------------ function safeVal(path, fallback) { try { const s = getState(path); if (!s || s.val === null || s.val === undefined) return fallback; const n = parseFloat(s.val); return isNaN(n) ? fallback : n; } catch(e) { return fallback; } } function safeBool(path, fallback) { try { const s = getState(path); if (!s || s.val === null) return fallback; return !!s.val; } catch(e) { return fallback; } } // ------------------------------------------------------------ // Hauptregelung // ------------------------------------------------------------ function regeln() { if (!safeBool(DP + 'einstellungen.aktiv', true)) { log('⏸️ Steuerung deaktiviert'); return; } const maxW = safeVal(DP + 'einstellungen.max_w_pro_speicher', MAX_W); const minW = safeVal(DP + 'einstellungen.min_w', MIN_W); // Werte lesen const tibber = safeVal(TIBBER, 0); const plug28 = safeVal(PLUG_28, 0); const plug29 = safeVal(PLUG_29, 0); const plugGes = plug28 + plug29; // Diagnose-Datenpunkte setState(DP + 'status.tibber_w', Math.round(tibber)); setState(DP + 'status.plug_28_w', Math.round(plug28)); setState(DP + 'status.plug_29_w', Math.round(plug29)); // Real entladene Leistung (negativ = Entladen beim Shelly Plug) const realEntladen = plugGes < -EV_W ? Math.abs(plugGes) : 0; // Tempomat-Formel let zielGesamt = realEntladen + tibber; zielGesamt = Math.max(0, zielGesamt); setState(DP + 'status.ziel_gesamt_w', Math.round(zielGesamt)); // Unter Minimum -> Standby if (zielGesamt < minW) { log('⏸️ Standby: Ziel ' + Math.round(zielGesamt) + 'W < Min ' + minW + 'W'); sendManual(IP_28, 0); sendManual(IP_29, 0); setState(DP + 'status.gesendet_28_w', 0); setState(DP + 'status.gesendet_29_w', 0); setState(DP + 'status.letzte_regelung', new Date().toLocaleTimeString() + ' | Standby'); return; } // 50/50 aufteilen, begrenzen let w28 = Math.min(Math.round(zielGesamt / 2), maxW); let w29 = Math.min(Math.round(zielGesamt / 2), maxW); // Senden sendManual(IP_28, w28); sendManual(IP_29, w29); setState(DP + 'status.gesendet_28_w', w28); setState(DP + 'status.gesendet_29_w', w29); const ts = new Date().toLocaleTimeString(); const info = ts + ' | Tibber: ' + Math.round(tibber) + 'W' + ' | Plug: ' + Math.round(plugGes) + 'W' + ' | Ziel: ' + Math.round(zielGesamt) + 'W' + ' | 3528: ' + w28 + 'W | 3529: ' + w29 + 'W'; setState(DP + 'status.letzte_regelung', info); log('📤 ' + info); } // ------------------------------------------------------------ // Start / Stop // ------------------------------------------------------------ function start() { log('🚀 MARSTEK_3 Hauptskript v1.0.0 gestartet'); initDP(); initUDP(); setTimeout(function() { regeln(); // sofort einmal ivMain = setInterval(regeln, INTERVALL_MS); log('✅ Regelintervall: 60s'); }, 2000); } start(); onStop(function() { if (ivMain) { clearInterval(ivMain); } if (udpClient) { sendManual(IP_28, 0); sendManual(IP_29, 0); setTimeout(function() { try { udpClient.close(); } catch(e) {} }, 500); } log('🛑 MARSTEK_3 gestoppt - 0W gesendet'); }, 1500); -
@Hansi1234 habe das Regelskript mal sehr verkürzt und viel Funktionen rausgenommen (von ca. 1300 Zeilen auf ca. 200) und teste dies gerade.
Neuberechnung auch nur 1x je Minute.
Sieht bisher sauber aus, evtl. wollte ich auch direkt zu viel auf einmal.Dies ist mein aktuelles kleines Testscript: (aktuell nur für das endladen)
Aber nur für Testzwecke, denn es sind keine Fallbacks etc. eingebaut - lasse ich gerade auch nur unter Aufsicht laufen)// ============================================================ // MARSTEK_3 Hauptskript // v1.0.0 // // Datenpfad: 0_userdata.0.Akku_PV.Marstek_3. // // Logik: // - Jede Minute: Tibber lesen // - Formel: plug_real + tibber = neues Ziel // - 50/50 auf beide Speicher // - Keine Suppression, keine Verteilung, keine Sonderfaelle // ============================================================ const dgram = require('dgram'); const DP = '0_userdata.0.Akku_PV.Marstek_3.'; const TIBBER = 'tibberlink.0.LocalPulse.0.Power'; const PLUG_28 = 'shelly.1.shellyplugsg3#3528#1.Relay0.Power'; const PLUG_29 = 'shelly.1.shellyplugsg3#3529#1.Relay0.Power'; const IP_28 = '192.168.145.179'; const IP_29 = '192.168.145.155'; const PORT = 30000; const INTERVALL_MS = 60000; // 1 Minute const MAX_W = 800; // Hartgrenze pro Speicher const MIN_W = 50; // unter MIN_W -> 0W senden const EV_W = 10; // Eigenverbrauch Speicher (Elektronik) let udpClient = null; let ivMain = null; // ------------------------------------------------------------ // Datenpunkte anlegen // ------------------------------------------------------------ function initDP() { createState(DP + 'einstellungen.aktiv', true, { name: 'Steuerung aktiv', type: 'boolean', read: true, write: true }); createState(DP + 'einstellungen.max_w_pro_speicher', MAX_W, { name: 'Max W pro Speicher', type: 'number', unit: 'W', read: true, write: true }); createState(DP + 'einstellungen.min_w', MIN_W, { name: 'Min W (unter diesem Wert -> 0W)', type: 'number', unit: 'W', read: true, write: true }); createState(DP + 'status.tibber_w', 0, { name: 'Tibber aktuell', type: 'number', unit: 'W', read: true, write: false }); createState(DP + 'status.plug_28_w', 0, { name: 'Plug 3528', type: 'number', unit: 'W', read: true, write: false }); createState(DP + 'status.plug_29_w', 0, { name: 'Plug 3529', type: 'number', unit: 'W', read: true, write: false }); createState(DP + 'status.ziel_gesamt_w', 0, { name: 'Ziel gesamt', type: 'number', unit: 'W', read: true, write: false }); createState(DP + 'status.gesendet_28_w', 0, { name: 'Gesendet 3528', type: 'number', unit: 'W', read: true, write: false }); createState(DP + 'status.gesendet_29_w', 0, { name: 'Gesendet 3529', type: 'number', unit: 'W', read: true, write: false }); createState(DP + 'status.letzte_regelung', '', { name: 'Letzte Regelung', type: 'string', read: true, write: false }); } // ------------------------------------------------------------ // UDP // ------------------------------------------------------------ function initUDP() { udpClient = dgram.createSocket('udp4'); udpClient.on('error', function(e) { log('UDP Fehler: ' + e.message, 'warn'); }); udpClient.bind(30002, function() { log('✅ UDP bereit (Port 30002)'); }); } function sendManual(ip, watt) { if (!udpClient) return; const power = Math.round(watt); // positiv = entladen, negativ = laden const msg = JSON.stringify({ id: 1, method: 'ES.SetMode', params: { id: 0, config: { mode: 'Manual', manual_cfg: { time_num: 1, start_time: '00:00:00', end_time: '23:59:59', week_set: 127, power: power, enable: 1 } }} }); udpClient.send(Buffer.from(msg), PORT, ip, function(err) { if (err) log('UDP Send Fehler (' + ip + '): ' + err.message, 'warn'); }); } // ------------------------------------------------------------ // Hilfsfunktionen // ------------------------------------------------------------ function safeVal(path, fallback) { try { const s = getState(path); if (!s || s.val === null || s.val === undefined) return fallback; const n = parseFloat(s.val); return isNaN(n) ? fallback : n; } catch(e) { return fallback; } } function safeBool(path, fallback) { try { const s = getState(path); if (!s || s.val === null) return fallback; return !!s.val; } catch(e) { return fallback; } } // ------------------------------------------------------------ // Hauptregelung // ------------------------------------------------------------ function regeln() { if (!safeBool(DP + 'einstellungen.aktiv', true)) { log('⏸️ Steuerung deaktiviert'); return; } const maxW = safeVal(DP + 'einstellungen.max_w_pro_speicher', MAX_W); const minW = safeVal(DP + 'einstellungen.min_w', MIN_W); // Werte lesen const tibber = safeVal(TIBBER, 0); const plug28 = safeVal(PLUG_28, 0); const plug29 = safeVal(PLUG_29, 0); const plugGes = plug28 + plug29; // Diagnose-Datenpunkte setState(DP + 'status.tibber_w', Math.round(tibber)); setState(DP + 'status.plug_28_w', Math.round(plug28)); setState(DP + 'status.plug_29_w', Math.round(plug29)); // Real entladene Leistung (negativ = Entladen beim Shelly Plug) const realEntladen = plugGes < -EV_W ? Math.abs(plugGes) : 0; // Tempomat-Formel let zielGesamt = realEntladen + tibber; zielGesamt = Math.max(0, zielGesamt); setState(DP + 'status.ziel_gesamt_w', Math.round(zielGesamt)); // Unter Minimum -> Standby if (zielGesamt < minW) { log('⏸️ Standby: Ziel ' + Math.round(zielGesamt) + 'W < Min ' + minW + 'W'); sendManual(IP_28, 0); sendManual(IP_29, 0); setState(DP + 'status.gesendet_28_w', 0); setState(DP + 'status.gesendet_29_w', 0); setState(DP + 'status.letzte_regelung', new Date().toLocaleTimeString() + ' | Standby'); return; } // 50/50 aufteilen, begrenzen let w28 = Math.min(Math.round(zielGesamt / 2), maxW); let w29 = Math.min(Math.round(zielGesamt / 2), maxW); // Senden sendManual(IP_28, w28); sendManual(IP_29, w29); setState(DP + 'status.gesendet_28_w', w28); setState(DP + 'status.gesendet_29_w', w29); const ts = new Date().toLocaleTimeString(); const info = ts + ' | Tibber: ' + Math.round(tibber) + 'W' + ' | Plug: ' + Math.round(plugGes) + 'W' + ' | Ziel: ' + Math.round(zielGesamt) + 'W' + ' | 3528: ' + w28 + 'W | 3529: ' + w29 + 'W'; setState(DP + 'status.letzte_regelung', info); log('📤 ' + info); } // ------------------------------------------------------------ // Start / Stop // ------------------------------------------------------------ function start() { log('🚀 MARSTEK_3 Hauptskript v1.0.0 gestartet'); initDP(); initUDP(); setTimeout(function() { regeln(); // sofort einmal ivMain = setInterval(regeln, INTERVALL_MS); log('✅ Regelintervall: 60s'); }, 2000); } start(); onStop(function() { if (ivMain) { clearInterval(ivMain); } if (udpClient) { sendManual(IP_28, 0); sendManual(IP_29, 0); setTimeout(function() { try { udpClient.close(); } catch(e) {} }, 500); } log('🛑 MARSTEK_3 gestoppt - 0W gesendet'); }, 1500); -
@Hansi1234 Nein, natürlich nicht.
Hatte doch geschrieben, das es nur ein sehr kurzes Testskript war (um Fehlerquellen- auszuschließen). Mein vorherigen Code war x mal so lange und hatte sehr viel mehr features drin.Aber gestern Abend ging es für mich "back to the roots" und nach 21 Uhr gab es eben keine Sonne ;)
Hatte es noch ein wenig erweitert, so das ich es habe laufen lassen, und nun bin ich gerade aufgewacht, und es hat bereits geladen, bis Pmax. je Speicher.
Und es hat unseren, obwohl sehr einfach geschrieben, Zukauf von Strom, heute bislang auf 60 Watt begrenzt. -
Hallo !
vielen Dank für den Code !Api freischalten geht seit etwa Dezember direkt in der App.
Geht einwandfrei !
--> ein bischen Offtopic: Sollte jemand lieber mit Blockly "programmieren" geht das sehr einfach mit exec aufrufen (mit netcat = nc)
echo '{"id":1,"method":"Bat.GetStatus","params":{"id":0}}' | nc -u -w 1 192.168.178.xyz 30000
Alles andere aus der API kann man damit auch abfragen / übertragen / Modes setzen etc. Habs grad probiert. Wichtig ist nur das vorher einmalig :
echo '{ "id": 0, "method": "Marstek.GetDevice", "params": { "ble_mac":"0" } } ' | nc -u -w 1 192.168.178.255 30000 gesendet wird damit er die UDP API aktiviert.
Manchmnal antwort mein Marstek Venus E v3 erst beim 2ten mal. Offenbar muss er nach längerer Zeit immer erst auffwachen.
@derkleinschreiber sagte in Venus V3.0 API Token:
--> ein bischen Offtopic: Sollte jemand lieber mit Blockly "programmieren" geht das sehr einfach mit exec aufrufen (mit netcat = nc)
echo '{"id":1,"method":"Bat.GetStatus","params":{"id":0}}' | nc -u -w 1 192.168.178.xyz 30000
Ich bin dabei die Marstek Venus 3.0 per bash Script abzufragen.
Wenn ich den o.g. Befehl in einer interaktiven bash-shell aufrufe bekomme ich einereguläre Antwort.
Wenn ich es alerdings in einem bash-script aufrufe mit dieser Zeile:BATT_String=$(echo '{"id":1,"method":"Bat.GetStatus","params":{"id":0}}' | nc -u -w 1 192.168.2.16 30000)bekomme ich nur eine partielle Antwort:
API Result={ "id": 0Irgendwelche ideen, was ich falsch mache???
-
@Gismoh sagte in Venus V3.0 API Token:
Habe nun auch komplett umgeschwenkt auf die Api Steuerung.
Mit der neuen Firmware hatte ich zwar auch die Api Funktion in der App, aber der Speicher hatte nicht geantwortet.
Der Support hatte mir dann neuere Firmware freigeschaltet, und ja hat geklappt, die Speicher kann ich nun via API regeln.
Das Laden hatte via Skript schon mal geklappt, mal sehen, wie es sich nachher verhält, beim endladen. Das neue Skript ist noch "frisch".Kannst du deinen Stand hier aktuell halten? Mein Script ist überhaupt noch nicht ausgereift und mit meiner Shelly Emulation hab ich nur Probleme
@Hansi1234 sagte in Venus V3.0 API Token:
Kannst du deinen Stand hier aktuell halten? Mein Script ist überhaupt noch nicht ausgereift und mit meiner Shelly Emulation hab ich nur Probleme
@hansi1234
dieses Skript läuft nun seid ein paar Tagen bei mir, und sieht ganz gut aus, werde daran aber noch weiter arbeiten und auch noch PV-Forecast wieder einbinden. Aber aktuell läuft es so.// ============================================================ // MARSTEK_3 Hauptskript // v1.2.1 // // Datenpfad: 0_userdata.0.Akku_PV.Marstek_3. // // Changelog: // v1.2.1 - SOC-basierte Lastgewichtung beim Laden und Entladen (sanft, max 65/35) // v1.2.0 - Shelly 3EM als Tibber-Fallback; Plug-Ausfall sicher abgefangen; // Ausfallerkennung deaktiviert wenn kein Plug vorhanden oder ausgefallen; // Fault-Reset bei Plug-Ausfall damit Speicher nicht dauerhaft blockiert bleibt // v1.1.8 - Lade-Logik auf real verfuegbaren Ueberschuss mit einstellbarem Prozent-Puffer umgebaut; PMAX-Clamp bleibt erhalten // v1.1.7 - Echter Ein-Speicher-Fallback in der Leistungsverteilung: verfuegbarer Speicher uebernimmt innerhalb seiner Grenzen das volle Ziel // v1.1.6 - Alternierender Modus vorbereitet: auto-Teilintervall, Verfuegbarkeitspruefung, Ein-Speicher-Fallback // v1.1.5 - Interne Leistungsquellen nur vorbereitet, 3529 ohne spekulativen Ersatzwert, sicherer Shelly-Fallback // v1.1.4 - Konfigurierbare Leistungsquellen, sichere Voll-Ladepause bei vollen Speichern, Default weiter Shelly // v1.1.3 - SOC-Polling entkoppelt, alternierend, bereichsabhaengig und per Datenpunkten konfigurierbar // v1.1.2 - Haertung, 3529-SOC via Bat.GetStatus/soc, api_ongrid_28, safeSetState, robuster Start // v1.1.1 - Laden bei PV-Ueberschuss eingebaut // v1.0.0 - Erste Version // // Logik: // - Regelintervall aktuell 30 Sekunden (INTERVALL_MS) // - Optional alternierender Modus mit Auto-Teilintervall nach Anzahl verfuegbarer Speicher // - Tibber lesen mit Alterscheck und Haltezeit; Fallback auf Shelly 3EM wenn Tibber ausfaellt // - Formel: plug_real + tibber = neues Ziel // - 50/50 nur wenn beide Speicher fuer die Richtung verfuegbar sind // - Ein verfuegbarer Speicher uebernimmt das volle Ziel innerhalb seiner Grenzen // - SOC-Polling entkoppelt, alternierend und bereichsabhaengig // - Konfigurierbare Leistungsquellen (Default: Shelly Plug) // - Voll-Ladepause fuer bereits volle Speicher // - Plug-Ausfall wird sicher abgefangen: Regelung laeuft weiter ohne Feedback // ============================================================ const dgram = require('dgram'); const DP = '0_userdata.0.Akku_PV.Marstek_3.'; const TIBBER = 'tibberlink.0.LocalPulse.0.Power'; // ------------------------------------------------------- // Leistungsmessung direkt am Speicher (Shelly Plug, Tasmota, o.ae.) // Tragt hier den ioBroker-Datenpunkt eures Leistungsmessers ein. // Der Datenpunkt muss die aktuelle Wattleistung des Speichers liefern. // // WARUM ist eine Leistungsmessung am Speicher sinnvoll? // Das Skript weiss dann was der Speicher wirklich tut - nicht nur was // es ihm befohlen hat. Das ermoeglicht automatische Fehlerkorrektur // und erkennt wenn ein Speicher nicht reagiert. // // Kein Leistungsmesser vorhanden? Einfach '' (leere Anführungszeichen) eintragen. // Das Skript regelt dann trotzdem - aber ohne Rueckmeldung vom Speicher. // Faellt ein eingetragener Messer aus, schaltet das Skript automatisch // in diesen Modus und wieder zurueck sobald Werte zurueckkommen. const PLUG_28 = 'shelly.1.shellyplugsg3#3528#1.Relay0.Power'; // <-- Datenpunkt eintragen oder '' wenn keiner vorhanden const PLUG_29 = 'shelly.1.shellyplugsg3#3529#1.Relay0.Power'; // <-- Datenpunkt eintragen oder '' wenn keiner vorhanden // ------------------------------------------------------- // Netzstrommessung fuer das gesamte Haus (Tibber-Fallback) // Dieser Datenpunkt wird genutzt wenn der Tibber-Adapter ausfaellt. // Muss den aktuellen Netzbezug/-einspeisung in Watt liefern. // Positiv = Netzbezug, Negativ = Einspeisung. const SHELLY_3EM_TOTAL = 'alias.0.Haus.Strom.Strom-1.Shelly3EM-InstantPower'; const IP_28 = '192.168.145.179'; const IP_29 = '192.168.145.155'; const PORT = 30000; const INTERVALL_MS = 30000; const MAX_ENTLADEN_W = 800; const MAX_LADEN_W = 1250; const MIN_W = 50; const EV_W = 10; const TIBBER_MAX_MS = 25000; const HALTE_MS = 240000; const SOC_MIN_PCT = 12; const SOC_NORMAL_MIN = 25; const SOC_NORMAL_MAX = 85; const SOC_POLL_NORMAL_S = 180; const SOC_POLL_RAND_S = 60; const API_ONGRID_FACTOR_28 = 1; const FULL_SOC_PCT = 100; const FULL_POWER_MAX_W = 15; const ALTERNIEREND_AKTIV = false; const AUSFALLERKENNUNG_AKTIV = true; const AUSFALL_MIN_SOLL_W = 150; const AUSFALL_MIN_REAL_RATIO = 0.35; const AUSFALL_ZYKLEN = 3; const AUSFALL_COOLDOWN_S = 120; const AUSFALL_PROBE_FAKTOR_PCT = 30; let udpClient = null; let ivMain = null; let currentLoopMs = INTERVALL_MS; let lastTibberGutTs = 0; let lastTibberGutW = 0; let soc28 = 50; let soc29 = 50; let lastSocTs28 = 0; let lastSocTs29 = 0; let nextSocTarget = '28'; let fullChargeLock28 = false; let fullChargeLock29 = false; let alternatingIndex = 0; let lastCmd28 = 0; let lastCmd29 = 0; let lastTarget28 = 0; let lastTarget29 = 0; let fault28 = false; let fault29 = false; let failCount28 = 0; let failCount29 = 0; let cooldownUntil28 = 0; let cooldownUntil29 = 0; let probe28 = false; let probe29 = false; // ------------------------------------------------------------ // Datenpunkte anlegen // ------------------------------------------------------------ function initDP() { createState(DP + 'einstellungen.aktiv', true, { name: 'Steuerung aktiv', type: 'boolean', read: true, write: true }); createState(DP + 'einstellungen.max_entladen_w_pro_speicher', MAX_ENTLADEN_W, { name: 'Max W pro Speicher (Entladen)', type: 'number', unit: 'W', read: true, write: true }); createState(DP + 'einstellungen.max_laden_w_pro_speicher', MAX_LADEN_W, { name: 'Max W pro Speicher (Laden)', type: 'number', unit: 'W', read: true, write: true }); createState(DP + 'einstellungen.min_w', MIN_W, { name: 'Min W (unter diesem Wert -> 0W)', type: 'number', unit: 'W', read: true, write: true }); createState(DP + 'einstellungen.laden_drossel_soc', 90, { name: 'Laden drosseln ab SOC [%]', type: 'number', unit: '%', read: true, write: true }); createState(DP + 'einstellungen.laden_drossel_faktor', 50, { name: 'Drossel-Faktor [%]', type: 'number', unit: '%', read: true, write: true }); createState(DP + 'einstellungen.laden_drossel_max_w', 800, { name: 'Drossel max W pro Speicher', type: 'number', unit: 'W', read: true, write: true }); createState(DP + 'einstellungen.lade_puffer_pct', 20, { name: 'Lade-Puffer [%] vom verfuegbaren Ueberschuss', type: 'number', unit: '%', read: true, write: true }); createState(DP + 'einstellungen.soc_normal_min_pct', SOC_NORMAL_MIN, { name: 'SOC Normalbereich Minimum [%]', type: 'number', unit: '%', read: true, write: true }); createState(DP + 'einstellungen.soc_normal_max_pct', SOC_NORMAL_MAX, { name: 'SOC Normalbereich Maximum [%]', type: 'number', unit: '%', read: true, write: true }); createState(DP + 'einstellungen.soc_poll_normal_s', SOC_POLL_NORMAL_S, { name: 'SOC Polling Normalbereich [s]', type: 'number', unit: 's', read: true, write: true }); createState(DP + 'einstellungen.soc_poll_rand_s', SOC_POLL_RAND_S, { name: 'SOC Polling Randbereich [s]', type: 'number', unit: 's', read: true, write: true }); createState(DP + 'einstellungen.nutze_shelly_28', true, { name: 'Nutze Shelly 3528', type: 'boolean', read: true, write: true }); createState(DP + 'einstellungen.nutze_shelly_29', true, { name: 'Nutze Shelly 3529', type: 'boolean', read: true, write: true }); createState(DP + 'einstellungen.api_ongrid_faktor_28', API_ONGRID_FACTOR_28, { name: 'api_ongrid Faktor 3528', type: 'number', read: true, write: true }); createState(DP + 'einstellungen.voll_ladepause_aktiv', true, { name: 'Voll-Ladepause aktiv', type: 'boolean', read: true, write: true }); createState(DP + 'einstellungen.voll_soc_pct', FULL_SOC_PCT, { name: 'Voll ab SOC [%]', type: 'number', unit: '%', read: true, write: true }); createState(DP + 'einstellungen.voll_power_max_w', FULL_POWER_MAX_W, { name: 'Voll wenn Realleistung kleiner [W]', type: 'number', unit: 'W', read: true, write: true }); createState(DP + 'einstellungen.alternierend_aktiv', ALTERNIEREND_AKTIV, { name: 'Alternierender Modus aktiv', type: 'boolean', read: true, write: true }); createState(DP + 'einstellungen.ausfallerkennung_aktiv', AUSFALLERKENNUNG_AKTIV, { name: 'Ausfallerkennung aktiv', type: 'boolean', read: true, write: true }); createState(DP + 'einstellungen.ausfall_min_soll_w', AUSFALL_MIN_SOLL_W, { name: 'Ausfall min Soll [W]', type: 'number', unit: 'W', read: true, write: true }); createState(DP + 'einstellungen.ausfall_min_real_ratio', AUSFALL_MIN_REAL_RATIO, { name: 'Ausfall min Realverhaeltnis', type: 'number', read: true, write: true }); createState(DP + 'einstellungen.ausfall_zyklen', AUSFALL_ZYKLEN, { name: 'Ausfall Zyklen', type: 'number', read: true, write: true }); createState(DP + 'einstellungen.ausfall_cooldown_s', AUSFALL_COOLDOWN_S, { name: 'Ausfall Cooldown [s]', type: 'number', unit: 's', read: true, write: true }); createState(DP + 'einstellungen.ausfall_probe_faktor_pct', AUSFALL_PROBE_FAKTOR_PCT, { name: 'Ausfall Probe Faktor [%]', type: 'number', unit: '%', read: true, write: true }); createState(DP + 'status.tibber_w', 0, { name: 'Tibber aktuell', type: 'number', unit: 'W', read: true, write: true }); createState(DP + 'status.plug_28_w', 0, { name: 'Plug 3528', type: 'number', unit: 'W', read: true, write: true }); createState(DP + 'status.plug_29_w', 0, { name: 'Plug 3529', type: 'number', unit: 'W', read: true, write: true }); createState(DP + 'status.ziel_gesamt_w', 0, { name: 'Ziel gesamt', type: 'number', unit: 'W', read: true, write: true }); createState(DP + 'status.gesendet_28_w', 0, { name: 'Gesendet 3528', type: 'number', unit: 'W', read: true, write: true }); createState(DP + 'status.gesendet_29_w', 0, { name: 'Gesendet 3529', type: 'number', unit: 'W', read: true, write: true }); createState(DP + 'status.letzte_regelung', '', { name: 'Letzte Regelung', type: 'string', read: true, write: true }); createState(DP + 'status.soc_28', 0, { name: 'SOC 3528', type: 'number', unit: '%', read: true, write: true }); createState(DP + 'status.soc_29', 0, { name: 'SOC 3529', type: 'number', unit: '%', read: true, write: true }); createState(DP + 'status.tibber_quelle', '', { name: 'Tibber Quelle', type: 'string', read: true, write: true }); createState(DP + 'status.api_ongrid_28', 0, { name: 'Marstek intern 3528 [W]', type: 'number', unit: 'W', read: true, write: true }); createState(DP + 'status.api_real_29_test', 0, { name: 'Marstek intern 3529 Test [W]', type: 'number', unit: 'W', read: true, write: true }); createState(DP + 'status.soc_alter_28_s', 0, { name: 'SOC Alter 3528 [s]', type: 'number', unit: 's', read: true, write: true }); createState(DP + 'status.soc_alter_29_s', 0, { name: 'SOC Alter 3529 [s]', type: 'number', unit: 's', read: true, write: true }); createState(DP + 'status.soc_poll_naechst', '28', { name: 'Naechster SOC-Poll', type: 'string', read: true, write: true }); createState(DP + 'status.leistungsquelle_28', 'shelly', { name: 'Leistungsquelle 3528', type: 'string', read: true, write: true }); createState(DP + 'status.leistungsquelle_29', 'shelly', { name: 'Leistungsquelle 3529', type: 'string', read: true, write: true }); createState(DP + 'status.real_28_w', 0, { name: 'Realleistung 3528', type: 'number', unit: 'W', read: true, write: true }); createState(DP + 'status.real_29_w', 0, { name: 'Realleistung 3529', type: 'number', unit: 'W', read: true, write: true }); createState(DP + 'status.modus_28', 'Standby', { name: 'Modus 3528', desc: 'Realer Betriebszustand von Speicher 3528 fuer VIS. > +10W = Laden, < -10W = Entladen, sonst Standby.', type: 'string', read: true, write: true }); createState(DP + 'status.modus_29', 'Standby', { name: 'Modus 3529', desc: 'Realer Betriebszustand von Speicher 3529 fuer VIS. > +10W = Laden, < -10W = Entladen, sonst Standby.', type: 'string', read: true, write: true }); createState(DP + 'status.modus_gesamt', 'Standby', { name: 'Modus Gesamt', desc: 'Realer Gesamtzustand beider Speicher fuer VIS. > +10W = Laden, < -10W = Entladen, sonst Standby.', type: 'string', read: true, write: true }); createState(DP + 'status.voll_ladepause_28', false, { name: 'Voll-Ladepause 3528', type: 'boolean', read: true, write: true }); createState(DP + 'status.voll_ladepause_29', false, { name: 'Voll-Ladepause 3529', type: 'boolean', read: true, write: true }); createState(DP + 'status.verfuegbare_speicher_laden', 0, { name: 'Verfuegbare Speicher Laden', type: 'number', read: true, write: true }); createState(DP + 'status.verfuegbare_speicher_entladen', 0, { name: 'Verfuegbare Speicher Entladen', type: 'number', read: true, write: true }); createState(DP + 'status.teilintervall_ms', INTERVALL_MS, { name: 'Teilintervall [ms]', type: 'number', unit: 'ms', read: true, write: true }); createState(DP + 'status.alternierender_speicher', '', { name: 'Aktiver Speicher im Wechselmodus', type: 'string', read: true, write: true }); createState(DP + 'status.fehler_28_aktiv', false, { name: 'Fehler 3528 aktiv', type: 'boolean', read: true, write: true }); createState(DP + 'status.fehler_29_aktiv', false, { name: 'Fehler 3529 aktiv', type: 'boolean', read: true, write: true }); createState(DP + 'status.probe_28_aktiv', false, { name: 'Probe 3528 aktiv', type: 'boolean', read: true, write: true }); createState(DP + 'status.probe_29_aktiv', false, { name: 'Probe 3529 aktiv', type: 'boolean', read: true, write: true }); } function safeSetState(path, value, ack) { try { if (!existsState(path)) { log('State fehlt bei setState: ' + path, 'warn'); return; } setState(path, value, ack); } catch(e) { log('setState Fehler [' + path + ']: ' + e.message, 'warn'); } } // ------------------------------------------------------------ // UDP // ------------------------------------------------------------ function initUDP() { udpClient = dgram.createSocket('udp4'); udpClient.on('error', function(e) { log('UDP Fehler: ' + e.message, 'warn'); }); initSOCListener(); udpClient.bind(30000, function() { log('✅ UDP bereit (Port 30000)'); }); } function querySOC(ip) { if (!udpClient) return; const method = (ip === IP_28) ? 'ES.GetStatus' : 'Bat.GetStatus'; udpClient.send(Buffer.from(JSON.stringify( { id: 1, method: method, params: { id: 0 } } )), PORT, ip, function() {}); } function initSOCListener() { udpClient.on('message', function(msg, rinfo) { try { const data = JSON.parse(msg.toString()); if (!data || !data.result) return; const r = data.result; if (r.ongrid_power !== undefined && rinfo.address === IP_28) { safeSetState(DP + 'status.api_ongrid_28', Math.round(parseFloat(r.ongrid_power)), true); } let socVal = null; if (r.bat_soc !== undefined) socVal = parseFloat(r.bat_soc); else if (r.soc !== undefined) socVal = parseFloat(r.soc); if (socVal !== null && !isNaN(socVal)) { if (rinfo.address === IP_28) { soc28 = socVal; lastSocTs28 = Date.now(); safeSetState(DP + 'status.soc_28', Math.round(socVal), true); } else if (rinfo.address === IP_29) { soc29 = socVal; lastSocTs29 = Date.now(); safeSetState(DP + 'status.soc_29', Math.round(socVal), true); } } } catch(e) { log('UDP Listener Fehler von ' + (rinfo && rinfo.address ? rinfo.address : 'unbekannt') + ': ' + e.message, 'warn'); } }); } function sendManual(ip, watt) { if (!udpClient) return; const power = Math.round(watt); const msg = JSON.stringify({ id: 1, method: 'ES.SetMode', params: { id: 0, config: { mode: 'Manual', manual_cfg: { time_num: 1, start_time: '00:00:00', end_time: '23:59:59', week_set: 127, power: power, enable: 1 } } } }); udpClient.send(Buffer.from(msg), PORT, ip, function(err) { if (err) log('UDP Send Fehler (' + ip + '): ' + err.message, 'warn'); }); } // ------------------------------------------------------------ // Hilfsfunktionen // ------------------------------------------------------------ function safeVal(path, fallback) { try { const s = getState(path); if (!s || s.val === null || s.val === undefined) return fallback; const n = parseFloat(s.val); return isNaN(n) ? fallback : n; } catch(e) { return fallback; } } function safeBool(path, fallback) { try { const s = getState(path); if (!s || s.val === null) return fallback; return !!s.val; } catch(e) { return fallback; } } // Liest den summierten Hausstromwert vom Shelly 3EM. // Wird als Fallback genutzt wenn der Tibber-Adapter ausfaellt. // Gibt null zurueck wenn kein gueltiger Wert verfuegbar ist. function getShelly3EMTotal() { try { const s = getState(SHELLY_3EM_TOTAL); if (!s || s.val === null || s.val === undefined) return null; const v = parseFloat(s.val); return isNaN(v) ? null : v; } catch(e) { return null; } } function getStateTs(path, fallback) { try { const s = getState(path); if (!s || s.ts === null || s.ts === undefined) return fallback; const ts = Number(s.ts); return isNaN(ts) ? fallback : ts; } catch(e) { return fallback; } } function getSocPollIntervalMs(socVal) { const normalMin = safeVal(DP + 'einstellungen.soc_normal_min_pct', SOC_NORMAL_MIN); const normalMax = safeVal(DP + 'einstellungen.soc_normal_max_pct', SOC_NORMAL_MAX); const normalMs = safeVal(DP + 'einstellungen.soc_poll_normal_s', SOC_POLL_NORMAL_S) * 1000; const randMs = safeVal(DP + 'einstellungen.soc_poll_rand_s', SOC_POLL_RAND_S) * 1000; return (socVal >= normalMin && socVal <= normalMax) ? normalMs : randMs; } function updateSocAgeStatus() { const now = Date.now(); const age28 = lastSocTs28 > 0 ? Math.max(0, Math.round((now - lastSocTs28) / 1000)) : 0; const age29 = lastSocTs29 > 0 ? Math.max(0, Math.round((now - lastSocTs29) / 1000)) : 0; safeSetState(DP + 'status.soc_alter_28_s', age28, true); safeSetState(DP + 'status.soc_alter_29_s', age29, true); safeSetState(DP + 'status.soc_poll_naechst', nextSocTarget, true); } function maybePollSOC() { const now = Date.now(); const order = nextSocTarget === '28' ? ['28', '29'] : ['29', '28']; for (let i = 0; i < order.length; i++) { const target = order[i]; const ip = target === '28' ? IP_28 : IP_29; const socVal = target === '28' ? soc28 : soc29; const lastTs = target === '28' ? lastSocTs28 : lastSocTs29; const intervalMs = getSocPollIntervalMs(socVal); const ageMs = lastTs > 0 ? (now - lastTs) : Number.MAX_SAFE_INTEGER; if (ageMs >= intervalMs) { querySOC(ip); nextSocTarget = target === '28' ? '29' : '28'; safeSetState(DP + 'status.soc_poll_naechst', nextSocTarget, true); return; } } } // Liest die tatsaechliche Leistung von Speicher 3528. // Wenn kein Plug eingetragen (PLUG_28 = '') oder der Plug gerade // keine gueltigen Werte liefert, wird 'keine_messung' zurueckgegeben. // Das Skript regelt dann weiter - aber ohne Feedback vom Speicher. function getRealPower28() { if (!PLUG_28) { return { value: 0, source: 'keine_messung' }; } const useShelly = safeBool(DP + 'einstellungen.nutze_shelly_28', true); if (useShelly) { try { const s = getState(PLUG_28); if (!s || s.val === null || s.val === undefined) return { value: 0, source: 'keine_messung' }; const v = parseFloat(s.val); if (isNaN(v)) return { value: 0, source: 'keine_messung' }; return { value: v, source: 'shelly' }; } catch(e) { return { value: 0, source: 'keine_messung' }; } } const factor = safeVal(DP + 'einstellungen.api_ongrid_faktor_28', API_ONGRID_FACTOR_28); return { value: safeVal(DP + 'status.api_ongrid_28', 0) * factor, source: 'api_ongrid_28' }; } // Liest die tatsaechliche Leistung von Speicher 3529. // Gleiches Verhalten wie getRealPower28 - siehe Kommentar dort. function getRealPower29() { if (!PLUG_29) { return { value: 0, source: 'keine_messung' }; } const useShelly = safeBool(DP + 'einstellungen.nutze_shelly_29', true); if (useShelly) { try { const s = getState(PLUG_29); if (!s || s.val === null || s.val === undefined) return { value: 0, source: 'keine_messung' }; const v = parseFloat(s.val); if (isNaN(v)) return { value: 0, source: 'keine_messung' }; return { value: v, source: 'shelly' }; } catch(e) { return { value: 0, source: 'keine_messung' }; } } return { value: 0, source: 'keine_messung' }; } function updateFullChargeLock(which, socVal, realPower, currentCmd, isChargingMode) { const active = safeBool(DP + 'einstellungen.voll_ladepause_aktiv', true); const fullSoc = safeVal(DP + 'einstellungen.voll_soc_pct', FULL_SOC_PCT); const powerMax = safeVal(DP + 'einstellungen.voll_power_max_w', FULL_POWER_MAX_W); const wasDischarging = realPower < -EV_W; const ineffectiveCharge = Math.abs(realPower) <= powerMax; if (which === '28') { if (fullChargeLock28 && (socVal < fullSoc || wasDischarging)) fullChargeLock28 = false; if (active && isChargingMode && currentCmd < -MIN_W && socVal >= fullSoc && ineffectiveCharge) fullChargeLock28 = true; safeSetState(DP + 'status.voll_ladepause_28', fullChargeLock28, true); return fullChargeLock28; } if (fullChargeLock29 && (socVal < fullSoc || wasDischarging)) fullChargeLock29 = false; if (active && isChargingMode && currentCmd < -MIN_W && socVal >= fullSoc && ineffectiveCharge) fullChargeLock29 = true; safeSetState(DP + 'status.voll_ladepause_29', fullChargeLock29, true); return fullChargeLock29; } function isAvailableForCharge(which) { if (isFaultBlocked(which)) return false; return which === '28' ? !fullChargeLock28 : !fullChargeLock29; } function isAvailableForDischarge(which) { if (isFaultBlocked(which)) return false; return which === '28' ? soc28 >= SOC_MIN_PCT : soc29 >= SOC_MIN_PCT; } function getAvailableTargets(modus) { const targets = []; if (modus === 'Laden') { if (isAvailableForCharge('28')) targets.push('28'); if (isAvailableForCharge('29')) targets.push('29'); safeSetState(DP + 'status.verfuegbare_speicher_laden', targets.length, true); } else if (modus === 'Entladen') { if (isAvailableForDischarge('28')) targets.push('28'); if (isAvailableForDischarge('29')) targets.push('29'); safeSetState(DP + 'status.verfuegbare_speicher_entladen', targets.length, true); } else { safeSetState(DP + 'status.verfuegbare_speicher_laden', 0, true); safeSetState(DP + 'status.verfuegbare_speicher_entladen', 0, true); } return targets; } function getTeilintervallMs(count) { const n = Math.max(1, count); return Math.max(1000, Math.round(INTERVALL_MS / n)); } function distributeDischargePower(totalW, availableTargets, maxEntladenW) { const result = { '28': 0, '29': 0 }; const count = Math.max(1, availableTargets.length); const totalClamped = Math.max(0, Math.min(totalW, maxEntladenW * count)); if (availableTargets.length <= 0) return result; if (availableTargets.length === 1) { result[availableTargets[0]] = Math.min(Math.round(totalClamped), maxEntladenW); return result; } // SOC-Gewichtung: wer mehr Ladung hat entlaedt mehr (sanft, max 65/35) // Bei gleichem SOC: exakt 50/50 const socDiff = soc28 - soc29; // positiv = 28 hat mehr SOC const shift = Math.max(-0.15, Math.min(0.15, socDiff / 200)); const w28 = Math.round(totalClamped * (0.5 + shift)); const w29 = Math.round(totalClamped * (0.5 - shift)); result['28'] = Math.min(w28, maxEntladenW); result['29'] = Math.min(w29, maxEntladenW); return result; } function distributeChargePower(totalW, availableTargets, maxLadenW) { const result = { '28': 0, '29': 0 }; const count = Math.max(1, availableTargets.length); const totalClamped = Math.min(0, Math.max(totalW, -(maxLadenW * count))); if (availableTargets.length <= 0) return result; if (availableTargets.length === 1) { result[availableTargets[0]] = Math.max(Math.round(totalClamped), -maxLadenW); return result; } // SOC-Gewichtung: wer weniger Ladung hat laedt mehr (sanft, max 65/35) // Bei gleichem SOC: exakt 50/50 const socDiff = soc28 - soc29; // positiv = 28 hat mehr SOC → 28 bekommt weniger Ladung const shift = Math.max(-0.15, Math.min(0.15, socDiff / 200)); const w28 = Math.round(totalClamped * (0.5 - shift)); const w29 = Math.round(totalClamped * (0.5 + shift)); result['28'] = Math.max(w28, -maxLadenW); result['29'] = Math.max(w29, -maxLadenW); return result; } function getEffectiveRealFromLastCmd(sentW, realW) { if (sentW > 0) return Math.max(0, -realW); if (sentW < 0) return Math.max(0, realW); return 0; } function updateFaultStatusDPs() { safeSetState(DP + 'status.fehler_28_aktiv', fault28, true); safeSetState(DP + 'status.fehler_29_aktiv', fault29, true); safeSetState(DP + 'status.probe_28_aktiv', probe28, true); safeSetState(DP + 'status.probe_29_aktiv', probe29, true); } // Prueft ob ein Speicher auf Befehle reagiert. // Wird nur aufgerufen wenn eine Leistungsmessung vorhanden ist (source !== 'keine_messung'). // Wenn kein Plug vorhanden oder ausgefallen: wird diese Funktion nicht aufgerufen // und ein bestehender Fault-Zustand wird zurueckgesetzt damit der Speicher // nicht dauerhaft blockiert bleibt. function updateDeviceFaultState(which, realW) { if (!safeBool(DP + 'einstellungen.ausfallerkennung_aktiv', AUSFALLERKENNUNG_AKTIV)) { return; } const minSoll = safeVal(DP + 'einstellungen.ausfall_min_soll_w', AUSFALL_MIN_SOLL_W); const minRatio = safeVal(DP + 'einstellungen.ausfall_min_real_ratio', AUSFALL_MIN_REAL_RATIO); const failCycles = Math.max(1, Math.round(safeVal(DP + 'einstellungen.ausfall_zyklen', AUSFALL_ZYKLEN))); const cooldownMs = Math.max(1000, Math.round(safeVal(DP + 'einstellungen.ausfall_cooldown_s', AUSFALL_COOLDOWN_S) * 1000)); const now = Date.now(); let lastCmd = which === '28' ? lastCmd28 : lastCmd29; let fault = which === '28' ? fault28 : fault29; let failCount = which === '28' ? failCount28 : failCount29; let cooldownUntil = which === '28' ? cooldownUntil28 : cooldownUntil29; let probe = which === '28' ? probe28 : probe29; if (fault && now >= cooldownUntil) { probe = true; } if (Math.abs(lastCmd) < minSoll) { failCount = 0; } else { const effectiveReal = getEffectiveRealFromLastCmd(lastCmd, realW); const ratio = effectiveReal / Math.max(1, Math.abs(lastCmd)); if (fault && probe) { if (ratio >= minRatio) { fault = false; probe = false; failCount = 0; cooldownUntil = 0; log('✅ Speicher ' + which + ' reagiert wieder - Fehlerstatus aufgehoben'); } else { fault = true; probe = false; cooldownUntil = now + cooldownMs; failCount = failCycles; log('⚠️ Speicher ' + which + ' Probe fehlgeschlagen - erneut auf 0W und Cooldown'); } } else if (!fault) { if (ratio < minRatio) { failCount++; if (failCount >= failCycles) { fault = true; probe = false; cooldownUntil = now + cooldownMs; failCount = failCycles; log('⚠️ Speicher ' + which + ' liefert wiederholt nicht wie angefordert - temporär ausgeblendet'); } } else { failCount = 0; } } } if (which === '28') { fault28 = fault; failCount28 = failCount; cooldownUntil28 = cooldownUntil; probe28 = probe; } else { fault29 = fault; failCount29 = failCount; cooldownUntil29 = cooldownUntil; probe29 = probe; } updateFaultStatusDPs(); } // Setzt den Fault-Zustand eines Speichers zurueck. // Wird aufgerufen wenn der Plug ausgefallen ist oder nie vorhanden war, // damit der Speicher nicht dauerhaft auf 0W eingefroren bleibt. function clearFaultState(which) { if (which === '28') { if (fault28) { fault28 = false; failCount28 = 0; cooldownUntil28 = 0; probe28 = false; log('ℹ️ Speicher 28: Plug nicht verfuegbar - Fehlerstatus zurueckgesetzt, Regelung ohne Feedback'); } } else { if (fault29) { fault29 = false; failCount29 = 0; cooldownUntil29 = 0; probe29 = false; log('ℹ️ Speicher 29: Plug nicht verfuegbar - Fehlerstatus zurueckgesetzt, Regelung ohne Feedback'); } } updateFaultStatusDPs(); } function isFaultBlocked(which) { const now = Date.now(); if (which === '28') return fault28 && now < cooldownUntil28; return fault29 && now < cooldownUntil29; } function isProbeActive(which) { if (which === '28') return probe28; return probe29; } function applyFaultAndProbe(modus, w28, w29, maxEntladenW, maxLadenW) { let target28 = w28; let target29 = w29; const probeFactor = Math.max(0, Math.min(1, safeVal(DP + 'einstellungen.ausfall_probe_faktor_pct', AUSFALL_PROBE_FAKTOR_PCT) / 100)); // Nie beide Speicher gleichzeitig in Fault/Probe-Reduktion laufen lassen. // Sonst kann die Gesamtleistung kuenstlich stark einbrechen. if ((probe28 || isFaultBlocked('28')) && (probe29 || isFaultBlocked('29'))) { return { '28': Math.round(w28), '29': Math.round(w29) }; } if (isFaultBlocked('28')) target28 = 0; if (isFaultBlocked('29')) target29 = 0; if (isProbeActive('28')) target28 = Math.round(target28 * probeFactor); if (isProbeActive('29')) target29 = Math.round(target29 * probeFactor); if (modus === 'Entladen') { const desiredTotal = Math.max(0, w28 + w29); let currentTotal = Math.max(0, target28 + target29); let rest = desiredTotal - currentTotal; if (rest > 0) { if (!isFaultBlocked('28') && !isProbeActive('28')) { const free28 = Math.max(0, maxEntladenW - target28); const add28 = Math.min(rest, free28); target28 += add28; rest -= add28; } if (rest > 0 && !isFaultBlocked('29') && !isProbeActive('29')) { const free29 = Math.max(0, maxEntladenW - target29); const add29 = Math.min(rest, free29); target29 += add29; rest -= add29; } } } else if (modus === 'Laden') { const desiredTotal = Math.min(0, w28 + w29); let currentTotal = Math.min(0, target28 + target29); let rest = desiredTotal - currentTotal; if (rest < 0) { if (!isFaultBlocked('28') && !isProbeActive('28')) { const free28 = Math.max(0, maxLadenW - Math.abs(target28)); const add28 = Math.min(Math.abs(rest), free28); target28 -= add28; rest += add28; } if (rest < 0 && !isFaultBlocked('29') && !isProbeActive('29')) { const free29 = Math.max(0, maxLadenW - Math.abs(target29)); const add29 = Math.min(Math.abs(rest), free29); target29 -= add29; rest += add29; } } } return { '28': Math.round(target28), '29': Math.round(target29) }; } function sendPairCommands(w28, w29) { sendManual(IP_28, w28); sendManual(IP_29, w29); lastCmd28 = w28; lastCmd29 = w29; safeSetState(DP + 'status.gesendet_28_w', w28, true); safeSetState(DP + 'status.gesendet_29_w', w29, true); } function sendAlternatingCommand(activeTarget, target28, target29) { let send28 = lastCmd28; let send29 = lastCmd29; if (activeTarget === '28') send28 = target28; if (activeTarget === '29') send29 = target29; sendManual(IP_28, send28); sendManual(IP_29, send29); lastCmd28 = send28; lastCmd29 = send29; safeSetState(DP + 'status.gesendet_28_w', send28, true); safeSetState(DP + 'status.gesendet_29_w', send29, true); safeSetState(DP + 'status.alternierender_speicher', activeTarget, true); } function applyLoopInterval(newMs) { const targetMs = Math.max(1000, Math.round(newMs)); if (ivMain && currentLoopMs === targetMs) return; if (ivMain) clearInterval(ivMain); ivMain = setInterval(regeln, targetMs); currentLoopMs = targetMs; log('⏱️ Regelintervall aktiv: ' + targetMs + 'ms'); } // ------------------------------------------------------------ // Hauptregelung // ------------------------------------------------------------ function regeln() { if (!safeBool(DP + 'einstellungen.aktiv', true)) { log('⏸️ Steuerung deaktiviert'); return; } const maxEntladenW = safeVal(DP + 'einstellungen.max_entladen_w_pro_speicher', MAX_ENTLADEN_W); const maxLadenW = safeVal(DP + 'einstellungen.max_laden_w_pro_speicher', MAX_LADEN_W); const minW = safeVal(DP + 'einstellungen.min_w', MIN_W); maybePollSOC(); soc28 = safeVal(DP + 'status.soc_28', soc28); soc29 = safeVal(DP + 'status.soc_29', soc29); if (lastSocTs28 <= 0) lastSocTs28 = getStateTs(DP + 'status.soc_28', 0); if (lastSocTs29 <= 0) lastSocTs29 = getStateTs(DP + 'status.soc_29', 0); updateSocAgeStatus(); let tibber; let tibberQuelle; try { const tibberState = getState(TIBBER); const tibberAlter = tibberState ? (Date.now() - Number(tibberState.ts)) : 999999; if (tibberAlter <= TIBBER_MAX_MS) { tibber = parseFloat(tibberState.val) || 0; lastTibberGutTs = Date.now(); lastTibberGutW = tibber; tibberQuelle = 'live'; } else if ((Date.now() - lastTibberGutTs) <= HALTE_MS) { tibber = lastTibberGutW; tibberQuelle = 'gehalten (' + Math.round((Date.now() - lastTibberGutTs) / 1000) + 's)'; log('⏳ Tibber veraltet - halte letzten Wert: ' + Math.round(tibber) + 'W', 'warn'); } else { // Tibber ausgefallen und Haltezeit abgelaufen. // Fallback auf Shelly 3EM als zweite unabhaengige Netzstrommessung. const shellyFallback = getShelly3EMTotal(); if (shellyFallback !== null) { tibber = shellyFallback; tibberQuelle = 'shelly3em_fallback'; log('⚠️ Tibber ausgefallen - nutze Shelly 3EM: ' + Math.round(tibber) + 'W', 'warn'); } else { // Beide Quellen ausgefallen - sicherster Zustand ist 0W. log('❌ Tibber UND Shelly 3EM ausgefallen - Standby', 'warn'); sendPairCommands(0, 0); safeSetState(DP + 'status.tibber_quelle', 'AUSGEFALLEN', true); safeSetState(DP + 'status.letzte_regelung', new Date().toLocaleTimeString() + ' | Standby: beide Quellen ausgefallen', true); return; } } } catch(e) { log('❌ Tibber Lesefehler: ' + e.message, 'warn'); return; } const real28Obj = getRealPower28(); const real29Obj = getRealPower29(); const plug28 = real28Obj.value; const plug29 = real29Obj.value; const plugGes = plug28 + plug29; // Ausfallerkennung nur wenn eine Leistungsmessung vorhanden und aktiv ist. // Bei 'keine_messung': Fault-Zustand zuruecksetzen damit Speicher nicht blockiert bleibt. if (real28Obj.source !== 'keine_messung') { updateDeviceFaultState('28', plug28); } else { clearFaultState('28'); } if (real29Obj.source !== 'keine_messung') { updateDeviceFaultState('29', plug29); } else { clearFaultState('29'); } safeSetState(DP + 'status.tibber_w', Math.round(tibber), true); safeSetState(DP + 'status.plug_28_w', Math.round(plug28), true); safeSetState(DP + 'status.plug_29_w', Math.round(plug29), true); safeSetState(DP + 'status.tibber_quelle', tibberQuelle, true); safeSetState(DP + 'status.leistungsquelle_28', real28Obj.source, true); safeSetState(DP + 'status.leistungsquelle_29', real29Obj.source, true); safeSetState(DP + 'status.real_28_w', Math.round(plug28), true); safeSetState(DP + 'status.real_29_w', Math.round(plug29), true); let modus28 = 'Standby'; let modus29 = 'Standby'; let modusGesamtReal = 'Standby'; if (plug28 > EV_W) { modus28 = 'Laden'; } else if (plug28 < -EV_W) { modus28 = 'Entladen'; } if (plug29 > EV_W) { modus29 = 'Laden'; } else if (plug29 < -EV_W) { modus29 = 'Entladen'; } if (plugGes > EV_W) { modusGesamtReal = 'Laden'; } else if (plugGes < -EV_W) { modusGesamtReal = 'Entladen'; } safeSetState(DP + 'status.modus_28', modus28, true); safeSetState(DP + 'status.modus_29', modus29, true); safeSetState(DP + 'status.modus_gesamt', modusGesamtReal, true); const realEntladen = plugGes < -EV_W ? Math.abs(plugGes) : 0; let zielGesamt = 0; let modus = 'Standby'; if (plugGes < -EV_W) { zielGesamt = realEntladen + tibber; modus = 'Entladen'; } else if (tibber > EV_W) { zielGesamt = tibber; modus = 'Entladen'; } else if (tibber < -EV_W) { const realLaden = plugGes > EV_W ? plugGes : 0; const verfuegbarerUeberschuss = realLaden + Math.abs(tibber); const ladePufferPctRaw = safeVal(DP + 'einstellungen.lade_puffer_pct', 20); const ladePufferPct = Math.max(0, Math.min(100, ladePufferPctRaw)) / 100; const nutzbarerUeberschuss = Math.round(verfuegbarerUeberschuss * (1 - ladePufferPct)); zielGesamt = -nutzbarerUeberschuss; modus = 'Laden'; log('🔋 Ladepuffer: verfuegbar ' + Math.round(verfuegbarerUeberschuss) + 'W, Puffer ' + Math.round(ladePufferPct * 100) + '%, nutzbar ' + Math.round(nutzbarerUeberschuss) + 'W'); } let w28 = 0; let w29 = 0; if (modus === 'Entladen') { const availableDischarge = getAvailableTargets('Entladen'); if (availableDischarge.length <= 0) { log('🔋 SOC-Schutz: kein Speicher verfuegbar fuer Entladen - Standby'); modus = 'Standby'; zielGesamt = 0; } else if (zielGesamt < minW) { modus = 'Standby'; zielGesamt = 0; } else { const dist = distributeDischargePower(zielGesamt, availableDischarge, maxEntladenW); w28 = dist['28']; w29 = dist['29']; zielGesamt = w28 + w29; } } else if (modus === 'Laden') { let ladeZiel = Math.max(zielGesamt, -(maxLadenW * 2)); const drosselSoc = safeVal(DP + 'einstellungen.laden_drossel_soc', 90); const drosselFaktor = safeVal(DP + 'einstellungen.laden_drossel_faktor', 50) / 100; const drosselMaxW = safeVal(DP + 'einstellungen.laden_drossel_max_w', 800); const maxSoc = Math.max(soc28, soc29); if (maxSoc >= drosselSoc) { const nachFaktor = ladeZiel * drosselFaktor; const nachAbsolut = Math.max(ladeZiel, -(drosselMaxW * 2)); ladeZiel = Math.max(nachFaktor, nachAbsolut); log('🔋 Ladedrosselung SOC ' + Math.round(maxSoc) + '%: Faktor→' + Math.round(nachFaktor) + 'W, Absolut→' + Math.round(nachAbsolut) + 'W, Gewählt: ' + Math.round(ladeZiel) + 'W'); } if (Math.abs(ladeZiel) < minW) { modus = 'Standby'; zielGesamt = 0; } else { const availableChargePre = []; if (!fullChargeLock28) availableChargePre.push('28'); if (!fullChargeLock29) availableChargePre.push('29'); const dist = distributeChargePower(ladeZiel, availableChargePre, maxLadenW); w28 = dist['28']; w29 = dist['29']; zielGesamt = w28 + w29; } } if (updateFullChargeLock('28', soc28, plug28, w28, modus === 'Laden')) { if (modus === 'Laden' && w28 < 0) w28 = 0; } if (updateFullChargeLock('29', soc29, plug29, w29, modus === 'Laden')) { if (modus === 'Laden' && w29 < 0) w29 = 0; } if (modus === 'Laden') { const availableChargePost = getAvailableTargets('Laden'); const ladeGesamtNachDrossel = w28 + w29; if (availableChargePost.length <= 0) { w28 = 0; w29 = 0; modus = 'Standby'; zielGesamt = 0; } else { const distPost = distributeChargePower(ladeGesamtNachDrossel, availableChargePost, maxLadenW); w28 = distPost['28']; w29 = distPost['29']; zielGesamt = w28 + w29; } } const faultAdjusted = applyFaultAndProbe(modus, w28, w29, maxEntladenW, maxLadenW); w28 = faultAdjusted['28']; w29 = faultAdjusted['29']; zielGesamt = w28 + w29; safeSetState(DP + 'status.ziel_gesamt_w', Math.round(zielGesamt), true); lastTarget28 = w28; lastTarget29 = w29; const alternatingActive = safeBool(DP + 'einstellungen.alternierend_aktiv', ALTERNIEREND_AKTIV); const availableTargets = getAvailableTargets(modus); const teilintervallMs = getTeilintervallMs(availableTargets.length); safeSetState(DP + 'status.teilintervall_ms', teilintervallMs, true); const desiredLoopMs = (alternatingActive && availableTargets.length > 1 && modus !== 'Standby') ? teilintervallMs : INTERVALL_MS; applyLoopInterval(desiredLoopMs); if (!alternatingActive || availableTargets.length <= 1 || modus === 'Standby') { if (modus === 'Standby') safeSetState(DP + 'status.alternierender_speicher', '', true); sendPairCommands(w28, w29); } else { const activeTarget = availableTargets[alternatingIndex % availableTargets.length]; alternatingIndex = (alternatingIndex + 1) % availableTargets.length; sendAlternatingCommand(activeTarget, w28, w29); } const ts = new Date().toLocaleTimeString(); let logModus = modus; let logEmoji = modus === 'Entladen' ? '⬇️' : modus === 'Laden' ? '⬆️' : '⏸️'; const ladepauseAktiv = (modus === 'Laden') && (w28 === 0 && w29 === 0) && (fullChargeLock28 || fullChargeLock29); if (ladepauseAktiv) { logModus = 'Ladepause (voll)'; logEmoji = '⏸️'; } const activeLogTarget = (alternatingActive && availableTargets.length > 1 && modus !== 'Standby') ? safeVal(DP + 'status.alternierender_speicher', '') : 'alle'; const info = ts + ' | ' + logEmoji + ' ' + logModus + ' | Tibber: ' + Math.round(tibber) + 'W (' + tibberQuelle + ')' + ' | Gesamt real: ' + Math.round(plugGes) + 'W' + ' | Ziel gesamt: ' + Math.round(zielGesamt) + 'W' + ' | Ziel 3528: ' + w28 + 'W | Gesendet 3528: ' + lastCmd28 + 'W | Ist 3528: ' + Math.round(plug28) + 'W | Quelle 3528: ' + real28Obj.source + ' | SOC 3528: ' + Math.round(soc28) + '%' + ' | Ziel 3529: ' + w29 + 'W | Gesendet 3529: ' + lastCmd29 + 'W | Ist 3529: ' + Math.round(plug29) + 'W | Quelle 3529: ' + real29Obj.source + ' | SOC 3529: ' + Math.round(soc29) + '%' + ' | Aktiv neu gesetzt: ' + activeLogTarget; safeSetState(DP + 'status.letzte_regelung', info, true); log('📤 ' + info); } // ------------------------------------------------------------ // Start / Stop // ------------------------------------------------------------ function start() { log('🚀 MARSTEK_3 Hauptskript v1.2.1 gestartet'); initDP(); setTimeout(function() { initUDP(); setTimeout(function() { regeln(); applyLoopInterval(INTERVALL_MS); log('✅ Start-Regelintervall: ' + (INTERVALL_MS / 1000) + 's'); }, 1500); }, 1000); } start(); onStop(function() { if (ivMain) clearInterval(ivMain); if (udpClient) { sendManual(IP_28, 0); sendManual(IP_29, 0); setTimeout(function() { try { udpClient.close(); } catch(e) {} }, 500); } log('🛑 MARSTEK_3 gestoppt - 0W gesendet'); }, 1500);Im Vis2 sieht es bei mir so aus:

Hey! Du scheinst an dieser Unterhaltung interessiert zu sein, hast aber noch kein Konto.
Hast du es satt, bei jedem Besuch durch die gleichen Beiträge zu scrollen? Wenn du dich für ein Konto anmeldest, kommst du immer genau dorthin zurück, wo du zuvor warst, und kannst dich über neue Antworten benachrichtigen lassen (entweder per E-Mail oder Push-Benachrichtigung). Du kannst auch Lesezeichen speichern und Beiträge positiv bewerten, um anderen Community-Mitgliedern deine Wertschätzung zu zeigen.
Mit deinem Input könnte dieser Beitrag noch besser werden 💗
Registrieren Anmelden