LW IT Solutions
« Blog Overview /Smart Home/Tutorials / Tutorial: Reading SunSpec Registers From a PV...
Read this article in other languages:

Tutorial: Reading SunSpec Registers From a PV Inverter Over Modbus TCP

Tutorial: Reading SunSpec Registers From a PV Inverter Over Modbus TCP
Contents
  1. Why a Register Number Alone Is Not Enough
  2. Finding the Base Address and the Marker
  3. Walking the Model Chain
  4. Scale Factors, Data Types and the Sentinels
  5. Reading One Value End to End
  6. Putting It into Home Assistant
  7. Questions and answers
  8. Sources

A register number found in a forum post works. It works until a firmware update, until the next model of the same inverter, until a second device joins that stores the same value somewhere else. Then the number is wrong, and nothing says so: Modbus returns whatever is at that address, and a plausible number is indistinguishable from a correct one.

SunSpec exists for this reason. It is a directory laid over the register space, and reading it takes four steps that work identically on every inverter that implements it.

A register map from address 40000 with the SunS marker, two model headers with their blocks and the end marker, beside a decode of one value together with its scale factor register
The chain is self-describing: every model states its own length, so the address of the next one follows from the current one. Only the starting point has to be found.

Why a Register Number Alone Is Not Enough

Modbus has no notion of names, units or types. A read returns sixteen bits, and every interpretation of those bits happens in the reader. A register holding 1234 might be 1234 watts, 123.4 volts or 12.34 amperes, and the protocol has nothing to say about which.

SunSpec adds three things on top of that, and all three are stored in the register space itself: a marker that says the map is present, a chain of blocks that says which measurements exist and where, and per group of values a scale factor that says where the decimal point goes.

The practical consequence is that a reader written once works on the next device too. That is worth more than it sounds, because the alternative – a table of addresses per manufacturer and firmware version – is exactly the thing that quietly goes out of date.

Finding the Base Address and the Marker

The map begins at one of three addresses, and the first two registers say whether it does. They hold the four characters SunS, which as a 32-bit value is 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 at", basis)
        break

Two stumbling blocks live in that short piece of code. The first is the off-by-one that catches everybody once: documentation numbers registers from one, the protocol addresses them from zero. An address printed as 40001 in a manual is offset 40000 on the wire, and reading at 40001 returns the second half of the marker and no match.

The second is the unit id. An inverter with a built-in smart meter answers as two devices on the same connection, each with its own id and its own SunSpec map. Reading the meter’s power from the inverter’s id gives a value that is not an error and not the truth. Which ids are in use is shown in the Modbus page of the device’s own web interface – and that page is also where Modbus TCP has to be switched on in the first place, because it is off by default on most inverters.

Walking the Model Chain

After the marker comes a sequence of blocks, and each block begins with two registers: its model number and its length. Adding the length to the current position gives the start of the next block, and a model number of 65535 ends the chain.

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"model {modell:5d}  length {laenge:3d}  data at {pos + 2}")
    pos += 2 + laenge

A typical inverter answers with three or four models, and the numbers say what each of them is.

Model Content
1 Common: manufacturer, model designation, serial number, firmware version
101, 102, 103 Inverter, single phase, split phase, three phase – integers with scale factors
111, 112, 113 The same measurements as floating point numbers, without scale factors
160 The individual strings, one repeating block per MPP tracker
201 to 204 Meter, one model per wiring type

Whether a device offers 103 or 113 is usually a setting rather than a property. Many inverters have a switch between integer with scale factors and floating point, and it decides which of the two model families appears in the chain. A reader that expects one and finds the other reports no measurements at all, which is a confusing symptom for a setting nobody remembers changing.

Scale Factors, Data Types and the Sentinels

Inside a model block, the position of a value is fixed by the specification and counted from the start of the data. The phase A current sits at offset 2, and the scale factor that belongs to it at offset 5.

Model 103, data starting at 40071

  offset 1   A      uint16   total current
  offset 2   AphA   uint16   phase A
  offset 3   AphB   uint16   phase B
  offset 4   AphC   uint16   phase C
  offset 5   A_SF   int16    scale factor for all four

  40072 = 1234        raw value
  40075 = 0xFFFE      two's complement, so −2

  1234 × 10⁻² = 12.34 A

The scale factor is a signed exponent to base ten and applies to a whole group of values, not to one. That is why it appears once for four currents, and why reading a current without it produces a number a hundred times too large – which on a domestic system still looks like a plausible reading.

