Overview

Complete Modbus register map for Wattsonic MATIC hybrid inverter series. Includes both implemented registers and proposed additions for development team review.

315
Total Registers
228
Implemented
87
Proposed (New)
49
P0 Must-Have
33
P1 Important
5
P2 Planned
Implemented (Current)
P0 Must-Have
P1 Important
P2 Planned

Communication Parameters

Protocol
Modbus RTU / TCP
Baud Rate
9600, 8N1
Slave Address
1 (configurable 1-255)
Byte Order
Big-Endian (MSB first)
Word Order
High-word first (32-bit)
Poll Interval
1-5s realtime, 30-60s energy
Applicable Models
MATIC 10 / 12 / 15 / 20 / 25 kW (50A)

Supported Modbus Functions

0x03 Read Holding
Read monitoring data and settings
0x06 Write Single
Write control commands and settings
0x10 Write Multiple
Write multi-register values (32-bit, strings)

Register Address Map

10000 - 10799
Real-Time Monitoring RO — AC / PV / Battery / Grid / Meter / Energy
10800 - 10812
Load Power & System Key Performance Indicators RO P1 — Load power, self-consumption & autarky rates
11000 - 11249
System Settings RW — Mode / SOC / TOU / Export Limit / Power
11250 - 11254
TOU Enhancement RW P0/P1 — Charge/discharge limits, target SOC, day-of-week mask
11300 - 11319
Reactive Power Control RW P0 — PF / Q / Cosφ(P) / Q(U) curves
11320 - 11324
Power Limit % & Ramp Rate RW P0 — EN 50549 % curtailment & WGra ramp rate
11330 - 11336
Frequency-Watt P(f) RW P0 — LFSM-O/U droop response (EN 50549 mandatory)
11340 - 11355
Grid Service / Demand / Smart Home RW P0/P1/P2 — VPP, demand limit, SG-Ready
12000 - 12006
Control Commands W — On/Off / Fault Clear / Remote / Restart
12007 - 12013
Safety Unlock, Emergency Stop & Watchdog W P0 — Safety unlock (0x55AA), emergency stop, comms watchdog
13000 - 13042
Device Information & Diagnostics RO — SN / Model / Firmware / Protocol Version / Modbus diagnostics
14000 - 14030
System Status RO — Working Mode / State / On-Off Status
14031 - 14032
Watchdog Status RO P0 — Comms watchdog state
14033 - 14035
Power Limit Feedback RO P0 — Applied limit readback & source bitmap
14036 - 14046
Derating, Audit & EMS Readback RO P0/P1/P2 — Derating, timestamp, RSSI, EMS active status, mode change reason
14100 - 14199
Alarms & Faults ALARM — Grid / Inverter / Battery / System

Sign Convention

ParameterPositive (+)Negative (-)
Grid PowerImport from gridExport to grid
Battery PowerDischargeCharge
Active Power SetpointDischargeCharge from grid
Reactive PowerLeading (capacitive)Lagging (inductive)

EMS Integration Notes

  1. Enable EMS mode by writing 1 to register 11060 (EMS_Switch_Set) before sending control commands.
  2. Set Remote_Mode (12002) = 1 to enable remote control.
  3. All power values use scale factor as specified. E.g., register value 2500 with scale factor 0.01 kW = 25.00 kW.
  4. SOC values use scale factor 0.01%. E.g., register value 8000 = 80.00%.
  5. TOU time values are in minutes from midnight (0-1440). E.g., 480 = 08:00, 1080 = 18:00.
  6. Meter Current Scale: Meter current registers (10758-10760) use scale 0.1 A while inverter/grid currents use 0.01 A. Handle both scale factors in your parser.
  7. [Proposed] Read Protocol_Version (13036) at connection time to determine available registers for firmware-safe integration.
  8. [Proposed] Enable communications watchdog 12010 and send heartbeat to 12012 periodically for failsafe operation.

Reference Standards

  • SunSpec Alliance — IEEE 1547 / SunSpec Models 1, 103, 120, 124, 160
  • IEC 61850 — MMXU, MMDC, DBAT, DRCS, DRCT logical nodes
  • EN 50549-1/2 — European grid code for DER controllability
  • VDE-AR-N 4105:2018 — German LV grid code (Cosφ(P), P(f) droop, ramp rates)
  • SG-Ready — German heat pump coordination standard (BWP / EHPA)

Data Types

Register data formats used across the Modbus register map. All multi-byte values use Big-Endian byte order.

TypeLengthDescriptionExampleBytes
VALUE_U162 bytesUnsigned 16-bit integer, Big-Endian65244FE DC
VALUE_S162 bytesSigned 16-bit integer, Big-Endian-292FE DC
VALUE_U32_BE4 bytesUnsigned 32-bit, Big-Endian (2 registers, high word first)4275878552FE DC BA 98
VALUE_S32_BE4 bytesSigned 32-bit, Big-Endian (2 registers, high word first)-19088744FE DC BA 98
ASCII_STRING3232 bytesASCII string, 32 bytes (16 registers)ABC123...41 42 43 ...
BIT_BOOLEAN1 bitBoolean flag within a 16-bit register (bit 0-15)bit0=100 01

Modbus Protocol Compliance

SPEC
ParameterValueNotes
Supported Functions0x03, 0x06, 0x100x04 (Read Input) is NOT supported. All registers use 0x03 (Read Holding).
Max Registers per Read125250 bytes max payload per Modbus spec. Larger reads return Exception 02.
Inter-Frame Silence (RTU)3.5 chars (~4ms @9600)Minimum gap between frames. Frames arriving faster may be discarded.
Min Poll Interval200 msMinimum time between consecutive requests. Faster polling may overrun the serial buffer.
Write ValidationFirmware-enforcedOut-of-range values return Exception 03 (Illegal Data Value). Values are clamped to min/max documented per register.

Modbus Exception Codes

ERROR
CodeNameTrigger Conditions
01Illegal FunctionFunction code 0x06/0x10 used on read-only register (10xxx, 13xxx, 14xxx). Function code 0x04 sent (not supported).
02Illegal Data AddressRegister address does not exist or falls in a reserved/gap range. Read request spans beyond valid address block.
03Illegal Data ValueWritten value exceeds documented min/max range. Destructive command sent without Safety_Unlock (12008). Watchdog timeout < 10s.
04Server Device FailureInternal firmware error during register access. Retry after 1 second.
06Server Device BusyFirmware update in progress. All writes rejected. Retry after update completes.

Recommended Polling Groups

OPTIMIZATION
GroupAddress RangeRegistersIntervalPurpose
1. AC & Grid10000 - 10064651-2sInverter output, grid, backup power (single read)
2. PV / DC10087 - 10105192-5sPV strings + temperatures
3. Battery Port 110671 - 10697272-5sSOC, voltage, current, energy, alarms
4. Battery Port 210725 - 10751272-5sBattery Port 2 (if installed)
5. Meter10752 - 10783321-2sExternal meter data
6. Energy10785 - 107991530-60sEnergy counters (daily + lifetime)
6a. Load & KPIs10800 - 10812131-2sLoad power + system KPIs
7. Status14000 - 14046472-5sOperating state, alarms, feedback
8. Settings RB11000 - 11006760sRTC, verify settings (on-change)

Data Type Audit — v2.0 Migration Notes

ACTION REQUIRED
  1. Voltage registers S16 → U16: RMS voltage is always ≥ 0. Registers 10001-10003 (Inverter), 10023-10025 (Grid), 10044-10046 (Backup), 10752-10754 (Meter), PV1-5 Voltage should be U16. Current S16 type wastes half the range and creates parser ambiguity. SunSpec Model 103 uses U16 for voltage.
  2. Frequency registers S16 → U16: Grid frequency is always positive (49-51 Hz EU). Registers 10000, 10022, 10778 should be U16. Scale 0.01 Hz is correct for VDE-AR-N 4105 resolution requirements.
  3. PV current/power S16 → U16: PV strings produce DC power in one direction only. Registers 10089-10102 (PV1-5 current/power), 10087 (PV_Total_Power) should be U16. SunSpec Model 160 uses U16 for DC current.
  4. Power Factor scale inconsistency: Meter_Power_Factor_Total (10777) uses scale 0.01, but Inverter_Power_Factor (10009) uses 0.001. Standardize all PF registers to 0.001 for VDE-AR-N 4105 compliance verification.
  5. Boolean switches S16 → U16: All enable/disable registers (11060, 11106, 11107, 11057-11058, 11137) use S16 for 0/1 values. Should be U16. Document behavior when writing values ≠ 0 or 1.
  6. SOC settings S16 → U16: SOC is always 0-100%. Settings 11108-11111, 11162-11163 use S16 but the readback (10671, 10725) uses U16. Standardize to U16.
  7. Address gap documentation: Registers 10007-10008, 10011-10012, 10014-10015, 10017-10018, 10029-10031, 10033-10034, 10036-10037, 10039-10040, 10050-10061 are reserved/undefined. Block reads across these addresses return undefined values. EMS parsers must skip gap addresses.
  8. U32 register addressing: All U32_BE registers occupy 2 consecutive addresses (high word first). E.g., 10677 = BMS_Bat1_Charge_Energy occupies 10677 (high) + 10678 (low). The second address is implicitly consumed.

Monitoring Registers

Real-time monitoring data. All registers are read-only (function code 0x03). Poll at 1-5 second intervals for real-time data.

Implemented (98 registers)
Proposed P1 (15 registers)
Proposed P2 (2 registers)
Show:

Monitoring Registers

READ-ONLY
StatusRegisterNameDescriptionTypeUnitScaleNotes
1. AC Inverter Output
Done10000Inverter_FrequencyInverter Output FrequencyS16Hz0.01
Done10001Inverter_Voltage_APhase A Inverter Voltage (RMS)S16V0.1
Done10002Inverter_Voltage_BPhase B Inverter Voltage (RMS)S16V0.1
Done10003Inverter_Voltage_CPhase C Inverter Voltage (RMS)S16V0.1
Done10004Inverter_Current_APhase A Inverter Current (RMS)S16A0.01
Done10005Inverter_Current_BPhase B Inverter Current (RMS)S16A0.01
Done10006Inverter_Current_CPhase C Inverter Current (RMS)S16A0.01
Done10010Inverter_Active_Power_AInverter Phase A Active PowerS16kW0.01
Done10013Inverter_Active_Power_BInverter Phase B Active PowerS16kW0.01
Done10016Inverter_Active_Power_CInverter Phase C Active PowerS16kW0.01
Done10019Inverter_Active_Power_TotalTotal Active PowerS16kW0.01
Done10020Inverter_Reactive_Power_TotalTotal Reactive PowerS16kVar0.01
Done10021Inverter_Apparent_Power_TotalTotal Apparent PowerS16kVA0.01
P110009Inverter_Power_FactorInverter Displacement Power FactorS160.001-1.000 to +1.000. Verify reactive power control is working.Ref: SunSpec 103 PF, SMA 30821
2. AC Grid Side
Done10022Grid_FrequencyGrid Frequency at PCCS16Hz0.01
Done10023Grid_Voltage_APhase A Grid Voltage (RMS)S16V0.1
Done10024Grid_Voltage_BPhase B Grid Voltage (RMS)S16V0.1
Done10025Grid_Voltage_CPhase C Grid Voltage (RMS)S16V0.1
Done10026Grid_Current_APhase A Grid Current (RMS)S16A0.01
Done10027Grid_Current_BPhase B Grid Current (RMS)S16A0.01
Done10028Grid_Current_CPhase C Grid Current (RMS)S16A0.01
Done10032Grid_Active_Power_APhase A Grid Active PowerS16kW0.01+import / -export
Done10035Grid_Active_Power_BPhase B Grid Active PowerS16kW0.01+import / -export
Done10038Grid_Active_Power_CPhase C Grid Active PowerS16kW0.01+import / -export
Done10041Grid_Active_Power_TotalTotal Grid Active PowerS16kW0.01+import / -export
Done10042Grid_Reactive_Power_TotalTotal Grid Reactive PowerS16kVar0.01
Done10043Grid_Apparent_Power_TotalTotal Grid Apparent PowerS16kVA0.01
3. Backup Load Output
Done10044Backup_Voltage_ABackup Output Voltage Phase A (RMS)S16V0.1
Done10045Backup_Voltage_BBackup Output Voltage Phase B (RMS)S16V0.1
Done10046Backup_Voltage_CBackup Output Voltage Phase C (RMS)S16V0.1
Done10047Backup_Current_ABackup Output Current Phase A (RMS)S16A0.01
Done10048Backup_Current_BBackup Output Current Phase B (RMS)S16A0.01
Done10049Backup_Current_CBackup Output Current Phase C (RMS)S16A0.01
Done10062Backup_Active_Power_TotalTotal Backup Active PowerS16kW0.01
Done10063Backup_Reactive_Power_TotalTotal Backup Reactive PowerS16kVar0.01
Done10064Backup_Apparent_Power_TotalTotal Backup Apparent PowerS16kVA0.01
4. PV / DC Input
Done10087PV_Total_PowerTotal PV DC Input PowerS16kW0.01
Done10088PV1_VoltageMPPT Tracker 1 DC Input VoltageS16V0.1
Done10089PV1_CurrentMPPT Tracker 1 DC Input CurrentS16A0.01
Done10090PV1_PowerMPPT Tracker 1 DC Input PowerS16kW0.01
Done10091PV2_VoltageMPPT Tracker 2 DC Input VoltageS16V0.1
Done10092PV2_CurrentMPPT Tracker 2 DC Input CurrentS16A0.01
Done10093PV2_PowerMPPT Tracker 2 DC Input PowerS16kW0.01
Done10094PV3_VoltageMPPT Tracker 3 DC Input VoltageS16V0.1
Done10095PV3_CurrentMPPT Tracker 3 DC Input CurrentS16A0.01
Done10096PV3_PowerMPPT Tracker 3 DC Input PowerS16kW0.01
Done10097PV4_VoltageMPPT Tracker 4 DC Input VoltageS16V0.1
Done10098PV4_CurrentMPPT Tracker 4 DC Input CurrentS16A0.01
Done10099PV4_PowerMPPT Tracker 4 DC Input PowerS16kW0.01
Done10100PV5_VoltageMPPT Tracker 5 DC Input VoltageS16V0.1Not available on all models
Done10101PV5_CurrentMPPT Tracker 5 DC Input CurrentS16A0.01Not available on all models
Done10102PV5_PowerMPPT Tracker 5 DC Input PowerS16kW0.01Not available on all models
5. Battery (Pack Level)
Done10671BMS_Bat1_SOCBattery Port 1 State of ChargeU16%0.010-10000 (0-100.00%)
Done10672BMS_Bat1_SOHBattery Port 1 State of HealthU16%0.010-10000 (0-100.00%)
Done10674BMS_Bat1_VoltageBattery Port 1 DC VoltageU16V0.1
Done10675BMS_Bat1_CurrentBattery Port 1 DC CurrentS16A0.1+discharge / -charge
Done10676BMS_Bat1_TemperatureBattery Port 1 TemperatureS16°C0.1
Done10677BMS_Bat1_Charge_EnergyBattery Port 1 Total Charge EnergyU32_BEkWh0.12 registers
Done10679BMS_Bat1_Discharge_EnergyBattery Port 1 Total Discharge EnergyU32_BEkWh0.12 registers
Done10681BMS_Bat1_StatusBattery Port 1 Status WordU32_BE12 registers
Done10683BMS_Bat1_Cycle_CountBattery Port 1 Cycle CountU32_BE12 registers
P110685BMS_Bat1_PowerBattery Port 1 Active PowerS16kW0.01+discharge / -charge. Direct readout avoids V×I calculation errors.Ref: SMA 30775, Fronius StorCtl_W
P110686BMS_Bat1_Max_Charge_PowerBMS Allowed Max Charge PowerU16kW0.01RO. BMS-reported maximum allowable charge power (may differ from EMS setting due to temp/SOC).Ref: Solinteg 41020, Sigenergy BMS limits
P110687BMS_Bat1_Max_Discharge_PowerBMS Allowed Max Discharge PowerU16kW0.01RO. BMS-reported maximum allowable discharge power (may be lower at low temp/SOC).
P210688BMS_Bat1_Rated_CapacityBattery Port 1 Rated CapacityU16kWh0.01RO. Rated total capacity. For VPP dispatch planning.
P210689BMS_Bat1_Available_EnergyBattery Port 1 Available EnergyU16kWh0.01RO. Usable energy remaining (SOC x usable capacity).
Done10693BMS_Bat1_AlarmBattery Port 1 Alarm CodeU32_BE12 registers
Done10695BMS_Bat1_WarningBattery Port 1 Warning CodeU32_BE12 registers
Done10697BMS_Bat1_Force_ChargeBattery Port 1 Force Charge StatusU161
Done10725BMS_Bat2_SOCBattery Port 2 State of ChargeU16%0.010-10000 (0-100.00%)
Done10726BMS_Bat2_SOHBattery Port 2 State of HealthU16%0.010-10000 (0-100.00%)
Done10728BMS_Bat2_VoltageBattery Port 2 DC VoltageU16V0.1
Done10729BMS_Bat2_CurrentBattery Port 2 DC CurrentS16A0.1+discharge / -charge
Done10730BMS_Bat2_TemperatureBattery Port 2 TemperatureS16°C0.1
Done10731BMS_Bat2_Charge_EnergyBattery Port 2 Total Charge EnergyU32_BEkWh0.12 registers
Done10733BMS_Bat2_Discharge_EnergyBattery Port 2 Total Discharge EnergyU32_BEkWh0.12 registers
P110739BMS_Bat2_PowerBattery Port 2 Active PowerS16kW0.01+discharge / -charge. Direct readout.
P110740BMS_Bat2_Max_Charge_PowerBMS2 Allowed Max Charge PowerU16kW0.01RO. BMS-reported maximum allowable charge power.
P110741BMS_Bat2_Max_Discharge_PowerBMS2 Allowed Max Discharge PowerU16kW0.01RO. BMS-reported maximum allowable discharge power.
Done10747BMS_Bat2_AlarmBattery Port 2 Alarm CodeU32_BE12 registers
Done10749BMS_Bat2_WarningBattery Port 2 Warning CodeU32_BE12 registers
6. External Meter
Done10752Meter_Voltage_AMeter Voltage Phase A (RMS)S16V0.1
Done10753Meter_Voltage_BMeter Voltage Phase B (RMS)S16V0.1
Done10754Meter_Voltage_CMeter Voltage Phase C (RMS)S16V0.1
Done10758Meter_Current_AMeter Current Phase A (RMS)S16A0.1
Done10759Meter_Current_BMeter Current Phase B (RMS)S16A0.1
Done10760Meter_Current_CMeter Current Phase C (RMS)S16A0.1
Done10762Meter_Active_Power_AMeter Phase A Active PowerS16kW0.01+import / -export
Done10763Meter_Active_Power_BMeter Phase B Active PowerS16kW0.01+import / -export
Done10764Meter_Active_Power_CMeter Phase C Active PowerS16kW0.01+import / -export
Done10765Meter_Active_Power_TotalMeter Total Active PowerS16kW0.01+import / -export
Done10769Meter_Reactive_Power_TotalMeter Total Reactive PowerS16kVar0.01
Done10777Meter_Power_Factor_TotalMeter Total Power FactorS160.01-1.00 to 1.00
Done10778Meter_FrequencyGrid Frequency at PCCS16Hz0.01
Done10781Meter_Energy_ImportTotal Import EnergyU32_BEkWh0.12 registers
Done10783Meter_Energy_ExportTotal Export EnergyU32_BEkWh0.12 registers
7. Energy Counters
Done10785PV_To_Grid_Energy_TodayToday PV to Grid EnergyU16kWh0.1
Done10786PV_To_Load_Energy_TodayToday PV to Load EnergyS16kWh0.1Note: S16 limits range to 3276.7 kWh/day. Future: migrate to U16.
Done10787Battery_To_Grid_TodayToday Battery to Grid EnergyS16kWh0.1Note: S16 limits range to 3276.7 kWh/day. Future: migrate to U16.
Done10788Battery_To_Load_Energy_TodayToday Battery to Load EnergyU16kWh0.1
Done10789PV_To_Grid_Energy_TotalTotal PV to Grid EnergyU32_BEkWh0.12 registers
Done10791PV_To_Load_Energy_TotalTotal PV to Load EnergyU32_BEkWh0.12 registers
Done10793Battery_To_Grid_TotalTotal Battery to Grid EnergyU32_BEkWh0.12 registers
Done10795Battery_To_Load_Energy_TotalTotal Battery to Load EnergyU32_BEkWh0.12 registers
P110797PV_Energy_TodayTotal PV Production TodayU16kWh0.1Sum of all MPPT tracker production regardless of destination.Ref: SMA 30535 (Day yield), Fronius DAY_ENERGY
P110798–10799PV_Energy_TotalLifetime PV ProductionU32_BEkWh0.12 registers. Cumulative total PV energy.Ref: SMA 30529 (Total yield), SolarEdge E_Total
8. Inverter Temperature
Done10103Ambient_Temp_ExternalExternal Ambient TemperatureS16°C0.1
Done10104Ambient_Temp_InternalInternal Ambient TemperatureS16°C0.1
Done10105Radiator_TempHeatsink TemperatureS16°C0.1
9. Load Power Monitoring P1 NEW
P110800–10801Load_Power_TotalTotal Load Active PowerS32_BEkW0.012 registers. EMS needs load data for optimization.Ref: Solinteg 40020, SMA 30775
P110802Load_Power_APhase A Load PowerS16kW0.01Per-phase load for unbalanced optimization
P110803Load_Power_BPhase B Load PowerS16kW0.01
P110804Load_Power_CPhase C Load PowerS16kW0.01
10. System Key Performance Indicators P1 IMPORTANT
P110810Self_Consumption_RateSelf-Consumption RateU16%0.10-100.0%. (PV self-consumed / PV total) × 100. Core dashboard KPI for residential EMS.Ref: SMA Home Manager, Fronius Solar.web, SolarEdge monitoring
P110812Autarky_RateSelf-Sufficiency (Autarky) RateU16%0.10-100.0%. (Self-supplied energy / Total consumption) × 100. Core KPI for energy independence.Ref: sonnen Autarkie, E3DC Autarkiegrad, SMA Self-Sufficiency

