LW IT Solutions
« Blog Overview /Smart Home/Tutorials / Tutorial: SunSpec-Register eines PV-Wechselrichters über Modbus TCP...
This post in other languages:

Tutorial: SunSpec-Register eines PV-Wechselrichters über Modbus TCP auslesen

Tutorial: SunSpec-Register eines PV-Wechselrichters über Modbus TCP auslesen
Inhalt
  1. Warum eine Registernummer allein nicht genügt
  2. Die Basisadresse und die Marke finden
  3. Die Modellkette ablaufen
  4. Skalierungsfaktoren, Datentypen und die Ersatzwerte
  5. Einen Wert von Anfang bis Ende lesen
  6. Das Ganze in Home Assistant bringen
  7. Quellen

Eine Registernummer aus einem Forenbeitrag funktioniert. Sie funktioniert bis zur nächsten Firmware, bis zum nächsten Modell desselben Wechselrichters, bis ein zweites Gerät hinzukommt, das denselben Wert woanders ablegt. Dann stimmt die Nummer nicht mehr, und nichts sagt es: Modbus liefert, was an dieser Adresse steht, und eine plausible Zahl ist von einer richtigen nicht zu unterscheiden.

Aus diesem Grund gibt es SunSpec. Es ist ein Verzeichnis, das über den Registerraum gelegt ist, und es zu lesen kostet vier Schritte, die auf jedem Wechselrichter mit dieser Unterstützung gleich funktionieren.

Eine Registerkarte ab Adresse 40000 mit der SunS-Marke, zwei Modellköpfen samt Blöcken und der Endmarke, daneben die Entschlüsselung eines Werts samt seinem Skalierungsregister
Die Kette beschreibt sich selbst: Jedes Modell nennt seine eigene Länge, die Adresse des nächsten ergibt sich also aus dem laufenden. Nur der Anfang muss gefunden werden.

Warum eine Registernummer allein nicht genügt

Modbus kennt keine Namen, keine Einheiten und keine Typen. Ein Lesevorgang liefert sechzehn Bit, und jede Deutung dieser Bits geschieht beim Lesenden. Ein Register mit dem Inhalt 1234 kann 1234 Watt bedeuten, 123,4 Volt oder 12,34 Ampere – und das Protokoll sagt dazu nichts.

SunSpec ergänzt drei Dinge, und alle drei stehen im Registerraum selbst: eine Marke, die sagt, dass das Verzeichnis vorhanden ist, eine Kette von Blöcken, die sagt, welche Messwerte es gibt und wo, und je Gruppe von Werten ein Skalierungsfaktor, der sagt, wo das Komma steht.

Die praktische Folge ist, dass ein einmal geschriebener Leser auch beim nächsten Gerät funktioniert. Das wiegt mehr, als es klingt, denn die Alternative – eine Adresstabelle je Hersteller und Firmwarestand – ist genau das, was stillschweigend veraltet.

Die Basisadresse und die Marke finden

Das Verzeichnis beginnt an einer von drei Adressen, und die ersten beiden Register sagen, ob es dort beginnt. Sie enthalten die vier Zeichen SunS, als 32-Bit-Wert also 0x53756E53.

from pymodbus.client import ModbusTcpClient

c = ModbusTcpClient("192.168.1.50", port=502)
c.connect()

for basis in (40000, 0, 50000):
    r = c.read_holding_registers(basis, count=2, slave=1)
    if not r.isError() and r.registers == [0x5375, 0x6E53]:
        print("SunSpec ab", basis)
        break

In diesem kurzen Stück Code stecken zwei Stolpersteine. Der erste ist die Verschiebung um eins, über die jeder einmal fällt: Die Dokumentation zählt Register ab eins, das Protokoll adressiert sie ab null. Eine im Handbuch als 40001 gedruckte Adresse ist auf der Leitung der Offset 40000, und ein Lesevorgang bei 40001 liefert die zweite Hälfte der Marke und keine Übereinstimmung.

Der zweite ist die Geräteadresse. Ein Wechselrichter mit eingebautem Zähler antwortet als zwei Geräte auf derselben Verbindung, jedes mit eigener Kennung und eigenem SunSpec-Verzeichnis. Die Leistung des Zählers unter der Kennung des Wechselrichters zu lesen liefert einen Wert, der kein Fehler und nicht die Wahrheit ist. Welche Kennungen in Gebrauch sind, zeigt die Modbus-Seite der Weboberfläche des Geräts – und dort wird Modbus TCP überhaupt erst eingeschaltet, denn bei den meisten Wechselrichtern ist es ab Werk aus.

