Contents

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.

/images/modbus/modbus-adapter.png

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

/images/modbus/adapter.png

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.

/images/modbus/register.png

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.

/images/modbus/registers-in-object-tab.png

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 ersetzen
  • 502: Modbus Port am WR, idR 502
  • -s 1: Eure ClientID, idR 1 oder 100
  • 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 Register
  • 2022: Der Wert in dezimal. Hier: Das aktuelle Jahr 2022
  • 0x7e6: 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

Antwortet der Einrichtungsassistent
ist die IP-Adresse falsch

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

/images/modbus/sungrow-app.png

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).

/images/modbus/blockly.png

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:

  1. PV Strom

    • speist zunächst die Hauslast
    • dann die Batterie
    • Rest wird ins Netz eingespeist
  2. Strom aus der Batterie

    • speist zunächst die Hauslast
    • dann das Netz (bspw. Kalibrierung)
  3. 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.

/images/modbus/extractRunningStates.png

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:

/images/modbus/jarvis-app.png

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