Settings Registers

Configurable settings. Use function code 0x06 (single register) or 0x10 (multiple registers). Changes take effect immediately unless noted.

Implemented (60 registers)
P0 Must-Have (34 registers)
P1 Important (11 registers)
P2 Planned (2 registers)
Show:

Settings Registers

READ/WRITE
StatusRegisterNameDescriptionTypeUnitScaleDefaultMinMaxValues / Notes
1. System Time
Done11000RTC_Year_SetRTC YearS161020002099
Done11001RTC_Month_SetRTC MonthS1610112
Done11002RTC_Day_SetRTC DayS1610131
Done11003RTC_Hour_SetRTC HourS1610023
Done11004RTC_Minute_SetRTC MinuteS1610059
Done11005RTC_Second_SetRTC SecondS1610059
Done11006Time_Zone_SetTime ZoneS16111650Lookup table: 1=UTC-12, ..., 13=UTC+0 (GMT), 14=UTC+1 (CET), 15=UTC+2 (EET), ..., 25=UTC+12. Half-hour zones: 530=UTC+5:30 (IST), 545=UTC+5:45 (NPT).
CRITICAL: Correct TZ required for TOU scheduling
2. EMS Control Enable
Done11060EMS_Switch_SetExternal EMS Dispatch EnableS1610010: Disabled  1: Enabled
MUST enable before EMS commands take effect
3. Active Power Control
Done11045Active_Power_SetActive Power Setpoint (Total)S16kW0.012500-60006000Positive = discharge, Negative = charge from grid. Model-dependent: single-phase models apply full value; three-phase models distribute equally across phases unless Per_Phase_Power_Enable=1.
P111046Per_Phase_Power_EnableEnable Per-Phase Power ControlS1610010: Total only  1: Per-phaseRef: Sigenergy, Solinteg 50202-50206
P111047Active_Power_Set_APhase A Power SetpointS16kW0.010-60006000Requires Per_Phase_Power_Enable=1
P111048Active_Power_Set_BPhase B Power SetpointS16kW0.010-60006000
P111049Active_Power_Set_CPhase C Power SetpointS16kW0.010-60006000
4. Working Mode
Done11151Run_Mode_SetOperating ModeS16100200: General (Self-Consumption)
1: Grid Balancing
2: Economic (Peak Shaving)
3: UPS / Backup Priority
4: TOU (Time-of-Use)
5: Forecast
6: Off-Grid
P211155Safety_Code_SetSafety Regulation CodeU1610099Country-specific grid code selectorRef: SMA 40163, SolarEdge F142
5. SOC Limits
Done11108Grid_tied_SOC_MINGrid-tied Minimum SOCS16%0.011000010000Do not discharge below this SOC
Done11109Grid_tied_SOC_MAXGrid-tied Maximum SOCS16%0.0110000010000Do not charge above this SOC
Done11110Off_grid_SOC_MINOff-grid Minimum SOCS16%0.011000010000Backup reserve SOC
Done11111Off_grid_SOC_MAXOff-grid Maximum SOCS16%0.0110000010000
Done11162Gen_Mode_Min_SOC_SetGeneral Mode Min SOCS16%0.011000010000
Done11163Gen_Mode_Max_SOC_SetGeneral Mode Max SOCS16%0.018000010000
6. Grid Export / Import Limits
Done11106Import_Limits_SwitchImport Power Limit EnableS1610010: Disabled  1: Enabled
Done11107Export_Limits_SwitchExport Power Limit EnableS1610010: Disabled  1: Enabled
Done11112Import_LimitsMax Import PowerS16kW0.01250005000
Done11113Export_LimitsMax Export PowerS16kW0.01250002500Set to 0 for zero-export
Done11164Gen_Mode_Max_Buy_PowerGeneral Mode Max ImportS16kW0.01250005000
Done11165Gen_Mode_Max_Sell_PowerGeneral Mode Max ExportS16kW0.01250005000
P111116Export_On_Comms_LossExport Control on Comms LossS1610020: Keep last setting
1: Zero export
2: Limit to value in 11117Ref: SunSpec DERCtl Model 701 timeout action
P111117Export_Limit_On_Comms_LossExport Limit When Comms LostS16kW0.01002500Used when 11116 = 2
7. Battery Power Limits
Done11057Bat_1_EnableBattery Port 1 EnableS1611010: Disabled  1: Enabled
Done11058Bat_2_EnableBattery Port 2 EnableS1610010: Disabled  1: Enabled
Done11068Bat1_Max_Chg_PowerBattery Port 1 Max Charge PowerS16kW0.01250003200
Done11069Bat1_Max_Dchg_PowerBattery Port 1 Max Discharge PowerS16kW0.01250003200
Done11076Bat2_Max_Chg_PowerBattery Port 2 Max Charge PowerS16kW0.01250003200
Done11077Bat2_Max_Dchg_PowerBattery Port 2 Max Discharge PowerS16kW0.01250003200
8. TOU Schedule (Economic Mode)
Time values in minutes from midnight (0-1440). E.g., 480 = 08:00, 1080 = 18:00.
Peak_Sel: 0 = Flat, 1 = Peak (discharge), 2 = Valley (charge)
Done11190Eco_Start1_SetTOU Period 1 Start TimeS16min1001440
Done11191Eco_End1_SetTOU Period 1 End TimeS16min1001440
Done11192Eco_Peak_Sel1_SetTOU Period 1 ModeS1610020: Flat  1: Peak  2: Valley
Done11193–11195Eco_Period2TOU Period 2 (Start/End/Mode)S16min1001440Same structure as Period 1
Done11196–11198Eco_Period3TOU Period 3 (Start/End/Mode)S16min1001440
Done11199–11201Eco_Period4TOU Period 4 (Start/End/Mode)S16min1001440
Done11202–11204Eco_Period5TOU Period 5 (Start/End/Mode)S16min1001440
Done11205–11207Eco_Period6TOU Period 6 (Start/End/Mode)S16min1001440
Done11208–11210Eco_Period7TOU Period 7 (Start/End/Mode)S16min1001440
Done11211–11213Eco_Period8TOU Period 8 (Start/End/Mode)S16min1001440
Done11214–11216Eco_Period9TOU Period 9 (Start/End/Mode)S16min1001440
Done11217–11219Eco_Period10TOU Period 10 (Start/End/Mode)S16min1001440
Periods 11-20 follow same pattern (registers 11220-11249)
P111250Eco_Charge_Power_SetTOU Charge Power LimitS16kW0.01250006000Max charge power during Valley periodsRef: Solinteg 50204, SMA WChaMax
P111251Eco_Discharge_Power_SetTOU Discharge Power LimitS16kW0.01250006000Max discharge power during Peak periods
P111252Eco_Target_SOC_SetTOU Target SOCS16%0.0110000010000Valley charging target SOC
P111253Eco_Min_SOC_SetTOU Minimum SOCS16%0.011000010000Min SOC during Peak discharge
P011254TOU_Day_Of_Week_MaskTOU Day-of-Week Schedule MaskU1611270127Bitmap: bit0=Mon, bit1=Tue, bit2=Wed, bit3=Thu, bit4=Fri, bit5=Sat, bit6=Sun. 31=Weekdays, 96=Weekend, 127=All.
CRITICAL: Dynamic tariffs (Tibber/aWATTar) have different rates weekday vs weekend. All competitors support this.Ref: SMA TOU schedule, Fronius Time-of-Use, sonnen smart charging
9. Backup / Off-Grid
Done11137Backup_SwitchBackup Output EnableS1611010: Disabled  1: Enabled
Done11041On_Off_Grid_Switch_EnablePlanned Off-Grid EnableS1611010: Disabled  1: Enabled
10. Reactive Power Control P0 MUST-HAVE
P011300Reactive_Power_ModeReactive Power Control ModeS1610040: Off (default)
1: Fixed Power Factor
2: Fixed Reactive Power
3: Cosφ(P) curve
4: Q(U) curveRequired by EN 50549. Ref: SMA 40200, Fronius 124, SolarEdge F170
P011301Reactive_Power_PF_SetPower Factor SetpointS160.0011000-10001000-1.000 to +1.000 (mode 1)
P011302Reactive_Power_Q_SetReactive Power SetpointS16kVar0.010-25002500+leading/-lagging (mode 2)
P011303Reactive_Power_Q_MaxMax Reactive PowerS16kVar0.01250002500Rated reactive capacity
Cosφ(P) Curve Points — 4 points defining PF as function of active power %
P011304CosPhiP_P1Curve Point 1 Power %S16%1200100
P011305CosPhiP_Cos1Curve Point 1 CosφS160.0011000-10001000
P011306CosPhiP_P2Curve Point 2 Power %S16%1500100
P011307CosPhiP_Cos2Curve Point 2 CosφS160.0011000-10001000
P011308CosPhiP_P3Curve Point 3 Power %S16%1800100
P011309CosPhiP_Cos3Curve Point 3 CosφS160.001950-10001000
P011310CosPhiP_P4Curve Point 4 Power %S16%11000100
P011311CosPhiP_Cos4Curve Point 4 CosφS160.001900-10001000
Q(U) Curve Points — 4 points defining reactive power % as function of voltage %
P011312QU_V1Curve Point 1 Voltage %S16%19280120
P011313QU_Q1Curve Point 1 Q %S16%1100-100100
P011314QU_V2Curve Point 2 Voltage %S16%19880120
P011315QU_Q2Curve Point 2 Q %S16%10-100100
P011316QU_V3Curve Point 3 Voltage %S16%110280120
P011317QU_Q3Curve Point 3 Q %S16%10-100100
P011318QU_V4Curve Point 4 Voltage %S16%110880120
P011319QU_Q4Curve Point 4 Q %S16%1-100-100100
11. Active Power Limit Percentage P0 MUST-HAVE
EN 50549 & VDE-AR-N 4105 require % of rated power curtailment. German 70% rule (§9 EEG), grid operator dispatch commands all use %. SMA 40016, Fronius WMaxLimPct, SunSpec Model 123.
P011320Active_Power_Limit_Pct_EnableEnable Percentage Power LimitS1610010: Use absolute kW (11045)  1: Use % limit (11321)Ref: SMA 40016, SunSpec 123 WMaxLimPct_Ena
P011321Active_Power_Limit_PctActive Power Limit as % of PnU16%0.11000010000-100.0% of rated power. 700 = 70.0% (German 70% rule).Ref: SMA WMaxLimPct, Fronius WMaxLimPct, SolarEdge F140
12. Power Ramp Rate / Gradient P0 MUST-HAVE
EN 50549-1 §5.4.2: After grid reconnection, power must ramp at ≤10% Pn/min. Required for all EU grid connection certificates. SMA WGra (40236), Fronius WGra (SunSpec 124). Without this register, inverter cannot pass EU grid certification for EMS installations.
P011322Power_Ramp_Rate_UpPower Increase Ramp RateU16%Pn/s0.110011000Rate of active power increase. 100 = 10.0%/s.Ref: SMA WGra 40236, SunSpec 124 WGra
P011323Power_Ramp_Rate_DownPower Decrease Ramp RateU16%Pn/s0.1100011000Rate of active power decrease. Typically faster than ramp-up.
P011324Reconnect_Ramp_RatePost-Reconnection Ramp RateU16%Pn/min0.1100101000Ramp rate after grid reconnection. Default 10.0%/min per EN 50549.Ref: EN 50549-1 §5.4.2, VDE-AR-N 4105
13. Frequency-Watt P(f) Response P0 MUST-HAVE
EN 50549-1 §5.4.4: LFSM-O (over-frequency) and LFSM-U (under-frequency) active power response is mandatory for all EU-connected DER. VDE-AR-N 4105 requires 5% droop. All competitors (SMA, Fronius, SolarEdge, Huawei) implement P(f). SunSpec Model 712 (Freq-Watt). Without this, MATIC cannot obtain EU grid certificates for EMS-controlled installations.
P011330Freq_Watt_EnableFrequency-Watt Response EnableS1610010: Disabled  1: EnabledRef: SunSpec 712 Ena, SMA P(f) settings
P011331Freq_Watt_HzStrOver-Freq Start PointU16Hz0.01502045005500Start reducing power. Default 50.20Hz (EU).
P011332Freq_Watt_HzStopOver-Freq Stop PointU16Hz0.01515045005500Zero power output. Default 51.50Hz.
P011333Freq_Watt_WGraOver-Freq Droop GradientU16%Pn/Hz0.1400101000Power reduction per Hz. 400 = 40.0%/Hz (5% droop).
P011334Freq_Watt_HzStrLowUnder-Freq Start PointU16Hz0.01498045005500Start increasing power. Default 49.80Hz (LFSM-U).
P011335Freq_Watt_HzStopLowUnder-Freq Stop PointU16Hz0.01485045005500Maximum power increase point.
P011336Freq_Watt_WGraLowUnder-Freq Response GradientU16%Pn/Hz0.1400101000Power increase per Hz below start point.
14. Grid Service / VPP Mode P2 PLANNED
P211340Grid_Service_ModeGrid Service Participation ModeS1610040: None  1: FCR  2: aFRR  3: VPP  4: Demand ResponseRef: sonnen VPP, SMA ennexOS grid services
15. Demand Power Management P0 MUST-HAVE
C&I installations with contracted demand (MDI) limits need the inverter to respect the site’s maximum demand import. EMS uses this to prevent exceeding grid connection capacity and avoid demand penalty charges. Essential for commercial peak shaving.
P011350Demand_Power_LimitSite Demand Power Limit (MDI)U16kW0.010065535Maximum site grid import power. 0 = no limit. Used for commercial peak shaving & demand charge management.
CRITICAL for C&I: Prevents exceeding contracted demand capacityRef: SMA Grid Guard demand limits, Huawei SmartLogger demand management
16. Smart Home Coordination P1 IMPORTANT
SG-Ready implements the German BWP/EHPA SG-Ready standard (VDI 4645) for heat pump coordination. 2-bit signal tells heat pumps when surplus PV/battery energy is available. Mandatory for German market heat pump integration. EEBUS/SEMP interface enables broader smart home energy management.
P111355SG_Ready_ModeSG-Ready Heat Pump SignalU1611141: Block (EVU-Sperre)  2: Normal operation  3: Recommended ON (surplus available)  4: Force ON (excess PV/battery)Ref: SMA SG-Ready, E3DC SmartPower, sonnen heat pump integration