Die Modellkette ablaufen

Nach der Marke folgt eine Reihe von Blöcken, und jeder Block beginnt mit zwei Registern: seiner Modellnummer und seiner Länge. Die Länge zur laufenden Position addiert ergibt den Anfang des nächsten Blocks, und eine Modellnummer von 65535 beendet die Kette.

pos = basis + 2
while True:
    kopf = c.read_holding_registers(pos, count=2, slave=1).registers
    modell, laenge = kopf[0], kopf[1]
    if modell == 0xFFFF:
        break
    print(f"Modell {modell:5d}  Laenge {laenge:3d}  Daten ab {pos + 2}")
    pos += 2 + laenge

Ein üblicher Wechselrichter antwortet mit drei oder vier Modellen, und die Nummern sagen, was jedes davon ist.

Modell Inhalt
1 Gemeinsames: Hersteller, Modellbezeichnung, Seriennummer, Firmwarestand
101, 102, 103 Wechselrichter ein-, zwei-, dreiphasig – ganze Zahlen mit Skalierungsfaktoren
111, 112, 113 Dieselben Messwerte als Gleitkommazahlen, ohne Skalierungsfaktoren
160 Die einzelnen Strings, ein wiederholter Block je MPP-Regler
201 bis 204 Zähler, ein Modell je Anschlussart

Ob ein Gerät 103 oder 113 anbietet, ist meist eine Einstellung und keine Eigenschaft. Viele Wechselrichter haben einen Schalter zwischen ganzen Zahlen mit Skalierungsfaktoren und Gleitkomma, und er bestimmt, welche der beiden Modellfamilien in der Kette erscheint. Ein Leser, der das eine erwartet und das andere vorfindet, meldet überhaupt keine Messwerte – ein verwirrendes Anzeichen für eine Einstellung, an deren Änderung sich niemand erinnert.

Skalierungsfaktoren, Datentypen und die Ersatzwerte

Innerhalb eines Modellblocks steht die Position eines Werts in der Spezifikation und wird ab dem Anfang der Daten gezählt. Der Strom der Phase A sitzt auf Offset 2, der dazugehörige Skalierungsfaktor auf Offset 5.

Modell 103, Daten ab 40071

  Offset 1   A      uint16   Gesamtstrom
  Offset 2   AphA   uint16   Phase A
  Offset 3   AphB   uint16   Phase B
  Offset 4   AphC   uint16   Phase C
  Offset 5   A_SF   int16    Skalierungsfaktor fuer alle vier

  40072 = 1234        Rohwert
  40075 = 0xFFFE      Zweierkomplement, also −2

  1234 × 10⁻² = 12,34 A

Der Skalierungsfaktor ist ein vorzeichenbehafteter Zehnerexponent und gilt für eine ganze Gruppe von Werten, nicht für einen. Deshalb erscheint er einmal für vier Ströme, und deshalb liefert ein ohne ihn gelesener Strom eine hundertfach zu grosse Zahl – die in einer Hausanlage immer noch nach einem plausiblen Messwert aussieht.

Drei weitere Eigenschaften der Kodierung zählen in der Praxis. Werte über zwei Register stehen mit dem höherwertigen Wort zuerst, was der Modbus-Konvention entspricht und was fast jede Bibliothek von allein richtig macht. Textfelder haben eine feste Länge und werden mit Nullbytes aufgefüllt statt von ihnen beendet. Und ein Wert, den das Gerät nicht führt, ist nicht null, sondern ein Ersatzwert: 0x8000 bei int16, 0xFFFF bei uint16, 0x80000000 bei int32. Ein Leser, der diese als Zahlen behandelt, meldet 65535 Volt – und eine Regel, die sie verwirft, ist drei Zeilen lang.

LEER = {"int16": 0x8000, "uint16": 0xFFFF,
        "int32": 0x80000000, "uint32": 0xFFFFFFFF}

def wert(roh, typ, sf):
    if roh == LEER.get(typ):
        return None
    return roh * (10 ** sf)

Einen Wert von Anfang bis Ende lesen

Die Teile zusammen ergeben einen Leser, der das Modell findet, statt seine Adresse anzunehmen.

def modell_finden(c, basis, gesucht, slave=1):
    pos = basis + 2
    while True:
        m, laenge = c.read_holding_registers(pos, count=2, slave=slave).registers
        if m == 0xFFFF:
            return None
        if m == gesucht:
            return pos + 2, laenge
        pos += 2 + laenge

def vorzeichen(r):
    return r - 0x10000 if r > 0x7FFF else r

