SunSpec Modbus Documentation
C-Battery Energy Storage System Integration Guide
This documentation serves as your guide to the SunSpec-compatible C-battery modbus server. Our system makes standardized SunSpec models available over Modbus TCP, allowing external energy management systems etc to easily monitor and control the battery system.
Overview
What is SunSpec?
Instead of every manufacturer creating their own proprietary communication protocol, SunSpec defines a common way for energy devices (like solar inverters, battery storage systems, and meters) to share their data over Modbus. This means you can integrate devices from different manufacturers without having to integrate a new protocol each time.
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)
- Manufacturer: C-Battery
- Models Implemented: 1, 701, 704, 713, 714, 802
SunSpec Discovery Process
Register Layout
Our system follows the standard SunSpec structure:
Address Content Purpose
------- ------- -------
40000-40001: "SunS" SunSpec identifier (0x5375, 0x6e53)
40002-40003: Model 1 ID Common model identifier (0x0001)
40004-40005: Model 1 Length Number of data registers in Model 1
40006-40XXX: Model 1 Data Device identification data
40XXX-40XXX: Model 701 ID AC Inverter model
40XXX-40XXX: Model 701 Length Number of data registers in Model 701
40XXX-40XXX: Model 701 Data AC inverter measurements and status
...repeat for each model...
40XXX-40XXX: End Marker 0xFFFF (marks end of SunSpec data)
Discovery Algorithm
Here’s how you can automatically discover what’s available on our system:
from pymodbus.client import ModbusTcpClient
# Connect to device
client = ModbusTcpClient('172.20.0.123', port=502)
if not client.connect():
raise Exception("Failed to connect to Modbus device")
print("Connected to Modbus device")
try:
# 1. Verify SunSpec compliance
response = client.read_holding_registers(40000, count=2, device_id=1)
if response.isError():
raise Exception(f"Failed to read SunSpec identifier: {response}")
if response.registers != [0x5375, 0x6E53]: # "SunS"
raise Exception("Device is not SunSpec compliant")
# 2. Walk models sequentially
address = 40002 # Start after SunS identifier
models = {}
while True:
# Read Model ID and Length
response = client.read_holding_registers(address, count=2, device_id=1)
if response.isError():
print(f"Error reading at address {address}: {response}")
break
if len(response.registers) < 2:
print(f"Insufficient data at address {address}: got {len(response.registers)} registers")
break
model_id = response.registers[0]
model_length = response.registers[1]
if model_id == 0xFFFF: # End marker
break
# Record model location
models[model_id] = {
'start_address': address + 2, # Data starts after ID/Length
'length': model_length
}
print(f"Found Model {model_id} at address {address + 2}, length {model_length}")
# Skip to next model
address += 2 + model_length
print(f"Discovered models: {list(models.keys())}")
# Expected output: [1, 701, 704, 713, 714, 802]
# Print key addresses based on your actual system:
print("\nKey Register Addresses:")
print(f"Model 1 (Common): {models.get(1, {}).get('start_address', 'N/A')}") # Should be 40004
print(f"Model 701 (AC): {models.get(701, {}).get('start_address', 'N/A')}") # Should be 40072
print(f"Model 704 (Controls): {models.get(704, {}).get('start_address', 'N/A')}")# Should be 40279
print(f"Model 713 (Storage): {models.get(713, {}).get('start_address', 'N/A')}") # Should be 40346
print(f"Model 714 (DC): {models.get(714, {}).get('start_address', 'N/A')}") # Should be 40355
print(f"Model 802 (Battery): {models.get(802, {}).get('start_address', 'N/A')}") # Should be 40409
finally:
client.close()Multi-Battery Support
Our system is designed to handle multiple battery units, each with its own unique identifier:
Multi rack aggregation not yet implemented
- Battery 0: Unit ID 1 (for single battery applications)
- Battery N: Unit ID 100 + N (for multi battery applications)
For example, if you want to read data from different batteries:
# Read aggregated or single battery (Battery 0)
response = client.read_holding_registers(40006, count=1, device_id=1)
# Read multi battery (Battery 0)
response = client.read_holding_registers(40006, count=1, device_id=101)
# Read multi battery (Battery 1)
response = client.read_holding_registers(40006, count=1, device_id=102)Data Types and Scaling
SunSpec Data Types
- uint16: 16-bit unsigned integer
- int16: 16-bit signed integer
- uint32: 32-bit unsigned integer (2 registers, MSW first)
- int32: 32-bit signed integer (2 registers, MSW first)
- string: ASCII string (multiple registers, null-terminated)
- enum16: 16-bit enumerated value
- bitfield16: 16-bit bitfield
Scale Factors
Many numeric values use scale factors (SF) defined within each model: - Formula: actual_value = raw_value × 10^(scale_factor) - Example: If raw value = 1500 and W_SF = 2, then actual watts = 1500 × 10² = 150,000 W
SunSpec Models Reference
Model 1: Common Model (Identification)
Purpose: Device identification and basic information
Access: Read-only
| Point | Type | Description | Example | Units/Notes |
|---|---|---|---|---|
ID |
uint16 | Model ID | 1 | Always 1 for Common model |
L |
uint16 | Model length | 66 | Number of data registers |
Mn |
string32 | Manufacturer | “C-Battery” | 32-byte ASCII string |
Md |
string32 | Model | “” | 32-byte ASCII string |
Opt |
string16 | Options | “” | 16-byte ASCII string |
Vr |
string16 | Version | “” | 16-byte ASCII string |
SN |
string32 | Serial Number | “” | 32-byte ASCII string |
DA |
uint16 | Device Address | 1 | Modbus device address (1-247) |
Model 701: AC Inverter Model
Purpose: This is where you’ll find all the AC-side measurements and inverter status information
Access: Read-only (you can monitor but not control these values)
Length: Approximately 153 registers
| Point | Type | Description | Scale | Units | Example |
|---|---|---|---|---|---|
ID |
uint16 | Model ID | - | - | 701 |
L |
uint16 | Model length | - | - | ~50 |
InvSt |
enum16 | Inverter State | - | See inverter states | 3 |
Alrm |
bitfield16 | Active Alarms | - | Bitmask of active alarms | 0 |
W |
int16 | AC Power | W_SF=2 | Watts | 1500 (= 150,000W) |
PF |
int16 | Power Factor | PF_SF=-3 | 0.01% | 950 (= 0.95) |
Hz |
uint32 | AC Frequency | Hz_SF=-3 | Hz | 50010 (= 50.01Hz) |
AL1 |
uint16 | L1 Current | A_SF=-1 | Amps | 25 (= 2.5A) |
AL2 |
uint16 | L2 Current | A_SF=-1 | Amps | 25 (= 2.5A) |
AL3 |
uint16 | L3 Current | A_SF=-1 | Amps | 25 (= 2.5A) |
VL1L2 |
uint16 | L1-L2 Voltage | V_SF=-1 | Volts | 4000 (= 400V) |
VL2L3 |
uint16 | L2-L3 Voltage | V_SF=-1 | Volts | 4000 (= 400V) |
VL3L1 |
uint16 | L3-L1 Voltage | V_SF=-1 | Volts | 4000 (= 400V) |
MnAlrm |
bitfield32 | Manufacturer alarms | - | See mapping below | 0 |
Inverter State Enumerations: - 0 = Off, 1 = Sleeping, 2 = Starting, 3 = Running, 4 = Throttled, 5 = Shutting Down, 6 = Fault, 7 = Standby
Mnlarm Bitfield Mapping
The MnAlrm field (implemented by us as bitmask) in Model 701 represents active alarms/faults. In our implementation, this is a 32-bit aggregated fault mask (MnAlrmInfo), with bits 0-15 mapping directly to fault table 1 of the inverter. The full 32-bit mapping (including Fault Table 2 in higher bits) is provided below for completeness, as it may be relevant for extended monitoring or aggregation.
| Bit Position | Fault Description |
|---|---|
| 0 | DC Bus Overvoltage |
| 1 | DC Bus Undervoltage |
| 2 | DC Bus Overcurrent |
| 3 | Abnormal Battery Insulation |
| 4 | Battery Overvoltage |
| 5 | Battery Undervoltage |
| 6 | Battery Reverse Connection |
| 7 | Battery Overcurrent |
| 8 | On Grid Inverter Overcurrent |
| 9 | Grid Overvoltage |
| 10 | Grid Undervoltage |
| 11 | Grid Overfrequency |
| 12 | Grid Underfrequency |
| 13 | Island Protection |
| 14 | Independent Inverter Overvoltage |
| 15 | Independent Inverter Undervoltage |
| 16 | Module W Fault |
| 17 | Module V Fault |
| 18 | Module U Fault |
| 19 | Leakage Current Over Limit Protection |
| 20 | AC Current Imbalance |
| 21 | AC Voltage Imbalance |
| 22 | Independent Inverter Overcurrent |
| 23 | Environmental Over Temperature Protection |
| 24 | Abnormal DC Circuit Breaker |
| 25 | Abnormal DC Contactor |
| 26 | AC Contactor Abnormal |
| 27 | AC Circuit Breaker Abnormal |
| 28 | Abnormal Arrester |
| 29 | Module Over Temperature Protection |
| 30 | Reactor Over Temperature Protection |
| 31 | Emergency Stop |
Notes on Alarms: - Bit = 1: Fault/alarm is active. - Bit = 0: Normal/inactive.
Model 704: Basic Storage Controls Model
Purpose: Power control setpoints
Access: Read/Write (control points)
| Point | Type | Access | Description | Scale | Range | Units |
|---|---|---|---|---|---|---|
WSetEna |
enum16 | RW | Power Setpoint Enable | - | 0=Disabled, 1=Enabled | - |
WSetMod |
enum16 | RW | Power Setpoint Mode | - | 1 for constant power | - |
WSet |
int32 | RW | Power Setpoint | WSet_SF=2 | -20000 to 20000 | Watts |
Control Logic: 1. Set WSetEna = 1 to enable power control 2. Set WSetMod = 1 to select Watts set setpoint mode 3. Set WSet to desired power (-20000W to +20000W raw, scaled by WSet_SF=2) 4. Positive values = discharge/export, negative values = charge/import
Model 713: Storage Monitoring Model
Purpose: Battery state monitoring
Access: Read-only
| Point | Type | Description | Scale | Units | Example | Notes |
|---|---|---|---|---|---|---|
WHRtg |
uint16 | Energy Rating | WH_SF=2 | Wh | Total energy capacity | NOT IMPLEMENTED YET |
WHAvail |
uint16 | Available Energy | WH_SF=2 | Wh | Currently available energy | NOT IMPLEMENTED YET |
SoC |
uint16 | State of Charge | Pct_SF=-1 | % | 850 (= 85.0%) | - |
SoH |
uint16 | State of Health | Pct_SF=-1 | % | 920 (= 92.0%) | - |
Sta |
bitfield16 | Storage Status | - | Bitmask | Various status bits | - |
Model 714: Storage DC Measurements Model
Purpose: DC side measurements
Access: Read-only
| Point | Type | Description | Scale | Units | Example |
|---|---|---|---|---|---|
DCA |
int16 | DC Current | DCA_SF=-1 | Amps | 250 (= 25.0A) |
DCW |
int16 | DC Power | DCW_SF=2 | Watts | 120 (= 12,000W) |
Prt.1.DCV |
uint16 | Port 1 DC Voltage | DCV_SF=-1 | Volts | 7275 (= 727.5V) |
Model 802: Storage Capabilities Model
Purpose: Dynamic power and current limits
Access: Read-only
| Point | Type | Description | Scale | Units | Example |
|---|---|---|---|---|---|
WChaRteMax |
uint16 | Max Charge Power | WChaDisChaMax_SF=2 | Watts | 500 (= 50,000W) |
WDisChaRteMax |
uint16 | Max Discharge Power | WChaDisChaMax_SF=2 | Watts | 500 (= 50,000W) |
AChaMax |
uint16 | Max Charge Current | AMax_SF=-1 | Amps | 1000 (= 100.0A) |
ADisChaMax |
uint16 | Max Discharge Current | AMax_SF=-1 | Amps | 1000 (= 100.0A) |
Programming Examples
Reading Battery State of Charge
from pymodbus.client import ModbusTcpClient
def read_battery_soc():
"""Read battery State of Charge via SunSpec Model 713"""
client = ModbusTcpClient('172.20.0.123', port=502)
if not client.connect():
print("Connection failed")
return
try:
# Read SoC directly from known address (Model 713)
# Based on actual implementation: 40348 = SoC with value 850
response = client.read_holding_registers(40348, count=1, device_id=1)
if response.isError():
print(f"Error reading SoC: {response}")
return
soc_raw = response.registers[0]
# Apply scale factor: Pct_SF = -1, so actual = raw × 10^(-1) = raw × 0.1
soc_percent = soc_raw * 0.1
print(f"State of Charge: {soc_percent}%") # Example: 850 → 85.0%
finally:
client.close()
if __name__ == "__main__":
read_battery_soc()Setting Power Setpoint
from pymodbus.client import ModbusTcpClient
def set_power(power_watts=70):
"""Set power setpoint via SunSpec Model 704"""
client = ModbusTcpClient('172.20.0.123', port=502)
if not client.connect():
print("Connection failed")
return
try:
# Set device to `ON`
client.write_register(40299, value=1, device_id=1)
# Set to `set power mode`
client.write_register(40300, value=1, device_id=1)
# Set power (32-bit value across 2 registers)
high = (power_watts >> 16) & 0xFFFF
low = power_watts & 0xFFFF
client.write_registers(40301, values=[high, low], device_id=1)
print(f"Power set to {power_watts}W")
finally:
client.close()
if __name__ == "__main__":
set_power(70) # Set 7000W discharge (SCALE FACTOR)