Three more properties of the encoding matter in practice. Values spanning two registers are big-endian with the high word first, which is the Modbus convention and which almost every library gets right by default. Text fields are fixed-length and padded with null bytes rather than terminated by them. And a value that the device does not implement is not zero but a sentinel: 0x8000 for int16, 0xFFFF for uint16, 0x80000000 for int32. A reader that treats those as numbers reports 65535 volts, and a rule that discards them is three lines long.

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)

Reading One Value End to End

The pieces together give a reader that finds the model rather than assuming its address.

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 counted from one
sf    = vorzeichen(block[4])     # A_SF, offset 5
print(f"{strom * 10 ** sf:.2f} A")

Worth reading the whole block in one request rather than each value on its own. A Modbus read takes a few milliseconds of round trip regardless of length, so six registers in one call cost what one register in one call costs – and, more importantly, all six values then come from the same moment. Six separate reads of a current, a voltage and a power give three quantities that do not multiply together, and a plausibility check built on them fails for no reason.

A note on polling frequency: an inverter is not a database. A request every five seconds is unproblematic, once a second is at the limit on some devices, and faster than that produces timeouts that look like network faults. The measured values themselves rarely update faster than every second anyway.

Putting It into Home Assistant

With the addresses known, the same reading works declaratively. Home Assistant’s Modbus integration handles the data type and the sign; only the scale factor has to be entered as a number, because the integration does not read it from the device.

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

The hard-coded scale is the compromise in this approach, and it is worth noting where it comes from. The scale factor was read once with the script above; it is stored in the device and practically never changes, but it is not guaranteed. After a firmware update, one comparison between the displayed value and the inverter’s own screen takes ten seconds and catches a factor of a hundred immediately.

One last field decides whether the value ends up in the energy statistics rather than only on a card. A power reading in watts is device_class: power with state_class: measurement; a meter reading in kilowatt-hours is device_class: energy with state_class: total_increasing. The two are not interchangeable, and only the second can be selected as an energy source.

Questions and answers

Why is the scale factor stored in the device rather than fixed in the specification?

Because sixteen bits only hold a limited range of numbers, and the same model applies to devices of very different sizes. Power as an int16 with a factor of 0 only reaches 32,767 watts; a larger inverter has to fall back to steps of ten, while a small inverter resolves more finely with a negative factor. The trade-off between range and resolution is the manufacturer’s to make, and the register tells the reader which one was chosen.

What needs attention with a meter reading intended for the Home Assistant energy statistics?

The lifetime yield sits in the inverter model as an accumulator spanning two registers, high word first, in watt-hours and with a scale factor of its own. In the Modbus integration it becomes a sensor with an unsigned 32-bit data type, device_class: energy and state_class: total_increasing, as described in the article.

The trap lies in total_increasing itself. Home Assistant interprets a drop in the value as the start of a new meter cycle. If the inverter returns 0 once and then the real reading again, the statistics count the entire yield since commissioning as the yield of that one hour.

For accumulators, 0 is also precisely the sentinel: unlike int16 and uint16, the accumulator types of the SunSpec specification use zero to mean “not implemented”, and the LEER table from the article needs another entry for it. In Home Assistant, the nan_value option of the Modbus integration turns such a raw value into “unavailable”, and an unavailable sensor leaves the statistics untouched.

How risky is it to switch on Modbus TCP on the inverter?

Modbus TCP has neither authentication nor encryption. Any device that reaches port 502 on the inverter can read the same registers as Home Assistant and, if writing is enabled, also change settings, such as imposing a power limit. Three measures keep the risk small:

  • Read-only access. If the device’s web interface offers the choice, read access is enough for everything in the article.
  • No port forwarding. Opening the port on the router turns the home system into a device that every scanner on the internet can reach.
  • A separate network segment for the inverter, to which only the Home Assistant machine has access. Within the same segment no firewall sees the connection, so a rule only takes effect between two segments.
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.

Write a comment

Experience with other devices or integrations and questions about the setup are welcome here.

The email address is not published. Required fields are marked with an asterisk.

Articles & categories

CCTV

Follow this category by RSS

Cloud & AI

All 17 articles in this category Follow this category by RSS

Data Privacy

All 19 articles in this category Follow this category by RSS

Digital Analytics

All 58 articles in this category Follow this category by RSS

Digital Marketing

All 37 articles in this category Follow this category by RSS

IT & Networks

All 19 articles in this category Follow this category by RSS

Music Production

All 17 articles in this category Follow this category by RSS

Raspberry PI

Follow this category by RSS

SaaS & Internet Earning

Follow this category by RSS

Smart Home

All 18 articles in this category Follow this category by RSS

Web Development

All 11 articles in this category Follow this category by RSS

WordPress Plugins & Tricks

All 14 articles in this category Follow this category by RSS