SunSpec Modbus Documentation

C-Battery Energy Storage System Integration Guide

Author
Published

October 20, 2025

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)