Integration guide
How to drive any C-Battery system except the 75 kWh rack from an EMS, using the V2 SunSpec interface. The implementation is fully SunSpec-compliant and adds Model 802 for operational control alongside the existing Model 704 power-setpoint controls.
At a glance
| Property | Value |
|---|---|
| Protocol | SunSpec over Modbus TCP |
| Port | 502 |
| Base address | 40000 |
| Models implemented | 1, 701, 702, 704, 713, 714, 715, 802 |
| Device type | Battery 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:
| Concern | Model | Registers | Access |
|---|---|---|---|
| Operational state | 802 (Battery Base) | SetOp, SetInvState | RW |
| Power setpoint | 704 (DER AC Controls) | WSetEna, WSetMod, WSet | RW |
| Status verification | 701 (AC Inverter) | InvSt, ConnSt, St | R |
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)
| Register | Address | Value | Meaning |
|---|---|---|---|
SetInvState | 40458 | 3 | Start 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)
| Register | Address | Value | Meaning |
|---|---|---|---|
WSetEna | 40299 | 1 | DER follows the active power setpoint |
WSetMod | 40300 | 1 | Constant-power mode (the only supported mode) |
WSet | 40301–40302 | target | Power setpoint in watts (scaled by WSet_SF) |
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:
| Configuration | Unit ID | Use |
|---|---|---|
| Single battery / aggregated view | 1 | not yet available |
| Rack N | 100 + N | Rack 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
| Type | Size | Description |
|---|---|---|
uint16 / int16 | 1 register | 16-bit (un)signed integer |
uint32 / int32 | 2 registers | 32-bit (un)signed integer, MSW first |
string | N registers | ASCII, null-terminated |
enum16 | 1 register | 16-bit enumeration |
bitfield32 | 2 registers | 32-bit bitfield, MSW first |
sunssf | 1 register | Signed scale factor |
pad | 1 register | Padding, always 0 |
Actual values are computed as:
actual = raw × 10^SF
| Example | Raw | Scale factor | Actual |
|---|---|---|---|
| Power | 120 | W_SF = 2 | 12 000 W |
| Frequency | 49 937 | Hz_SF = -3 | 49.937 Hz |
| State of charge | 1000 | Pct_SF = -1 | 100.0 % |
Models reference
Model 1 — Common
Device identification. Read-only. Base address 40002.
| Point | Type | Notes |
|---|---|---|
Mn | string | Manufacturer ("C-Battery") |
Md | string | Model (e.g. "Cbat-IQ") |
Vr | string | Firmware version |
SN | string | Serial number |
DA | uint16 | Modbus device address (1–247) |
Model 701 — DER AC Measurement
Read-only AC-side measurements. Base address 40070, length 153.
Inverter status:
| Point | Address | Type | Description |
|---|---|---|---|
ACType | 40072 | enum16 | AC wiring type |
St | 40073 | enum16 | Operating state |
InvSt | 40074 | enum16 | Inverter state (see below) |
ConnSt | 40075 | enum16 | Connection status |
Alrm | 40076–40077 | bitfield32 | Active alarms |
DERMode | 40078–40079 | bitfield32 | Active DER control mode |
Inverter state values:
| Value | State | Description |
|---|---|---|
| 0 | Off | Inverter is not running |
| 1 | Sleeping | Auto-shutdown / night mode |
| 2 | Starting | Initializing |
| 3 | Running | Producing or consuming power |
| 4 | Throttled | Power limited externally |
| 5 | Shutting Down | Transitioning to off/standby |
| 6 | Fault | Check Alrm |
| 7 | Standby | Ready but not actively converting |
AC measurements:
| Point | Address | Scale | Units |
|---|---|---|---|
W | 40080 | W_SF (2) | W |
VA | 40081 | VA_SF (2) | VA |
Var | 40082 | Var_SF (2) | var |
PF | 40083 | PF_SF (-3) | — |
A | 40084 | A_SF (-1) | A |
Hz | 40087–40088 | Hz_SF (-3) | Hz |
Per-phase measurements:
| Phase | Current | Line-Line Voltage | Line-Neutral Voltage |
|---|---|---|---|
| L1 | AL1 (40115) | VL1L2 (40116) | VL1 (40117) |
| L2 | AL2 (40138) | VL2L3 (40139) | VL2 (40140) |
| L3 | AL3 (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.
| Point | Address | Type | Scale | Description |
|---|---|---|---|---|
WMaxRtg | 40227 | uint16 | W_SF (2) | Max rated active power (W) |
Model 704 — DER AC Controls
Read/write. Base address 40277.
| Point | Address | Access | Description |
|---|---|---|---|
WSetEna | 40299 | RW | 0 = Disabled, 1 = Enabled |
WSetMod | 40300 | RW | 1 = Constant power (only supported mode) |
WSet | 40301–40302 | RW | Power setpoint (W, scaled by WSet_SF) |
WSet_SF | 40332 | R | Scale factor (= 2) |
WSetEna is not a power switchWSetEna = 1→ DER followsWSet.WSetEna = 0→ DER ignoresWSet. 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.
| Point | Address | Scale | Description |
|---|---|---|---|
WHRtg | 40346 | WH_SF (2) | Energy rating (Wh) |
WHAvail | 40347 | WH_SF (2) | Available energy (Wh) |
SoC | 40348 | Pct_SF (-1) | State of charge (%) |
SoH | 40349 | Pct_SF (-1) | State of health (%) |
Sta | 40350 | — | Storage status |
Model 714 — DC Measurement
Read-only. Base address 40353.
| Point | Address | Scale | Description |
|---|---|---|---|
NPrt | 40357 | — | Number of DC ports (= 1) |
DCA | 40358 | DCA_SF (-1) | DC current (A) |
DCW | 40359 | DCW_SF (2) | DC power (W) |
Model 715 — DER Control
Base address 40398.
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).
| Point | Address | Access | Description |
|---|---|---|---|
AlarmReset | 40405 | RW | Write 1 to reset alarms |
OpCtl | 40406 | RW | Not used |
Model 802 — Battery Base
The primary model for operational control and battery-level data. Base address 40407.
Ratings and limits (read-only):
| Point | Address | Scale | Description |
|---|---|---|---|
AHRtg | 40409 | AHRtg_SF (1) | Amp-hour rating (Ah) |
WHRtg | 40410 | WHRtg_SF (2) | Energy rating (Wh) |
WChaRteMax | 40411 | WChaDisChaMax_SF (2) | Max charge power (W) |
WDisChaRteMax | 40412 | WChaDisChaMax_SF (2) | Max discharge power (W) |
State & measurements (read-only):
| Point | Address | Scale | Description |
|---|---|---|---|
SoC | 40418 | SoC_SF (0) | State of charge (%) |
Typ | 40428 | — | Battery chemistry (4 = Li-Ion) |
State | 40429 | — | Battery state (see below) |
V | 40441 | V_SF (-1) | Battery bus voltage (V) |
A | 40451 | A_SF (-1) | Battery current (A) |
AChaMax | 40452 | AMax_SF (-1) | Max charge current — dynamic (A) |
ADisChaMax | 40453 | AMax_SF (-1) | Max discharge current — dynamic (A) |
W | 40454 | W_SF (0) | Battery power (W) |
Battery state values:
| Value | State |
|---|---|
| 1 | Disconnected |
| 2 | Initializing |
| 3 | Connected |
| 4 | Absorb |
| 5 | Float |
| 6 | Discharging |
Events (read-only):
| Point | Address | Description |
|---|---|---|
Evt1 | 40433–40434 | SunSpec-defined battery events |
Evt2 | 40435–40436 | SunSpec-defined battery events (extended) |
EvtVnd1 | 40437–40438 | Vendor-specific events |
EvtVnd2 | 40439–40440 | Vendor-specific events (extended) |
Control (read/write):
| Point | Address | Description |
|---|---|---|
LocRemCtl | 40424 | 0 = Remote, 1 = Local |
AlmRst | 40427 | Write 1 to reset alarms |
SetOp | 40457 | 1 = Connect, 2 = Disconnect |
SetInvState | 40458 | 1 = Stopped, 2 = Standby, 3 = Started |
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.
Resetting rack faults
When a rack reports a fault (InvSt = 6, with details in Alrm), the EMS can clear it over Modbus once the underlying condition has been resolved.
| Register | Model | Access | Description |
|---|---|---|---|
AlmRst (40427) | 802 — Battery Base | RW | Write 1 to clear active rack faults. Self-clears back to 0 once the reset has been applied. |
AlarmReset (40405) | 715 — DER Control | RW | Alternative DER-side alarm reset; equivalent for most fault paths. |
Issue the write per rack — address each Unit ID individually (e.g. 101, 102, …). After writing, poll the register until it reads back 0, then confirm InvSt (40074) returns from 6 to 3 (Running) or 7 (Standby). No power cycle is required.
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
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")
finally:
client.close()