LW IT Solutions
« Blog Overview /Smart Home/Tutorials / Tutorial: Reading SunSpec Registers From a PV...

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. 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: 40084
        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.

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

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

ALL ARTICLES & CATEGORIES

CCTV

Follow this category by RSS

Cloud & AI

Follow this category by RSS

Data Privacy

All 12 articles in this category Follow this category by RSS

Digital Analytics

All 47 articles in this category Follow this category by RSS

Digital Marketing

All 29 articles in this category Follow this category by RSS

IT & Networks

All 16 articles in this category Follow this category by RSS

Music Production

Follow this category by RSS

Raspberry PI

Follow this category by RSS

Smart Home

All 18 articles in this category Follow this category by RSS

Web Development

Follow this category by RSS

WordPress Plugins & Tricks

Follow this category by RSS