flowchart LR
A["<b>1. Start</b><br/>Model 802<br/><code>SetInvState = 3</code>"]
B["<b>2. Verify</b><br/>Model 701<br/><code>InvSt = 3</code><br/>(Running)"]
C["<b>3. Set Power</b><br/>Model 704<br/><code>WSetEna = 1</code><br/><code>WSet = target</code>"]
A --> B --> C
SunSpec Protocol Documentation
C-Battery BESS Integration Guide -V2
This document describes the V2 SunSpec Modbus interface for C-Battery energy storage systems.
V2 further implements the Sunspec protocol in V1 and should be fully Sunspec compliant.
V2 properly separates operational control from power setpoint control:
| Concern | Model | Registers | Access |
|---|---|---|---|
| Operational state | 802 (Battery Base) | SetOp, SetInvState |
Read/Write |
| Power setpoint | 704 (DER AC Controls) | WSetEna, WSetMod, WSet |
Read/Write |
| Status verification | 701 (AC Inverter) | InvSt, ConnSt, St |
Read-only |
If you are migrating from V1: any logic that writes WSetEna = 1 to “turn on” the battery must be updated. Use Model 802 SetOp / SetInvState for operational control. See Control Flow.
System Architecture
- Protocol: SunSpec over Modbus TCP
- Port: 502 (standard Modbus TCP)
- Base Address: 40000 (0-based addressing)
- Device Type: Battery Energy Storage System (BESS)
- Models Implemented: 1, 701, 702, 704, 713, 714, 715, 802
Control Flow
This is the most important change from V1. Operational control and power setpoint control are separate concerns that map to different SunSpec models.
Starting the System and Setting Power
Step 1 -Command the inverter to start (Model 802)
Write to the operational control registers:
| Register | Address | Value | Meaning |
|---|---|---|---|
SetInvState |
40458 | 3 | Start the inverter |
Step 2 -Verify the inverter is running (Model 701)
Poll InvSt at address 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 (only supported mode) |
WSet |
40301–40302 | target | Power setpoint in watts (scaled by WSet_SF) |
WSetEna = 1 does not turn the system on. It means “the active power setpoint is active -the DER should follow WSet.” Setting WSetEna = 0 causes the DER to ignore WSet, but does not stop the inverter.
Changing the Power Target While Running
Once the system is running and WSetEna = 1, you can update the power target at any time by writing a new value to WSet (40301–40302). There is no need to re-write WSetEna -the SunSpec specification states that once enabled, the DER continues to follow the setpoint until it is explicitly disabled.
Sign Convention
Standard SunSpec DER sign convention:
- Positive = discharge / export to grid
- Negative = charge / import from grid
Example: WSet raw value of -120 with WSet_SF = 2 results in -120 x 102 = -12,000 W (charging at 12 kW).
SunSpec Discovery
Register Layout
The device follows the standard SunSpec register layout:
Address Content Purpose
───────── ────────────── ──────────────────────────────
40000–40001 "SunS" SunSpec identifier (0x5375, 0x6e53)
40002–40003 Model 1 ID + Len Common model
40004–40069 Model 1 Data Device identification
40070 Model 701 ID DER AC Measurement
...
40225 Model 702 ID DER Nameplate
...
40277 Model 704 ID DER AC Controls
...
40344 Model 713 ID Storage Monitoring
...
40353 Model 714 ID DC Measurement
...
40398 Model 715 ID DER Control
...
40407 Model 802 ID Battery Base
...
40469 End marker 0xFFFF
Multi-Battery Support
Each battery unit is addressed by its Modbus unit ID:
| Configuration | Unit ID | Usage |
|---|---|---|
| Single battery / aggregated view | 1 | Not available yet |
| Battery N (multi-rack) | 100 + N | e.g. Battery 1 = 101, Battery 2 = 102 |
Multi-rack aggregation is not yet implemented. For multi-rack systems, address each battery individually using its unit ID.
# Single battery or aggregated view
response = client.read_holding_registers(40348, count=1, slave=1)
# Battery 1 in a multi-rack setup
response = client.read_holding_registers(40348, count=1, slave=101)
# Battery 2 in a multi-rack setup
response = client.read_holding_registers(40348, count=1, slave=102)Data Types and Scaling
SunSpec Data Types
| Type | Size | Description |
|---|---|---|
uint16 |
1 register | 16-bit unsigned integer |
int16 |
1 register | 16-bit signed integer |
uint32 |
2 registers | 32-bit unsigned integer (MSW first) |
int32 |
2 registers | 32-bit signed integer (MSW first) |
string |
N registers | ASCII string, null-terminated |
enum16 |
1 register | 16-bit enumeration |
bitfield32 |
2 registers | 32-bit bitfield (MSW first) |
sunssf |
1 register | SunSpec scale factor (signed) |
pad |
1 register | Padding, always 0 |
Scale Factors
Numeric values are stored as raw integers. The actual value is computed using a scale factor defined within the same model:
actual_value = raw_value x 10^scale_factor^
| Example | Raw | Scale Factor | Actual |
|---|---|---|---|
| Power | 120 | W_SF = 2 | 120 x 102 = 12,000 W |
| Frequency | 49937 | Hz_SF = -3 | 49937 x 10-3 = 49.937 Hz |
| State of Charge | 1000 | Pct_SF = -1 | 1000 x 10-1 = 100.0% |
| Cell Voltage | 340 | CellV_SF = -2 | 340 x 10-2 = 3.40 V |
Models Reference
Model 1 -Common (Device Identification)
Purpose: Device identification and addressing. Access: Read-only. Base address: 40002.
| Point | Type | Description | Notes |
|---|---|---|---|
Mn |
string | Manufacturer | “C-Battery” |
Md |
string | Model name | “Cbat-IQ” |
Vr |
string | Firmware version | e.g. “1.0.0” |
SN |
string | Serial number | |
DA |
uint16 | Modbus device address | 1–247 |
Model 701 -DER AC Measurement
Purpose: AC-side measurements and inverter status. Access: Read-only. Base address: 40070. Length: 153 registers.
Inverter Status
| Point | Address | Type | Description |
|---|---|---|---|
ACType |
40072 | enum16 | AC wiring type |
St |
40073 | enum16 | Operating state |
InvSt |
40074 | enum16 | Inverter state (see table below) |
ConnSt |
40075 | enum16 | Connection status |
Alrm |
40076–40077 | bitfield32 | Active alarms |
DERMode |
40078–40079 | bitfield32 | Active DER control mode |
Inverter State (InvSt) values:
| Value | State | Description |
|---|---|---|
| 0 | Off | Inverter is not running |
| 1 | Sleeping | Auto-shutdown / night mode |
| 2 | Starting | Inverter is initializing |
| 3 | Running | Producing or consuming power |
| 4 | Throttled | Power limited by external condition |
| 5 | Shutting Down | Transitioning to off/standby |
| 6 | Fault | Fault condition, check Alrm |
| 7 | Standby | Ready but not actively converting |
AC Measurements
| Point | Address | Type | Scale | Units |
|---|---|---|---|---|
W |
40080 | int16 | W_SF (2) | Watts |
VA |
40081 | int16 | VA_SF (2) | Volt-amps |
Var |
40082 | int16 | Var_SF (2) | Volt-amps reactive |
PF |
40083 | int16 | PF_SF (-3) | Power factor |
A |
40084 | int16 | A_SF (-1) | Total AC current |
Hz |
40087–40088 | uint32 | Hz_SF (-3) | Frequency (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) |
All phase currents scale by A_SF (-1), all voltages by V_SF (-1).
Manufacturer Alarms (MnAlrm)
The MnAlrm bitfield is a 32-bit aggregated fault mask. Bits 0–15 map to the inverter’s Fault Table 1, bits 16–31 to Fault Table 2.
| Bit | Fault | Bit | Fault |
|---|---|---|---|
| 0 | DC Bus Overvoltage | 16 | Module W Fault |
| 1 | DC Bus Undervoltage | 17 | Module V Fault |
| 2 | DC Bus Overcurrent | 18 | Module U Fault |
| 3 | Abnormal Battery Insulation | 19 | Leakage Current Over Limit |
| 4 | Battery Overvoltage | 20 | AC Current Imbalance |
| 5 | Battery Undervoltage | 21 | AC Voltage Imbalance |
| 6 | Battery Reverse Connection | 22 | Independent Inverter Overcurrent |
| 7 | Battery Overcurrent | 23 | Environmental Over Temperature |
| 8 | On Grid Inverter Overcurrent | 24 | Abnormal DC Circuit Breaker |
| 9 | Grid Overvoltage | 25 | Abnormal DC Contactor |
| 10 | Grid Undervoltage | 26 | AC Contactor Abnormal |
| 11 | Grid Overfrequency | 27 | AC Circuit Breaker Abnormal |
| 12 | Grid Underfrequency | 28 | Abnormal Arrester |
| 13 | Island Protection | 29 | Module Over Temperature |
| 14 | Independent Inverter Overvoltage | 30 | Reactor Over Temperature |
| 15 | Independent Inverter Undervoltage | 31 | Emergency Stop |
Model 702 -DER Nameplate
Purpose: Rated capacity of the DER. Access: Read-only. Base address: 40225. Length: 50 registers.
| Point | Address | Type | Scale | Units | Description |
|---|---|---|---|---|---|
WMaxRtg |
40227 | uint16 | W_SF (2) | Watts | Maximum rated active power |
Model 704 -DER AC Controls
Purpose: Active power setpoint control. Access: Read/Write. Base address: 40277. Length: 65 registers.
| Point | Address | Type | Access | Description |
|---|---|---|---|---|
WSetEna |
40299 | enum16 | RW | 0 = Disabled, 1 = Enabled |
WSetMod |
40300 | enum16 | RW | 1 = Constant power (only supported mode) |
WSet |
40301–40302 | int32 | RW | Power setpoint (scaled by WSet_SF) |
WSet_SF |
40332 | sunssf | R | Scale factor (= 2) |
WSetEna is not a power switch. It controls whether the DER follows the WSet value:
WSetEna = 1: the DER applies the active power setpoint inWSet.WSetEna = 0: the DER ignoresWSet. The inverter continues running in whatever state it was in.
To start or stop the system, use Model 802 (SetOp, SetInvState).
Model 713 -Storage Monitoring
Purpose: Battery state and energy information. Access: Read-only. Base address: 40344. Length: 7 registers.
| Point | Address | Type | Scale | Units | Description |
|---|---|---|---|---|---|
WHRtg |
40346 | uint16 | WH_SF (2) | Wh | Total energy rating |
WHAvail |
40347 | uint16 | WH_SF (2) | Wh | Available energy |
SoC |
40348 | uint16 | Pct_SF (-1) | % | State of charge |
SoH |
40349 | uint16 | Pct_SF (-1) | % | State of health |
Sta |
40350 | enum16 | - | - | Storage status |
Model 714 -DC Measurement
Purpose: DC-side measurements (battery bus). Access: Read-only. Base address: 40353. Length: 43 registers.
| Point | Address | Type | Scale | Units | Description |
|---|---|---|---|---|---|
NPrt |
40357 | uint16 | - | - | Number of DC ports (= 1) |
DCA |
40358 | int16 | DCA_SF (-1) | A | DC current |
DCW |
40359 | int16 | DCW_SF (2) | W | DC power |
Model 715 -DER Control
Purpose: DER operational commands (start, stop, standby). Base address: 40398. Length: 7 registers.
Model 715 is present in the register map but OpCtl is not currently used for operational control. Start/stop commands are handled through Model 802 (SetOp, SetInvState).
| Point | Address | Type | Access | Description |
|---|---|---|---|---|
AlarmReset |
40405 | uint16 | RW | Write 1 to reset alarms |
OpCtl |
40406 | enum16 | RW | Not used -see Model 802 |
Model 802 -Battery Base Model
Purpose: Battery system state, capabilities, events, and operational control. Base address: 40407. Length: 62 registers.
This is the primary model for operational control and battery-level monitoring.
Ratings and Limits (Read-only)
| Point | Address | Type | Scale | Units | Description |
|---|---|---|---|---|---|
AHRtg |
40409 | uint16 | AHRtg_SF (1) | Ah | Nameplate amp-hour rating |
WHRtg |
40410 | uint16 | WHRtg_SF (2) | Wh | Nameplate energy rating |
WChaRteMax |
40411 | uint16 | WChaDisChaMax_SF (2) | W | Nameplate max charge power |
WDisChaRteMax |
40412 | uint16 | WChaDisChaMax_SF (2) | W | Nameplate max discharge power |
State and Measurements (Read-only)
| Point | Address | Type | Scale | Units | Description |
|---|---|---|---|---|---|
SoC |
40418 | uint16 | SoC_SF (0) | % | State of charge |
Typ |
40428 | enum16 | - | - | Battery chemistry (4 = Li-Ion) |
State |
40429 | enum16 | - | - | Battery operational state |
V |
40441 | uint16 | V_SF (-1) | V | Battery bus voltage |
CellVMax |
40444 | uint16 | CellV_SF (-2) | V | Highest cell voltage |
CellVMin |
40447 | uint16 | CellV_SF (-2) | V | Lowest cell voltage |
A |
40451 | int16 | A_SF (-1) | A | Battery current |
AChaMax |
40452 | uint16 | AMax_SF (-1) | A | Max charge current (dynamic) |
ADisChaMax |
40453 | uint16 | AMax_SF (-1) | A | Max discharge current (dynamic) |
W |
40454 | int16 | W_SF (0) | W | Battery power |
Battery State (State) values:
| Value | State |
|---|---|
| 1 | Disconnected |
| 2 | Initializing |
| 3 | Connected |
| 4 | Absorb |
| 5 | Float |
| 6 | Discharging |
Events (Read-only)
| Point | Address | Type | Description |
|---|---|---|---|
Evt1 |
40433–40434 | bitfield32 | SunSpec-defined battery events |
Evt2 |
40435–40436 | bitfield32 | SunSpec-defined battery events (extended) |
EvtVnd1 |
40437–40438 | bitfield32 | Vendor-specific events |
EvtVnd2 |
40439–40440 | bitfield32 | Vendor-specific events (extended) |
Control Registers (Read/Write)
These are the registers for operational control of the battery system.
| Point | Address | Type | Description |
|---|---|---|---|
LocRemCtl |
40424 | enum16 | 0 = Remote, 1 = Local |
AlmRst |
40427 | uint16 | Write 1 to reset alarms |
SetOp |
40457 | enum16 | 1 = Connect, 2 = Disconnect |
SetInvState |
40458 | enum16 | 1 = Stopped, 2 = Standby, 3 = Started |
LocRemCtl must be 0 (Remote) for the system to accept commands over Modbus. If set to 1 (Local), write commands to SetOp, SetInvState, and Model 704 registers will be ignored.
Programming Examples
Starting the System and Setting Power
from pymodbus.client import ModbusTcpClient
import time
def start_and_set_power(power_watts, ip='172.20.0.123', unit_id=1):
"""
Start the battery system and set a power target.
Follows the correct SunSpec control flow:
Model 802 (start) -> Model 701 (verify) -> Model 704 (power setpoint)
"""
client = ModbusTcpClient(ip, port=502)
if not client.connect():
raise ConnectionError("Failed to connect")
try:
# Step 1: Start the inverter (Model 802)
client.write_register(40457, value=1, slave=unit_id) # SetOp = Connect
client.write_register(40458, value=3, slave=unit_id) # SetInvState = Started
# Step 2: Wait for inverter to reach Running state (Model 701)
for _ in range(30):
resp = client.read_holding_registers(40074, count=1, slave=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 set the power target (Model 704)
# WSet_SF = 2, so raw_value = power_watts / 100
raw = power_watts // 100
high = (raw >> 16) & 0xFFFF
low = raw & 0xFFFF
client.write_register(40299, value=1, slave=unit_id) # WSetEna = Enabled
client.write_register(40300, value=1, slave=unit_id) # WSetMod = Constant power
client.write_registers(40301, values=[high, low], slave=unit_id) # WSet
print(f"System running, power setpoint: {power_watts} W")
finally:
client.close()
# Discharge at 12 kW
start_and_set_power(12000)
# Charge at 12 kW (negative = import)
start_and_set_power(-12000)Stopping the System
def stop_system(ip='172.20.0.123', unit_id=1):
"""Stop the battery system cleanly."""
client = ModbusTcpClient(ip, port=502)
if not client.connect():
raise ConnectionError("Failed to connect")
try:
# Disable power setpoint
client.write_register(40299, value=0, slave=unit_id) # WSetEna = Disabled
# Stop the inverter
client.write_register(40458, value=1, slave=unit_id) # SetInvState = Stopped
# Verify
for _ in range(30):
resp = client.read_holding_registers(40074, count=1, slave=unit_id)
if not resp.isError() and resp.registers[0] == 0:
print("System stopped")
return
time.sleep(1)
print("Warning: inverter did not confirm Off state within 30s")
finally:
client.close()Reading System State
def read_system_state(ip='172.20.0.123', unit_id=1):
"""Read key system metrics across multiple models."""
client = ModbusTcpClient(ip, port=502)
if not client.connect():
raise ConnectionError("Failed to connect")
try:
INV_STATES = {
0: "Off", 1: "Sleeping", 2: "Starting", 3: "Running",
4: "Throttled", 5: "Shutting Down", 6: "Fault", 7: "Standby"
}
# Model 701 -Inverter state and AC power
inv_st = client.read_holding_registers(40074, count=1, slave=unit_id).registers[0]
w_raw = client.read_holding_registers(40080, count=1, slave=unit_id).registers[0]
# W_SF = 2 → actual = raw * 100
# int16: convert unsigned to signed
ac_power = (w_raw if w_raw < 0x8000 else w_raw - 0x10000) * 100
# Model 713 -State of charge
soc_raw = client.read_holding_registers(40348, count=1, slave=unit_id).registers[0]
# Pct_SF = -1 → actual = raw * 0.1
soc = soc_raw * 0.1
# Model 802 -Battery voltage and cell voltages
v_raw = client.read_holding_registers(40441, count=1, slave=unit_id).registers[0]
cell_max = client.read_holding_registers(40444, count=1, slave=unit_id).registers[0]
cell_min = client.read_holding_registers(40447, count=1, slave=unit_id).registers[0]
print(f"Inverter: {INV_STATES.get(inv_st, 'Unknown')} ({inv_st})")
print(f"AC Power: {ac_power} W")
print(f"SoC: {soc:.1f}%")
print(f"Bus Voltage: {v_raw * 0.1:.1f} V")
print(f"Cell V range: {cell_min * 0.01:.2f} – {cell_max * 0.01:.2f} V")
finally:
client.close()