Control Commands

Write-only control registers. Use function code 0x06 to send commands. Values are not retained after execution.

Implemented (7 registers)
P0 Must-Have (6 registers)
Show:

Control Commands

WRITE ONLY
StatusRegisterNameDescriptionTypeDefaultMinMaxValues / Notes
Done12000PowerOn_SwitchPower On / OffS160010: Power Off  1: Power On
Done12001Malfunction_ClearFault ClearS160010: No action  1: Clear faults
Done12002Remote_ModeRemote Control ModeS160010: Local  1: Remote
Set to 1 for EMS control
Done12003Restore_Factory_SettingRestore Factory SettingsS160010: No action  1: Restore
USE WITH CAUTION
Done12004Communication_ResetCommunication Module ResetS160010: No action  1: Reset
Done12005Device_RestartDevice RestartS160010: No action  1: Restart
Done12006Device_Self_CheckDevice Self-CheckS160010: No action  1: Start self-check
Safety Unlock & Emergency Stop P0 MUST-HAVE
Destructive commands (Factory Reset 12003, Device Restart 12005, Emergency Stop 12007) must be protected against accidental writes from corrupted Modbus frames. SMA requires Grid Guard PIN. Fronius uses a PIN-protected mode. Write the unlock key value 0x55AA to Safety_Unlock within 5 seconds before any destructive command.
P012008Safety_UnlockSafety Unlock for Destructive CommandsU160065535Write 0x55AA (21930) to unlock destructive commands for 5 seconds. After timeout, auto-relocks.
Required before: Factory Reset (12003), Restart (12005), Emergency Stop (12007)Ref: SMA Grid Guard code, Fronius PIN protection
P012007Emergency_StopEmergency StopS160011: Immediately cease all power conversion. Requires Safety_Unlock first.
Safety-critical. Latches until cleared via 12001.Per IEC 62109-1 emergency stop requirements
Communications Watchdog P0 MUST-HAVE
P012010Comms_Watchdog_EnableWatchdog Timer EnableS160010: Disabled  1: Enabled
Enable for safety: auto-failsafe on comms lossRef: SMA 41193-41197, Solinteg 20085
P012011Comms_Watchdog_TimeoutWatchdog TimeoutS166010600Seconds before failsafe action triggers
P012012Comms_HeartbeatHeartbeat CounterU160065535Write any value to reset watchdog timer
P012013Comms_Timeout_ActionTimeout ActionS160030: Stop inverter
1: Self-consumption mode
2: Hold last state
3: Zero-export mode

Device Information

Static device identification and firmware information. Read once at connection time.

Implemented (9 registers)
P0 Must-Have (1 register)
P1 Important (3 registers)

Device Registers

READ-ONLY
StatusRegisterNameDescriptionTypeUnitScaleNotes
Done13000Device_SNDevice Serial NumberASCII32116 registers (32 bytes)
Done13016Device_InfoDevice Model CodeU161See model table below
Done13017DSP_AC_App_VersionDSP AC App VersionU161
Done13019DC_App_V_VerDSP DC App VersionU161
Done13021ARM_App_V_VerARM App VersionU161
Done13029Hardware_VerHardware VersionU161
Done13034Inverter_Rated_PowerRated PowerU16kW0.01
Done13035Inverter_Max_PowerMaximum Output PowerU16kW0.01
Done13120Inverter_Total_Working_TimeTotal Working TimeU32_BEh0.12 registers
Protocol & Communication Diagnostics P0 / P1
P013036Protocol_VersionModbus Protocol VersionU161EMS reads at connect to determine available registers. Format: major*100+minor (e.g., 200 = v2.0). Enables firmware-safe EMS integration.Ref: SunSpec Model 1 Vr, SMA protocol version register
P113040Modbus_Msg_CountTotal Modbus Messages ReceivedU161Rolling counter. Wraps at 65535. Useful for comms health monitoring.
P113041Modbus_Err_CountTotal Modbus ErrorsU161CRC errors + timeout errors + exception responses. Non-zero indicates wiring or configuration issues.
P113042Modbus_Last_Err_CodeLast Modbus Error CodeU1610: None  1: CRC Error  2: Timeout  3: Illegal Function  4: Illegal Address  5: Illegal Value

Model Code Reference

INFO
CodeModelRated PowerMax Power
10MATIC-10KW-50A10.00 kW10.00 kW
11MATIC-12KW-50A12.00 kW12.00 kW
12MATIC-15KW-50A15.00 kW15.00 kW
13MATIC-20KW-50A20.00 kW20.00 kW
14MATIC-25KW-50A25.00 kW25.00 kW

System Status

Current operating state of inverter subsystems. Poll these registers to determine system readiness and operating conditions.

Implemented (16 registers)
P0 Must-Have (8 registers)
P1 Important (4 registers)
P2 Planned (1 register)

Status Registers

READ-ONLY
StatusRegisterNameDescriptionTypeValues / States
Done14000PCS_Working_ModePCS Working ModeU160: General  1: Grid Balancing  2: Economic  3: UPS  4: TOU  5: Forecast  6: Off-Grid
Done14001TOU_SubmodeTOU Sub-ModeU160: Self-Use  1: Charge  2: Discharge  3: Peak Shaving  4: Battery Off
Done14002PCS_Working_StatusPCS Running StatusU160: Self-checking  1: Standby  2: Pre-charging  3: Grid-synchronizing  4: Waiting  5: Initializing  6: Starting  7: Connecting  8: Running
EMS commands accepted only in states 1 (Standby) and 8 (Running)
Done14003Battery1_Working_StatusBattery Port 1 StatusU160: Stop  1: Charging  2: Discharging  3: Not Connected  4: Abnormal
Done14004Battery2_Working_StatusBattery Port 2 StatusU16Same as Battery Port 1
Done14005PV1_Working_StatusPV1 StatusU160: Stop  1: Running  2: Not Connected  3: Abnormal  4: No Interface
Done14006PV2_Working_StatusPV2 StatusU16Same as PV1
Done14007PV3_Working_StatusPV3 StatusU16Same as PV1
Done14008PV4_Working_StatusPV4 StatusU16Same as PV1
Done14009PV5_Working_StatusPV5 StatusU16Same as PV1
Done14010Grid_Working_StatusGrid Connection StatusU160: On-Grid  1: Off-Grid (Islanded)
Done14011Diesel_Working_StatusDiesel Generator StatusU160: Stop  1: Running  3: Not Connected  4: Abnormal
Done14027AC_Power_On_Off_StatusAC Side On/OffU160: Off  1: On
Done14028DC_Power_On_Off_StatusDC Side On/OffU160: Off  1: On
Done14029AC_Run_StatusAC Running StateU160: Stop  1-7: Starting  8: Running
Done14030DC_Run_StatusDC Running StateU160: Stop  1: Soft-Start  2-3: Running
Communications Watchdog Status P0 MUST-HAVE
P014031Comms_Watchdog_StatusWatchdog StateU160: Inactive  1: Active (OK)  2: Timed Out
P014032Comms_Heartbeat_AgeLast Heartbeat AgeU16Seconds since last heartbeat received (unit: s)
Active Power Limit Feedback P0 MUST-HAVE
EMS must confirm its commands were applied. Without readback, EMS operates blind. All major competitors provide power limit feedback. SMA 30231, Fronius WMaxLimPct_RB.
P014033Applied_Power_Limit_PctApplied Power Limit (%)U160-1000 (0-100.0%). Current active power limit as % of Pn. RO readback.Ref: SMA 30231, Fronius WMaxLimPct_RB
P014034Applied_Power_Limit_AbsApplied Power Limit (kW)S16Scale 0.01 kW. Actual max power after all limits applied (EMS + grid code + derating).
P014035Active_Power_Limit_SourcePower Limit SourceU16Bitmap: bit0=EMS, bit1=Grid Code, bit2=Thermal Derating, bit3=Frequency Response, bit4=External Signal
Derating Feedback P1 IMPORTANT
P114036Derating_StatusCurrent Derating SourceU16Bitmap: bit0=Temperature, bit1=Voltage, bit2=Overload, bit3=BMS Limit, bit4=Grid Code. 0 = no derating.Ref: SMA 30233 derating source
P114037Derating_Power_LimitDerated Max PowerU16Scale 0.01 kW. Maximum power inverter can deliver right now (after derating). Compare to rated power to see how much is lost.
Configuration Readback P2 PLANNED
P214038Active_Safety_CodeActive Grid CodeU16Currently active safety regulation code (readback of 11155). Verify correct grid code is applied.
Command Audit & Communication Health P0 / P1
EMS needs to verify when the last command was executed for audit trails and debugging. Communication signal quality is essential for remote diagnostics of Modbus RTU installations.
P014039–14040Last_Cmd_TimestampLast Command Execution TimestampU32_BEUnix epoch seconds (2 registers). Time when the last EMS write command was executed. Essential for audit trails, debugging stale commands, and VPP compliance logging.Ref: SMA event log timestamps, Fronius command logging
P114043Comms_RSSICommunication Signal QualityS16WiFi/4G RSSI in dBm. 0 = wired connection. Helps remote diagnostics of communication issues.Ref: Huawei SmartLogger RSSI, SMA Speedwire diagnostics
EMS Control & Mode Readback P0 MUST-HAVE
EMS writes Remote_Mode=1 and EMS_Switch_Set=1, but has no way to confirm these are active. If a local user disables EMS from the front panel, or firmware reboots reset these flags, the EMS continues sending commands that are silently ignored. Closed-loop control is impossible without readback. SMA provides ExternalControl status. Every VPP platform requires this.
P014044EMS_Control_ActiveEMS Control Active StatusU160: EMS disabled (local control)  1: EMS active (remote control)
Read-only readback of EMS_Switch_Set(11060) + Remote_Mode(12002) combined state.Ref: SMA ExternalControl status, Fronius remote control feedback
P014045Remote_Mode_StatusRemote Mode Active StatusU160: Local mode  1: Remote mode active
Readback of Remote_Mode(12002). If user overrides from panel, this returns 0.
P114046Mode_Change_ReasonLast Mode Change ReasonU16Bit field: bit0=EMS command, bit1=Front panel override, bit2=Grid fault protection, bit3=BMS protection, bit4=Watchdog timeout, bit5=Firmware auto-switch. Helps EMS understand WHY mode changed unexpectedly.

Alarms & Faults

Fault and alarm registers organized by severity. Each register contains bit-mapped flags. Monitor periodically for system health.

AC Side Derating Alarms WARNING
BitNameDescription
14022:0AC_External_Temp_DeratingAC External Ambient Temp Derating
14022:1AC_Internal_Temp_DeratingAC Internal Ambient Temp Derating
14022:2AC_Bus_DeratingAC Bus Derating
14022:4Grid_Voltage_DeratingGrid Voltage Derating
14022:5AC_Overload_DeratingAC Overload Derating
14022:6AC_Radiator_Temp_DeratingAC Heatsink Temp Derating
DC Side Derating Alarms WARNING
BitNameDescription
14023:3DC_External_Temp_DeratingDC External Ambient Temp Derating
14023:4DC_Inside_Temp_DeratingDC Internal Ambient Temp Derating
14023:5Battery_Bus_Voltage_DeratingBattery Bus Voltage Derating
14023:6PV_Bus_Voltage_DeratingPV Bus Voltage Derating
14023:8BMS1_Charge_ProhibitedBMS1 Charge Prohibited
14023:9BMS1_Discharge_ProhibitedBMS1 Discharge Prohibited
14023:10BMS2_Charge_ProhibitedBMS2 Charge Prohibited
14023:11BMS2_Discharge_ProhibitedBMS2 Discharge Prohibited
Grid Voltage / Frequency Alarms ALARM
BitNameDescription
14100:0Grid_LV1_OV_AGrid Level-1 Over-Voltage Phase A
14100:3Grid_LV2_OV_AGrid Level-2 Over-Voltage Phase A
14100:6Grid_LV3_OV_AGrid Level-3 Over-Voltage Phase A
14100:9Grid_10min_OV_AGrid 10-min Over-Voltage Phase A
14100:12Grid_LV1_UV_AGrid Level-1 Under-Voltage Phase A
14101:6Grid_LV1_OFGrid Level-1 Over-Frequency
14101:7Grid_LV2_OFGrid Level-2 Over-Frequency
14101:8Grid_LV1_UFGrid Level-1 Under-Frequency
14101:9Grid_LV2_UFGrid Level-2 Under-Frequency
14101:10Grid_Phase_SequenceGrid Phase Sequence Error
14101:12Grid_Phase_Loss_AGrid Phase Loss A
14101:15ROCOFRate of Change of Frequency
Critical Faults FAULT
RegisterNameDescription
14109:0-14Grid_Voltage_FaultsGrid Over/Under Voltage Faults (3 levels, 3 phases)
14110:0-5Grid_VF_Faults_LV2_3Grid Level-2/3 Under-Voltage + Frequency Faults
14110:10-14Grid_Phase_FaultsGrid Phase Sequence / Loss / Lock Faults
14112:0-2BackUp_OvercurrentBackup Output Overcurrent (A/B/C)
14112:3-5BackUp_Overload_TimeoutBackup Overload Timeout (A/B/C)
14113:3-6Inv_HW_OvercurrentInverter Hardware Overcurrent
14113:7-9Inv_SW_OvercurrentInverter Software Overcurrent (A/B/C)
14115:0-2Inv_Short_CircuitInverter Short Circuit (A/B/C)
14104:0Anti_IslandingAnti-Islanding Protection Triggered
14104:1Heatsink_OvertempInverter Heatsink Over-Temperature
14104:3CT_ReverseCT Reverse Connection Detected (Alarm)
14104:7Anti_Backfeed_FailureAnti-Backfeed Failure

For complete fault register details (689 fault definitions), refer to the full internal protocol document.

Quick Start Guide

Step-by-step guide for integrating with a Wattsonic MATIC inverter via Modbus. Steps marked P0 require proposed registers.

1

Connect & Identify

  • Establish Modbus RTU/TCP connection (default: address 1, baud 9600)
  • Read Device_SN (13000) and Device_Info (13016) to verify device identity
  • Read Inverter_Rated_Power (13034) to determine rated capacity
2

Enable EMS Control

  • Write Remote_Mode (12002) = 1 to enable remote control
  • Write EMS_Switch_Set (11060) = 1 to enable external EMS dispatch
  • P0 Write Comms_Watchdog_Enable (12010) = 1 to enable safety watchdog
  • P0 Write Comms_Watchdog_Timeout (12011) = 60 (60s timeout)
  • P0 Write Comms_Timeout_Action (12013) = 1 (self-consumption on timeout)
3

Monitor System State

Poll real-time data at 1-5 second intervals:

  • Grid power: Meter_Active_Power_Total (10765)
  • PV power: PV_Total_Power (10087)
  • Battery SOC: BMS_Bat1_SOC (10671)
  • Battery power: BMS_Bat1_Current (10675) x BMS_Bat1_Voltage (10674)
  • P1 Load power: Load_Power_Total (10800)
  • Working mode: PCS_Working_Mode (14000)
  • Grid status: Grid_Working_Status (14010)
  • P0 Watchdog: Comms_Watchdog_Status (14031)
  • Check alarms: Registers 14022-14026, 14100+
4

Send Heartbeat (Loop)

P0 Write to heartbeat register every 15-30 seconds:

Heartbeat Loop Write Comms_Heartbeat (12012) = counter++
Interval: every 15-30 seconds
If Comms_Watchdog_Status (14031) = 2 → comms lost, check connection
5

Control Operations

  • Set Working Mode: Write Run_Mode_Set (11151) = 0 / 2 / 4
  • Set Active Power: Write Active_Power_Set (11045)
Active Power Examples Write 1500 = discharge 15.00 kW
Write -1000 = charge 10.00 kW from grid
  • Export Limit (Zero Export): Write Export_Limits_Switch (11107) = 1, Write Export_Limits (11113) = 0
  • SOC Limits: Write Grid_tied_SOC_MIN (11108) = 1000 (10.00%)
  • P0 Reactive Power: Write Reactive_Power_Mode (11300) and PF/Q setpoints
6

Safety & Error Handling

  • Monitor fault registers periodically
  • Use Malfunction_Clear (12001) = 1 to clear recoverable faults
  • P0 Emergency: Write Safety_Unlock (12008) = 0x55AA, then Emergency_Stop (12007) = 1 within 5 seconds
  • P0 Verify EMS: Poll EMS_Control_Active (14044) periodically. If it returns 0, EMS has been overridden from the front panel.
  • Before shutdown: Write PowerOn_Switch (12000) = 0
  • When disconnecting EMS: Write EMS_Switch_Set (11060) = 0

Integration Scenarios

Ten typical EMS integration scenarios with recommended communication paths (Modbus RTU / TCP) and production-ready code examples in Python, Node.js, C#, and Go. Each scenario demonstrates the complete register read/write sequence, scale factor handling, and sign conventions.

RTU RS-485 serial (on-site controllers)
TCP Ethernet / WiFi (cloud & SCADA)
RTU + TCP Both supported
1

Real-Time Monitoring Dashboard

RTU + TCP
Read inverter output, PV production, battery SOC, and grid/meter power to build a live monitoring dashboard. Poll every 1-2 seconds for near-real-time visibility into system state.

