SunSpec
SunSpec defines a standard Modbus information model used by most PV inverters, meters and batteries. Each point has a fixed data type and a reserved unimplemented value the device sends when the point is absent.
modbus_connection.model.sunspec provides field factories that build model fields
with the right width, sign and sentinel — so an unimplemented point decodes to
None automatically. They are the same fields you’d otherwise hand-roll with the
generic factories, minus the boilerplate.
from modbus_connection.model import Componentfrom modbus_connection.model.sunspec import acc32, int16, sunssf, uint16
class Inverter(Component): a = uint16(2, scale_register=5) # AC current, scaled by A_SF a_sf = sunssf(5) wh = acc32(8) # lifetime energy, WhWord order is big-endian throughout, per the SunSpec spec.
Scale factors (sunssf)
Section titled “Scale factors (sunssf)”Scaled SunSpec points reference a scale-factor register — a signed int16
power-of-ten exponent. Pass scale_register= its address, and the value is
returned as raw * 10**sf, with sf read alongside the point on each update:
class Meter(Component): w = int16(10, scale_register=11) # active power, scaled by W_SF w_sf = sunssf(11) # the exponent register itselfThe scale register is read for you by the planner; you also declare it as a
sunssf field if you want to read its raw value directly. Writing a
dynamically-scaled point is unsupported.
Numeric points
Section titled “Numeric points”Each factory bakes in the SunSpec “unimplemented” sentinel for its type, so an
absent point decodes to None.
| Factory | Registers | Sentinel |
|---|---|---|
int16 |
1 | 0x8000 |
uint16 |
1 | 0xFFFF |
int32 |
2 | 0x80000000 |
uint32 |
2 | 0xFFFFFFFF |
int64 |
4 | 0x8000… |
uint64 |
4 | 0xFFFF… |
int16(address, *, scale=1.0, scale_register=None, scale_register_stride=0, stride=0, writable=False, unit=None) -> NumberField[float]# uint16 / int32 / uint32 / int64 / uint64 share this signature.Accumulators
Section titled “Accumulators”Accumulators are monotonic counters; SunSpec uses 0 to mean “not accumulated”,
which decodes to None.
| Factory | Registers |
|---|---|
acc16 |
1 |
acc32 |
2 |
acc64 |
4 |
acc32(address, *, scale=1.0, stride=0, unit=None) -> NumberField[int]Scale-factor point
Section titled “Scale-factor point”sunssf(address, *, stride=0) -> NumberField[int]A signed int16 power-of-ten exponent (unimplemented 0x8000). Reference it from a
scaled point with scale_register=, and optionally declare it as its own field.
Enumerations and bitfields
Section titled “Enumerations and bitfields”Pass an IntEnum / IntFlag to decode to members; omit it for the raw integer.
Both have enum16/enum32 and bitfield16/bitfield32/bitfield64 variants.
enum16(address, enum=None, *, stride=0, writable=False)enum32(address, enum=None, *, stride=0, writable=False)bitfield16(address, flags=None, *, stride=0, writable=False)bitfield32(address, flags=None, *, stride=0, writable=False)bitfield64(address, flags=None, *, stride=0, writable=False)from enum import IntEnumfrom modbus_connection.model.sunspec import enum16
class OperatingState(IntEnum): OFF = 1 SLEEPING = 2 MPPT = 4 THROTTLED = 5
class Inverter(Component): st = enum16(38, OperatingState) # decodes to a member, or None if 0xFFFFFloats and strings
Section titled “Floats and strings”float32(address, *, stride=0, writable=False, unit=None) -> FloatFieldfloat64(address, *, stride=0, writable=False, unit=None) -> FloatFieldstring(address, length, *, stride=0, writable=False) -> StringFieldfloat32 / float64 decode NaN (any NaN, sentinel 0x7FC00000) to None.
string is a fixed-length null-padded ASCII string over length registers.
Address points
Section titled “Address points”SunSpec models carry network addresses in registers. These are read-only:
ipaddr(address, *, stride=0) -> IPv4Field # IPv4 over 2 registersipv6addr(address, *, stride=0) -> IPv6Field # IPv6 over 8 registerseui48(address, *, stride=0) -> Eui48Field # EUI-48 / MAC over 3 registersfrom modbus_connection.model.sunspec import ipaddr, eui48
class Comms(Component): ip = ipaddr(10) # -> ipaddress.IPv4Address | None mac = eui48(20) # -> str | None ("00:1a:2b:3c:4d:5e")Multiple-MPPT and other repeats
Section titled “Multiple-MPPT and other repeats”A SunSpec model 160 (multiple MPPT) advertises how many modules follow in an N
point read at poll time. Model one module as a Component and size the list at
runtime with repeating_group:
from modbus_connection.model import Component, integer, repeating_groupfrom modbus_connection.model.sunspec import uint16
class MPPTModule(Component): # one module, at instance 0's addresses dc_w = integer(11, scale_register=2) dc_v = integer(10, scale_register=1)
class Inverter(Component): modules = repeating_group(uint16(8), MPPTModule, stride=20) # N at register 8A repeating block’s scale factors sit in the shared fixed block, so
scale_register addresses are not shifted per instance — see
Repeated sub-units for the full story on
base_offset, stride, and index.