Marstek Venus E lokal steuern: Modbus TCP, UDP-API und warum man jeden Schreibvorgang prüfen sollte
Der Marstek Venus E ist ein günstiger Plug-in-Hausakku mit 5,12 kWh. Er regelt sich selbst: Ein Smart Meter am Netzanschluss – bei mir ein Shelly Pro 3EM – meldet ihm, ob gerade Strom bezogen oder eingespeist wird. Der Akku lädt oder entlädt dann so, dass am Zähler möglichst null steht.
Das funktioniert gut, solange der Akku der einzige „intelligente“ Verbraucher ist. Sobald eine Wallbox dazukommt, wird es kompliziert. Mein Energiemanagement JG Energy greift deshalb lokal in die Regelung ein – nicht mit eigener Leistungsregelung, sondern mit zwei einfachen Grenzwerten.
Das Problem: Der Akku lädt das Auto
Der Akku sieht nur den Netzzähler. Startet die Wallbox mit 11 kW, sieht er plötzlich Netzbezug – und entlädt mit voller Leistung, um ihn auszugleichen. Der Hausakku lädt dann also das Auto.
Ob das gewollt ist, hängt von der Situation ab. In JG Energy gibt es dafür auf der Wallbox-Seite die Einstellung „Hausbatterie beim Autoladen“ mit zwei Möglichkeiten:
- „Hilft mit“: Der Akku darf beim Überschussladen mit entladen. Damit die Regelung seinen Strom nicht für PV-Überschuss hält, zieht JG Energy die Entladeleistung bei der Überschussberechnung wieder ab. Bei einem festen Stromtarif ist das energetisch neutral: Jede kWh aus dem Akku ersetzt eine kWh Netzstrom, ob sie im Auto oder abends im Haus landet.
- „Bleibt aus“: Solange das Auto mit Überschuss lädt, wird das Entladen gesperrt, das Laden aber erlaubt. Scheint die Sonne kräftig, nimmt der Akku also weiter Überschuss auf – und seine Ladung bleibt für den Abend im Haus.
Die Einstellung gilt für das Überschussladen. Lädt das Auto im Schnellmodus mit voller Leistung, verhält sich der Akku ganz normal. Während einer laufenden Ladung lässt sie sich nicht umschalten, damit die Register nicht mitten im Vorgang wechseln.
Zwei Schnittstellen: UDP zum Lesen, Modbus zum Schreiben
Der Venus E 3.0 bietet im lokalen Netz zwei Wege. Die UDP-Schnittstelle heißt „Local API“ und muss in der Marstek-App aktiviert sein. Modbus TCP war bei mir ohne weitere Einstellung auf Port 502 erreichbar.
Lesen über die UDP-API (Port 30000). Das ist ein einfaches JSON-RPC über UDP:
{ "id": 1, "method": "ES.GetStatus", "params": { "id": 0 } }
Die Antwort enthält unter anderem bat_soc (Ladestand), bat_cap (Kapazität), ongrid_power (aktuelle Leistung, positiv = Entladen) sowie Zählerstände für geladene und abgegebene Energie. JG Energy fragt das alle 2 Sekunden ab.
Schreiben über Modbus TCP (Port 502, Unit-ID 1). Für die Steuerung reichen mir zwei Holding-Register:
| Register | Inhalt | Werte |
|---|---|---|
44002 | maximale Ladeleistung | 0–2500 W |
44003 | maximale Entladeleistung | 0–2500 W |
35000 | interne Temperatur (lesen) | int16, × 0,1 °C |
Damit ergeben sich drei Zustände:
| Zustand | 44002 | 44003 |
|---|---|---|
| normal | 2500 | 2500 |
| nur laden („Bleibt aus“, Auto lädt) | 2500 | 0 |
| komplett aus | 0 | 0 |
Achtung: Die Generation 3 verwendet andere Registeradressen als ältere Venus-Modelle. Anleitungen für den Venus E der ersten Generationen passen nicht.
Warum nicht alles über Modbus?
Ursprünglich wollte ich komplett auf Modbus umsteigen und UDP abschalten. Ein Messvergleich hat mich davon abgebracht:
- UDP: Eine einzige Anfrage liefert alle Werte auf einmal – Ladestand, Leistung, Energiezähler. Dauer: rund 100 ms.
- Modbus über eine offene Verbindung: Für dieselben Werte braucht es mehrere Registerabfragen, und der Marstek bremst Abfragen auf derselben Verbindung aus. Ein Durchlauf dauerte rund 800 ms – bei einem Abfragetakt von 2 Sekunden viel zu langsam.
- Modbus mit neuer Verbindung je Abfrage: Im Test war das mit rund 50 ms sogar schneller als UDP. Im Dauerbetrieb alle 2 Sekunden hat der Marstek aber nach kurzer Zeit alle Modbus-Verbindungen abgewiesen (
ECONNREFUSED) – auch die Steuerbefehle. Erst ein Neustart des Akkus hat das behoben.
Deshalb die Aufteilung: UDP für das schnelle, häufige Lesen, Modbus nur für die seltenen Schreibvorgänge und einmal pro Minute für die Temperatur. So bleibt die Modbus-Schnittstelle frei, wenn es darauf ankommt.
Falle 1: Nur eine einzige TCP-Verbindung
Der Marstek erlaubt genau eine Modbus-TCP-Verbindung gleichzeitig. Fragt eine zweite Funktion an, während die erste noch verbunden ist, gibt es ECONNREFUSED.
In einem größeren Programm passiert das schneller, als man denkt: Bei mir liefen Temperaturabfrage, Entladesperre der Wallbox und Diagnose-Endpunkte unabhängig voneinander. Gelegentlich ging dabei ein Schreibvorgang verloren.
Die Lösung ist eine Warteschlange pro Gerät: Alle Zugriffe auf den Marstek laufen nacheinander. Dazu kommen zwei Details:
- Nach jedem Zugriff 400 ms Pause. Der Akku gibt seinen Verbindungsplatz nicht sofort frei – ohne Pause scheitert der nächste Verbindungsaufbau.
- Die Verbindung hart schließen und auf das Schließen warten. In der Node.js-Bibliothek
modbus-serialkehrtclose()sofort zurück, obwohl der Socket noch offen ist.destroy()mit Rückruf ist zuverlässiger.
Und noch eine Kleinigkeit in modbus-serial: Den Timeout mit setTimeout() vor connectTCP() setzen. Sonst kann schon der Verbindungsaufbau unbegrenzt hängen.
Falle 2: Bestätigt heißt nicht ausgeführt
Die zweite Eigenheit war tückischer: Der Marstek bestätigt Schreibvorgänge gelegentlich, ohne sie auszuführen. Die Modbus-Antwort ist positiv, im Register steht aber weiter der alte Wert.
Besonders unangenehm ist das bei Register 44003. Geht das Freigeben nach einer Ladung verloren, bleibt die Entladung gesperrt. Der Akku lädt dann nur noch, entlädt aber nie – und das fällt erst auf, wenn man sich wundert, warum abends Netzstrom fließt, obwohl der Akku voll ist.
Deshalb schreibt JG Energy jetzt und liest danach in derselben Verbindung zurück. Stimmt der Wert nicht, wird einmal neu geschrieben. Bleibt die Abweichung, meldet die Funktion einen Fehler:
const ModbusRTU = require('modbus-serial');
async function schreibenUndPruefen(ip, paare) { // paare: [[44003, 0], ...]
const client = new ModbusRTU();
client.setTimeout(3000); // vor connectTCP!
await client.connectTCP(ip, { port: 502 });
client.setID(1);
try {
for (let versuch = 1; versuch <= 2; versuch++) {
for (const [reg, wert] of paare) await client.writeRegister(reg, wert);
const falsch = [];
for (const [reg, wert] of paare) {
const r = await client.readHoldingRegisters(reg, 1);
if (r.data[0] !== wert) falsch.push(reg);
}
if (!falsch.length) return true;
}
return false;
} finally {
await new Promise(ok => client.destroy(ok));
}
}
Falle 3: Wenn der Akku trotzdem nichts tut
Selbst mit Prüfung kann es passieren, dass der Akku nicht entlädt, obwohl er sollte. Deshalb läuft zusätzlich ein kleiner Wächter:
- Seit 60 Sekunden mehr als 300 W Netzbezug,
- der Akku hat mindestens 14 % Ladestand,
- er ist laut JG Energy nicht gesperrt,
- aber er gibt keine Leistung ab.
Dann liest der Wächter die Register direkt aus dem Gerät und schreibt den Befund ins Log: Steht 44003 auf 0, ist die Sperre im Gerät hängen geblieben. Erlauben die Register das Entladen, begrenzt der Akku selbst – etwa wegen der Entladegrenze in der App. Damit ist schon beim ersten Blick ins Log klar, wo man suchen muss.
Der Akku als Brücke
Steht die Einstellung auf „Hilft mit“, geht JG Energy noch einen Schritt weiter. Fällt beim Überschussladen die PV-Leistung kurz unter die Mindestleistung der Wallbox (6 A, also 1,4 bzw. 4,1 kW), müsste die Ladung pausieren. Dann darf der Akku gezielt die Lücke füllen – aber nur:
- für eine laufende Ladung, nie für einen Neustart,
- nur bis zur Mindestleistung, nicht darüber,
- nur solange die PV noch etwas liefert,
- nur oberhalb von 14 % Ladestand.
Das Ergebnis: deutlich weniger Ein-Aus-Zyklen an der Wallbox.
Fazit
Zwei Register reichen, um den Marstek sinnvoll mit einer Wallbox zusammenarbeiten zu lassen. Die eigentliche Arbeit steckt in der Zuverlässigkeit: eine Verbindung nach der anderen, jeden Schreibvorgang zurücklesen und im Betrieb überwachen, ob das Gerät tut, was es soll.
Eine Lücke bleibt bei „Bleibt aus“: Fällt der Server aus, während die Entladung gesperrt ist, bleibt sie gesperrt, bis er wieder läuft. Anders als die Wallbox kennt der Marstek keine Sperre mit Ablaufzeit. Bei mir ist das ein seltener Fall, den ich bewusst in Kauf nehme.
Links