Key Registers

  • 10000 Inverter_Frequency — Read, scale 0.01 Hz
  • 10001-10003 Inverter_Voltage_A/B/C — Read, scale 0.1 V
  • 10087 PV_Total_Power — Read, scale 0.01 kW
  • 10088 / 10091 PV1/PV2_Voltage — Read, scale 0.1 V
  • 10671 BMS_Bat1_SOC — Read, scale 0.01 %
  • 10675 BMS_Bat1_Current — Read, scale 0.1 A, signed (+discharge/−charge)
  • 10765 Meter_Active_Power_Total — Read, scale 0.01 kW, signed (+import/−export)
  • 14002 PCS_Working_Status — Read
SETUP Connect RTU or TCP
READ AC & Grid 10000–10064 65 regs
READ PV / DC 10087–10105 19 regs
READ Battery 10671–10697 27 regs
READ Meter 10765 Total kW
READ Status 14002 PCS State
LOOP 1-2s
from pymodbus.client import ModbusTcpClient  # or ModbusSerialClient
import time, struct

# RTU: client = ModbusSerialClient(port="/dev/ttyUSB0", baudrate=9600)
client = ModbusTcpClient("192.168.1.100", port=502)
client.connect()

def signed16(val):
    return val - 65536 if val >= 32768 else val

while True:
    # Group 1: inverter basics (10000-10003)
    g1 = client.read_holding_registers(10000, 4, slave=1)
    freq = g1.registers[0] * 0.01          # Hz
    v_a  = g1.registers[1] * 0.1           # V
    v_b  = g1.registers[2] * 0.1           # V
    v_c  = g1.registers[3] * 0.1           # V

    # Group 2: PV production (10087-10091)
    g2 = client.read_holding_registers(10087, 5, slave=1)
    pv_total = g2.registers[0] * 0.01      # kW
    pv1_v    = g2.registers[1] * 0.1       # V
    pv2_v    = g2.registers[4] * 0.1       # V

    # Group 3: battery (10671-10675)
    g3 = client.read_holding_registers(10671, 5, slave=1)
    soc     = g3.registers[0] * 0.01       # %
    bat_cur = signed16(g3.registers[4]) * 0.1  # A (+dis/-chg)

    # Meter power
    m = client.read_holding_registers(10765, 1, slave=1)
    meter_w = signed16(m.registers[0]) * 0.01  # kW (+imp/-exp)

    # PCS status
    s = client.read_holding_registers(14002, 1, slave=1)
    print(f"Grid:{freq:.1f}Hz V:{v_a:.0f}/{v_b:.0f}/{v_c:.0f}V "
          f"PV:{pv_total:.2f}kW SOC:{soc:.0f}% Meter:{meter_w:.2f}kW")
    time.sleep(1)
const ModbusRTU = require('modbus-serial');
const client = new ModbusRTU();

// RTU: await client.connectRTUBuffered("/dev/ttyUSB0", { baudRate: 9600 });
await client.connectTCP("192.168.1.100", { port: 502 });
client.setID(1);

function signed16(v) { return v >= 32768 ? v - 65536 : v; }

setInterval(async () => {
    // Group 1: inverter basics
    const g1 = await client.readHoldingRegisters(10000, 4);
    const freq = g1.data[0] * 0.01;        // Hz
    const vA = g1.data[1] * 0.1;           // V

    // Group 2: PV production
    const g2 = await client.readHoldingRegisters(10087, 5);
    const pvTotal = g2.data[0] * 0.01;     // kW
    const pv1V = g2.data[1] * 0.1;         // V

    // Group 3: battery
    const g3 = await client.readHoldingRegisters(10671, 5);
    const soc = g3.data[0] * 0.01;         // %
    const batCur = signed16(g3.data[4]) * 0.1; // A

    // Meter power (+import / -export)
    const m = await client.readHoldingRegisters(10765, 1);
    const meterW = signed16(m.data[0]) * 0.01; // kW

    // PCS working status
    const s = await client.readHoldingRegisters(14002, 1);
    console.log(`PV:${pvTotal.toFixed(2)}kW SOC:${soc.toFixed(0)}% `
              + `Meter:${meterW.toFixed(2)}kW Freq:${freq.toFixed(1)}Hz`);
}, 1000);
using NModbus;
using System.Net.Sockets;

// RTU: var port = new SerialPort("/dev/ttyUSB0", 9600);
//      var master = factory.CreateRtuMaster(port);
var tcp = new TcpClient("192.168.1.100", 502);
var factory = new ModbusFactory();
var master = factory.CreateMaster(tcp);

short Signed16(ushort v) => v >= 32768 ? (short)(v - 65536) : (short)v;

while (true) {
    // Group 1: inverter basics (10000-10003)
    var g1 = master.ReadHoldingRegisters(1, 10000, 4);
    double freq = g1[0] * 0.01;            // Hz
    double vA   = g1[1] * 0.1;             // V

    // Group 2: PV production (10087-10091)
    var g2 = master.ReadHoldingRegisters(1, 10087, 5);
    double pvTotal = g2[0] * 0.01;         // kW

    // Group 3: battery (10671-10675)
    var g3 = master.ReadHoldingRegisters(1, 10671, 5);
    double soc    = g3[0] * 0.01;          // %
    double batCur = Signed16(g3[4]) * 0.1; // A

    // Meter power
    var m = master.ReadHoldingRegisters(1, 10765, 1);
    double meterW = Signed16(m[0]) * 0.01; // kW (+imp/-exp)

    Console.WriteLine($"PV:{pvTotal:F2}kW SOC:{soc:F0}% Meter:{meterW:F2}kW");
    Thread.Sleep(1000);
}
package main
import (
    "fmt"; "time"
    "github.com/goburrow/modbus"
    "encoding/binary"
)

func signed16(v uint16) int16 { return int16(v) }

func main() {
    // RTU: handler := modbus.NewRTUClientHandler("/dev/ttyUSB0")
    handler := modbus.NewTCPClientHandler("192.168.1.100:502")
    handler.SlaveId = 1
    handler.Connect()
    defer handler.Close()
    client := modbus.NewClient(handler)

    for {
        // Group 1: inverter basics
        g1, _ := client.ReadHoldingRegisters(10000, 4)
        freq := float64(binary.BigEndian.Uint16(g1[0:2])) * 0.01

        // Group 2: PV production
        g2, _ := client.ReadHoldingRegisters(10087, 5)
        pvTotal := float64(binary.BigEndian.Uint16(g2[0:2])) * 0.01

        // Group 3: battery SOC and current
        g3, _ := client.ReadHoldingRegisters(10671, 5)
        soc := float64(binary.BigEndian.Uint16(g3[0:2])) * 0.01
        batCur := float64(signed16(binary.BigEndian.Uint16(g3[8:10]))) * 0.1

        // Meter power (+import / -export)
        m, _ := client.ReadHoldingRegisters(10765, 1)
        meterW := float64(signed16(binary.BigEndian.Uint16(m[0:2]))) * 0.01

        fmt.Printf("PV:%.2fkW SOC:%.0f%% Meter:%.2fkW Freq:%.1fHz\n",
            pvTotal, soc, meterW, freq)
        time.Sleep(1 * time.Second)
    }
}
2

Zero-Export Control

RTU + TCP
Prevent any power export to the grid by dynamically adjusting inverter output to match local load. Essential for markets requiring zero feed-in compliance (e.g., Germany §14a EEG, certain Australian DNSP rules).

Key Registers

  • 10765 Meter_Active_Power_Total — Read, scale 0.01 kW, signed (+import/−export)
  • 10087 PV_Total_Power — Read, scale 0.01 kW
  • 10671 BMS_Bat1_SOC — Read, scale 0.01 %
  • 11107 Export_Limits_Switch — Write, 1 = enable
  • 11113 Export_Limits — Write, 0 = zero export
  • 11060 EMS_Switch_Set — Write, 1 = enable EMS
  • 11045 Active_Power_Set — Write, scale 0.01 kW, signed (+discharge/−charge)
WRITE EMS Enable 11060 = 1
WRITE Export Limit 11113 = 0
READ Meter Power 10765 kW ±
DECIDE Calculate if export→reduce
WRITE Power Set 11045 kW ±
LOOP 1s
from pymodbus.client import ModbusTcpClient
import time

# RTU: from pymodbus.client import ModbusSerialClient
#      client = ModbusSerialClient(port="/dev/ttyUSB0", baudrate=9600)
client = ModbusTcpClient("192.168.1.100", port=502)
client.connect()

def signed16(v): return v - 65536 if v >= 32768 else v
def to_unsigned(v): return v + 65536 if v < 0 else v

# Enable EMS and zero-export limits
client.write_register(11060, 1, slave=1)      # EMS on
client.write_register(11107, 1, slave=1)      # export limit on
client.write_register(11113, 0, slave=1)      # zero export

MARGIN = 0.05   # 50W import bias to avoid oscillation
power_set = 0.0 # current battery dispatch (kW)

while True:
    # Read meter power (+import / -export)
    m = client.read_holding_registers(10765, 1, slave=1)
    meter_kw = signed16(m.registers[0]) * 0.01

    # Read battery SOC
    s = client.read_holding_registers(10671, 1, slave=1)
    soc = s.registers[0] * 0.01

    # Control: if exporting, charge battery; if importing too much, discharge
    error = meter_kw + MARGIN  # target small import
    power_set += error * 0.5   # proportional step
    power_set = max(-10.0, min(10.0, power_set))  # clamp to inverter range

    # Anti-windup: stop charging if SOC > 98%, stop discharging if SOC < 10%
    if soc > 98.0 and power_set < 0: power_set = 0
    if soc < 10.0 and power_set > 0: power_set = 0

    # Write battery dispatch (+discharge / -charge)
    client.write_register(11045, to_unsigned(int(power_set * 100)), slave=1)
    print(f"Meter:{meter_kw:+.2f}kW SOC:{soc:.0f}% Dispatch:{power_set:+.2f}kW")
    time.sleep(1)
const ModbusRTU = require('modbus-serial');
const client = new ModbusRTU();

// RTU: await client.connectRTUBuffered("/dev/ttyUSB0", { baudRate: 9600 });
await client.connectTCP("192.168.1.100", { port: 502 });
client.setID(1);

function signed16(v) { return v >= 32768 ? v - 65536 : v; }
function toUnsigned(v) { return v < 0 ? v + 65536 : v; }

// Enable EMS and zero-export
await client.writeRegister(11060, 1);   // EMS on
await client.writeRegister(11107, 1);   // export limit on
await client.writeRegister(11113, 0);   // zero export

const MARGIN = 0.05; // 50W import bias
let powerSet = 0.0;

setInterval(async () => {
    const m = await client.readHoldingRegisters(10765, 1);
    const meterKw = signed16(m.data[0]) * 0.01;

    const s = await client.readHoldingRegisters(10671, 1);
    const soc = s.data[0] * 0.01;

    // Proportional control toward small import target
    const error = meterKw + MARGIN;
    powerSet = Math.max(-10, Math.min(10, powerSet + error * 0.5));

    // Anti-windup at SOC limits
    if (soc > 98 && powerSet < 0) powerSet = 0;
    if (soc < 10 && powerSet > 0) powerSet = 0;

    await client.writeRegister(11045, toUnsigned(Math.round(powerSet * 100)));
    console.log(`Meter:${meterKw.toFixed(2)}kW SOC:${soc.toFixed(0)}% Set:${powerSet.toFixed(2)}kW`);
}, 1000);
using NModbus;
using System.Net.Sockets;

// RTU: var port = new SerialPort("/dev/ttyUSB0", 9600);
//      var master = factory.CreateRtuMaster(port);
var tcp = new TcpClient("192.168.1.100", 502);
var factory = new ModbusFactory();
var master = factory.CreateMaster(tcp);

short Signed16(ushort v) => v >= 32768 ? (short)(v - 65536) : (short)v;
ushort ToUnsigned(int v) => (ushort)(v < 0 ? v + 65536 : v);

// Enable EMS and zero-export limits
master.WriteSingleRegister(1, 11060, 1);   // EMS on
master.WriteSingleRegister(1, 11107, 1);   // export limit on
master.WriteSingleRegister(1, 11113, 0);   // zero export

double powerSet = 0.0;
const double MARGIN = 0.05; // 50W import bias

while (true) {
    var m = master.ReadHoldingRegisters(1, 10765, 1);
    double meterKw = Signed16(m[0]) * 0.01;
    var s = master.ReadHoldingRegisters(1, 10671, 1);
    double soc = s[0] * 0.01;

    double error = meterKw + MARGIN;
    powerSet = Math.Clamp(powerSet + error * 0.5, -10.0, 10.0);

    if (soc > 98 && powerSet < 0) powerSet = 0;  // anti-windup
    if (soc < 10 && powerSet > 0) powerSet = 0;

    master.WriteSingleRegister(1, 11045, ToUnsigned((int)(powerSet * 100)));
    Console.WriteLine($"Meter:{meterKw:+0.00}kW SOC:{soc:F0}% Set:{powerSet:+0.00}kW");
    Thread.Sleep(1000);
}
package main
import (
    "fmt"; "time"; "math"
    "github.com/goburrow/modbus"
    "encoding/binary"
)

func signed16(v uint16) int16 { return int16(v) }
func toUnsigned(v int16) uint16 { return uint16(v) }

func main() {
    // RTU: handler := modbus.NewRTUClientHandler("/dev/ttyUSB0")
    handler := modbus.NewTCPClientHandler("192.168.1.100:502")
    handler.SlaveId = 1
    handler.Connect()
    defer handler.Close()
    client := modbus.NewClient(handler)

    // Enable EMS and zero-export
    client.WriteSingleRegister(11060, 1) // EMS on
    client.WriteSingleRegister(11107, 1) // export limit on
    client.WriteSingleRegister(11113, 0) // zero export

    powerSet := 0.0
    margin := 0.05 // 50W import bias

    for {
        m, _ := client.ReadHoldingRegisters(10765, 1)
        meterKw := float64(signed16(binary.BigEndian.Uint16(m))) * 0.01

        s, _ := client.ReadHoldingRegisters(10671, 1)
        soc := float64(binary.BigEndian.Uint16(s)) * 0.01

        err := meterKw + margin
        powerSet = math.Max(-10, math.Min(10, powerSet+err*0.5))
        if soc > 98 && powerSet < 0 { powerSet = 0 }
        if soc < 10 && powerSet > 0 { powerSet = 0 }

        client.WriteSingleRegister(11045, toUnsigned(int16(powerSet*100)))
        fmt.Printf("Meter:%+.2fkW SOC:%.0f%% Set:%+.2fkW\n", meterKw, soc, powerSet)
        time.Sleep(1 * time.Second)
    }
}
3

TOU Schedule Programming

TCP
Program time-of-use tariff schedules with peak, valley, and flat periods. Charge the battery during off-peak (valley) hours and discharge during peak hours to minimize electricity costs. Applicable to dynamic tariff markets such as Germany, UK (Octopus Agile), and China.

Key Registers

  • 11151 Run_Mode_Set — Write, 4 = TOU mode
  • 11190 Eco_Start1_Set — Write, minutes from midnight (0-1440)
  • 11191 Eco_End1_Set — Write, minutes from midnight
  • 11192 Eco_Peak_Sel1_Set — Write, 0=Flat, 1=Peak, 2=Valley
  • 11193-11195 Eco_Period2 — Write, same 3-register pattern
  • 11196-11219 Eco_Period3 through Period10 — Write, 3 registers each
  • 11254 TOU_Day_Of_Week_Mask — Write, bitmap (bit0=Mon, bit6=Sun)
  • 11250 Eco_Charge_Power_Set — Write, scale 0.01 kW
  • 11108 Grid_tied_SOC_MIN — Write, scale 0.01 %
  • 11109 Grid_tied_SOC_MAX — Write, scale 0.01 %
WRITE Mode = TOU 11151 = 4
WRITE Period 1 11190–11192 Start/End/Mode
WRITE Period 2 11193–11195 Start/End/Mode
WRITE Period 3 11196–11198 Start/End/Mode
WRITE Weekday Mask 11254 = 0x7F
WRITE SOC Limits 11108–11109 Min/Max
READ Readback 11190+ Verify all
VERIFY Confirmed Values match
from pymodbus.client import ModbusTcpClient

# RTU: from pymodbus.client import ModbusSerialClient
#      client = ModbusSerialClient(port="/dev/ttyUSB0", baudrate=9600)
client = ModbusTcpClient("192.168.1.100", port=502)
client.connect()

# Set TOU mode
client.write_register(11151, 4, slave=1)       # Run_Mode = TOU

# SOC limits and charge power
client.write_register(11108, int(10.0 * 100), slave=1)   # SOC_MIN = 10%
client.write_register(11109, int(100.0 * 100), slave=1)  # SOC_MAX = 100%
client.write_register(11250, int(5.0 * 100), slave=1)    # charge power = 5 kW

# Period 1: Valley 00:00-07:00 (charge)
client.write_registers(11190, [0, 420, 2], slave=1)      # start=0, end=420, valley=2

# Period 2: Flat 07:00-17:00
client.write_registers(11193, [420, 1020, 0], slave=1)   # start=420, end=1020, flat=0

# Period 3: Peak 17:00-23:00 (discharge)
client.write_registers(11196, [1020, 1380, 1], slave=1)  # start=1020, end=1380, peak=1

# Enable for all days (Mon-Sun = bits 0-6 = 0x7F)
client.write_register(11254, 0x7F, slave=1)

# Verify by readback
r = client.read_holding_registers(11190, 9, slave=1)
for i in range(3):
    start, end, mode = r.registers[i*3], r.registers[i*3+1], r.registers[i*3+2]
    label = ["Flat", "Peak", "Valley"][mode]
    print(f"Period {i+1}: {start//60:02d}:{start%60:02d}-{end//60:02d}:{end%60:02d} [{label}]")
client.close()
const ModbusRTU = require('modbus-serial');
const client = new ModbusRTU();

