Skip to main content
Version: v1

SunSpec Protocol — Other Systems

This page describes the SunSpec Modbus interface used by all C-Battery systems except the 75 kWh rack. The implementation is fully SunSpec-compliant and adds Model 802 for operational control alongside the existing Model 704 power-setpoint controls.

At a glance

PropertyValue
ProtocolSunSpec over Modbus TCP
Port502
Base address40000
Models implemented1, 701, 702, 704, 713, 714, 715, 802
Device typeBattery Energy Storage System (BESS)

Key concept: control is separated

The most important thing to understand — especially if you've integrated with the 75 kWh rack before — is that starting/stopping the system and setting a power target are now two separate concerns, mapped to different SunSpec models:

ConcernModelRegistersAccess
Operational state802 (Battery Base)SetOp, SetInvStateRW
Power setpoint704 (DER AC Controls)WSetEna, WSetMod, WSetRW
Status verification701 (AC Inverter)InvSt, ConnSt, StR
Migrating from the 75 kWh rack

On the 75 kWh rack, writing WSetEna = 1 effectively turned the system on. That is no longer the case. WSetEna only controls whether the DER follows WSet — it does not start the inverter. Use Model 802 (SetInvState) to start/stop.

Control flow

The correct sequence to start the system and apply a power target:

Step 1 — Start the inverter (Model 802)

RegisterAddressValueMeaning
SetInvState404583Start the inverter

Step 2 — Verify it's running (Model 701)

Poll InvSt at 40074 until it reads 3 (Running).

Step 3 — Enable the power setpoint (Model 704)

RegisterAddressValueMeaning
WSetEna402991DER follows the active power setpoint
WSetMod403001Constant-power mode (the only supported mode)
WSet4030140302targetPower setpoint in watts (scaled by WSet_SF)
Changing the power target later

Once WSetEna = 1, you can update WSet at any time without re-writing WSetEna. The SunSpec spec says the DER continues to follow the setpoint until you explicitly disable it.

Sign convention

Standard SunSpec DER convention:

  • Positive = discharge / export to grid
  • Negative = charge / import from grid

For example, WSet = -120 with WSet_SF = 2 resolves to -120 × 10² = -12 000 W (charging at 12 kW).

Discovery & register layout

Address Content Purpose
───────── ────────────── ────────────────────────────
40000 – 40001 "SunS" SunSpec identifier
40002 – 40069 Model 1 Common (device identification)
40070 – … Model 701 DER AC Measurement
… Model 702 DER Nameplate
… Model 704 DER AC Controls
… Model 713 Storage Monitoring
… Model 714 DC Measurement
… Model 715 DER Control
… Model 802 Battery Base
end 0xFFFF End marker

Discovery follows the standard SunSpec walk: verify the "SunS" marker at 40000, then iterate model IDs starting at 40002.

Multi-rack addressing

Each battery rack is reachable via its Modbus Unit ID:

ConfigurationUnit IDUse
Single battery / aggregated view1not yet available
Rack N100 + NRack 1 = 101, Rack 2 = 102, …
# Read SoC for rack 1
client.read_holding_registers(40348, count=1, device_id=101)

# Read SoC for rack 2
client.read_holding_registers(40348, count=1, device_id=102)

Data types and scaling

TypeSizeDescription
uint16 / int161 register16-bit (un)signed integer
uint32 / int322 registers32-bit (un)signed integer, MSW first
stringN registersASCII, null-terminated
enum161 register16-bit enumeration
bitfield322 registers32-bit bitfield, MSW first
sunssf1 registerSigned scale factor
pad1 registerPadding, always 0

Actual values are computed as:

actual = raw × 10^SF

ExampleRawScale factorActual
Power120W_SF = 212 000 W
Frequency49 937Hz_SF = -349.937 Hz
State of charge1000Pct_SF = -1100.0 %
Cell voltage340CellV_SF = -23.40 V

Models reference

Model 1 — Common

Device identification. Read-only. Base address 40002.

PointTypeNotes
MnstringManufacturer ("C-Battery")
MdstringModel (e.g. "Cbat-IQ")
VrstringFirmware version
SNstringSerial number
DAuint16Modbus device address (1–247)

Model 701 — DER AC Measurement

