Sungrow Wechselrichter mit Modbus in Iobroker in Echtzeit auslesen
Endlich ist es soweit: 9,75kwp Photovoltaik schimmern im Sonnenlicht auf dem Dach - mit Wechselrichter und Speicher von Sungrow. Die nützliche App des Anbieters mit all ihren Analysemöglichkeiten kann aber nur kurz den Wunsch nach einem direkteren Zugriff auf die Daten zwecks Einbindung in die Hausautomatisierung stillen. Nach einigem Experimentieren funktioniert der Zugriff nun - die Erfahrung möchte ich teilen.
Kurze Einführung
Über die Modbus-Schnittstelle des Wechselrichters können verschiedene Daten der Photovoltaik-Anlage ermittelt werden, wie beispielsweise:
- Ertrag der Anlage (Momentan und Tagessumme)
- Eigenbedarf (Momentan und Tagessumme)
- Einspeisung/Bezug ins/aus dem Netz (Momentan und Tagessumme)
- Lade- und Entladestrom der Batterie (Momentan und Tagessumme)
- Verschiedene Monatswerte
- Spannung und Strom der Strings
- Ladestand der Anlage
Aktuell lese ich etwa 100 Werte aus - davon werden natürlich nicht alle benötigt. Grundsätzlich ist der Modbus-Standard unabhängig von der eingesetzten Hausautomatisierungslösung - ich nutze in meinem Fall Iobroker, auch mit anderen Tools und Skripten lassen sich die Daten bei Bedarf verarbeiten.
Einrichtung
Verbindung
Beim Standard “Modbus über TCP” kann das Modbus-Gerät (hier: der Wechselrichter) einfach über das Heimnetz verbunden werden. Sungrow bietet für die hier beschriebenen Wechselrichter unterschiedliche Arten der Verbindung an:
- LAN-Verbindung direkt am Wechselrichter
- LAN-Verbindung via WiNet-S Dongle
- Wifi-Verbindung via WiNet-S Dongle
Gemäß verschiedenen Foren-Berichten, die auf den Sungrow-Support verweisen, sowie meiner eigenen Erfahrung ist eine Modbus-Verbindung NUR via LAN-Anschluss direkt am Wechselrichter möglich. Der Wechselrichter wird also direkt via LAN-Kabel mit dem Router/Switch im Heimnetz verbunden.
Installation in IoBroker
Installiere den Modbus-Adapter in IoBroker indem ihr im Reiter Instanzen nach Modbus sucht und den Adapter installiert.
Verbindungseinstellungen
Unter Instanzen->Modbus kann der Adapter nun konfiguriert werden. Im Tab Allgemein werden folgende Daten hinterlegt:
- Partner IP-Adresse: IP eures Sungrow WRs
- Port: 502
- Geräte ID: 1, ggf. abweichend
- Typ: Master
- Aliases benutzen: Deaktivieren / Nein
Eingangsregister
Die vom Wechselrichter bereitgestellten Informationen müssen aus sog. Registern ausgelesen werden. Für die Anbindung müssen neben Adresse und Datentyp auch Länge und Faktor hinterlegt sein, idealerweise auch Einheit und eine kurze Beschreibung zur späteren Identifikation.
Da das händische Hinterlegen der Register nicht praktikabel ist, können diese auch importiert werden. Über den Doppelpfeil (zweites Icon von links auf der linken Seite) können die Register als TSV (Tab seperated Value) hinterlegt werden. Ein TSV mit den Registern ist hier zu finden. Dort auf “Raw” klicken um zum TSV zu gelangen.
Nach dem Einrichten kann die Konfiguration gespeichert werden.
Prüfen
Im Navigationsbaum kann nun Objekte geöffnet werden. Unter modbus.0.inputRegisters
sollten sich nun die Datenpunkte finden.
Hier sollte recht schnell ersichtlich werden, ob die Verbindung funktioniert, bspw. anhand der Register 13001 (PV Erzeugung heute) oder 13007 (Wirkleistung). Sind hier keine oder falsche Werte zu sehen, gibt es Probleme mit der Verbindung. Unter Protokoll in der Navigation finden sich weitere Informationen dazu.
Fehlerbehandlung
Die Modbus-Verbindung des Wechselrichters erscheint teilweise etwas fragil. Im Iobroker-Forum gibt es viele Berichte von Verbindungsproblemen aller Art. Im Folgenden ein paar Tipps dazu:
Fehleranalyse mit modbus-cli
Um zu prüfen ob zumindest die Verbindung zum Wechselrichter funktioniert, können Modbus Tools wie bspw. Modbus-CLI genutzt werden.
Ein einfaches Beispiel:
daniel@laptop ~ % modbus 192.168.1.123:502 -s 1 4999
192.168.1.123
: Durch deine IP ersetzen502
: Modbus Port am WR, idR 502-s 1
: Eure ClientID, idR1
oder100
4999
: Das Register das ausgelesen werden soll
Das Ergebnis sollte nun wie folgt aussehen
Parsed 0 registers definitions from 1 files
4999: 2022 0x7e6
Interpretation der Rückgabe:
4999
: Das ausgelesene Register2022
: Der Wert in dezimal. Hier: Das aktuelle Jahr 20220x7e6
: Der Wer in hexadezimal.
Anstatt des Register 4999
können natürlich auch beliebige andere Register ausgelesen werden. Aber Achtung: Die Erfahrung zeigt, dass es gerade mit diesem Tool nicht immer leicht ist, die richtigen Datentypen zu hinterlegen - selbst bei korrekter Verbindung kommen so fehlerhafte Rückgaben zu stande. Meine Empfehlung daher: Nutzt das Tool eher um sicherzustellen, dass die Verbindung funktioniert und haltet euch nicht damit auf, andere Werte auslesen zu wollen.
Richtiges Gerät finden
In der Regel werdet ihr den Wechselrichter via WLAN-Stick mit dem Internet verbunden haben und zusätzlich über den direkten Anschluss die Modbus-Verbindung vornehmen. Damit hat der Wechselrichter zwei IPs - einmal via WLAN und einmal via LAN. Über beide IPs ist Modbus auch (theoretisch) erreichbar - ihr müsst nun sicherstellen, dass ihr Modbus über den LAN-Anschluss konfiguriert.
Die korrekte IP kann bspw. über Fritzbox gefunden werden: Dort lassen sich unter Heimnetz->Netzwerk alle Netzwerk-Geräte finden. Die Sungrow-Geräte haben in der Regel eine Mac-Adresse, die mit “AC” beginnt (Mac-Adressen ggf. über den mit “+/-” beschrifteten Button oben rechts anzeigen lassen). Weiterhin scheinen sich die Geräte als espressif-sungrow oder espressif zu identifizieren.
Alternativ kann auch das Netzwerk recht einfach mit nmap
nach Geräten durchsucht werden, die auf Port 502 (Modbus) lauschen:
daniel@rechner ~ % nmap -p502 --open 192.168.178.1/24
Starting Nmap 7.92 ( https://nmap.org ) at 2022-10-10 13:33 CEST
Nmap scan report for espressif.fritz.box (192.168.178.85)
Host is up (0.13s latency).
PORT STATE SERVICE
502/tcp open mbap
Nmap scan report for espressif-sungrow.fritz.box (192.168.178.89)
Host is up (0.22s latency).
PORT STATE SERVICE
502/tcp open mbap
Nmap done: 256 IP addresses (24 hosts up) scanned in 49.12 seconds
daniel@rechner ~ %
Auch hier fördert der Scan zwei IPs zu Tage. Welche die richtige ist, offenbart ein einfacher Test: Ruft ihr die IP-Adresse im Browser auf und euch wird ein Setup-Assistent angezeigt, seit ihr mit dem WLAN-Stick verbunden - die IP ist also nicht die richtige für die Modbus-Verbindung.
Verbindungsfehler
Gefürchtet sind Modbus-Fehler wie
error: modbus.0 (21335) Client in error state.
warn: modbus.0 (21335) Poll error count: 1 code: {"err":"timeout","timeout":5000}
oder in meinem Fall auch gerne
On error: {"errno":-104,"code":"ECONNRESET","syscall":"read"}
im Iobroker-Protokoll. Eine genaue Ursache scheint nicht immer ersichtlich, empfohlen werden folgende Maßnahmen:
- Penible(!) Prüfung der Konfiguration
- Sicherstellen dass die Read- und Write-Register keine Fehler enthalten (ggf. alle bis auf ein Register probeweise entfernen)
- Neuinstallation des Modbus-Adapters (Konfiguration, insb. Register vorher sichern / kopieren)
- Neustart Wechselrichter
- Aktualisierung Firmware Wechselrichter
- Umstecken das Kabels in anderen Switch-Port (in meinem Fall Zyxel, ggf. haben die vielen MODBUS Anfragen ein Limit gerissen?) => Hat reproduzierbar und mit sofortiger Wirkung zu einer Wiederherstellung der Verbindung geführt
Reduzierung von Abfrageintervallen und Registeranzahl
Gerade wenn die Verbindung nach einigen Tagen abbricht und durch Wahl eines anderen LAN-Ports am Router/Switch wiederhergestellt werden kann, sollte geprüft werden, ob nicht ggf. eine zu große Zahl an Registern in zu kurzer Zeit gelesen wird. Mögliche Abhilfe:
- Reduzierte Anzahl an Registern (Beispiel Gist / RAW)
- Reduzierte Anzahl an Lese- und Schreiboperationen im Modbus-Adapter durch Anpassung der Konfigirationen
Leseinterval
,Wartezeit
Jetzt wird ausgelesen!
Ist der WR korrekt in IoBroker eignebunden, ist die Freude zu nächst groß. Register wie
- 5016 Total DC Power
- 13007 Load power
- 13009 Export power
- 13021 Battery power
versprechen schnelle Erfolge. Allerdings stehen nicht alle Werte, die in der App ersichtlich sind, auch direkt zum Auslesen zur Verfügung:
Im obigen Bild ist bspw. ersichtlich, dass die Last des Hauses mit 83 W aus der PV-Anlage sowie 133 W aus dem Akku bedient wird. Weiterhin speist der Akku mit 2,003 kW ins Netz ein (es handelt sich hierbei um eine Kalibrierung des AKkus, ansonsten tritt der Fall natürlich nicht auf). Mit den Informationen in den o.g. Registern ist lediglich ersichtlich, dass 83 W PV-Strom erzeugt werden, die Batterie eine Last von 2,136 kW hat und ins Netz 2,003 kW eingespeist werden und im Haus eine Last von 216 W anfällt. Wie die Ströme sich genau verteilen ist über die Register nicht ersichtlich.
Um diese Information zu ermitteln werden die sog. “Running States” benötigt.
13000_Running_State
Im Register 13000
“Running State” findet sich der aktuelle Betriebsstatus des Wechselrichters. Hierbei handelt es sich um eine Bitmaske die folgende Informationen kodiert:
Bit0 PV power
- Bit0 == 0 No power generated from PV
- Bit0 == 1 Power generated from PV
Bit1 Battery charging
- Bit1 == 0 Not charging
- Bit1 == 1 Charging
Bit2 Battery discharging
- Bit2 == 0 Not discharging
- Bit2 == 1 Discharging
Bit3 Positive load power
- Bit3 == 0 Load is reactive
- Bit3 == 1 Load is active
Bit4 Feed-in power
- Bit4 == 0 No power feed-in the grid
- Bit4 == 1 Power feed-in the grid
Bit5 Import Power from grid
- Bit5 == 0 No power imported from the grid
- Bit5 == 1 Importing power from grid
Bit6 Reserved Bit6
Bit7 (Refitting System) Negative load power
- Bit7 == 0 No power generated from “Load”
- Bit7== 1 Power generated from “Load”
Mit dieser Information ist es bspw. erst möglich direkt zu ermitteln, ob die Batterie entlädt (Bit 3 ist 1) oder lädt (Bit 2 ist 1). Gleichzeitig kann hiermit ermittelt werden, ob bspw. gerade wirklich ins Netz eingespeist wird (Bit 4 ist 1) oder ob die in Register 13009 Export power
verzeichnet 3 W nur ein Overshoot aus der Batterie sind (Bit 4 ist 0).
Da die Bitmaske in IoBroker als Dezimalwert dargestellt wird (bspw. 25
) kursieren einige Beispiele in denen auf diese Werte geprüft wird. Korrekt ist es allerdings so nicht. Folgende JS Funktion kann in Blockly eingebunden werden um zu prüfen, ob ein bestimmtes Bit gesetzt ist:
function decToBit(dec, bitPosition) {
return (dec & (1 << bitPosition)) === 0 ? false : true;
}
Nun kann wie folgt geprüft werden, ob die Batterie lädt oder entlädt:
$runningState = getState("modbus.0.inputRegisters.13000_Running_State").val;
$batteryCharging = decToBit($runningState, 1);
$batteryDischarging = decToBit($runningState, 2);
Wohin fließt der Strom?
Um mit dieser neuen Information tatsächlich zu bestimmen, von wo nach wo der Strom fließt, sind weitere Überlegungen nötig. Hierzu lässt sich folgende Priorisierung aufstellen:
PV Strom
- speist zunächst die Hauslast
- dann die Batterie
- Rest wird ins Netz eingespeist
Strom aus der Batterie
- speist zunächst die Hauslast
- dann das Netz (bspw. Kalibrierung)
Strom aus dem Netz
- speist zunächst die Hauslast
- dann die Batterie (bspw. Kalibrierung)
Folgender Auszug aus Blockly / Javascript ermöglicht das Ganze: Wichtig ist, dass bei Aktualisierung des “Betriebstatus” und/oder der “Wirkleistung gesamt” die Methode extractRunningStates
aufgerufen wird und dieser als Parameter der Wert des Betriebstatus übergeben wird.
Die Methode extractRunningState
nimmt den Betriebstatus als Variable dec
entgegen und sieht wie folgt aus:
function decToBit(dec, bitPosition) {
return (dec & (1 << bitPosition)) === 0 ? false : true;
}
// Creates a state if it does not exist yet
function createStateIfNotExists(state, name)
{
// createState(state, 0, true, {name: name, type: "number", role: 'value'}, function () {});
if ( !existsState(state )) {
createState(state, 0, false, {name: name, type: "number", role: 'value'}, function () {});
}
}
// Create states
createStateIfNotExists("0_userdata.0.PV.PvToLoad", "Power from PV to load");
createStateIfNotExists("0_userdata.0.PV.PvToBat", "Power from PV to bat");
createStateIfNotExists("0_userdata.0.PV.PvToGrid", "Power from PV to grid");
createStateIfNotExists("0_userdata.0.PV.BatToLoad", "Power from Bat to load");
createStateIfNotExists("0_userdata.0.PV.BatToGrid", "Power from Bat to grid");
createStateIfNotExists("0_userdata.0.PV.GridToLoad", "Power from grid to load");
createStateIfNotExists("0_userdata.0.PV.GridToBat", "Power from Gridto battery");
createStateIfNotExists("0_userdata.0.PV.SignedBat", "Battery but with minus sign for charging");
// Decode running state flags
$powerGeneratedFromPV = decToBit(dec, 0);
$batteryCharging = decToBit(dec, 1);
$batteryDischarging = decToBit(dec, 2);
$loadActive = decToBit(dec, 3);
$powerFeedIntoGrid = decToBit(dec, 4);
$powerImportFromGrid = decToBit(dec, 5);
$powerGeneratedFromLoad = decToBit(dec, 7);
// Save running state more speaking fields
setState("0_userdata.0.PV.PowerGeneratedFromPV", $powerGeneratedFromPV, true);
setState("0_userdata.0.PV.BatteryCharging", $batteryCharging, true);
setState("0_userdata.0.PV.BatteryDischarging", $batteryDischarging, true);
setState("0_userdata.0.PV.LoadActive", $loadActive, true);
setState("0_userdata.0.PV.PowerFeedIntoGrid", $powerFeedIntoGrid, true);
setState("0_userdata.0.PV.PowerImportFromGrid", $powerImportFromGrid, true);
setState("0_userdata.0.PV.PowerGeneratedFromLoad", $powerGeneratedFromLoad, true);
// Read current power levels of bat, pv, load and grid
$load = getState("modbus.0.inputRegisters.13007_Load_power_").val;
$grid = getState("modbus.0.inputRegisters.13009_Export_power").val;
$pv = getState("modbus.0.inputRegisters.5016_Total_DC_Power").val;
$battery = getState("modbus.0.inputRegisters.13021_Battery_power_").val;
// Write signed bat
setState("0_userdata.0.PV.SignedBat", $batteryCharging ? $battery * -1 : $battery, true);
// Calculate PV
$pvToBat = 0;
$pvToLoad = 0;
$pvToGrid = 0;
$loadRemaining = $load;
if ($powerGeneratedFromPV) {
$remaining = $pv;
if ($remaining > $loadRemaining) {
$pvToLoad = $loadRemaining;
$remaining -= $load;
$loadRemaining = 0;
} else {
$pvToLoad = $remaining;
$loadRemaining = $load - $remaining;
$remaining = 0;
}
if ($batteryCharging) {
if ($remaining > $battery) {
$pvToBat = $battery;
$remaining -= $battery;
} else {
$pvToBat = $remaining;
$remaining = 0;
}
}
if ($grid > 0) {
$pvToGrid = Math.min($grid, $remaining);
}
}
setState("0_userdata.0.PV.PvToLoad", $pvToLoad);
setState("0_userdata.0.PV.PvToBat", $pvToBat);
setState("0_userdata.0.PV.PvToGrid", $pvToGrid);
// Calculate Bat
$batToLoad = 0;
$batToGrid = 0;
if ($batteryDischarging) {
$remaining = $battery;
if ($remaining > $loadRemaining) {
$batToLoad = $loadRemaining;
$remaining -= $loadRemaining;
$loadRemaining = 0;
} else {
$batToLoad = $remaining;
$loadRemaining -= $remaining;
$remaining = 0;
}
if ($powerFeedIntoGrid) {
$batToGrid = Math.min($grid, $remaining);
}
}
setState("0_userdata.0.PV.BatToLoad", $batToLoad);
setState("0_userdata.0.PV.BatToGrid", $batToGrid);
// Calculate grid
$gridToBat = 0;
$gridToLoad = 0;
if ($grid < 0) {
$remaining = Math.abs($grid);
if ($remaining > $loadRemaining) {
$gridToLoad = $loadRemaining;
$remaining -= $loadRemaining;
$loadRemaining = 0;
} else {
$gridToLoad = $remaining;
$loadRemaining -= $remaining;
$remaining = 0;
}
if ($batteryCharging) {
$gridToBat = Math.min($battery, $remaining);;
}
}
setState("0_userdata.0.PV.GridToLoad", $gridToLoad);
setState("0_userdata.0.PV.GridToBat", $gridToBat);
Mit dieser Logik lässt sich die Sungrow-Darstellung nährungsweise nachbilden:
In diesem Fall wird neben dem PV-Ertrag oben auch angezeigt, wie viel VA Leistung von den beiden Strings kommen (Ost/West Ausrichtung).
Regelverluste
Auch wenn über die Running States sehr klar ersichtlich ist, ob gerade ins Netz eingespeist oder der Akku geladen wird: Regelverluste (also bspw. kurzzeitge Einspeisungen aus dem Akku ins Netz, die entstehen weil die Last im Haus schneller abgefallen ist als der Akku abregeln konnte) werden über die Running States nicht immer korrekt wiedergegeben. Insofern kann es sinnvoll sein davon auszugehen, dass die 30W, die vom Akku abgehen und nicht mehr zur Hauslast passen, eben doch ins Netz gehen - selbst wenn Bit 4 (power feed in) nicht gesetzt ist.
Download
Ich habe einige Rückmeldungen bekommen, dass die Nachbildung auf Basis der Snippets oben etwas aufwendig ist. Bitte bedenkt, dass ihr in jedem Fall die Objekt-IDs anpassen müsst, die werden bei euch ja vermutlich anders lauten.
Hier könnt ihr das Blockly meines aktuell genutzten Setups runterladen und in Iobroker importieren:
https://gist.github.com/dnoegel/51339bb9de2ced510f1420ee088a801d
Den Typescript-Code findet ihr hier:
https://gist.github.com/dnoegel/c6cb7f176d25199c0575dce97ee87253