Integration guide
How to control a 75 kWh rack from an EMS over SunSpec/Modbus TCP. The rack exposes standard SunSpec models so any SunSpec-aware client can read measurements and issue control commands.
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: WSetEna gates EMS control, WSet drives the power
Two registers in Model 704 do all the work:
WSetEna(40299) — the EMS control gate.1= rack followsWSet.0= rack ignoresWSetand enters Standby. There is no fully "off" state from the EMS — the rack always sits in either Running or Standby.WSet(40301–40302) — the power setpoint. WhileWSetEna = 1, the inverter state follows the value ofWSet: a non-zero target → Running, a0target → Standby.
| Concern | Model | Registers | Access |
|---|---|---|---|
| EMS control gate | 704 (Basic Storage Controls) | WSetEna | RW |
| Power setpoint | 704 (Basic Storage Controls) | WSet | RW |
| Status | 701 (AC Inverter) | InvSt | R |
| Limits | 802 (Battery Base) | WChaRteMax, WDisChaRteMax, AChaMax, ADisChaMax | R |
| Fault reset | 802 (Battery Base) | 40427 | RW |
Control flow
The rack only ever sits in one of two states — Running or Standby — and the EMS reaches them by writing WSetEna and WSet:
The rack now actively manages a low-self-consumption Standby state (InvSt = 7). Standby is entered whenever the active setpoint is zero — either because WSet = 0 or because WSetEna = 0 (in which case the rack ignores WSet entirely). Writing a non-zero WSet with WSetEna = 1 returns it to Running automatically — no separate state command is required from the EMS.
In v1 the rack stayed in Running while WSetEna = 1 regardless of the setpoint value, which kept self-consumption high during idle periods. If your EMS already runs against v1, no code change is required to benefit from this — it simply consumes less when you leave WSet at 0.
Running and Standby are the two steady states the EMS interacts with. The other InvSt values are either transient or off-path:
Under normal conditions every state below transitions to Running automatically once WSetEna = 1 and WSet ≠ 0 are in place; the EMS does not have to drive the transitions itself.
InvSt | State | Meaning |
|---|---|---|
0 | Off | Rack is not initialized or has been powered off. Once WSetEna = 1 and WSet ≠ 0 are written, the rack moves toward Running automatically. |
1 | Sleeping | Idle between initialization steps. Resumes toward Running on its own with WSetEna = 1 and WSet ≠ 0 in place. |
2 | Starting | Boot sequence and self-checks in progress; the rack settles into Running automatically once the checks pass with WSetEna = 1 and WSet ≠ 0. |
4 | Throttled | Not used under normal conditions on this rack. Would resume Running automatically with WSetEna = 1 and WSet ≠ 0. |
5 | Shutting Down | Not used. There is no shutdown command exposed to the EMS; Standby covers low-power operation while keeping response times fast. If reached, the rack still resumes Running on its own with WSetEna = 1 and WSet ≠ 0. |
6 | Fault | Operation blocked by an active fault. Clear with AlmRst (see Resetting rack faults); the rack then returns to Running automatically with WSetEna = 1 and WSet ≠ 0. |
Step 1 — Enable EMS control (WSetEna = 1)
| Register | Address | Value | Meaning |
|---|---|---|---|
WSetEna | 40299 | 1 | Rack follows WSet. Set once after commissioning; the rack remembers the setting. |
WSetEna | 40299 | 0 | Rack ignores WSet and enters Standby. Use to hand control back to the rack (e.g. for maintenance). |
WSetMod (40300) is fixed at 1 (constant-power mode, the only supported mode) and does not need to be written by the EMS.
Step 2 — Drive WSet
| Register | Address | Type | Meaning |
|---|---|---|---|
WSet | 40301–40302 | int32 | Power setpoint in watts, scaled by WSet_SF = 2. |
While WSetEna = 1, the rack reacts to the current value of WSet:
WSet | Resulting state | InvSt |
|---|---|---|
0 | Standby (low self-consumption) | 7 |
≠ 0 (e.g. 5 000) | Running, following the setpoint | 3 |
Sign convention: positive = discharge / export, negative = charge / import.
Step 3 — Verify state (Model 701, optional)
Poll InvSt at 40074 if you need a confirmation; you should see 3 (Running) shortly after writing a non-zero WSet, and 7 (Standby) shortly after writing WSet = 0 (or after disabling with WSetEna = 0). All transitions happen automatically — there is no extra command to send.
Discovery & register layout
SunSpec devices expose their registers as a sequence of models, each identified by a numeric ID. The 75 kWh rack lays them out as:
Address Content Purpose
───────── ────────────── ─────────────────────────────
40000 – 40001 "SunS" SunSpec identifier (0x5375, 0x6E53)
40002 – 40003 Model 1 ID + Length Common (device identification)
40004 – 40069 Model 1 Data Manufacturer, model, serial, …
40070 – … Model 701 AC inverter measurements
… Model 704 Power setpoint controls
… Model 713 Storage monitoring (SoC, SoH)
… Model 714 DC-side measurements
… Model 802 Battery base
end 0xFFFF End marker
To find a register you don't read a fixed address — you discover the layout by walking model IDs from 40002:
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient('192.0.2.100', port=502)
client.connect()
# 1. Verify it's a SunSpec device
ident = client.read_holding_registers(40000, count=2, device_id=1).registers
assert ident == [0x5375, 0x6E53], "Not a SunSpec device"
# 2. Walk the model list
address = 40002
models = {}
while True:
header = client.read_holding_registers(address, count=2, device_id=1).registers
model_id, length = header
if model_id == 0xFFFF: # End marker
break
models[model_id] = {
'data_address': address + 2,
'length': length,
}
address += 2 + length
print(models)
# {1: ..., 701: ..., 704: ..., 713: ..., 714: ..., 802: ...}
Multi-battery addressing
Each battery rack is reachable via its Modbus Unit ID:
| Unit ID | Use |
|---|---|
1 | Single battery / aggregated view |
101, 102, … | Rack 1, Rack 2, … in multi-rack setups |
Multi-rack aggregation through Unit ID 1 is not yet available. For multi-rack systems, address each rack individually.
Data types and scaling
SunSpec uses fixed-width integer types. Real-world values are reconstructed by applying a scale factor (SF) stored alongside the data:
actual = raw × 10^SF
Examples:
| Quantity | Raw | Scale factor | Actual value |
|---|---|---|---|
| AC power | 1500 | W_SF = 2 | 150 000 W |
| Frequency | 50010 | Hz_SF = -3 | 50.010 Hz |
| State of charge | 850 | Pct_SF = -1 | 85.0 % |
Common types: uint16, int16, uint32 / int32 (2 registers, MSW first), string (multiple registers, null-terminated), enum16, bitfield16, bitfield32.
Models reference
Model 1 — Common (device identification)
Read-only metadata about the device: manufacturer, model, serial number, Modbus device address. See the register reference for exact fields.
Model 701 — AC Inverter
Read-only AC-side measurements. Key fields:
| Point | Type | Description |
|---|---|---|
InvSt | enum16 | Inverter state (see below) |
W | int16 | AC power (W, scaled by W_SF) |
Hz | uint32 | Frequency (Hz, scaled by Hz_SF) |
AL1 / AL2 / AL3 | uint16 | Phase currents L1/L2/L3 |
VL1L2 / VL2L3 / VL3L1 | uint16 | Line-to-line voltages |
Inverter state (InvSt) values:
| Value | State |
|---|---|
| 0 | Off |
| 1 | Sleeping |
| 2 | Starting |
| 3 | Running |
| 4 | Throttled |
| 5 | Shutting Down |
| 6 | Fault |
| 7 | Standby |
On this rack the state follows the value of WSet: write 0 and the rack enters Standby (7); write any non-zero value and it returns to Running (3). See Control flow.
Model 704 — Basic Storage Controls
Read/write. The primary control surface on this system — see Control flow for the sequence.
| Point | Access | Description |
|---|---|---|
WSetEna | RW | EMS control gate. 1 = rack follows WSet. 0 = rack ignores WSet and enters Standby. |
WSetMod | RW | Fixed at 1 (constant power) — no EMS write required. |
WSet | RW | Power setpoint in watts (int32, scaled by WSet_SF = 2). Positive = discharge, negative = charge. While WSetEna = 1: WSet = 0 → Standby, non-zero → Running. |
Model 713 — Storage Monitoring
Read-only battery state.
| Point | Description | Notes |
|---|---|---|
WHRtg | Energy rating (Wh) | Scaled by WH_SF |
WHAvail | Available energy (Wh) | Scaled by WH_SF |
SoC | State of charge (%) | Scaled by Pct_SF |
SoH | State of health (%) | Scaled by Pct_SF |
Model 714 — DC Measurements
Read-only DC-side measurements.
| Point | Description |
|---|---|
DCA | DC current (A, scaled by DCA_SF) |
DCW | DC power (W, scaled by DCW_SF) |
Model 802 — Battery Base
Read-only dynamic limits — these tell you how hard you can push the battery right now.
The V point (total battery DC voltage) at register 40441 is new in v2.0.
| Point | Description |
|---|---|
V | Total battery DC voltage (V). New in v2.0. |
WChaRteMax | Max charge power (W) |
WDisChaRteMax | Max discharge power (W) |
AChaMax | Max charge current (A) |
ADisChaMax | Max discharge current (A) |
AlmRst | Rack fault reset. Write 1 to clear active rack faults (40427). See Resetting rack faults. |
Resetting rack faults
When a rack reports a fault (InvSt = 6), the EMS can clear it over Modbus once the underlying condition has been resolved.
Register 40427 is new in v2.0. Previously, clearing a rack fault required a power cycle or on-site intervention — an EMS could not recover the rack remotely. With v2.0 the EMS can issue a reset with a single Modbus write and confirm completion by polling the same register.
| Register | Type | Access | Description |
|---|---|---|---|
40427 | uint16 | RW | Write 1 to clear active rack faults. The register self-clears back to 0 once the reset has been applied. |
Issue the write per rack — address each Unit ID individually (e.g. 101, 102, …); there is no aggregated reset on Unit ID 1. After writing, poll 40427 until it reads back 0, then confirm InvSt returns from 6 to 3 (Running) or 7 (Standby). No power cycle is required.
AlmRstis not intended to be written continuously or on a timer. One write per reset suffices; the rack acknowledges by self-clearing the register back to0.- You can identify the fault origin by checking your AC grid history for recent disturbances, or by correlating against a known planned manual intervention. If either lines up with the fault, the EMS can issue the reset directly once the underlying condition has cleared.
- For any other fault, contact us before resetting. That way we can assess the cause together so the underlying issue is not masked by a repeated clear.
- If the fault has not cleared after 3 reset attempts, stop and contact us. Beyond that point a successful reset is unlikely and additional writes only delay diagnosis.
Examples
Read state of charge
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient('192.0.2.100', port=502)
client.connect()
# Model 713 SoC is at 40348 once discovery has been done
raw = client.read_holding_registers(40348, count=1, device_id=1).registers[0]
soc = raw * 0.1 # Pct_SF = -1
print(f"SoC: {soc:.1f}%")
Set a power target
from pymodbus.client import ModbusTcpClient
POWER_WATTS = 7000 # discharge at 7 kW
client = ModbusTcpClient('192.0.2.100', port=502)
client.connect()
# Model 704 controls — WSetMod is fixed at 1 (constant power), no write needed
client.write_register(40299, value=1, device_id=1) # WSetEna = enabled
raw = POWER_WATTS // 100 # WSet_SF = 2 → raw = watts / 100
high = (raw >> 16) & 0xFFFF
low = raw & 0xFFFF
client.write_registers(40301, values=[high, low], device_id=1)
For the full register listing, see the register reference.