Modbus TCP¶
The WattWächter Plus exposes a built-in Modbus TCP server. Your smart meter readings can be queried directly as Modbus registers — suitable for Home Assistant, evcc, openHAB, ioBroker, SMA, Loxone, SCADA systems, and any other Modbus TCP client.
SunSpec-compliant
The server implements SunSpec Model 1 (Common Block), Model 203 (Three Phase Meter, Wye) and — as of firmware 1.2.1 — Model 213 (the same measurements as floating-point numbers). Systems with SunSpec discovery (e.g. Home Assistant, evcc, openHAB, SMA Sunny Home Manager) auto-detect the WattWächter as a three-phase meter. Loxone does not have SunSpec discovery — see Integrations → Loxone.
Prerequisites¶
- WattWächter Plus set up and connected to WiFi — if not yet done, follow the Getting Started guide first
- Firmware 1.2.0 or newer (for Model 213: 1.2.1 or newer)
- A Modbus TCP client (or SunSpec client) on the same network
- Port 502 (default) reachable between client and WattWächter
Enabling Modbus TCP¶
Modbus is disabled out of the box and must be enabled once. You have three options:
Via the web interface (recommended) — open the WattWächter in your browser, either via its mDNS name http://wattwaechter-XXXXXXXXXXXX.local or via its IP address, and click Settings. Expand the Modbus TCP section, switch Enable Modbus TCP server on and confirm with Save at the bottom.
The Port field is set to 502 and only needs changing if that port is already taken on your network.
Via the API Explorer — open the WattWächter web UI, click "API Explorer", select the POST /api/v1/settings endpoint and send this body:
{ "modbus": { "enable": true, "port": 502 } }
Via curl — if you'd rather do it from the command line:
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
}
}'
The change takes effect immediately — no reboot required. You can change the port if 502 is already in use.
API authentication disabled?
If API authentication is off (factory default), you can omit the Authorization header. See REST API.
Connection details¶
| Parameter | Value |
|---|---|
| Protocol | Modbus TCP |
| Port | 502 (configurable) |
| Supported function codes | 0x03 (Read Holding Registers), 0x04 (Read Input Registers) |
| Max. concurrent clients | 2 |
| Idle timeout | 5 minutes |
| Byte order | Big-endian (Modbus standard) |
| Register base | 40000 (SunSpec standard) |
Use the mDNS name (wattwaechter-XXXXXXXXXXXX.local) or the device's IP address as host. How to find the IP is described in the FAQ.
Loxone: IP address only
Not every client resolves mDNS. The Loxone Miniserver cannot handle .local
names — it needs the IP address, backed by a DHCP reservation or a static IP. Details
under Integrations → Loxone.
Address convention¶
Every address on this page is a protocol address — the exact number that travels in the Modbus frame. 40088 is the number on the wire, not a Modicon reference in the classic "4xxxx" sense.
This matters because clients take that number in different ways:
| Client | Entry for protocol address 40088 | Rule |
|---|---|---|
| pymodbus, Home Assistant, openHAB, Node-RED, Loxone, ioBroker | 40088 |
unchanged |
modpoll at its default setting, and other tools that count from 1 |
40089 |
+1 — or switch the tool to zero-based counting (modpoll: -0) |
Siemens S7 MB_CLIENT (parameter MB_DATA_ADDR) |
440089 |
+400001 |
Why the S7 is special: MB_DATA_ADDR expects a Modicon reference and subtracts 40001 from it — within the range 40001–49999, which only reaches protocol address 9998. Our addresses therefore require the extended range from 400001, from which the block subtracts 400001. Details under Integrations → Siemens S7.
If your client returns Modbus exception 02 (illegal data address) or reads nothing but zeros, this conversion is almost always the cause — see Troubleshooting.
One-shot self-test
Read 2 registers from protocol address 40000. If you get back 21365 and 28243
(hex 0x5375 0x6E53, as text "SunS"), your addressing is right — and every other
address on this page converts by the same rule.
SunSpec register map¶
The address range starts at 40000 and contains four blocks:
| Address | Content | Size |
|---|---|---|
| 40000–40001 | SunSpec ID "SunS" (0x53756E53) |
2 registers |
| 40002–40069 | Model 1 — Common Block (manufacturer, model, serial, firmware) | 68 registers |
| 40070–40176 | Model 203 — Three Phase Meter (Wye), int16 with scale factor | 107 registers |
| 40177–40302 | Model 213 — the same measurements as float32 (firmware 1.2.1 and newer) | 126 registers |
| 40303–40304 | End marker (0xFFFF, 0x0000) | 2 registers |
Both meter models are served at the same time and carry the same measurements. Your client reads one or the other — not both.
Unpopulated fields are marked as not implemented: in Model 203 with the SunSpec sentinel 0x8000 (int16) or 0xFFFF (uint16), in Model 213 with NaN (0x7FC00000).
Addresses as of firmware 1.2.1
Before 1.2.1 the model chain ended right after Model 203 and the end marker sat at 40177–40178. Clients that walk the chain from 40000 (every true SunSpec client) are unaffected. Only hard-coded addresses beyond 40176 would be — and there were none before.
Model 1 — Common Block¶
| Field | Address | Type | Content |
|---|---|---|---|
| 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 address (hex, 12 chars) |
| DA (Device Address) | 40068 | uint16 | 1 |
Model 203 — Three Phase Meter (Wye)¶
All measurement values use a scale factor (_SF) to fit into integer registers. The real value is:
value = raw × 10^SF
| Address | Description | Field | Type | Unit | SF | OBIS |
|---|---|---|---|---|---|---|
| 40070 | 203 |
ID | uint16 | — | — | — |
| 40071 | 105 |
L | uint16 | — | — | — |
| 40072 | Total current (computed) | A | int16 | A | A_SF | — |
| 40073 | Current L1 | AphA | int16 | A | A_SF | 1-0:31.7.0 |
| 40074 | Current L2 | AphB | int16 | A | A_SF | 1-0:51.7.0 |
| 40075 | Current L3 | AphC | int16 | A | A_SF | 1-0:71.7.0 |
| 40076 | Current scale factor (-2 → 0.01 A) |
A_SF | int16 | — | — | — |
| 40077 | LN voltage (average) | PhV | int16 | V | V_SF | — |
| 40078 | Voltage L1 | PhVphA | int16 | V | V_SF | 1-0:32.7.0 |
| 40079 | Voltage L2 | PhVphB | int16 | V | V_SF | 1-0:52.7.0 |
| 40080 | Voltage L3 | PhVphC | int16 | V | V_SF | 1-0:72.7.0 |
| 40085 | Voltage scale factor (-1 → 0.1 V) |
V_SF | int16 | — | — | — |
| 40086 | Grid frequency | Hz | int16 | Hz | Hz_SF | 1-0:14.7.0 |
| 40087 | Frequency scale factor (-2 → 0.01 Hz) |
Hz_SF | int16 | — | — | — |
| 40088 | Total active power | W | int16 | W | W_SF | 1-0:16.7.0 |
| 40089 | Power L1 | WphA | int16 | W | W_SF | 1-0:21.7.0 |
| 40090 | Power L2 | WphB | int16 | W | W_SF | 1-0:41.7.0 |
| 40091 | Power L3 | WphC | int16 | W | W_SF | 1-0:61.7.0 |
| 40092 | Power scale factor (1 → 10 W, firmware 1.2.0 and newer) |
W_SF | int16 | — | — | — |
| 40108–40109 | Total export | TotWhExp | acc32 | Wh | TotWh_SF | 1-0:2.8.0 |
| 40116–40117 | Total import | TotWhImp | acc32 | Wh | TotWh_SF | 1-0:1.8.0 |
| 40124 | Energy scale factor (0 → 1 Wh) |
TotWh_SF | int16 | — | — | — |
Data types
- int16 — 16-bit signed, 1 register. Negative for power export.
- acc32 — 32-bit unsigned accumulator, 2 registers (high word first).
The value range follows from the data type and the scale factor. With
W_SF = 1 the power registers cover roughly ±327 kW, the acc32 energy
counters roughly 4.29 GWh. Beyond that the device reports not implemented
(0x8000) rather than a clamped value — a clamped number would be
indistinguishable from a real reading.
Model 213 — Three Phase Meter (Wye, float)¶
Firmware 1.2.1 and newer. Model 213 carries the same measurements as Model 203, but as IEEE-754 float32 spanning two registers each (high word first). There are no scale factors — the value you read is already the measurement in the unit given.
| Address | Description | Field | Type | Unit | OBIS |
|---|---|---|---|---|---|
| 40177 | 213 |
ID | uint16 | — | — |
| 40178 | 124 |
L | uint16 | — | — |
| 40179–40180 | Total current (computed) | A | float32 | A | — |
| 40181–40182 | Current L1 | AphA | float32 | A | 1-0:31.7.0 |
| 40183–40184 | Current L2 | AphB | float32 | A | 1-0:51.7.0 |
| 40185–40186 | Current L3 | AphC | float32 | A | 1-0:71.7.0 |
| 40187–40188 | Voltage LN (average) | PhV | float32 | V | — |
| 40189–40190 | Voltage L1 | PhVphA | float32 | V | 1-0:32.7.0 |
| 40191–40192 | Voltage L2 | PhVphB | float32 | V | 1-0:52.7.0 |
| 40193–40194 | Voltage L3 | PhVphC | float32 | V | 1-0:72.7.0 |
| 40203–40204 | Line frequency | Hz | float32 | Hz | 1-0:14.7.0 |
| 40205–40206 | Total real power | W | float32 | W | 1-0:16.7.0 |
| 40207–40208 | Power L1 | WphA | float32 | W | 1-0:21.7.0 |
| 40209–40210 | Power L2 | WphB | float32 | W | 1-0:41.7.0 |
| 40211–40212 | Power L3 | WphC | float32 | W | 1-0:61.7.0 |
| 40237–40238 | Total export | TotWhExp | float32 | Wh | 1-0:2.8.0 |
| 40245–40246 | Total import | TotWhImp | float32 | Wh | 1-0:1.8.0 |
The line-to-line voltages (PPV, 40195–40202) as well as apparent and reactive power stay NaN — the WattWächter populates the same fields in both models.
When is Model 213 worth it?
Model 203 represents power with W_SF = 1, i.e. in 10 W steps. At small loads this
causes visible rounding differences — the total W then does not exactly match
WphA + WphB + WphC. Model 213 transmits the full resolution and knows neither
quantisation nor the int16 limit of ±327 kW.
It is also considerably more convenient for generic Modbus clients (Loxone, modpoll): read two registers, interpret as float32, done — no SF register, no power of ten.
Reading float32 correctly
The value is stored high word first (big-endian, the Modbus standard). Many clients
call this FLOAT32 or REAL with byte order ABCD. If you see a nonsensical number,
the word order is usually swapped (CDAB).
Fields your meter does not deliver hold NaN — not 0. That is deliberate: a 0
would be indistinguishable from a genuine reading ("0 W right now"). A client that does
not recognise NaN will show nan, 0 or an extreme value depending on its
implementation — when in doubt, check which fields are valid via the
status endpoint.
Exception: the three core registers W, TotWhExp and TotWhImp start out at 0
rather than NaN, so that clients without NaN handling — Loxone above all — always find
a number there. Model 203 does the same for the same registers (0 instead of
0x8000).
Which registers are actually available?¶
Which fields are populated depends on what your smart meter sends. If it only provides the total import 1-0:1.8.0 and instantaneous power 1-0:16.7.0, the per-phase registers stay at not implemented — 0x8000 in Model 203, NaN in Model 213.
To check which registers currently hold valid values, query the status endpoint — GET on /api/v1/modbus/status returns a valid flag for each register:
# Show only invalid registers (not delivered by the meter)
curl -s http://wattwaechter-XXXXXXXXXXXX.local/api/v1/modbus/status \
| jq '.registers[] | select(.valid == false)'
See the status endpoint section below for details.
Example access¶
Read total power (modpoll)¶
By default modpoll counts from 1 — so -r sits one above the protocol address. For register 40088 (W) that means -r 40089:
# Read protocol address 40088 (W) — 1 register
modpoll -m tcp -a 1 -r 40089 -c 1 -t 3 wattwaechter-XXXXXXXXXXXX.local
# The same with zero-based counting — then the address goes straight after -r
modpoll -m tcp -a 1 -0 -r 40088 -c 1 -t 3 wattwaechter-XXXXXXXXXXXX.local
The most common pitfall
Without -0 and without the +1, modpoll reads protocol address 40087 — that is
Hz_SF, not W. It holds -2, a perfectly plausible number, so the mistake goes
unnoticed. When in doubt, run the self-test.
Read total import (Python / pymodbus)¶
pymodbus takes the protocol address at address= directly — nothing to convert here:
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient("wattwaechter-XXXXXXXXXXXX.local", port=502)
client.connect()
# TotWhImp: 2 registers starting at 40116 (acc32)
rr = client.read_holding_registers(address=40116, count=2, slave=1)
tot_wh_imp = (rr.registers[0] << 16) | rr.registers[1]
# Scale factor (TotWh_SF) from 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"Import: {kwh:.3f} kWh")
client.close()
The same via Model 213 (no scale factor)¶
import struct
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient("wattwaechter-XXXXXXXXXXXX.local", port=502)
client.connect()
# TotWhImp as float32: 2 registers from 40245, high word first
rr = client.read_holding_registers(address=40245, count=2, slave=1)
wh = struct.unpack(">f", struct.pack(">HH", *rr.registers))[0]
print(f"Import: {wh / 1000:.3f} kWh") # no SF register needed
client.close()
Status endpoint¶
For testing and debugging, the REST API provides a live view of the Modbus state and every register the device serves — including the raw register content and the scale factor. A Modbus client (e.g. Loxone) can be configured from this response alone, without the SunSpec specification at hand:
curl http://wattwaechter-XXXXXXXXXXXX.local/api/v1/modbus/status \
-H "Authorization: Bearer READ_TOKEN"
Response (abbreviated):
{
"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 }
]
}
| Field | Description |
|---|---|
enabled |
Modbus server enabled in settings |
running |
Server task is up and listening on the port |
active_connections |
Current number of Modbus clients connected (max. 2) |
registers[].register |
Modbus address; acc32 values occupy two consecutive registers |
registers[].obis |
OBIS code of the meter value that populates the register — empty for computed values |
registers[].value |
Scaled value in unit; null if not available |
registers[].valid |
true if the meter delivers this value. Otherwise the register holds the sentinel 0x8000 (−32768) |
registers[].raw |
Register content exactly as a Modbus client reads it (int16, or acc32 as unsigned) |
registers[].scale_factor |
Decimal exponent: value = raw × 10^scale_factor |
registers[].scale_register |
Address of the SF register that publishes this factor over Modbus |
registers[].derived |
true for A (40072) and PhV (40077) — the device computes these from the phase values; they have no OBIS code |
The fields raw, scale_factor, scale_register and derived, as well as the entries
A and PhV, are available from firmware 1.2.0. Older firmware only lists the
OBIS-mapped registers with value, unit and valid.
Read the scale factor directly
If you configure a client by hand, take the factor from scale_factor instead of
guessing it: -45 × 10^1 = -450 W. The five SF registers themselves (A_SF, V_SF,
Hz_SF, W_SF, TotWh_SF) do not appear as rows of their own — otherwise every
client rendering the list would show them as sensors. scale_register tells you where
they live; the complete layout is in the register map.
See also the REST API.
Troubleshooting¶
Connection refused / timeout
- Is Modbus enabled in settings? Check
/api/v1/modbus/status→enabled: true,running: true. - Are you using the correct port? Default is 502.
- Is the client on the same network/subnet? Guest WiFi usually blocks client-to-client traffic.
- At most 2 concurrent connections are supported. Additional clients are rejected until a connection closes or times out after 5 min idle.
Registers only return zeros
Almost always a question of the address convention. The WattWächter answers addresses outside its register block with Modbus exception 02 (illegal data address) — deliberately, so that a wrong address fails loudly instead of returning plausible-looking numbers. Many clients, however, surface that exception only briefly or not at all and simply leave their data area at 0.
- Run the self-test: 2 registers from protocol address 40000 must return
21365and28243. If that fails, the conversion is at fault — not the device. - Make your client's error output latch. On PLC blocks,
ERRORandSTATUSare often set for a single scan cycle and are otherwise never seen. - Are you reading addresses from 40179 upwards on a device still running firmware older than 1.2.1? Then Model 213 does not exist there yet and the device correctly answers with exception 02. The firmware version is shown in the web interface and in registers 40044–40051.
- Is the connection up at all?
active_connectionsin/api/v1/modbus/statusshows whether your client got through.
Registers only return 0x8000 / -32768
0x8000 is the SunSpec sentinel for not implemented. That means your smart meter doesn't provide this OBIS code. Check the dashboard to see which OBIS codes actually arrive — only those are written to the Modbus registers. In Model 213 the same situation shows up as NaN.
Values look off by 10x / 100x
The SunSpec scale factors (A_SF, V_SF, W_SF, Hz_SF, TotWh_SF) of Model 203 must be applied on every read. Example: current register AphA = 123 with A_SF = -2 means 1.23 A. True SunSpec clients (Home Assistant, evcc, openHAB modbus.sunspec, pymodbus with SunSpec parser) handle this automatically — generic Modbus clients (Loxone, modpoll) require you to do it yourself.
As of firmware 1.2.1 you can side-step this entirely: Model 213 provides the same measurements as floating-point numbers with no scale factors at all — see Model 213.
Port 502 already in use
Already running another device on port 502? Configure an alternative port, e.g. 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}}'
