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) und 203 (Three Phase Meter, Wye). 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
- 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.
Ü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.
SunSpec Register-Map¶
Der Adressraum startet bei 40000 und enthält drei 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) | 107 Register |
| 40177–40178 | End-Marker (0xFFFF, 0x0000) | 2 Register |
Nicht befüllte Felder sind mit dem SunSpec-Sentinel 0x8000 (int16) bzw. 0xFFFF (uint16) als not implemented markiert.
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.
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 (0x8000).
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)¶
# Register 40088 (W) lesen — 1 Register
modpoll -m tcp -a 1 -r 40088 -c 1 -t 3 wattwaechter-XXXXXXXXXXXX.local
Zählerstand Bezug lesen (Python / pymodbus)¶
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()
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/status→enabled: 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 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.
Werte erscheinen um Faktor 10 / 100 verschoben
Die SunSpec-Skalierungsfaktoren (A_SF, V_SF, W_SF, Hz_SF, TotWh_SF) 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.
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}}'
