Zum Inhalt

Modbus TCP

Der WattWächter Plus stellt einen integrierten Modbus TCP Server bereit. Die Messdaten deines Stromzählers sind damit direkt als Modbus-Register abrufbar — geeignet für Home Assistant, evcc, openHAB, ioBroker, SMA, Loxone, SCADA-Systeme und jeden anderen Modbus-TCP-Client.

SunSpec-kompatibel

Der Server implementiert die SunSpec-Modelle 1 (Common Block), 203 (Three Phase Meter, Wye) und — ab Firmware 1.2.1 — 213 (dieselben Messwerte als Fließkommazahlen). Systeme mit SunSpec-Discovery (z.B. Home Assistant, evcc, openHAB, SMA Sunny Home Manager) erkennen den WattWächter dadurch automatisch als Dreiphasen-Zähler. Loxone hat keine SunSpec-Discovery — siehe Integrationen → Loxone.

Voraussetzungen

  • WattWächter Plus eingerichtet und mit dem WLAN verbunden — falls noch nicht geschehen, folge zunächst der Anleitung unter Erste Schritte
  • Firmware 1.2.0 oder neuer (für Model 213: 1.2.1 oder neuer)
  • Modbus-TCP-Client bzw. SunSpec-Client im selben Netzwerk
  • Port 502 (Standard) erreichbar zwischen Client und WattWächter

Modbus TCP aktivieren

Die Modbus-Schnittstelle ist ab Werk deaktiviert und muss einmalig eingeschaltet werden. Du hast drei Möglichkeiten:

Über die Weboberfläche (empfohlen) — ruf den WattWächter im Browser auf, entweder über seinen mDNS-Namen http://wattwaechter-XXXXXXXXXXXX.local oder über seine IP-Adresse, und klicke auf Einstellungen. Klapp dort den Abschnitt Modbus TCP auf, schalte Modbus-TCP-Server aktivieren ein und bestätige unten mit Speichern.

Das Feld Port steht auf 502 und muss nur angefasst werden, wenn dieser Port bei dir schon belegt ist.

Weboberfläche: Modbus TCP in den Einstellungen aktivieren

Über den API-Explorer — öffne die Web-UI des WattWächters, klicke auf „API-Explorer", wähle den Endpunkt POST /api/v1/settings und sende folgenden Body:

{ "modbus": { "enable": true, "port": 502 } }

Per curl — falls du es lieber von der Kommandozeile erledigst:

curl -X POST http://wattwaechter-XXXXXXXXXXXX.local/api/v1/settings \
  -H "Authorization: Bearer WRITE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "modbus": {
      "enable": true,
      "port": 502
    }
  }'

Die Änderung wird sofort übernommen — ein Neustart ist nicht erforderlich. Du kannst den Port anpassen, falls 502 bereits belegt ist.

API-Authentifizierung deaktiviert?

Ist die API-Authentifizierung ausgeschaltet (Werkseinstellung), kannst du den Authorization-Header weglassen. Siehe REST-API.


Verbindungsdaten

Parameter Wert
Protokoll Modbus TCP
Port 502 (konfigurierbar)
Unterstützte Function Codes 0x03 (Read Holding Registers), 0x04 (Read Input Registers)
Max. gleichzeitige Clients 2
Idle-Timeout 5 Minuten
Byte-Order Big-Endian (Modbus-Standard)
Register-Basis 40000 (SunSpec-Standard)

Als Host wird der mDNS-Name (wattwaechter-XXXXXXXXXXXX.local) oder die IP-Adresse des Geräts verwendet. Wie du die IP-Adresse findest, ist in den FAQs beschrieben.

Loxone: nur die IP-Adresse

Nicht jeder Client löst mDNS auf. Der Loxone Miniserver kann mit .local-Namen nichts anfangen — dort muss die IP-Adresse stehen, abgesichert über eine DHCP-Reservierung oder eine feste IP. Details unter Integrationen → Loxone.


Adresskonvention