// RTU: await client.connectRTUBuffered("/dev/ttyUSB0", { baudRate: 9600 });
await client.connectTCP("192.168.1.100", { port: 502 });
client.setID(1);

// Set TOU mode and SOC limits
await client.writeRegister(11151, 4);                    // TOU mode
await client.writeRegister(11108, Math.round(10 * 100)); // SOC_MIN = 10%
await client.writeRegister(11109, Math.round(100 * 100));// SOC_MAX = 100%
await client.writeRegister(11250, Math.round(5.0 * 100));// charge = 5kW

// Period 1: Valley 00:00-07:00 (charge during off-peak)
await client.writeRegisters(11190, [0, 420, 2]);

// Period 2: Flat 07:00-17:00
await client.writeRegisters(11193, [420, 1020, 0]);

// Period 3: Peak 17:00-23:00 (discharge during peak)
await client.writeRegisters(11196, [1020, 1380, 1]);

// All days of week (Mon-Sun)
await client.writeRegister(11254, 0x7F);

// Readback verification
const r = await client.readHoldingRegisters(11190, 9);
const labels = ['Flat', 'Peak', 'Valley'];
for (let i = 0; i < 3; i++) {
    const s = r.data[i*3], e = r.data[i*3+1], m = r.data[i*3+2];
    const fmt = (v) => `${String(Math.floor(v/60)).padStart(2,'0')}:${String(v%60).padStart(2,'0')}`;
    console.log(`Period ${i+1}: ${fmt(s)}-${fmt(e)} [${labels[m]}]`);
}
client.close();
using NModbus;
using System.Net.Sockets;

// RTU: var port = new SerialPort("/dev/ttyUSB0", 9600);
//      var master = factory.CreateRtuMaster(port);
var tcp = new TcpClient("192.168.1.100", 502);
var factory = new ModbusFactory();
var master = factory.CreateMaster(tcp);

// Set TOU mode and SOC limits
master.WriteSingleRegister(1, 11151, 4);                     // TOU mode
master.WriteSingleRegister(1, 11108, (ushort)(10.0 * 100));  // SOC_MIN=10%
master.WriteSingleRegister(1, 11109, (ushort)(100.0 * 100)); // SOC_MAX=100%
master.WriteSingleRegister(1, 11250, (ushort)(5.0 * 100));   // charge=5kW

// Period 1: Valley 00:00-07:00 (charge)
master.WriteMultipleRegisters(1, 11190, new ushort[] { 0, 420, 2 });
// Period 2: Flat 07:00-17:00
master.WriteMultipleRegisters(1, 11193, new ushort[] { 420, 1020, 0 });
// Period 3: Peak 17:00-23:00 (discharge)
master.WriteMultipleRegisters(1, 11196, new ushort[] { 1020, 1380, 1 });

// All days (Mon-Sun = 0x7F)
master.WriteSingleRegister(1, 11254, 0x7F);

// Readback verification
var r = master.ReadHoldingRegisters(1, 11190, 9);
string[] labels = { "Flat", "Peak", "Valley" };
for (int i = 0; i < 3; i++) {
    int s = r[i*3], e = r[i*3+1], m = r[i*3+2];
    Console.WriteLine($"Period {i+1}: {s/60:D2}:{s%60:D2}-{e/60:D2}:{e%60:D2} [{labels[m]}]");
}
tcp.Close();
package main
import (
    "fmt"
    "github.com/goburrow/modbus"
    "encoding/binary"
)

func main() {
    // RTU: handler := modbus.NewRTUClientHandler("/dev/ttyUSB0")
    handler := modbus.NewTCPClientHandler("192.168.1.100:502")
    handler.SlaveId = 1
    handler.Connect()
    defer handler.Close()
    client := modbus.NewClient(handler)

    // Set TOU mode, SOC limits, charge power
    client.WriteSingleRegister(11151, 4)          // TOU mode
    client.WriteSingleRegister(11108, uint16(10*100))  // SOC_MIN=10%
    client.WriteSingleRegister(11109, uint16(100*100)) // SOC_MAX=100%
    client.WriteSingleRegister(11250, uint16(5*100))   // charge=5kW

    // Period 1: Valley 00:00-07:00, Period 2: Flat 07:00-17:00, Period 3: Peak 17:00-23:00
    periods := []uint16{0, 420, 2, 420, 1020, 0, 1020, 1380, 1}
    buf := make([]byte, len(periods)*2)
    for i, v := range periods { binary.BigEndian.PutUint16(buf[i*2:], v) }
    client.WriteMultipleRegisters(11190, uint16(len(periods)), buf)

    client.WriteSingleRegister(11254, 0x7F) // all days

    // Readback
    r, _ := client.ReadHoldingRegisters(11190, 9)
    labels := []string{"Flat", "Peak", "Valley"}
    for i := 0; i < 3; i++ {
        s := binary.BigEndian.Uint16(r[i*6:])
        e := binary.BigEndian.Uint16(r[i*6+2:])
        m := binary.BigEndian.Uint16(r[i*6+4:])
        fmt.Printf("Period %d: %02d:%02d-%02d:%02d [%s]\n", i+1, s/60, s%60, e/60, e%60, labels[m])
    }
}
4

Peak Shaving / Demand Management

RTU + TCP
Limit maximum grid import power below the contracted demand threshold (MDI) for commercial and industrial installations. Use battery storage to absorb excess load above the limit, reducing demand charges and avoiding penalty tariffs.

Key Registers

  • 10765 Meter_Active_Power_Total — Read, scale 0.01 kW, signed (+import/−export)
  • 10671 BMS_Bat1_SOC — Read, scale 0.01 %
  • 11060 EMS_Switch_Set — Write, 1 = enable
  • 11350 Demand_Power_Limit — Write, scale 0.01 kW
  • 11045 Active_Power_Set — Write, scale 0.01 kW, signed (+discharge/−charge)
  • 14033 Applied_Power_Limit_Abs — Read, readback of applied limit
WRITE EMS Enable 11060 = 1
WRITE Demand Limit 11350 kW
READ Meter Power 10765 kW import
DECIDE Exceeds Limit? import > demand
WRITE Discharge 11045 kW +
READ Feedback 14033 Applied limit
LOOP 1s
from pymodbus.client import ModbusTcpClient
import time

# RTU: from pymodbus.client import ModbusSerialClient
#      client = ModbusSerialClient(port="/dev/ttyUSB0", baudrate=9600)
client = ModbusTcpClient("192.168.1.100", port=502)
client.connect()

def signed16(v): return v - 65536 if v >= 32768 else v
def to_unsigned(v): return v + 65536 if v < 0 else v

DEMAND_LIMIT = 50.0  # contracted max demand in kW
SOC_RESERVE  = 15.0  # min SOC reserve (%)

# Enable EMS and set demand limit
client.write_register(11060, 1, slave=1)                           # EMS on
client.write_register(11350, int(DEMAND_LIMIT * 100), slave=1)    # 50kW limit

# Verify applied limit
r = client.read_holding_registers(14033, 1, slave=1)
print(f"Applied limit: {r.registers[0] * 0.01:.1f} kW")

while True:
    m = client.read_holding_registers(10765, 1, slave=1)
    meter_kw = signed16(m.registers[0]) * 0.01      # +import/-export

    s = client.read_holding_registers(10671, 1, slave=1)
    soc = s.registers[0] * 0.01

    # Calculate required battery discharge
    if meter_kw > DEMAND_LIMIT and soc > SOC_RESERVE:
        discharge = meter_kw - DEMAND_LIMIT          # shave the peak
    elif meter_kw < 0:
        discharge = -abs(meter_kw) * 0.5             # charge with surplus
    else:
        discharge = 0

    discharge = max(-10.0, min(10.0, discharge))     # clamp to inverter range
    client.write_register(11045, to_unsigned(int(discharge * 100)), slave=1)
    print(f"Meter:{meter_kw:.1f}kW Limit:{DEMAND_LIMIT}kW "
          f"SOC:{soc:.0f}% Bat:{discharge:+.1f}kW")
    time.sleep(1)
const ModbusRTU = require('modbus-serial');
const client = new ModbusRTU();

// RTU: await client.connectRTUBuffered("/dev/ttyUSB0", { baudRate: 9600 });
await client.connectTCP("192.168.1.100", { port: 502 });
client.setID(1);

function signed16(v) { return v >= 32768 ? v - 65536 : v; }
function toUnsigned(v) { return v < 0 ? v + 65536 : v; }

const DEMAND_LIMIT = 50.0; // kW contracted max
const SOC_RESERVE = 15.0;  // min battery reserve %

// Enable EMS and set demand limit
await client.writeRegister(11060, 1);
await client.writeRegister(11350, Math.round(DEMAND_LIMIT * 100));

// Verify
const lim = await client.readHoldingRegisters(14033, 1);
console.log(`Applied limit: ${(lim.data[0] * 0.01).toFixed(1)} kW`);

setInterval(async () => {
    const m = await client.readHoldingRegisters(10765, 1);
    const meterKw = signed16(m.data[0]) * 0.01;

    const s = await client.readHoldingRegisters(10671, 1);
    const soc = s.data[0] * 0.01;

    let discharge = 0;
    if (meterKw > DEMAND_LIMIT && soc > SOC_RESERVE) {
        discharge = meterKw - DEMAND_LIMIT;          // shave excess
    } else if (meterKw < 0) {
        discharge = -Math.abs(meterKw) * 0.5;        // charge with surplus
    }
    discharge = Math.max(-10, Math.min(10, discharge));

    await client.writeRegister(11045, toUnsigned(Math.round(discharge * 100)));
    console.log(`Meter:${meterKw.toFixed(1)}kW SOC:${soc.toFixed(0)}% Bat:${discharge.toFixed(1)}kW`);
}, 1000);
using NModbus;
using System.Net.Sockets;

// RTU: var port = new SerialPort("/dev/ttyUSB0", 9600);
//      var master = factory.CreateRtuMaster(port);
var tcp = new TcpClient("192.168.1.100", 502);
var factory = new ModbusFactory();
var master = factory.CreateMaster(tcp);

short Signed16(ushort v) => v >= 32768 ? (short)(v - 65536) : (short)v;
ushort ToUnsigned(int v) => (ushort)(v < 0 ? v + 65536 : v);

const double DemandLimit = 50.0; // kW
const double SocReserve = 15.0;  // %

master.WriteSingleRegister(1, 11060, 1);                         // EMS on
master.WriteSingleRegister(1, 11350, (ushort)(DemandLimit * 100));

// Verify applied limit
var lim = master.ReadHoldingRegisters(1, 14033, 1);
Console.WriteLine($"Applied limit: {lim[0] * 0.01:F1} kW");

while (true) {
    var m = master.ReadHoldingRegisters(1, 10765, 1);
    double meterKw = Signed16(m[0]) * 0.01;
    var s = master.ReadHoldingRegisters(1, 10671, 1);
    double soc = s[0] * 0.01;

    double discharge = 0;
    if (meterKw > DemandLimit && soc > SocReserve)
        discharge = meterKw - DemandLimit;
    else if (meterKw < 0)
        discharge = -Math.Abs(meterKw) * 0.5;
    discharge = Math.Clamp(discharge, -10.0, 10.0);

    master.WriteSingleRegister(1, 11045, ToUnsigned((int)(discharge * 100)));
    Console.WriteLine($"Meter:{meterKw:F1}kW SOC:{soc:F0}% Bat:{discharge:+F1}kW");
    Thread.Sleep(1000);
}
package main
import (
    "fmt"; "time"; "math"
    "github.com/goburrow/modbus"
    "encoding/binary"
)

func signed16(v uint16) int16 { return int16(v) }
func toUnsigned(v int16) uint16 { return uint16(v) }

func main() {
    // RTU: handler := modbus.NewRTUClientHandler("/dev/ttyUSB0")
    handler := modbus.NewTCPClientHandler("192.168.1.100:502")
    handler.SlaveId = 1
    handler.Connect()
    defer handler.Close()
    client := modbus.NewClient(handler)

    demandLimit := 50.0 // kW contracted max
    socReserve := 15.0  // min SOC %

    client.WriteSingleRegister(11060, 1)                       // EMS on
    client.WriteSingleRegister(11350, uint16(demandLimit*100)) // set limit

    for {
        m, _ := client.ReadHoldingRegisters(10765, 1)
        meterKw := float64(signed16(binary.BigEndian.Uint16(m))) * 0.01
        s, _ := client.ReadHoldingRegisters(10671, 1)
        soc := float64(binary.BigEndian.Uint16(s)) * 0.01

        discharge := 0.0
        if meterKw > demandLimit && soc > socReserve {
            discharge = meterKw - demandLimit
        } else if meterKw < 0 {
            discharge = -math.Abs(meterKw) * 0.5
        }
        discharge = math.Max(-10, math.Min(10, discharge))

        client.WriteSingleRegister(11045, toUnsigned(int16(discharge*100)))
        fmt.Printf("Meter:%.1fkW SOC:%.0f%% Bat:%+.1fkW\n", meterKw, soc, discharge)
        time.Sleep(1 * time.Second)
    }
}
5

Self-Consumption Optimization

RTU + TCP
Maximize use of PV production for local load consumption. Charge the battery with surplus PV energy and discharge to cover load when PV is insufficient. This is the core residential EMS function for reducing grid dependence and electricity bills.

Key Registers

  • 10087 PV_Total_Power — Read, scale 0.01 kW
  • 10765 Meter_Active_Power_Total — Read, scale 0.01 kW, signed (+import/−export)
  • 10671 BMS_Bat1_SOC — Read, scale 0.01 %
  • 10810 Self_Consumption_Rate — Read, scale 0.1 %
  • 11060 EMS_Switch_Set — Write, 1 = enable
  • 11045 Active_Power_Set — Write, scale 0.01 kW, signed (+discharge/−charge)
  • 11108 Grid_tied_SOC_MIN — Write, scale 0.01 %, reserve for evening use
WRITE EMS Enable 11060 = 1
WRITE SOC Reserve 11108 = 10%
READ PV Power 10087 kW
READ Meter 10765 kW ±
READ Battery SOC 10671 %
DECIDE PV Surplus? export → charge
WRITE Power Set 11045 kW ±
LOOP 1s
from pymodbus.client import ModbusTcpClient
import time

# RTU: from pymodbus.client import ModbusSerialClient
#      client = ModbusSerialClient(port="/dev/ttyUSB0", baudrate=9600)
client = ModbusTcpClient("192.168.1.100", port=502)
client.connect()

def signed16(v): return v - 65536 if v >= 32768 else v
def to_unsigned(v): return v + 65536 if v < 0 else v

SOC_MIN = 20.0    # reserve for evening
RAMP = 0.5        # max step per cycle (kW)

# Enable EMS and set SOC reserve
client.write_register(11060, 1, slave=1)                       # EMS on
client.write_register(11108, int(SOC_MIN * 100), slave=1)      # SOC_MIN = 20%

current_set = 0.0  # current dispatch (kW)

while True:
    pv = client.read_holding_registers(10087, 1, slave=1)
    pv_kw = pv.registers[0] * 0.01

    m = client.read_holding_registers(10765, 1, slave=1)
    meter_kw = signed16(m.registers[0]) * 0.01   # +import/-export

    s = client.read_holding_registers(10671, 1, slave=1)
    soc = s.registers[0] * 0.01

    # Target: zero grid exchange. Meter reading IS the error signal.
    target = -meter_kw   # if importing, discharge (+); if exporting, charge (-)

    # Apply ramp rate for smooth transitions
    delta = max(-RAMP, min(RAMP, target - current_set))
    current_set += delta
    current_set = max(-10.0, min(10.0, current_set))

    # SOC protection
    if soc <= SOC_MIN and current_set > 0: current_set = 0  # stop discharge
    if soc >= 99.5 and current_set < 0: current_set = 0     # stop charge

    client.write_register(11045, to_unsigned(int(current_set * 100)), slave=1)

    sc = client.read_holding_registers(10810, 1, slave=1)
    sc_rate = sc.registers[0] * 0.1
    print(f"PV:{pv_kw:.1f}kW Meter:{meter_kw:+.1f}kW SOC:{soc:.0f}% "
          f"Bat:{current_set:+.1f}kW SC:{sc_rate:.0f}%")
    time.sleep(1)
const ModbusRTU = require('modbus-serial');
const client = new ModbusRTU();

// RTU: await client.connectRTUBuffered("/dev/ttyUSB0", { baudRate: 9600 });
await client.connectTCP("192.168.1.100", { port: 502 });
client.setID(1);

function signed16(v) { return v >= 32768 ? v - 65536 : v; }
function toUnsigned(v) { return v < 0 ? v + 65536 : v; }

const SOC_MIN = 20.0; // reserve for evening
const RAMP = 0.5;     // max kW step per cycle
let currentSet = 0.0;

// Enable EMS and set SOC reserve
await client.writeRegister(11060, 1);
await client.writeRegister(11108, Math.round(SOC_MIN * 100));

setInterval(async () => {
    const pv = await client.readHoldingRegisters(10087, 1);
    const pvKw = pv.data[0] * 0.01;

    const m = await client.readHoldingRegisters(10765, 1);
    const meterKw = signed16(m.data[0]) * 0.01;

    const s = await client.readHoldingRegisters(10671, 1);
    const soc = s.data[0] * 0.01;

    // Target zero grid exchange; ramp toward target
    const target = -meterKw;
    const delta = Math.max(-RAMP, Math.min(RAMP, target - currentSet));
    currentSet = Math.max(-10, Math.min(10, currentSet + delta));

    // SOC protection
    if (soc <= SOC_MIN && currentSet > 0) currentSet = 0;
    if (soc >= 99.5 && currentSet < 0) currentSet = 0;

    await client.writeRegister(11045, toUnsigned(Math.round(currentSet * 100)));

    const sc = await client.readHoldingRegisters(10810, 1);
    console.log(`PV:${pvKw.toFixed(1)}kW Meter:${meterKw.toFixed(1)}kW `
              + `SOC:${soc.toFixed(0)}% Bat:${currentSet.toFixed(1)}kW SC:${(sc.data[0]*0.1).toFixed(0)}%`);
}, 1000);
using NModbus;
using System.Net.Sockets;