Read-only AC-side measurements. Base address 40070, length 153.

Inverter status:

PointAddressTypeDescription
ACType40072enum16AC wiring type
St40073enum16Operating state
InvSt40074enum16Inverter state (see below)
ConnSt40075enum16Connection status
Alrm4007640077bitfield32Active alarms
DERMode4007840079bitfield32Active DER control mode

Inverter state values:

ValueStateDescription
0OffInverter is not running
1SleepingAuto-shutdown / night mode
2StartingInitializing
3RunningProducing or consuming power
4ThrottledPower limited externally
5Shutting DownTransitioning to off/standby
6FaultCheck Alrm
7StandbyReady but not actively converting

AC measurements:

PointAddressScaleUnits
W40080W_SF (2)W
VA40081VA_SF (2)VA
Var40082Var_SF (2)var
PF40083PF_SF (-3)
A40084A_SF (-1)A
Hz4008740088Hz_SF (-3)Hz

Per-phase measurements:

PhaseCurrentLine-Line VoltageLine-Neutral Voltage
L1AL1 (40115)VL1L2 (40116)VL1 (40117)
L2AL2 (40138)VL2L3 (40139)VL2 (40140)
L3AL3 (40161)VL3L1 (40162)VL3 (40163)

Phase currents scale by A_SF (-1); voltages by V_SF (-1).

Model 702 — DER Nameplate

Read-only nameplate ratings. Base address 40225.

PointAddressTypeScaleDescription
WMaxRtg40227uint16W_SF (2)Max rated active power (W)

Model 704 — DER AC Controls

Read/write. Base address 40277.

PointAddressAccessDescription
WSetEna40299RW0 = Disabled, 1 = Enabled
WSetMod40300RW1 = Constant power (only supported mode)
WSet4030140302RWPower setpoint (W, scaled by WSet_SF)
WSet_SF40332RScale factor (= 2)
WSetEna is not a power switch
  • WSetEna = 1 → DER follows WSet.
  • WSetEna = 0 → DER ignores WSet. The inverter keeps running in whatever state it was in.

Use Model 802 (SetInvState) to actually start or stop.

Model 713 — Storage Monitoring

Read-only. Base address 40344.

PointAddressScaleDescription
WHRtg40346WH_SF (2)Energy rating (Wh)
WHAvail40347WH_SF (2)Available energy (Wh)
SoC40348Pct_SF (-1)State of charge (%)
SoH40349Pct_SF (-1)State of health (%)
Sta40350Storage status

Model 714 — DC Measurement

Read-only. Base address 40353.

PointAddressScaleDescription
NPrt40357Number of DC ports (= 1)
DCA40358DCA_SF (-1)DC current (A)
DCW40359DCW_SF (2)DC power (W)

Model 715 — DER Control

Base address 40398.

note

Model 715 is present in the register map but OpCtl is not used for operational control on this implementation. Start/stop is handled by Model 802 (SetOp, SetInvState).

PointAddressAccessDescription
AlarmReset40405RWWrite 1 to reset alarms
OpCtl40406RWNot used

Model 802 — Battery Base

The primary model for operational control and battery-level data. Base address 40407.

Ratings and limits (read-only):

PointAddressScaleDescription
AHRtg40409AHRtg_SF (1)Amp-hour rating (Ah)
WHRtg40410WHRtg_SF (2)Energy rating (Wh)
WChaRteMax40411WChaDisChaMax_SF (2)Max charge power (W)
WDisChaRteMax40412WChaDisChaMax_SF (2)Max discharge power (W)

State & measurements (read-only):

PointAddressScaleDescription
SoC40418SoC_SF (0)State of charge (%)
Typ40428Battery chemistry (4 = Li-Ion)
State40429Battery state (see below)
V40441V_SF (-1)Battery bus voltage (V)
CellVMax40444CellV_SF (-2)Highest cell voltage (V)
CellVMin40447CellV_SF (-2)Lowest cell voltage (V)
A40451A_SF (-1)Battery current (A)
AChaMax40452AMax_SF (-1)Max charge current — dynamic (A)
ADisChaMax40453AMax_SF (-1)Max discharge current — dynamic (A)
W40454W_SF (0)Battery power (W)