start, _ = modell_finden(c, 40000, 103)
block    = c.read_holding_registers(start, count=6, slave=1).registers

strom = block[1]                 # AphA, Offset 2 ab eins gezaehlt
sf    = vorzeichen(block[4])     # A_SF, Offset 5
print(f"{strom * 10 ** sf:.2f} A")

Es lohnt sich, den ganzen Block in einer Anfrage zu lesen statt jeden Wert einzeln. Ein Modbus-Lesevorgang kostet unabhängig von der Länge einige Millisekunden Umlaufzeit – sechs Register in einem Aufruf kosten also so viel wie ein Register in einem Aufruf. Wichtiger noch: Alle sechs Werte stammen dann aus demselben Augenblick. Sechs getrennte Lesevorgänge für Strom, Spannung und Leistung liefern drei Grössen, die nicht zusammenpassen, und eine darauf gebaute Plausibilitätsprüfung schlägt grundlos an.

Eine Anmerkung zur Abfragehäufigkeit: Ein Wechselrichter ist keine Datenbank. Eine Anfrage alle fünf Sekunden ist unproblematisch, einmal je Sekunde ist bei manchen Geräten die Grenze, und schneller erzeugt Zeitüberschreitungen, die wie Netzfehler aussehen. Die Messwerte selbst aktualisieren sich ohnehin selten schneller als sekündlich.

Das Ganze in Home Assistant bringen

Sind die Adressen bekannt, funktioniert dasselbe Lesen auch beschreibend. Die Modbus-Anbindung von Home Assistant kümmert sich um Datentyp und Vorzeichen; nur der Skalierungsfaktor muss als Zahl eingetragen werden, denn die Anbindung liest ihn nicht aus dem Gerät.

modbus:
  - name: wechselrichter
    type: tcp
    host: 192.168.1.50
    port: 502
    sensors:
      - name: "Strom Phase A"
        slave: 1
        address: 40072
        data_type: uint16
        scale: 0.01
        precision: 2
        unit_of_measurement: "A"
        device_class: current
        state_class: measurement
        scan_interval: 10
      - name: "Wirkleistung"
        slave: 1
        address: 40084
        data_type: int16
        scale: 1
        precision: 0
        unit_of_measurement: "W"
        device_class: power
        state_class: measurement
        scan_interval: 10

Der fest eingetragene scale ist der Kompromiss dieses Weges, und es lohnt sich festzuhalten, woher er kommt. Der Skalierungsfaktor wurde einmal mit dem Skript von oben gelesen; er steht im Gerät und ändert sich praktisch nie, garantiert ist das aber nicht. Nach einer Firmware-Aktualisierung dauert ein Vergleich zwischen angezeigtem Wert und dem Display des Wechselrichters zehn Sekunden und fängt einen Faktor hundert sofort ab.

Ein letztes Feld entscheidet, ob der Wert in der Energiestatistik landet statt nur auf einer Karte. Ein Leistungswert in Watt ist device_class: power mit state_class: measurement; ein Zählerstand in Kilowattstunden ist device_class: energy mit state_class: total_increasing. Beides ist nicht austauschbar, und nur das Zweite lässt sich als Energiequelle auswählen.

Lukas Wojcik

Lukas Wojcik

Systems architect and technology enthusiast specializing in scalable tracking solutions, GMP Stack (GA4 & GTM), and robust backend architectures. Advocate for clean code and privacy-first design.

Get in Touch

Briefly describe your project or inquiry for a tailored response. This site is protected by reCAPTCHA.

Kommentar schreiben

Die E-Mail-Adresse wird nicht veröffentlicht. Pflichtfelder sind mit einem Stern versehen.

ALL ARTICLES & CATEGORIES

CCTV

Diese Rubrik per RSS verfolgen

Cloud & AI

Diese Rubrik per RSS verfolgen

Data Privacy

Alle 12 Artikel dieser Rubrik Diese Rubrik per RSS verfolgen

Digital Analytics

Alle 47 Artikel dieser Rubrik Diese Rubrik per RSS verfolgen

Digital Marketing

Alle 29 Artikel dieser Rubrik Diese Rubrik per RSS verfolgen

IT & Networks

Alle 16 Artikel dieser Rubrik Diese Rubrik per RSS verfolgen

Music Production

Diese Rubrik per RSS verfolgen

Raspberry Pi

Diese Rubrik per RSS verfolgen

Smart Home

Alle 18 Artikel dieser Rubrik Diese Rubrik per RSS verfolgen

Web Entwicklung

Diese Rubrik per RSS verfolgen

WordPress-Plugins & Tricks

Diese Rubrik per RSS verfolgen