Alle Adressen auf dieser Seite sind Protokolladressen — also genau die Zahl, die im Modbus-Telegramm steht. 40088 ist die Zahl auf dem Draht, keine Modicon-Referenz im Sinne des klassischen „4xxxx"-Schemas.

Das ist wichtig, weil Clients diese Zahl unterschiedlich entgegennehmen:

Client Eingabe für Protokolladresse 40088 Regel
pymodbus, Home Assistant, openHAB, Node-RED, Loxone, ioBroker 40088 unverändert
modpoll in der Voreinstellung und andere Werkzeuge, die ab 1 zählen 40089 +1 — oder das Werkzeug auf 0-basierte Zählung umstellen (bei modpoll: -0)
Siemens S7 MB_CLIENT (Parameter MB_DATA_ADDR) 440089 +400001

Der Sonderfall bei der S7: MB_DATA_ADDR erwartet eine Modicon-Referenz und zieht davon 40001 ab — im Bereich 40001–49999, der aber nur bis zur Protokolladresse 9998 reicht. Für unsere Adressen ist deshalb der erweiterte Bereich ab 400001 nötig, von dem der Baustein 400001 abzieht. Details unter Integrationen → Siemens S7.

Gibt dein Client eine Modbus-Ausnahme 02 (illegal data address) zurück oder liefert er nur Nullen, ist fast immer diese Umrechnung die Ursache — siehe Fehlerbehebung.

Selbsttest in einem Zug

Lies 2 Register ab Protokolladresse 40000. Kommen die Werte 21365 und 28243 zurück (hex 0x5375 0x6E53, als Text "SunS"), stimmt deine Adressierung — dann kannst du alle weiteren Adressen dieser Seite nach derselben Regel umrechnen.


SunSpec Register-Map

Der Adressraum startet bei 40000 und enthält vier Blöcke:

Adresse Inhalt Größe
40000–40001 SunSpec-ID "SunS" (0x53756E53) 2 Register
40002–40069 Model 1 — Common Block (Hersteller, Modell, Seriennummer, Firmware) 68 Register
40070–40176 Model 203 — Three Phase Meter (Wye), int16 mit Skalierungsfaktor 107 Register
40177–40302 Model 213 — dieselben Messwerte als float32 (ab Firmware 1.2.1) 126 Register
40303–40304 End-Marker (0xFFFF, 0x0000) 2 Register

Beide Meter-Modelle werden gleichzeitig bedient und enthalten dieselben Messwerte. Dein Client liest das eine oder das andere — nicht beide.

Nicht befüllte Felder sind als not implemented markiert: in Model 203 mit dem SunSpec-Sentinel 0x8000 (int16) bzw. 0xFFFF (uint16), in Model 213 mit NaN (0x7FC00000).

Adressen ab Firmware 1.2.1

Vor 1.2.1 endete die Modellkette direkt hinter Model 203, der End-Marker lag auf 40177–40178. Clients, die die Kette von 40000 an durchlaufen (alle echten SunSpec-Clients), merken davon nichts. Nur fest eingetragene Adressen jenseits von 40176 — die gab es vorher nicht — wären betroffen.

Model 1 — Common Block

Feld Adresse Typ Inhalt
ID 40002 uint16 1
L 40003 uint16 66
Mn (Manufacturer) 40004–40019 string32 SmartCircuits GmbH
Md (Model) 40020–40035 string32 WattWaechter
Vr (Version) 40044–40051 string16 Firmware-Version
SN (Serial Number) 40052–40067 string32 MAC-Adresse (hex, 12 Zeichen)
DA (Device Address) 40068 uint16 1

Model 203 — Three Phase Meter (Wye)

Alle Messwerte werden per Skalierungsfaktor (_SF) in ganzzahlige Register umgerechnet. Der tatsächliche Wert ergibt sich aus:

Wert = Rohwert × 10^SF
Adresse Beschreibung Feld Typ Einheit SF OBIS
40070 203 ID uint16
40071 105 L uint16
40072 Summenstrom (berechnet) A int16 A A_SF
40073 Strom L1 AphA int16 A A_SF 1-0:31.7.0
40074 Strom L2 AphB int16 A A_SF 1-0:51.7.0
40075 Strom L3 AphC int16 A A_SF 1-0:71.7.0
40076 Skalierung Strom (-2 → 0,01 A) A_SF int16
40077 Spannung LN (Mittelwert) PhV int16 V V_SF
40078 Spannung L1 PhVphA int16 V V_SF 1-0:32.7.0
40079 Spannung L2 PhVphB int16 V V_SF 1-0:52.7.0
40080 Spannung L3 PhVphC int16 V V_SF 1-0:72.7.0
40085 Skalierung Spannung (-1 → 0,1 V) V_SF int16
40086 Netzfrequenz Hz int16 Hz Hz_SF 1-0:14.7.0
40087 Skalierung Frequenz (-2 → 0,01 Hz) Hz_SF int16
40088 Gesamtwirkleistung W int16 W W_SF 1-0:16.7.0
40089 Leistung L1 WphA int16 W W_SF 1-0:21.7.0
40090 Leistung L2 WphB int16 W W_SF 1-0:41.7.0
40091 Leistung L3 WphC int16 W W_SF 1-0:61.7.0
40092 Skalierung Leistung (1 → 10 W, ab Firmware 1.2.0) W_SF int16
40108–40109 Gesamte Einspeisung TotWhExp acc32 Wh TotWh_SF 1-0:2.8.0
40116–40117 Gesamter Bezug TotWhImp acc32 Wh TotWh_SF 1-0:1.8.0
40124 Skalierung Energie (0 → 1 Wh) TotWh_SF int16

Datentypen

  • int16 — 16-Bit signed, 1 Register. Negative Werte bei Einspeisung (Leistung).
  • acc32 — 32-Bit unsigned Akkumulator, 2 Register (Highword zuerst).

Der Wertebereich ergibt sich aus Datentyp und Skalierungsfaktor. Die Leistungsregister decken mit W_SF = 1 rund ±327 kW ab, die Energiezähler als acc32 rund 4,29 GWh. Liegt ein Messwert darüber, meldet das Gerät not implemented (0x8000) statt eines abgeschnittenen Werts — eine geklemmte Zahl wäre von einem echten Messwert nicht zu unterscheiden.

Model 213 — Three Phase Meter (Wye, float)

Ab Firmware 1.2.1. Model 213 enthält dieselben Messwerte wie Model 203, jedoch als IEEE-754-float32 über je zwei Register (Highword zuerst). Es gibt keine Skalierungsfaktoren — der gelesene Wert ist bereits der Messwert in der angegebenen Einheit.

Adresse Beschreibung Feld Typ Einheit OBIS
40177 213 ID uint16
40178 124 L uint16
40179–40180 Summenstrom (berechnet) A float32 A
40181–40182 Strom L1 AphA float32 A 1-0:31.7.0
40183–40184 Strom L2 AphB float32 A 1-0:51.7.0
40185–40186 Strom L3 AphC float32 A 1-0:71.7.0
40187–40188 Spannung LN (Mittelwert) PhV float32 V
40189–40190 Spannung L1 PhVphA float32 V 1-0:32.7.0
40191–40192 Spannung L2 PhVphB float32 V 1-0:52.7.0
40193–40194 Spannung L3 PhVphC float32 V 1-0:72.7.0
40203–40204 Netzfrequenz Hz float32 Hz 1-0:14.7.0
40205–40206 Gesamtwirkleistung W float32 W 1-0:16.7.0
40207–40208 Leistung L1 WphA float32 W 1-0:21.7.0
40209–40210 Leistung L2 WphB float32 W 1-0:41.7.0
40211–40212 Leistung L3 WphC float32 W 1-0:61.7.0
40237–40238 Gesamte Einspeisung TotWhExp float32 Wh 1-0:2.8.0
40245–40246 Gesamter Bezug TotWhImp float32 Wh 1-0:1.8.0

Die Leiter-Leiter-Spannungen (PPV, 40195–40202) sowie Schein- und Blindleistung bleiben auf NaN — der WattWächter befüllt in beiden Modellen dieselben Felder.