// RTU: var port = new SerialPort("/dev/ttyUSB0", 9600);
//      var master = factory.CreateRtuMaster(port);
var tcp = new TcpClient("192.168.1.100", 502);
var factory = new ModbusFactory();
var master = factory.CreateMaster(tcp);

short Signed16(ushort v) => v >= 32768 ? (short)(v - 65536) : (short)v;
ushort ToUnsigned(int v) => (ushort)(v < 0 ? v + 65536 : v);

const double SocMin = 20.0;
const double Ramp = 0.5;
double currentSet = 0;

master.WriteSingleRegister(1, 11060, 1);                        // EMS on
master.WriteSingleRegister(1, 11108, (ushort)(SocMin * 100));   // SOC reserve

while (true) {
    var pv = master.ReadHoldingRegisters(1, 10087, 1);
    double pvKw = pv[0] * 0.01;

    var m = master.ReadHoldingRegisters(1, 10765, 1);
    double meterKw = Signed16(m[0]) * 0.01;

    var s = master.ReadHoldingRegisters(1, 10671, 1);
    double soc = s[0] * 0.01;

    // Target zero exchange; apply ramp rate
    double target = -meterKw;
    double delta = Math.Clamp(target - currentSet, -Ramp, Ramp);
    currentSet = Math.Clamp(currentSet + delta, -10.0, 10.0);

    if (soc <= SocMin && currentSet > 0) currentSet = 0;  // protect SOC
    if (soc >= 99.5 && currentSet < 0) currentSet = 0;

    master.WriteSingleRegister(1, 11045, ToUnsigned((int)(currentSet * 100)));
    var sc = master.ReadHoldingRegisters(1, 10810, 1);
    Console.WriteLine($"PV:{pvKw:F1}kW Meter:{meterKw:+F1}kW SOC:{soc:F0}% "
                    + $"Bat:{currentSet:+F1}kW SC:{sc[0]*0.1:F0}%");
    Thread.Sleep(1000);
}
package main
import (
    "fmt"; "time"; "math"
    "github.com/goburrow/modbus"
    "encoding/binary"
)

func signed16(v uint16) int16 { return int16(v) }
func toUnsigned(v int16) uint16 { return uint16(v) }

func main() {
    // RTU: handler := modbus.NewRTUClientHandler("/dev/ttyUSB0")
    handler := modbus.NewTCPClientHandler("192.168.1.100:502")
    handler.SlaveId = 1
    handler.Connect()
    defer handler.Close()
    client := modbus.NewClient(handler)

    socMin := 20.0
    ramp := 0.5
    currentSet := 0.0

    client.WriteSingleRegister(11060, 1)                        // EMS on
    client.WriteSingleRegister(11108, uint16(socMin*100))       // SOC reserve

    for {
        pv, _ := client.ReadHoldingRegisters(10087, 1)
        pvKw := float64(binary.BigEndian.Uint16(pv)) * 0.01

        m, _ := client.ReadHoldingRegisters(10765, 1)
        meterKw := float64(signed16(binary.BigEndian.Uint16(m))) * 0.01

        s, _ := client.ReadHoldingRegisters(10671, 1)
        soc := float64(binary.BigEndian.Uint16(s)) * 0.01

        target := -meterKw
        delta := math.Max(-ramp, math.Min(ramp, target-currentSet))
        currentSet = math.Max(-10, math.Min(10, currentSet+delta))
        if soc <= socMin && currentSet > 0 { currentSet = 0 }
        if soc >= 99.5 && currentSet < 0 { currentSet = 0 }

        client.WriteSingleRegister(11045, toUnsigned(int16(currentSet*100)))
        sc, _ := client.ReadHoldingRegisters(10810, 1)
        scRate := float64(binary.BigEndian.Uint16(sc)) * 0.1
        fmt.Printf("PV:%.1fkW Meter:%+.1fkW SOC:%.0f%% Bat:%+.1fkW SC:%.0f%%\n",
            pvKw, meterKw, soc, currentSet, scRate)
        time.Sleep(1 * time.Second)
    }
}
6

VPP (Virtual Power Plant) Remote Dispatch

TCP
VPP aggregator sends active power commands to a fleet of inverters via cloud dispatch. Requires safety unlock, EMS enable, watchdog heartbeat, and command audit trail. Designed for VPP operators such as sonnen VPP, Next Kraftwerke, and Octopus VPP.

Key Registers

  • 12008 Safety_Unlock — Write 0x55AA to unlock (required before destructive commands)
  • 11060 EMS_Switch_Set — Write 1 to enable EMS
  • 12002 Remote_Mode — Write 1 to enable remote control
  • 11045 Active_Power_Set — Write, scale 0.01 kW (+discharge / -charge)
  • 12010 Comms_Watchdog_Enable — Write 1 (proposed P0)
  • 12011 Comms_Watchdog_Timeout — Write seconds, e.g. 60 (proposed P0)
  • 12012 Comms_Heartbeat — Write any value periodically (proposed P0)
  • 14044 EMS_Control_Active — Read, 1 = active (proposed P0)
  • 14039-14040 Last_Cmd_Timestamp — Read U32_BE, unix epoch (proposed P0)
  • 14002 PCS_Working_Status — Read current inverter state
WRITE Safety Unlock 12008 = 0x55AA
WRITE EMS Enable 11060 = 1
WRITE Remote Mode 12002 = 1
WRITE Watchdog 12010–12011 Enable + 60s
WRITE Power Set 11045 kW ±
VERIFY EMS Active? 14044 = 1
WRITE Heartbeat 12012 keep-alive
READ Timestamp 14039–14040 U32 epoch
LOOP 30s
from pymodbus.client import ModbusTcpClient
import time, struct

client = ModbusTcpClient("192.168.1.100", port=502)
client.connect()
UNIT = 1

# Step 1: Safety unlock
client.write_register(12008, 0x55AA, slave=UNIT)

# Step 2: Enable EMS and remote mode
client.write_register(11060, 1, slave=UNIT)     # EMS on
client.write_register(12002, 1, slave=UNIT)     # Remote mode

# Step 3: Configure watchdog (proposed P0)
client.write_register(12010, 1, slave=UNIT)     # Watchdog enable
client.write_register(12011, 60, slave=UNIT)    # Timeout = 60 s

# Step 4: Set active power dispatch — 25.00 kW discharge
power_kw = 25.0
client.write_register(11045, int(power_kw * 100), slave=UNIT)

# Step 5: Verify EMS is active
rr = client.read_holding_registers(14044, 1, slave=UNIT)
ems_active = rr.registers[0]  # 1 = EMS control active

# Step 6: Read last command timestamp (U32_BE)
rr = client.read_holding_registers(14039, 2, slave=UNIT)
ts = (rr.registers[0] << 16) | rr.registers[1]

# Heartbeat loop — send before watchdog expires
while True:
    client.write_register(12012, 1, slave=UNIT)  # Heartbeat
    time.sleep(30)  # Send every 30 s (timeout is 60 s)
const ModbusRTU = require('modbus-serial');
const client = new ModbusRTU();

async function vppDispatch() {
    await client.connectTCP("192.168.1.100", { port: 502 });
    client.setID(1);

    // Step 1: Safety unlock
    await client.writeRegister(12008, 0x55AA);

    // Step 2: Enable EMS and remote mode
    await client.writeRegister(11060, 1);   // EMS on
    await client.writeRegister(12002, 1);   // Remote mode

    // Step 3: Configure watchdog (proposed P0)
    await client.writeRegister(12010, 1);   // Watchdog enable
    await client.writeRegister(12011, 60);  // Timeout = 60 s

    // Step 4: Dispatch 25.00 kW discharge
    const powerKw = 25.0;
    await client.writeRegister(11045, Math.round(powerKw * 100));

    // Step 5: Verify EMS control active
    const status = await client.readHoldingRegisters(14044, 1);
    const emsActive = status.data[0]; // 1 = active

    // Step 6: Heartbeat loop
    setInterval(async () => {
        await client.writeRegister(12012, 1);
    }, 30000); // Every 30 s
}
vppDispatch().catch(console.error);
using NModbus;
using System.Net.Sockets;

var tcp = new TcpClient("192.168.1.100", 502);
var master = new ModbusFactory().CreateMaster(tcp);
byte unit = 1;

// Step 1: Safety unlock
master.WriteSingleRegister(unit, 12008, 0x55AA);

// Step 2: Enable EMS and remote mode
master.WriteSingleRegister(unit, 11060, 1);  // EMS on
master.WriteSingleRegister(unit, 12002, 1);  // Remote mode

// Step 3: Configure watchdog (proposed P0)
master.WriteSingleRegister(unit, 12010, 1);  // Enable
master.WriteSingleRegister(unit, 12011, 60); // 60 s timeout

// Step 4: Dispatch 25.00 kW discharge
double powerKw = 25.0;
master.WriteSingleRegister(unit, 11045, (ushort)(powerKw * 100));

// Step 5: Verify EMS control
var status = master.ReadHoldingRegisters(unit, 14044, 1);
bool emsActive = status[0] == 1;

// Step 6: Heartbeat loop
while (true) {
    master.WriteSingleRegister(unit, 12012, 1);
    Thread.Sleep(30000); // Every 30 s
}
package main

import (
    "encoding/binary"
    "time"
    "github.com/goburrow/modbus"
)

func main() {
    handler := modbus.NewTCPClientHandler("192.168.1.100:502")
    handler.SlaveId = 1
    handler.Connect()
    defer handler.Close()
    client := modbus.NewClient(handler)

    // Step 1: Safety unlock
    client.WriteSingleRegister(12008, 0x55AA)

    // Step 2: Enable EMS + remote mode
    client.WriteSingleRegister(11060, 1) // EMS on
    client.WriteSingleRegister(12002, 1) // Remote mode

    // Step 3: Watchdog setup (proposed P0)
    client.WriteSingleRegister(12010, 1)  // Enable
    client.WriteSingleRegister(12011, 60) // 60 s timeout

    // Step 4: Dispatch 25.00 kW discharge
    powerVal := uint16(25.0 * 100) // scale 0.01 kW
    client.WriteSingleRegister(11045, powerVal)

    // Step 5: Verify EMS control active
    results, _ := client.ReadHoldingRegisters(14044, 1)
    emsActive := binary.BigEndian.Uint16(results) // 1 = active

    // Step 6: Heartbeat loop
    for {
        client.WriteSingleRegister(12012, 1)
        time.Sleep(30 * time.Second)
    }
    _ = emsActive
}
7

Emergency Stop & Safety Sequence

RTU
Execute emergency shutdown of the inverter in response to a safety event (grid fault, fire detection, operator panic button). Uses RTU for minimum latency on the safety-critical path. Per IEC 62109-1 requirements.

Key Registers

  • 12008 Safety_Unlock — Write 0x55AA (expires in 30 s)
  • 12009 Emergency_Stop — Write 1 to trigger e-stop (proposed P0)
  • 14002 PCS_Working_Status — Read to verify Standby (state = 1)
  • 12000 Power_On_Off — Write 1 to restart after human clearance
  • 14100-14199 Alarm registers — Read to check fault codes
WRITE Safety Unlock 12008 = 0x55AA
WRITE Emergency Stop 12009 = 1
READ PCS Status 14002 State
VERIFY Standby? State = 1
READ Alarm Codes 14100+ Check faults
SETUP Human Clearance Manual OK
WRITE Restart 12000 = 1
from pymodbus.client import ModbusSerialClient  # RTU for safety
# from pymodbus.client import ModbusTcpClient    # TCP alternative
import time

client = ModbusSerialClient(port="/dev/ttyUSB0", baudrate=9600)
# client = ModbusTcpClient("192.168.1.100", port=502)
client.connect()
UNIT = 1

# === EMERGENCY STOP SEQUENCE ===
# Step 1: Safety unlock (must complete e-stop within 30 s)
client.write_register(12008, 0x55AA, slave=UNIT)

# Step 2: Trigger emergency stop (proposed P0)
client.write_register(12009, 1, slave=UNIT)

# Step 3: Verify PCS entered Standby state
time.sleep(1)
rr = client.read_holding_registers(14002, 1, slave=UNIT)
pcs_state = rr.registers[0]
assert pcs_state == 1, f"Expected Standby(1), got {pcs_state}"

# Step 4: Read alarm registers for fault diagnosis
rr = client.read_holding_registers(14100, 20, slave=UNIT)
alarms = rr.registers
active_faults = [i for i, v in enumerate(alarms) if v != 0]
print(f"Active fault registers: {active_faults}")

# === RECOVERY (after human clearance only) ===
# input("Press Enter after safety clearance...")
# client.write_register(12008, 0x55AA, slave=UNIT)
# client.write_register(12000, 1, slave=UNIT)  # Power on
const ModbusRTU = require('modbus-serial');
const client = new ModbusRTU();

async function emergencyStop() {
    // RTU for minimum latency on safety path
    await client.connectRTUBuffered("/dev/ttyUSB0", { baudRate: 9600 });
    // await client.connectTCP("192.168.1.100", { port: 502 });
    client.setID(1);

    // Step 1: Safety unlock (30 s window)
    await client.writeRegister(12008, 0x55AA);

    // Step 2: Trigger emergency stop (proposed P0)
    await client.writeRegister(12009, 1);

    // Step 3: Verify Standby state
    await new Promise(r => setTimeout(r, 1000));
    const pcs = await client.readHoldingRegisters(14002, 1);
    console.log(`PCS state: ${pcs.data[0]} (1 = Standby)`);

    // Step 4: Read alarm registers for fault codes
    const alarms = await client.readHoldingRegisters(14100, 20);
    const faults = alarms.data
        .map((v, i) => v !== 0 ? i + 14100 : null)
        .filter(v => v !== null);
    console.log("Active fault registers:", faults);

    // Recovery: require human clearance
    // await client.writeRegister(12008, 0x55AA);
    // await client.writeRegister(12000, 1); // Power on
}
emergencyStop().catch(console.error);
using NModbus;
using System.IO.Ports;
// using System.Net.Sockets;

// RTU for minimum latency on safety path
var port = new SerialPort("/dev/ttyUSB0", 9600, Parity.None, 8, StopBits.One);
port.Open();
var master = new ModbusFactory().CreateRtuMaster(port);
// var tcp = new TcpClient("192.168.1.100", 502);
// var master = new ModbusFactory().CreateMaster(tcp);
byte unit = 1;

// Step 1: Safety unlock (30 s expiry window)
master.WriteSingleRegister(unit, 12008, 0x55AA);

// Step 2: Emergency stop (proposed P0)
master.WriteSingleRegister(unit, 12009, 1);

// Step 3: Verify Standby state
Thread.Sleep(1000);
var pcs = master.ReadHoldingRegisters(unit, 14002, 1);
Console.WriteLine($"PCS state: {pcs[0]} (1 = Standby)");

// Step 4: Read alarm registers
var alarms = master.ReadHoldingRegisters(unit, 14100, 20);
for (int i = 0; i < alarms.Length; i++)
    if (alarms[i] != 0)
        Console.WriteLine($"Fault at register {14100 + i}: {alarms[i]}");

// Recovery (after human clearance only):
// master.WriteSingleRegister(unit, 12008, 0x55AA);
// master.WriteSingleRegister(unit, 12000, 1);
package main

import (
    "encoding/binary"
    "fmt"
    "time"
    "github.com/goburrow/modbus"
)

func main() {
    // RTU for minimum latency on safety path
    handler := modbus.NewRTUClientHandler("/dev/ttyUSB0")
    handler.BaudRate = 9600
    handler.SlaveId = 1
    // handler := modbus.NewTCPClientHandler("192.168.1.100:502")
    handler.Connect()
    defer handler.Close()
    client := modbus.NewClient(handler)

    // Step 1: Safety unlock (30 s window)
    client.WriteSingleRegister(12008, 0x55AA)

    // Step 2: Emergency stop (proposed P0)
    client.WriteSingleRegister(12009, 1)

    // Step 3: Verify PCS entered Standby
    time.Sleep(1 * time.Second)
    results, _ := client.ReadHoldingRegisters(14002, 1)
    pcsState := binary.BigEndian.Uint16(results)
    fmt.Printf("PCS state: %d (1 = Standby)\n", pcsState)

    // Step 4: Read alarm registers
    alarms, _ := client.ReadHoldingRegisters(14100, 20)
    for i := 0; i < len(alarms)/2; i++ {
        val := binary.BigEndian.Uint16(alarms[i*2:])
        if val != 0 {
            fmt.Printf("Fault at reg %d: %d\n", 14100+i, val)
        }
    }
    // Recovery: require human clearance before restart
}
8

Multi-Battery Port Monitoring

RTU + TCP
Monitor and manage dual battery input ports. Compare SOC, temperature, and power distribution across both ports. Essential for systems with mixed battery configurations or capacity expansion.

Key Registers

  • 10671 BMS_Bat1_SOC — Read, scale 0.01 %
  • 10674 BMS_Bat1_Voltage — Read, scale 0.1 V
  • 10675 BMS_Bat1_Current — Read, scale 0.1 A (signed, +discharge/-charge)
  • 10676 BMS_Bat1_Temperature — Read, scale 0.1 °C
  • 10677-10678 BMS_Bat1_Charge_Energy — Read U32, scale 0.1 kWh
  • 10679-10680 BMS_Bat1_Discharge_Energy — Read U32, scale 0.1 kWh
  • 10683-10684 BMS_Bat1_Cycle_Count — Read U32
  • 10693-10694 BMS_Bat1_Alarm — Read U32 (fault bitmask)
  • 10725 BMS_Bat2_SOC, 10728 Voltage, 10729 Current, 10730 Temp
  • 10731-10732 Bat2_Charge_Energy, 10733-10734 Bat2_Discharge_Energy
  • 10747-10748 BMS_Bat2_Alarm — Read U32
  • 10685 BMS_Bat1_Power (proposed P1), 10739 BMS_Bat2_Power (proposed P1)