Battery state values:

ValueState
1Disconnected
2Initializing
3Connected
4Absorb
5Float
6Discharging

Events (read-only):

PointAddressDescription
Evt14043340434SunSpec-defined battery events
Evt24043540436SunSpec-defined battery events (extended)
EvtVnd14043740438Vendor-specific events
EvtVnd24043940440Vendor-specific events (extended)

Control (read/write):

PointAddressDescription
LocRemCtl404240 = Remote, 1 = Local
AlmRst40427Write 1 to reset alarms
SetOp404571 = Connect, 2 = Disconnect
SetInvState404581 = Stopped, 2 = Standby, 3 = Started
Remote control required

LocRemCtl must be 0 (Remote) for the system to accept Modbus write commands. If it's 1 (Local), writes to SetOp, SetInvState, and Model 704 registers are silently ignored.

Examples

Start the system and set a power target

from pymodbus.client import ModbusTcpClient
import time

def start_and_set_power(power_watts, ip='192.0.2.100', unit_id=1):
"""Start the battery and apply a power setpoint, the correct way."""
client = ModbusTcpClient(ip, port=502)
client.connect()

try:
# Step 1: start the inverter (Model 802)
client.write_register(40457, value=1, device_id=unit_id) # SetOp = Connect
client.write_register(40458, value=3, device_id=unit_id) # SetInvState = Started

# Step 2: wait for InvSt == Running (Model 701)
for _ in range(30):
resp = client.read_holding_registers(40074, count=1, device_id=unit_id)
if not resp.isError() and resp.registers[0] == 3:
break
time.sleep(1)
else:
raise TimeoutError("Inverter did not reach Running state")

# Step 3: enable and write the power setpoint (Model 704)
raw = power_watts // 100 # WSet_SF = 2 → raw = watts / 100
high = (raw >> 16) & 0xFFFF
low = raw & 0xFFFF

client.write_register(40299, value=1, device_id=unit_id) # WSetEna = Enabled
client.write_register(40300, value=1, device_id=unit_id) # WSetMod = Constant power
client.write_registers(40301, values=[high, low], device_id=unit_id) # WSet
finally:
client.close()


start_and_set_power(12000) # discharge 12 kW
start_and_set_power(-12000) # charge 12 kW

Stop the system cleanly

def stop_system(ip='192.0.2.100', unit_id=1):
client = ModbusTcpClient(ip, port=502)
client.connect()
try:
client.write_register(40299, value=0, device_id=unit_id) # WSetEna = Disabled
client.write_register(40458, value=1, device_id=unit_id) # SetInvState = Stopped

for _ in range(30):
resp = client.read_holding_registers(40074, count=1, device_id=unit_id)
if not resp.isError() and resp.registers[0] == 0:
return
time.sleep(1)
finally:
client.close()

Read a system snapshot

INV_STATES = {
0: 'Off', 1: 'Sleeping', 2: 'Starting', 3: 'Running',
4: 'Throttled', 5: 'Shutting Down', 6: 'Fault', 7: 'Standby',
}

def snapshot(ip='192.0.2.100', unit_id=1):
client = ModbusTcpClient(ip, port=502)
client.connect()
try:
def s16(x): # int16 → signed
return x if x < 0x8000 else x - 0x10000

inv_st = client.read_holding_registers(40074, count=1, device_id=unit_id).registers[0]
w_raw = client.read_holding_registers(40080, count=1, device_id=unit_id).registers[0]
soc = client.read_holding_registers(40348, count=1, device_id=unit_id).registers[0] * 0.1
v_bus = client.read_holding_registers(40441, count=1, device_id=unit_id).registers[0] * 0.1
cmin = client.read_holding_registers(40447, count=1, device_id=unit_id).registers[0] * 0.01
cmax = client.read_holding_registers(40444, count=1, device_id=unit_id).registers[0] * 0.01

print(f"Inverter: {INV_STATES.get(inv_st, '?')} ({inv_st})")
print(f"AC Power: {s16(w_raw) * 100} W")
print(f"SoC: {soc:.1f}%")
print(f"Bus Voltage: {v_bus:.1f} V")
print(f"Cell V range: {cmin:.2f}{cmax:.2f} V")
finally:
client.close()