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

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

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: 40083
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.
Fragen und Antworten
Warum steht der Skalierungsfaktor im Gerät und nicht fest in der Spezifikation?
Weil sechzehn Bit nur einen begrenzten Zahlenbereich fassen und dasselbe Modell für Geräte sehr unterschiedlicher Größe gilt. Eine Wirkleistung als int16 reicht mit dem Faktor 0 nur bis 32 767 Watt; ein größerer Wechselrichter muss auf Zehnerschritte ausweichen, ein kleiner kann mit einem negativen Faktor feiner auflösen. Die Abwägung zwischen Bereich und Auflösung trifft der Hersteller, und das Register teilt sie dem lesenden Programm mit.
Was ist bei einem Zählerstand für die Energiestatistik von Home Assistant zu beachten?
Der Gesamtertrag steht im Wechselrichtermodell als Zählwert über zwei Register, mit dem höherwertigen Wort zuerst, in Wattstunden und mit einem eigenen Skalierungsfaktor. In der Modbus-Anbindung wird daraus ein Sensor mit einem vorzeichenlosen 32-Bit-Datentyp, device_class: energy und state_class: total_increasing, wie im Artikel beschrieben.
Die Falle liegt in total_increasing selbst. Home Assistant deutet einen Rückgang des Werts als Beginn eines neuen Zählzyklus. Liefert der Wechselrichter einmal 0 und danach wieder den echten Stand, zählt die Statistik den gesamten Ertrag seit der Inbetriebnahme als Ertrag dieser einen Stunde.
Bei Zählwerten ist die 0 zudem genau der Ersatzwert: Anders als bei int16 und uint16 bedeutet sie bei den Zähltypen der SunSpec-Spezifikation „nicht geführt“, und die Liste LEER aus dem Artikel braucht dafür einen weiteren Eintrag. In Home Assistant verwandelt die Option nan_value der Modbus-Anbindung einen solchen Rohwert in „nicht verfügbar“, und ein nicht verfügbarer Sensor lässt die Statistik unberührt.
Wie riskant ist es, Modbus TCP am Wechselrichter einzuschalten?
Modbus TCP kennt weder Anmeldung noch Verschlüsselung. Jedes Gerät, das den Port 502 des Wechselrichters erreicht, kann dieselben Register lesen wie Home Assistant und, ist das Schreiben freigegeben, auch Einstellungen ändern, etwa eine Leistungsbegrenzung setzen. Drei Maßnahmen halten das Risiko klein:
- Nur Lesezugriff. Bietet die Weboberfläche des Geräts die Wahl, genügt für alles aus dem Artikel der Lesezugriff.
- Keine Portweiterleitung. Eine Freigabe am Router macht aus der Hausanlage ein Gerät, das jeder Scanner im Internet erreicht.
- Ein eigenes Netzsegment für den Wechselrichter, auf das nur der Home-Assistant-Rechner zugreifen darf. Im selben Segment sieht keine Firewall die Verbindung, deshalb wirkt eine Regel erst zwischen zwei Segmenten.