SETUP Connect RTU or TCP
READ Battery Port 1 10671–10697 SOC/V/I/T
READ Battery Port 2 10725–10751 SOC/V/I/T
DECIDE Compare SOC + Power
VERIFY Health Check Alarm regs No faults
LOOP 2-5s
from pymodbus.client import ModbusSerialClient, ModbusTcpClient
import struct

# client = ModbusSerialClient(port="/dev/ttyUSB0", baudrate=9600)
client = ModbusTcpClient("192.168.1.100", port=502)
client.connect()
UNIT = 1

def read_u32(addr):
    rr = client.read_holding_registers(addr, 2, slave=UNIT)
    return (rr.registers[0] << 16) | rr.registers[1]

def read_signed(addr):
    rr = client.read_holding_registers(addr, 1, slave=UNIT)
    val = rr.registers[0]
    return val - 65536 if val > 32767 else val

# Poll group 3: Battery port 1 (10671-10697)
rr1 = client.read_holding_registers(10671, 27, slave=UNIT)
bat1_soc       = rr1.registers[0] * 0.01       # %
bat1_voltage   = rr1.registers[3] * 0.1        # V
bat1_current   = read_signed(10675) * 0.1      # A (+dis/-chg)
bat1_temp      = rr1.registers[5] * 0.1        # deg C
bat1_chg_kwh   = read_u32(10677) * 0.1         # kWh
bat1_dis_kwh   = read_u32(10679) * 0.1         # kWh
bat1_cycles    = read_u32(10683)
bat1_alarm     = read_u32(10693)

# Poll group 4: Battery port 2 (10725-10751)
rr2 = client.read_holding_registers(10725, 27, slave=UNIT)
bat2_soc       = rr2.registers[0] * 0.01       # %
bat2_voltage   = rr2.registers[3] * 0.1        # V
bat2_current   = read_signed(10729) * 0.1      # A
bat2_temp      = rr2.registers[5] * 0.1        # deg C
bat2_alarm     = read_u32(10747)

# System-level weighted SOC (equal weight if same capacity)
total_soc = (bat1_soc + bat2_soc) / 2.0
print(f"Bat1: {bat1_soc:.1f}%  Bat2: {bat2_soc:.1f}%  System: {total_soc:.1f}%")
print(f"Alarms — Bat1: 0x{bat1_alarm:08X}  Bat2: 0x{bat2_alarm:08X}")
const ModbusRTU = require('modbus-serial');
const client = new ModbusRTU();

async function readBatteries() {
    // await client.connectRTUBuffered("/dev/ttyUSB0", { baudRate: 9600 });
    await client.connectTCP("192.168.1.100", { port: 502 });
    client.setID(1);

    const toSigned = (v) => v > 32767 ? v - 65536 : v;
    const readU32 = (d, offset) => (d[offset] << 16) | d[offset + 1];

    // Poll group 3: Battery port 1 (10671-10697)
    const b1 = await client.readHoldingRegisters(10671, 27);
    const bat1 = {
        soc:     b1.data[0] * 0.01,                  // %
        voltage: b1.data[3] * 0.1,                    // V
        current: toSigned(b1.data[4]) * 0.1,          // A (+dis/-chg)
        temp:    b1.data[5] * 0.1,                    // deg C
        chgKwh:  readU32(b1.data, 6) * 0.1,           // kWh
        disKwh:  readU32(b1.data, 8) * 0.1,           // kWh
        cycles:  readU32(b1.data, 12),
        alarm:   readU32(b1.data, 22)
    };

    // Poll group 4: Battery port 2 (10725-10751)
    const b2 = await client.readHoldingRegisters(10725, 27);
    const bat2 = {
        soc:     b2.data[0] * 0.01,
        voltage: b2.data[3] * 0.1,
        current: toSigned(b2.data[4]) * 0.1,
        temp:    b2.data[5] * 0.1,
        alarm:   readU32(b2.data, 22)
    };

    const systemSoc = (bat1.soc + bat2.soc) / 2.0;
    console.log(`Bat1: ${bat1.soc.toFixed(1)}%  Bat2: ${bat2.soc.toFixed(1)}%  System: ${systemSoc.toFixed(1)}%`);
}
readBatteries().catch(console.error);
using NModbus;
using System.Net.Sockets;
// using System.IO.Ports;

var tcp = new TcpClient("192.168.1.100", 502);
var master = new ModbusFactory().CreateMaster(tcp);
// var port = new SerialPort("/dev/ttyUSB0", 9600);
// port.Open();
// var master = new ModbusFactory().CreateRtuMaster(port);
byte unit = 1;

uint ReadU32(ushort[] regs, int offset) =>
    ((uint)regs[offset] << 16) | regs[offset + 1];
short ToSigned(ushort v) => (short)v;

// Poll group 3: Battery port 1 (10671-10697)
var b1 = master.ReadHoldingRegisters(unit, 10671, 27);
double bat1Soc     = b1[0] * 0.01;           // %
double bat1Voltage = b1[3] * 0.1;            // V
double bat1Current = ToSigned(b1[4]) * 0.1;  // A (+dis/-chg)
double bat1Temp    = b1[5] * 0.1;            // deg C
uint bat1Alarm     = ReadU32(b1, 22);

// Poll group 4: Battery port 2 (10725-10751)
var b2 = master.ReadHoldingRegisters(unit, 10725, 27);
double bat2Soc     = b2[0] * 0.01;
double bat2Voltage = b2[3] * 0.1;
double bat2Current = ToSigned(b2[4]) * 0.1;
uint bat2Alarm     = ReadU32(b2, 22);

double systemSoc = (bat1Soc + bat2Soc) / 2.0;
Console.WriteLine($"Bat1: {bat1Soc:F1}%  Bat2: {bat2Soc:F1}%  System: {systemSoc:F1}%");
Console.WriteLine($"Alarms - Bat1: 0x{bat1Alarm:X8}  Bat2: 0x{bat2Alarm:X8}");
package main

import (
    "encoding/binary"
    "fmt"
    "github.com/goburrow/modbus"
)

func main() {
    handler := modbus.NewTCPClientHandler("192.168.1.100:502")
    // handler := modbus.NewRTUClientHandler("/dev/ttyUSB0")
    // handler.BaudRate = 9600
    handler.SlaveId = 1
    handler.Connect()
    defer handler.Close()
    client := modbus.NewClient(handler)

    readU32 := func(data []byte, offset int) uint32 {
        hi := binary.BigEndian.Uint16(data[offset*2:])
        lo := binary.BigEndian.Uint16(data[offset*2+2:])
        return uint32(hi)<<16 | uint32(lo)
    }
    toSigned := func(v uint16) int16 { return int16(v) }

    // Poll group 3: Battery port 1 (10671-10697)
    b1, _ := client.ReadHoldingRegisters(10671, 27)
    bat1Soc := float64(binary.BigEndian.Uint16(b1[0:])) * 0.01
    bat1Cur := float64(toSigned(binary.BigEndian.Uint16(b1[8:]))) * 0.1
    bat1Alarm := readU32(b1, 22)

    // Poll group 4: Battery port 2 (10725-10751)
    b2, _ := client.ReadHoldingRegisters(10725, 27)
    bat2Soc := float64(binary.BigEndian.Uint16(b2[0:])) * 0.01
    bat2Alarm := readU32(b2, 22)

    systemSoc := (bat1Soc + bat2Soc) / 2.0
    fmt.Printf("Bat1: %.1f%%  Bat2: %.1f%%  System: %.1f%%\n", bat1Soc, bat2Soc, systemSoc)
    fmt.Printf("Current Bat1: %.1fA  Alarms: 0x%08X / 0x%08X\n", bat1Cur, bat1Alarm, bat2Alarm)
}
9

SG-Ready Heat Pump Coordination

RTU
Coordinate with an SG-Ready heat pump based on PV surplus. Set SG-Ready mode (1-4) to signal the heat pump to increase or decrease consumption. Per German BWP/EHPA SG-Ready standard (VDI 4645).

Key Registers

  • 11355 SG_Ready_Mode — Write 1-4 (proposed P1)
      1 = Blocked (grid emergency, reduce consumption)
      2 = Normal operation
      3 = Recommended on (surplus PV available)
      4 = Force on (maximum consumption requested)
  • 10087 PV_Total_Power — Read, scale 0.01 kW
  • 10765 Meter_Active_Power_Total — Read, scale 0.01 kW (negative = export)
  • 10671 BMS_Bat1_SOC — Read, scale 0.01 %
  • 11060 EMS_Switch_Set — Write 1 to enable EMS
WRITE EMS Enable 11060 = 1
READ PV Power 10087 kW
READ Meter 10765 kW ±
READ Battery SOC 10671 %
DECIDE Which Mode? 1/2/3/4
WRITE SG-Ready 11355 Mode 1-4
LOOP 60s
from pymodbus.client import ModbusSerialClient
# from pymodbus.client import ModbusTcpClient
import time

client = ModbusSerialClient(port="/dev/ttyUSB0", baudrate=9600)
# client = ModbusTcpClient("192.168.1.100", port=502)
client.connect()
UNIT = 1

# Enable EMS
client.write_register(11060, 1, slave=UNIT)

HYSTERESIS_W = 500    # 500 W deadband
HOLD_TIME = 60        # Seconds before mode change
last_mode = 2
last_change = 0

def read_signed(addr):
    rr = client.read_holding_registers(addr, 1, slave=UNIT)
    v = rr.registers[0]
    return (v - 65536 if v > 32767 else v)

while True:
    rr = client.read_holding_registers(10087, 1, slave=UNIT)
    pv_kw = rr.registers[0] * 0.01              # PV generation

    grid_w = read_signed(10765) * 10             # Meter power (W)
    surplus_kw = -grid_w / 1000.0 if grid_w < 0 else 0

    rr = client.read_holding_registers(10671, 1, slave=UNIT)
    soc = rr.registers[0] * 0.01                # Battery SOC %

    # SG-Ready state machine with hysteresis
    if surplus_kw > (5.0 + HYSTERESIS_W/1000) and soc >= 98:
        new_mode = 4   # Force on
    elif surplus_kw > (2.0 + HYSTERESIS_W/1000) and soc > 80:
        new_mode = 3   # Recommended on
    elif surplus_kw < 0.5:
        new_mode = 2   # Normal
    else:
        new_mode = last_mode  # Hold current

    if new_mode != last_mode and time.time() - last_change > HOLD_TIME:
        client.write_register(11355, new_mode, slave=UNIT)
        last_mode = new_mode
        last_change = time.time()

    time.sleep(10)  # Poll every 10 s
const ModbusRTU = require('modbus-serial');
const client = new ModbusRTU();

const HYSTERESIS_KW = 0.5;   // 500 W deadband
const HOLD_TIME_MS = 60000;  // 60 s hold timer
let lastMode = 2, lastChange = 0;

async function sgReadyLoop() {
    await client.connectRTUBuffered("/dev/ttyUSB0", { baudRate: 9600 });
    // await client.connectTCP("192.168.1.100", { port: 502 });
    client.setID(1);
    await client.writeRegister(11060, 1);  // Enable EMS

    const toSigned = (v) => v > 32767 ? v - 65536 : v;

    setInterval(async () => {
        const pv = await client.readHoldingRegisters(10087, 1);
        const pvKw = pv.data[0] * 0.01;

        const meter = await client.readHoldingRegisters(10765, 1);
        const gridW = toSigned(meter.data[0]) * 10;
        const surplusKw = gridW < 0 ? -gridW / 1000 : 0;

        const bat = await client.readHoldingRegisters(10671, 1);
        const soc = bat.data[0] * 0.01;

        // SG-Ready state machine
        let newMode = lastMode;
        if (surplusKw > 5.0 + HYSTERESIS_KW && soc >= 98) newMode = 4;
        else if (surplusKw > 2.0 + HYSTERESIS_KW && soc > 80) newMode = 3;
        else if (surplusKw < 0.5) newMode = 2;

        const now = Date.now();
        if (newMode !== lastMode && now - lastChange > HOLD_TIME_MS) {
            await client.writeRegister(11355, newMode);
            console.log(`SG-Ready: mode ${lastMode} -> ${newMode} (SOC: ${soc.toFixed(1)}%)`);
            lastMode = newMode;
            lastChange = now;
        }
    }, 10000); // Poll every 10 s
}
sgReadyLoop().catch(console.error);
using NModbus;
using System.IO.Ports;
// using System.Net.Sockets;

var port = new SerialPort("/dev/ttyUSB0", 9600, Parity.None, 8, StopBits.One);
port.Open();
var master = new ModbusFactory().CreateRtuMaster(port);
// var tcp = new TcpClient("192.168.1.100", 502);
// var master = new ModbusFactory().CreateMaster(tcp);
byte unit = 1;

master.WriteSingleRegister(unit, 11060, 1);  // Enable EMS

int lastMode = 2;
DateTime lastChange = DateTime.MinValue;
const double HysteresisKw = 0.5;

while (true) {
    var pv = master.ReadHoldingRegisters(unit, 10087, 1);
    double pvKw = pv[0] * 0.01;

    var meter = master.ReadHoldingRegisters(unit, 10765, 1);
    double gridW = (short)meter[0] * 10.0;
    double surplusKw = gridW < 0 ? -gridW / 1000 : 0;

    var bat = master.ReadHoldingRegisters(unit, 10671, 1);
    double soc = bat[0] * 0.01;

    // SG-Ready state machine with hysteresis
    int newMode = lastMode;
    if (surplusKw > 5.0 + HysteresisKw && soc >= 98) newMode = 4;
    else if (surplusKw > 2.0 + HysteresisKw && soc > 80) newMode = 3;
    else if (surplusKw < 0.5) newMode = 2;

    if (newMode != lastMode && (DateTime.Now - lastChange).TotalSeconds > 60) {
        master.WriteSingleRegister(unit, 11355, (ushort)newMode);
        Console.WriteLine($"SG-Ready: {lastMode} -> {newMode} (SOC: {soc:F1}%)");
        lastMode = newMode;
        lastChange = DateTime.Now;
    }
    Thread.Sleep(10000);
}
package main

import (
    "encoding/binary"
    "fmt"
    "time"
    "github.com/goburrow/modbus"
)

func main() {
    handler := modbus.NewRTUClientHandler("/dev/ttyUSB0")
    handler.BaudRate = 9600
    handler.SlaveId = 1
    // handler := modbus.NewTCPClientHandler("192.168.1.100:502")
    handler.Connect()
    defer handler.Close()
    client := modbus.NewClient(handler)

    client.WriteSingleRegister(11060, 1) // Enable EMS

    lastMode, hysteresisKw := 2, 0.5
    lastChange := time.Now().Add(-2 * time.Minute)

    for {
        pv, _ := client.ReadHoldingRegisters(10087, 1)
        pvKw := float64(binary.BigEndian.Uint16(pv)) * 0.01

        meter, _ := client.ReadHoldingRegisters(10765, 1)
        gridW := float64(int16(binary.BigEndian.Uint16(meter))) * 10
        surplusKw := 0.0
        if gridW < 0 { surplusKw = -gridW / 1000 }

        bat, _ := client.ReadHoldingRegisters(10671, 1)
        soc := float64(binary.BigEndian.Uint16(bat)) * 0.01

        // SG-Ready state machine
        newMode := lastMode
        if surplusKw > 5.0+hysteresisKw && soc >= 98 { newMode = 4
        } else if surplusKw > 2.0+hysteresisKw && soc > 80 { newMode = 3
        } else if surplusKw < 0.5 { newMode = 2 }

        if newMode != lastMode && time.Since(lastChange) > 60*time.Second {
            client.WriteSingleRegister(11355, uint16(newMode))
            fmt.Printf("SG-Ready: %d -> %d (SOC: %.1f%%, PV: %.1fkW)\n",
                lastMode, newMode, soc, pvKw)
            lastMode = newMode
            lastChange = time.Now()
        }
        time.Sleep(10 * time.Second)
    }
}
10

Grid Code Compliance Setup (EN 50549 / VDE-AR-N 4105)

TCP
Commission inverter for European grid code compliance. Configure reactive power control (Cosφ(P) curve), frequency-watt droop response P(f), power curtailment limits, and ramp rates. Required for grid connection approval in EU markets per EN 50549 and VDE-AR-N 4105.

Key Registers

  • 11300 Reactive_Power_Mode — Write (0=Off, 1=Fixed PF, 2=Fixed Q, 3=Cosφ(P), 4=Q(U)) (proposed P0)
  • 11301 Fixed_Power_Factor — Write, scale 0.001 (e.g. 950 = 0.950 lagging) (proposed P0)
  • 11330 Freq_Watt_Enable — Write 1 (proposed P0)
  • 11331 Freq_Watt_HzStr — Write, scale 0.01 Hz (e.g. 5020 = 50.20 Hz) (proposed P0)
  • 11332 Freq_Watt_HzStop — Write, scale 0.01 Hz (e.g. 5180 = 51.80 Hz) (proposed P0)
  • 11333 Freq_Watt_Droop_Pct — Write, scale 0.1 % (e.g. 50 = 5.0%) (proposed P0)
  • 11320 WMaxLimPct_Enable — Write 1 (proposed P0)
  • 11321 WMaxLimPct — Write, scale 0.1 % (e.g. 700 = 70.0% of rated) (proposed P0)
  • 11324 WGra — Write, scale 0.1 %Pn/s (e.g. 100 = 10.0 %Pn/s) (proposed P0)
WRITE Reactive Mode 11300 = 3 (Cosφ)
WRITE Power Factor 11301 = 0.950
WRITE Freq-Watt 11330–11333 Enable + droop
WRITE Power Limit % 11320–11321 70%
WRITE Ramp Rate 11324 10 %Pn/s
READ Readback All 11300+ Verify settings
VERIFY Commissioned All match
from pymodbus.client import ModbusTcpClient
# from pymodbus.client import ModbusSerialClient