Wann lohnt sich Model 213?

Model 203 bildet die Leistung mit W_SF = 1 ab, also in 10-W-Schritten. Bei kleinen Lasten führt das zu sichtbaren Rundungsdifferenzen — die Summe W stimmt dann nicht exakt mit WphA + WphB + WphC überein. Model 213 überträgt die volle Auflösung und kennt weder Quantisierung noch die int16-Grenze von ±327 kW.

Für generische Modbus-Clients (Loxone, modpoll) ist es außerdem deutlich bequemer: zwei Register lesen, als float32 interpretieren, fertig — kein SF-Register, keine Zehnerpotenz.

float32 richtig lesen

Der Wert steht Highword zuerst (Big-Endian, Modbus-Standard). Viele Clients nennen das FLOAT32 oder REAL mit Byte-Order ABCD. Steht dort eine unsinnige Zahl, ist meist die Wortreihenfolge vertauscht (CDAB).

Felder, die dein Zähler nicht liefert, enthalten NaN — nicht 0. Das ist Absicht: eine 0 wäre von einem echten Messwert („gerade 0 W") nicht zu unterscheiden. Ein Client, der NaN nicht erkennt, zeigt je nach Implementierung nan, 0 oder einen Extremwert an — prüfe im Zweifel über den Status-Endpunkt, welche Felder valid sind.

Ausnahme: Die drei Kernregister W, TotWhExp und TotWhImp stehen von Anfang an auf 0 statt auf NaN, damit Clients ohne NaN-Behandlung — allen voran Loxone — dort immer eine Zahl vorfinden. Model 203 hält es mit denselben Registern genauso (dort 0 statt 0x8000).

Welche Register sind verfügbar?

Welche Felder tatsächlich befüllt werden, hängt von deinem Stromzähler ab. Liefert er z.B. nur den Summenverbrauch 1-0:1.8.0 und die Momentanleistung 1-0:16.7.0, bleiben die Phasen-Register auf not implemented — in Model 203 als 0x8000, in Model 213 als NaN.

Welche Register aktuell gültige Werte enthalten, kannst du direkt über den Status-Endpunkt prüfen — ein GET auf /api/v1/modbus/status liefert für jedes Register ein valid-Flag:

# Nur ungültige (nicht vom Zähler gelieferte) Register anzeigen
curl -s http://wattwaechter-XXXXXXXXXXXX.local/api/v1/modbus/status \
  | jq '.registers[] | select(.valid == false)'

Details siehe Status-Endpunkt weiter unten.


Beispielzugriff

Gesamtleistung lesen (modpoll)

modpoll zählt in der Voreinstellung ab 1-r liegt damit um eins über der Protokolladresse. Für Register 40088 (W) also -r 40089:

# Protokolladresse 40088 (W) lesen — 1 Register
modpoll -m tcp -a 1 -r 40089 -c 1 -t 3 wattwaechter-XXXXXXXXXXXX.local

# Dasselbe mit 0-basierter Zählung — dann steht die Adresse direkt hinter -r
modpoll -m tcp -a 1 -0 -r 40088 -c 1 -t 3 wattwaechter-XXXXXXXXXXXX.local

Die häufigste Stolperfalle

Ohne -0 und ohne das +1 liest modpoll die Protokolladresse 40087 — das ist Hz_SF statt W. Dort steht mit -2 eine völlig plausible Zahl, der Fehler fällt also nicht auf. Im Zweifel den Selbsttest machen.

Zählerstand Bezug lesen (Python / pymodbus)

pymodbus nimmt an address= die Protokolladresse direkt entgegen — hier ist also nichts umzurechnen:

from pymodbus.client import ModbusTcpClient

client = ModbusTcpClient("wattwaechter-XXXXXXXXXXXX.local", port=502)
client.connect()

# TotWhImp: 2 Register ab 40116 (acc32)
rr = client.read_holding_registers(address=40116, count=2, slave=1)
tot_wh_imp = (rr.registers[0] << 16) | rr.registers[1]

# Skalierungsfaktor (TotWh_SF) aus Register 40124
sf = client.read_holding_registers(address=40124, count=1, slave=1).registers[0]
sf = sf if sf < 0x8000 else sf - 0x10000   # int16

kwh = tot_wh_imp * (10 ** sf) / 1000
print(f"Bezug: {kwh:.3f} kWh")

client.close()

Dasselbe über Model 213 (ohne Skalierungsfaktor)

import struct
from pymodbus.client import ModbusTcpClient

client = ModbusTcpClient("wattwaechter-XXXXXXXXXXXX.local", port=502)
client.connect()

# TotWhImp als float32: 2 Register ab 40245, Highword zuerst
rr = client.read_holding_registers(address=40245, count=2, slave=1)
wh = struct.unpack(">f", struct.pack(">HH", *rr.registers))[0]

print(f"Bezug: {wh / 1000:.3f} kWh")   # kein SF-Register nötig

client.close()

Status-Endpunkt

Zum Testen und Debuggen liefert die REST-API eine Live-Übersicht des Modbus-Zustands und aller Register, die das Gerät bedient — samt Rohwert und Skalierungsfaktor. Ein Modbus-Client (z. B. Loxone) lässt sich damit allein aus dieser Antwort konfigurieren, ohne die SunSpec-Spezifikation danebenzulegen:

curl http://wattwaechter-XXXXXXXXXXXX.local/api/v1/modbus/status \
  -H "Authorization: Bearer READ_TOKEN"

Antwort (gekürzt):

{
  "enabled": true,
  "running": true,
  "port": 502,
  "active_connections": 1,
  "registers": [
    { "register": 40073, "name": "AphA", "obis": "31.7.0",
      "value": 1.2300, "unit": "A", "valid": true,
      "raw": 123, "scale_factor": -2, "scale_register": 40076, "derived": false },
    { "register": 40086, "name": "Hz", "obis": "14.7.0",
      "value": null, "unit": "", "valid": false,
      "raw": -32768, "scale_factor": -2, "scale_register": 40087, "derived": false },
    { "register": 40088, "name": "W", "obis": "16.7.0",
      "value": -450.0000, "unit": "W", "valid": true,
      "raw": -45, "scale_factor": 1, "scale_register": 40092, "derived": false },
    { "register": 40116, "name": "TotWhImp", "obis": "1.8.0",
      "value": 12345600.0000, "unit": "Wh", "valid": true,
      "raw": 12345600, "scale_factor": 0, "scale_register": 40124, "derived": false },
    { "register": 40072, "name": "A", "obis": "",
      "value": 3.6900, "unit": "A", "valid": true,
      "raw": 369, "scale_factor": -2, "scale_register": 40076, "derived": true }
  ]
}
Feld Beschreibung
enabled Modbus-Server in den Einstellungen aktiviert
running Server-Task läuft aktuell und lauscht auf dem Port
active_connections Anzahl der aktuell verbundenen Modbus-Clients (max. 2)
registers[].register Modbus-Adresse; acc32-Werte belegen zwei aufeinanderfolgende Register
registers[].obis OBIS-Code des Zählers, aus dem das Register befüllt wird — leer bei berechneten Werten
registers[].value Skalierter Wert in unit; null, wenn nicht verfügbar
registers[].valid true, wenn der Zähler diesen Wert liefert. Sonst steht im Register der Sentinel 0x8000 (−32768)
registers[].raw Registerinhalt, so wie ihn ein Modbus-Client liest (int16 bzw. acc32 als unsigned)
registers[].scale_factor Dezimalexponent: value = raw × 10^scale_factor
registers[].scale_register Adresse des SF-Registers, das diesen Faktor über Modbus veröffentlicht
registers[].derived true bei A (40072) und PhV (40077) — die berechnet das Gerät aus den Phasenwerten, sie haben keinen OBIS-Code

Die Felder raw, scale_factor, scale_register und derived sowie die Einträge A und PhV gibt es ab Firmware 1.2.0. Ältere Firmware liefert nur die OBIS-gemappten Register mit value, unit und valid.

Skalierungsfaktor direkt ablesen

Wer einen Client von Hand konfiguriert, nimmt den Faktor aus scale_factor, statt ihn zu raten: -45 × 10^1 = -450 W. Die fünf SF-Register selbst (A_SF, V_SF, Hz_SF, W_SF, TotWh_SF) tauchen nicht als eigene Zeilen auf — sonst würde jeder Client, der die Liste rendert, sie als Sensoren anzeigen. Wo sie liegen, sagt scale_register; die vollständige Belegung steht in der Register-Map.

Siehe auch REST-API.


Fehlerbehebung

Verbindung wird abgelehnt / Timeout
  • Ist Modbus in den Einstellungen aktiviert? Prüfe /api/v1/modbus/statusenabled: true, running: true.
  • Verwendest du den richtigen Port? Standard ist 502.
  • Ist der Client im selben Netzwerk/Subnetz? In Gast-WLANs ist Client-zu-Client-Kommunikation meist gesperrt.
  • Es sind maximal 2 gleichzeitige Verbindungen möglich. Weitere Clients werden abgewiesen, bis eine Verbindung geschlossen wird oder nach 5 min Leerlauf.
Register liefern nur Nullen

Fast immer eine Frage der Adresskonvention. Der WattWächter beantwortet Adressen außerhalb seines Registerblocks mit der Modbus-Ausnahme 02 (illegal data address) — das ist Absicht, damit eine falsche Adresse auffällt, statt plausible Zahlen zu liefern. Viele Clients zeigen diese Ausnahme jedoch nur kurz oder gar nicht an und lassen ihren Datenbereich einfach auf 0 stehen.

  1. Mach den Selbsttest: 2 Register ab Protokolladresse 40000 müssen 21365 und 28243 liefern. Schlägt der fehl, liegt es an der Umrechnung — nicht am Gerät.
  2. Lass dir die Fehlerausgabe deines Clients gespeichert anzeigen. Bei SPS-Bausteinen stehen ERROR und STATUS oft nur einen einzigen Zyklus lang an und sind sonst nie zu sehen.
  3. Liest du Adressen ab 40179 und läuft auf dem Gerät noch Firmware vor 1.2.1? Dann gibt es Model 213 dort noch nicht, und das Gerät antwortet korrekterweise mit Ausnahme 02. Die Firmware-Version steht in der Weboberfläche und in den Registern 40044–40051.
  4. Steht die Verbindung überhaupt? active_connections in /api/v1/modbus/status zeigt, ob dein Client durchgekommen ist.
Register liefern nur 0x8000 / -32768

Der Wert 0x8000 ist der SunSpec-Sentinel für not implemented. Das bedeutet: dein Zähler liefert diesen OBIS-Code nicht. Prüfe im Dashboard, welche OBIS-Codes tatsächlich ankommen — nur diese werden in die Modbus-Register geschrieben. In Model 213 zeigt sich derselbe Fall als NaN.

Werte erscheinen um Faktor 10 / 100 verschoben

Die SunSpec-Skalierungsfaktoren (A_SF, V_SF, W_SF, Hz_SF, TotWh_SF) von Model 203 müssen bei jeder Abfrage angewendet werden. Beispiel: Strom-Register AphA = 123 mit A_SF = -2 ergibt 1,23 A. Echte SunSpec-Clients (Home Assistant, evcc, openHAB modbus.sunspec, pymodbus mit SunSpec-Parser) rechnen das automatisch um — bei generischen Modbus-Clients (Loxone, modpoll) musst du es selbst tun.

Ab Firmware 1.2.1 lässt sich das umgehen: Model 213 liefert dieselben Messwerte als Fließkommazahlen ganz ohne Skalierungsfaktoren — siehe Model 213.

Port 502 bereits belegt

Hast du bereits ein Gerät auf Port 502? Stelle in den Einstellungen einen alternativen Port ein, z.B. 1502:

curl -X POST http://wattwaechter-XXXXXXXXXXXX.local/api/v1/settings \
  -H "Authorization: Bearer WRITE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"modbus": {"port": 1502}}'