client = ModbusTcpClient("192.168.1.100", port=502)
# client = ModbusSerialClient(port="/dev/ttyUSB0", baudrate=9600)
client.connect()
UNIT = 1

# === VDE-AR-N 4105 Commissioning Script ===

# 1. Reactive Power: Cosfi(P) curve mode
client.write_register(11300, 3, slave=UNIT)      # Mode 3 = Cosfi(P)
client.write_register(11301, 950, slave=UNIT)     # PF = 0.950 (950 * 0.001)

# 2. Frequency-Watt (P(f)) droop response
client.write_register(11330, 1, slave=UNIT)       # Enable freq-watt
client.write_register(11331, 5020, slave=UNIT)    # Start: 50.20 Hz
client.write_register(11332, 5180, slave=UNIT)    # Stop:  51.80 Hz
client.write_register(11333, 50, slave=UNIT)      # Droop: 5.0% (50 * 0.1)

# 3. Power curtailment limit
client.write_register(11320, 1, slave=UNIT)       # Enable WMaxLim
client.write_register(11321, 700, slave=UNIT)     # Limit: 70.0% (700 * 0.1)

# 4. Ramp rate
client.write_register(11324, 100, slave=UNIT)     # 10.0 %Pn/s (100 * 0.1)

# 5. Verify by reading back all settings
readback = {
    "ReactiveMode": client.read_holding_registers(11300, 1, slave=UNIT).registers[0],
    "PowerFactor":  client.read_holding_registers(11301, 1, slave=UNIT).registers[0] * 0.001,
    "FreqWatt":     client.read_holding_registers(11330, 1, slave=UNIT).registers[0],
    "HzStart":      client.read_holding_registers(11331, 1, slave=UNIT).registers[0] * 0.01,
    "HzStop":       client.read_holding_registers(11332, 1, slave=UNIT).registers[0] * 0.01,
    "Droop%":       client.read_holding_registers(11333, 1, slave=UNIT).registers[0] * 0.1,
    "WMaxLim%":     client.read_holding_registers(11321, 1, slave=UNIT).registers[0] * 0.1,
    "RampRate":     client.read_holding_registers(11324, 1, slave=UNIT).registers[0] * 0.1,
}
for k, v in readback.items():
    print(f"  {k}: {v}")
print("VDE-AR-N 4105 commissioning complete.")
const ModbusRTU = require('modbus-serial');
const client = new ModbusRTU();

async function commissionVDE4105() {
    await client.connectTCP("192.168.1.100", { port: 502 });
    // await client.connectRTUBuffered("/dev/ttyUSB0", { baudRate: 9600 });
    client.setID(1);

    // 1. Reactive power: Cosfi(P) curve mode
    await client.writeRegister(11300, 3);        // Mode 3 = Cosfi(P)
    await client.writeRegister(11301, 950);      // PF = 0.950

    // 2. Frequency-Watt P(f) droop response
    await client.writeRegister(11330, 1);        // Enable
    await client.writeRegister(11331, 5020);     // Start: 50.20 Hz
    await client.writeRegister(11332, 5180);     // Stop:  51.80 Hz
    await client.writeRegister(11333, 50);       // Droop: 5.0%

    // 3. Power curtailment
    await client.writeRegister(11320, 1);        // Enable WMaxLim
    await client.writeRegister(11321, 700);      // Limit: 70.0%

    // 4. Ramp rate
    await client.writeRegister(11324, 100);      // 10.0 %Pn/s

    // 5. Verify readback
    const verify = async (addr, name, scale) => {
        const r = await client.readHoldingRegisters(addr, 1);
        console.log(`  ${name}: ${(r.data[0] * scale).toFixed(2)}`);
    };
    console.log("VDE-AR-N 4105 readback:");
    await verify(11300, "ReactiveMode", 1);
    await verify(11301, "PowerFactor", 0.001);
    await verify(11331, "HzStart", 0.01);
    await verify(11332, "HzStop", 0.01);
    await verify(11333, "Droop%", 0.1);
    await verify(11321, "WMaxLim%", 0.1);
    await verify(11324, "RampRate", 0.1);
    console.log("Commissioning complete.");
}
commissionVDE4105().catch(console.error);
using NModbus;
using System.Net.Sockets;
// using System.IO.Ports;

var tcp = new TcpClient("192.168.1.100", 502);
var master = new ModbusFactory().CreateMaster(tcp);
// var port = new SerialPort("/dev/ttyUSB0", 9600);
// port.Open();
// var master = new ModbusFactory().CreateRtuMaster(port);
byte unit = 1;

// === VDE-AR-N 4105 Commissioning ===

// 1. Reactive power: Cosfi(P) curve
master.WriteSingleRegister(unit, 11300, 3);       // Mode 3 = Cosfi(P)
master.WriteSingleRegister(unit, 11301, 950);     // PF = 0.950

// 2. Frequency-Watt droop
master.WriteSingleRegister(unit, 11330, 1);       // Enable
master.WriteSingleRegister(unit, 11331, 5020);    // Start: 50.20 Hz
master.WriteSingleRegister(unit, 11332, 5180);    // Stop:  51.80 Hz
master.WriteSingleRegister(unit, 11333, 50);      // Droop: 5.0%

// 3. Power curtailment
master.WriteSingleRegister(unit, 11320, 1);       // Enable
master.WriteSingleRegister(unit, 11321, 700);     // 70.0% of rated

// 4. Ramp rate
master.WriteSingleRegister(unit, 11324, 100);     // 10.0 %Pn/s

// 5. Verify readback
Console.WriteLine("VDE-AR-N 4105 readback:");
var pairs = new (ushort addr, string name, double scale)[] {
    (11300, "ReactiveMode", 1), (11301, "PowerFactor", 0.001),
    (11331, "HzStart", 0.01), (11332, "HzStop", 0.01),
    (11333, "Droop%", 0.1), (11321, "WMaxLim%", 0.1), (11324, "RampRate", 0.1)
};
foreach (var (addr, name, scale) in pairs) {
    var val = master.ReadHoldingRegisters(unit, addr, 1);
    Console.WriteLine($"  {name}: {val[0] * scale:F2}");
}
Console.WriteLine("Commissioning complete.");
package main

import (
    "encoding/binary"
    "fmt"
    "github.com/goburrow/modbus"
)

func main() {
    handler := modbus.NewTCPClientHandler("192.168.1.100:502")
    // handler := modbus.NewRTUClientHandler("/dev/ttyUSB0")
    // handler.BaudRate = 9600
    handler.SlaveId = 1
    handler.Connect()
    defer handler.Close()
    client := modbus.NewClient(handler)

    // 1. Reactive power: Cosfi(P) curve
    client.WriteSingleRegister(11300, 3)      // Mode 3
    client.WriteSingleRegister(11301, 950)    // PF = 0.950

    // 2. Frequency-Watt P(f) droop
    client.WriteSingleRegister(11330, 1)      // Enable
    client.WriteSingleRegister(11331, 5020)   // 50.20 Hz start
    client.WriteSingleRegister(11332, 5180)   // 51.80 Hz stop
    client.WriteSingleRegister(11333, 50)     // 5.0% droop

    // 3. Power curtailment
    client.WriteSingleRegister(11320, 1)      // Enable
    client.WriteSingleRegister(11321, 700)    // 70.0%

    // 4. Ramp rate
    client.WriteSingleRegister(11324, 100)    // 10.0 %Pn/s

    // 5. Verify readback
    type reg struct{ addr uint16; name string; scale float64 }
    regs := []reg{
        {11300, "ReactiveMode", 1}, {11301, "PowerFactor", 0.001},
        {11331, "HzStart", 0.01}, {11332, "HzStop", 0.01},
        {11333, "Droop%", 0.1}, {11321, "WMaxLim%", 0.1},
        {11324, "RampRate", 0.1},
    }
    fmt.Println("VDE-AR-N 4105 readback:")
    for _, r := range regs {
        data, _ := client.ReadHoldingRegisters(r.addr, 1)
        val := float64(binary.BigEndian.Uint16(data)) * r.scale
        fmt.Printf("  %s: %.2f\n", r.name, val)
    }
    fmt.Println("Commissioning complete.")
}

Development Roadmap

All proposed registers grouped by implementation priority. Based on competitor analysis (SMA, Fronius, SolarEdge, Huawei, Sigenergy, Solinteg) and EU grid code requirements (EN 50549).

P0
Must-Have (49 regs)
P1
Important (33 regs)
P2
Planned (5 regs)
P0 Must-Have — Required for EU Market & Safety 49 registers

Communications Watchdog & Emergency Stop

Every major competitor (SMA, Fronius, Solinteg) implements a watchdog timer. Without it, if the EMS crashes or loses connection, the inverter continues operating in the last-commanded state indefinitely — a safety hazard. Emergency stop is required by all EMS integrators.

12007Emergency_StopImmediately cease all power conversion. Latches until cleared.
12010Comms_Watchdog_EnableEnable communication watchdog timer
12011Comms_Watchdog_TimeoutTimeout duration in seconds (10-600)
12012Comms_HeartbeatWrite any value to reset the watchdog timer
12013Comms_Timeout_ActionFailsafe action: Stop / Self-consumption / Hold / Zero-export
14031Comms_Watchdog_StatusRead-only watchdog state feedback
14032Comms_Heartbeat_AgeRead-only time since last heartbeat

Reactive Power Control

Required by EN 50549-1/2 for European grid connection. German VDE-AR-N 4105 mandates Cosφ(P) curve. All competitors (SMA SunSpec 124, Fronius, SolarEdge) provide full reactive power control. Without these registers, MATIC cannot pass European grid certification for EMS-controlled installations.

11300Reactive_Power_ModeMode select: Off / Fixed PF / Fixed Q / Cosφ(P) / Q(U)
11301Reactive_Power_PF_SetPower factor setpoint (-1.000 to +1.000)
11302Reactive_Power_Q_SetReactive power setpoint in kVar
11303Reactive_Power_Q_MaxMaximum reactive power capability
11304-11CosPhiP Curve [8 regs]4 points (P%, Cosφ) defining power-factor-vs-power curve
11312-19QU Curve [8 regs]4 points (V%, Q%) defining reactive-power-vs-voltage curve

Active Power Limit Percentage (WMaxLimPct)

EN 50549 & VDE-AR-N 4105 require curtailment as % of rated power. German 70% rule (§9 EEG) and all grid operator dispatch commands use %. SMA 40016, Fronius WMaxLimPct, SunSpec 123. Without this, grid operators cannot remotely curtail the inverter per EU regulation.

11320Active_Power_Limit_Pct_EnableSwitch between absolute kW and percentage limit mode
11321Active_Power_Limit_PctPower limit as 0-100.0% of rated power (700 = 70%)

Power Ramp Rate / Gradient (WGra)

EN 50549-1 §5.4.2 mandates ≤10% Pn/min ramp rate for reconnection after grid trip. SMA WGra (40236), Fronius WGra (SunSpec 124). Without this, inverter cannot pass EU grid certification for EMS-controlled installations.

11322Power_Ramp_Rate_UpActive power increase rate (%Pn/s)
11323Power_Ramp_Rate_DownActive power decrease rate (%Pn/s)
11324Reconnect_Ramp_RatePost-reconnection ramp rate (%Pn/min, default 10)

Frequency-Watt P(f) Droop Response

EN 50549-1 §5.4.4 LFSM-O/LFSM-U: mandatory for all EU-connected DER. VDE-AR-N 4105 requires 5% droop. All competitors (SMA, Fronius, SolarEdge, Huawei) implement P(f). SunSpec Model 712. Without P(f), MATIC cannot obtain EU grid connection certificates.

11330Freq_Watt_EnableEnable frequency-dependent active power response
11331-33Over-Freq LFSM-O [3 regs]Start Hz, stop Hz, droop gradient for over-frequency power reduction
11334-36Under-Freq LFSM-U [3 regs]Start Hz, stop Hz, gradient for under-frequency power increase

Command Feedback / Power Limit Readback

EMS sends commands but has no way to confirm they were applied. All competitors provide readback registers. SMA 30231, Fronius WMaxLimPct_RB. Without feedback, EMS operates blind — VPP platforms will not integrate.

14033Applied_Power_Limit_PctCurrently applied power limit as % of Pn (readback)
14034Applied_Power_Limit_AbsActual max power after all limits applied
14035Active_Power_Limit_SourceBitmap showing which source is limiting power

TOU Weekday/Weekend & Demand Management

Dynamic tariff providers (Tibber, aWATTar, Octopus) use different rates for weekdays vs weekends. Without day-of-week awareness, TOU schedule is limited. Demand power limit is essential for C&I installations with contracted MDI limits to avoid penalty charges.

11254TOU_Day_Of_Week_MaskDay-of-week bitmap for TOU schedule (Mon-Sun, weekday/weekend differentiation)
11350Demand_Power_LimitSite maximum grid import limit (MDI) for commercial peak shaving

Protocol Version & Command Audit

EMS must identify protocol version at connection time for firmware-safe integration. Command timestamps enable audit trails and debugging of stale/lost commands — required by VPP compliance logging.

13036Protocol_VersionProtocol version number (major*100+minor) for EMS compatibility
14039–14040Last_Cmd_TimestampUnix epoch timestamp of last EMS command execution (U32_BE)

Safety Unlock & EMS Control Readback

Destructive commands (Factory Reset, Restart, Emergency Stop) must be protected against accidental writes from corrupted frames. EMS must also confirm its control flags are active — if a user disables EMS from the front panel, the EMS must detect this immediately. SMA requires Grid Guard PIN. Fronius uses PIN-protected mode.

12008Safety_UnlockWrite 0x55AA to unlock destructive commands for 5 seconds
14044EMS_Control_ActiveRead-only: is EMS control currently active? (0=disabled, 1=active)
14045Remote_Mode_StatusRead-only: is remote mode active? Detects front-panel override.
P1 Important — Competitive Feature Parity 33 registers

Per-Phase Power Control

Sigenergy and Solinteg support per-phase active power setpoints. Critical for unbalanced 3-phase loads in commercial installations and German market (phase balancing requirements).

11046Per_Phase_Power_EnableEnable per-phase control vs total-only
11047-49Active_Power_Set_A/B/CPer-phase active power setpoints (x0.01 kW each)

Load Power Monitoring

EMS systems need direct load power data for accurate optimization. Currently EMS must calculate load = PV + Battery + Grid - Export, which introduces errors. SMA (30775) and Solinteg (40020) expose load power directly.

10800–10801Load_Power_TotalTotal load active power (S32_BE, 2 registers)
10802–10804Load_Power_A/B/CPer-phase load power

TOU Enhancement & Export on Comms Loss

TOU charge/discharge power limits are essential for commercial peak shaving. Export control on comms loss is a safety feature (Fronius WGra, SMA implement this).

11250-51Eco_Charge/Discharge_PowerTOU period charge and discharge power limits
11252-53Eco_Target/Min_SOCTOU target SOC for valley charging and minimum SOC for peak
11116-17Export_On_Comms_LossExport behavior and limit when communication is lost

Battery Power Direct Readout & BMS Limits

Every competitor provides a direct battery power register (kW). Currently EMS must multiply V×I which is error-prone. BMS charge/discharge limits let EMS know what the battery actually allows vs what was requested.

10685BMS_Bat1_PowerBattery Port 1 active power (+discharge/-charge), direct kW readout
10739BMS_Bat2_PowerBattery Port 2 active power, direct kW readout
10686-87BMS_Bat1_Max_Charge/DischargeBMS-allowed charge and discharge limits (may differ from EMS setting)
10740-41BMS_Bat2_Max_Charge/DischargeBattery Port 2 BMS limits

PV Energy Totals & Inverter PF

Total PV DC input production (today/lifetime) sums all MPPT tracker output regardless of destination. SMA 30535/30529, Fronius DAY_ENERGY. Inverter PF needed to verify reactive power control.

10797PV_Energy_TodayTotal PV production today (kWh), regardless of destination
10798–10799PV_Energy_TotalLifetime PV production (kWh), 2 registers
10009Inverter_Power_FactorInverter displacement power factor for reactive power verification

Derating Feedback

EMS commands 15kW but inverter outputs only 10kW due to temperature derating. Without derating feedback, EMS cannot distinguish between a failed command and thermal protection. SMA 30233.

14036Derating_StatusBitmap of active derating sources (temp, voltage, BMS, etc.)
14037Derating_Power_LimitCurrent max power after derating (kW)

System Key Performance Indicators & Smart Home

Self-consumption and autarky rates are the #1 and #2 dashboard KPIs for residential users. Every competitor (SMA, sonnen, E3DC) displays these prominently. SG-Ready is mandatory for German heat pump coordination.

10810Self_Consumption_RateSelf-consumption rate % (PV self-consumed / PV total)
10812Autarky_RateSelf-sufficiency rate % (self-supplied / total consumption)
11355SG_Ready_ModeSG-Ready 2-bit signal for heat pump coordination (German standard)

Communication Diagnostics

Remote diagnostics of Modbus communication quality. Essential for troubleshooting wiring issues, baud rate mismatches, and WiFi/4G signal degradation in field installations.

13040Modbus_Msg_CountTotal Modbus messages received (rolling counter)
13041Modbus_Err_CountTotal Modbus errors (CRC + timeout + exceptions)
13042Modbus_Last_Err_CodeLast error type (CRC/Timeout/Illegal Function/Address/Value)
14043Comms_RSSIWiFi/4G signal strength in dBm for remote diagnostics
14046Mode_Change_ReasonBit field of last mode change trigger (EMS/panel/grid fault/BMS/watchdog)
P2 Planned — Future Capabilities 5 registers
11155Safety_Code_SetCountry-specific safety regulation selector (SMA 40163, SolarEdge F142)
11340Grid Service / VPP ModeFCR/aFRR/VPP participation mode selector
10688-89Battery Capacity & EnergyRated capacity (kWh) and available energy for VPP dispatch planning
14038Active Safety CodeCurrently active grid code readback