Overview
Complete Modbus register map for Wattsonic MATIC hybrid inverter series. Includes both implemented registers and proposed additions for development team review.
Communication Parameters
9600, 8N11 (configurable 1-255)Supported Modbus Functions
Register Address Map
Sign Convention
EMS Integration Notes
- Enable EMS mode by writing
1to register11060(EMS_Switch_Set) before sending control commands. - Set
Remote_Mode (12002) = 1to enable remote control. - All power values use scale factor as specified. E.g., register value
2500with scale factor 0.01 kW =25.00 kW. - SOC values use scale factor 0.01%. E.g., register value
8000=80.00%. - TOU time values are in minutes from midnight (0-1440). E.g.,
480= 08:00,1080= 18:00. - Meter Current Scale: Meter current registers (10758-10760) use scale
0.1 Awhile inverter/grid currents use0.01 A. Handle both scale factors in your parser. - [Proposed] Read
Protocol_Version (13036)at connection time to determine available registers for firmware-safe integration. - [Proposed] Enable communications watchdog
12010and send heartbeat to12012periodically 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.
| Type | Length | Description | Example | Bytes |
|---|---|---|---|---|
| VALUE_U16 | 2 bytes | Unsigned 16-bit integer, Big-Endian | 65244 | FE DC |
| VALUE_S16 | 2 bytes | Signed 16-bit integer, Big-Endian | -292 | FE DC |
| VALUE_U32_BE | 4 bytes | Unsigned 32-bit, Big-Endian (2 registers, high word first) | 4275878552 | FE DC BA 98 |
| VALUE_S32_BE | 4 bytes | Signed 32-bit, Big-Endian (2 registers, high word first) | -19088744 | FE DC BA 98 |
| ASCII_STRING32 | 32 bytes | ASCII string, 32 bytes (16 registers) | ABC123... | 41 42 43 ... |
| BIT_BOOLEAN | 1 bit | Boolean flag within a 16-bit register (bit 0-15) | bit0=1 | 00 01 |
Modbus Protocol Compliance
SPEC| Parameter | Value | Notes |
|---|---|---|
| Supported Functions | 0x03, 0x06, 0x10 | 0x04 (Read Input) is NOT supported. All registers use 0x03 (Read Holding). |
| Max Registers per Read | 125 | 250 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 Interval | 200 ms | Minimum time between consecutive requests. Faster polling may overrun the serial buffer. |
| Write Validation | Firmware-enforced | Out-of-range values return Exception 03 (Illegal Data Value). Values are clamped to min/max documented per register. |
Modbus Exception Codes
ERROR| Code | Name | Trigger Conditions |
|---|---|---|
| 01 | Illegal Function | Function code 0x06/0x10 used on read-only register (10xxx, 13xxx, 14xxx). Function code 0x04 sent (not supported). |
| 02 | Illegal Data Address | Register address does not exist or falls in a reserved/gap range. Read request spans beyond valid address block. |
| 03 | Illegal Data Value | Written value exceeds documented min/max range. Destructive command sent without Safety_Unlock (12008). Watchdog timeout < 10s. |
| 04 | Server Device Failure | Internal firmware error during register access. Retry after 1 second. |
| 06 | Server Device Busy | Firmware update in progress. All writes rejected. Retry after update completes. |
Recommended Polling Groups
OPTIMIZATION| Group | Address Range | Registers | Interval | Purpose |
|---|---|---|---|---|
| 1. AC & Grid | 10000 - 10064 | 65 | 1-2s | Inverter output, grid, backup power (single read) |
| 2. PV / DC | 10087 - 10105 | 19 | 2-5s | PV strings + temperatures |
| 3. Battery Port 1 | 10671 - 10697 | 27 | 2-5s | SOC, voltage, current, energy, alarms |
| 4. Battery Port 2 | 10725 - 10751 | 27 | 2-5s | Battery Port 2 (if installed) |
| 5. Meter | 10752 - 10783 | 32 | 1-2s | External meter data |
| 6. Energy | 10785 - 10799 | 15 | 30-60s | Energy counters (daily + lifetime) |
| 6a. Load & KPIs | 10800 - 10812 | 13 | 1-2s | Load power + system KPIs |
| 7. Status | 14000 - 14046 | 47 | 2-5s | Operating state, alarms, feedback |
| 8. Settings RB | 11000 - 11006 | 7 | 60s | RTC, verify settings (on-change) |
Data Type Audit — v2.0 Migration Notes
ACTION REQUIRED- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
Monitoring Registers
READ-ONLY| Status | Register | Name | Description | Type | Unit | Scale | Notes |
|---|---|---|---|---|---|---|---|
| 1. AC Inverter Output | |||||||
| Done | 10000 | Inverter_Frequency | Inverter Output Frequency | S16 | Hz | 0.01 | |
| Done | 10001 | Inverter_Voltage_A | Phase A Inverter Voltage (RMS) | S16 | V | 0.1 | |
| Done | 10002 | Inverter_Voltage_B | Phase B Inverter Voltage (RMS) | S16 | V | 0.1 | |
| Done | 10003 | Inverter_Voltage_C | Phase C Inverter Voltage (RMS) | S16 | V | 0.1 | |
| Done | 10004 | Inverter_Current_A | Phase A Inverter Current (RMS) | S16 | A | 0.01 | |
| Done | 10005 | Inverter_Current_B | Phase B Inverter Current (RMS) | S16 | A | 0.01 | |
| Done | 10006 | Inverter_Current_C | Phase C Inverter Current (RMS) | S16 | A | 0.01 | |
| Done | 10010 | Inverter_Active_Power_A | Inverter Phase A Active Power | S16 | kW | 0.01 | |
| Done | 10013 | Inverter_Active_Power_B | Inverter Phase B Active Power | S16 | kW | 0.01 | |
| Done | 10016 | Inverter_Active_Power_C | Inverter Phase C Active Power | S16 | kW | 0.01 | |
| Done | 10019 | Inverter_Active_Power_Total | Total Active Power | S16 | kW | 0.01 | |
| Done | 10020 | Inverter_Reactive_Power_Total | Total Reactive Power | S16 | kVar | 0.01 | |
| Done | 10021 | Inverter_Apparent_Power_Total | Total Apparent Power | S16 | kVA | 0.01 | |
| P1 | 10009 | Inverter_Power_Factor | Inverter Displacement Power Factor | S16 | 0.001 | -1.000 to +1.000. Verify reactive power control is working.Ref: SunSpec 103 PF, SMA 30821 | |
| 2. AC Grid Side | |||||||
| Done | 10022 | Grid_Frequency | Grid Frequency at PCC | S16 | Hz | 0.01 | |
| Done | 10023 | Grid_Voltage_A | Phase A Grid Voltage (RMS) | S16 | V | 0.1 | |
| Done | 10024 | Grid_Voltage_B | Phase B Grid Voltage (RMS) | S16 | V | 0.1 | |
| Done | 10025 | Grid_Voltage_C | Phase C Grid Voltage (RMS) | S16 | V | 0.1 | |
| Done | 10026 | Grid_Current_A | Phase A Grid Current (RMS) | S16 | A | 0.01 | |
| Done | 10027 | Grid_Current_B | Phase B Grid Current (RMS) | S16 | A | 0.01 | |
| Done | 10028 | Grid_Current_C | Phase C Grid Current (RMS) | S16 | A | 0.01 | |
| Done | 10032 | Grid_Active_Power_A | Phase A Grid Active Power | S16 | kW | 0.01 | +import / -export |
| Done | 10035 | Grid_Active_Power_B | Phase B Grid Active Power | S16 | kW | 0.01 | +import / -export |
| Done | 10038 | Grid_Active_Power_C | Phase C Grid Active Power | S16 | kW | 0.01 | +import / -export |
| Done | 10041 | Grid_Active_Power_Total | Total Grid Active Power | S16 | kW | 0.01 | +import / -export |
| Done | 10042 | Grid_Reactive_Power_Total | Total Grid Reactive Power | S16 | kVar | 0.01 | |
| Done | 10043 | Grid_Apparent_Power_Total | Total Grid Apparent Power | S16 | kVA | 0.01 | |
| 3. Backup Load Output | |||||||
| Done | 10044 | Backup_Voltage_A | Backup Output Voltage Phase A (RMS) | S16 | V | 0.1 | |
| Done | 10045 | Backup_Voltage_B | Backup Output Voltage Phase B (RMS) | S16 | V | 0.1 | |
| Done | 10046 | Backup_Voltage_C | Backup Output Voltage Phase C (RMS) | S16 | V | 0.1 | |
| Done | 10047 | Backup_Current_A | Backup Output Current Phase A (RMS) | S16 | A | 0.01 | |
| Done | 10048 | Backup_Current_B | Backup Output Current Phase B (RMS) | S16 | A | 0.01 | |
| Done | 10049 | Backup_Current_C | Backup Output Current Phase C (RMS) | S16 | A | 0.01 | |
| Done | 10062 | Backup_Active_Power_Total | Total Backup Active Power | S16 | kW | 0.01 | |
| Done | 10063 | Backup_Reactive_Power_Total | Total Backup Reactive Power | S16 | kVar | 0.01 | |
| Done | 10064 | Backup_Apparent_Power_Total | Total Backup Apparent Power | S16 | kVA | 0.01 | |
| 4. PV / DC Input | |||||||
| Done | 10087 | PV_Total_Power | Total PV DC Input Power | S16 | kW | 0.01 | |
| Done | 10088 | PV1_Voltage | MPPT Tracker 1 DC Input Voltage | S16 | V | 0.1 | |
| Done | 10089 | PV1_Current | MPPT Tracker 1 DC Input Current | S16 | A | 0.01 | |
| Done | 10090 | PV1_Power | MPPT Tracker 1 DC Input Power | S16 | kW | 0.01 | |
| Done | 10091 | PV2_Voltage | MPPT Tracker 2 DC Input Voltage | S16 | V | 0.1 | |
| Done | 10092 | PV2_Current | MPPT Tracker 2 DC Input Current | S16 | A | 0.01 | |
| Done | 10093 | PV2_Power | MPPT Tracker 2 DC Input Power | S16 | kW | 0.01 | |
| Done | 10094 | PV3_Voltage | MPPT Tracker 3 DC Input Voltage | S16 | V | 0.1 | |
| Done | 10095 | PV3_Current | MPPT Tracker 3 DC Input Current | S16 | A | 0.01 | |
| Done | 10096 | PV3_Power | MPPT Tracker 3 DC Input Power | S16 | kW | 0.01 | |
| Done | 10097 | PV4_Voltage | MPPT Tracker 4 DC Input Voltage | S16 | V | 0.1 | |
| Done | 10098 | PV4_Current | MPPT Tracker 4 DC Input Current | S16 | A | 0.01 | |
| Done | 10099 | PV4_Power | MPPT Tracker 4 DC Input Power | S16 | kW | 0.01 | |
| Done | 10100 | PV5_Voltage | MPPT Tracker 5 DC Input Voltage | S16 | V | 0.1 | Not available on all models |
| Done | 10101 | PV5_Current | MPPT Tracker 5 DC Input Current | S16 | A | 0.01 | Not available on all models |
| Done | 10102 | PV5_Power | MPPT Tracker 5 DC Input Power | S16 | kW | 0.01 | Not available on all models |
| 5. Battery (Pack Level) | |||||||
| Done | 10671 | BMS_Bat1_SOC | Battery Port 1 State of Charge | U16 | % | 0.01 | 0-10000 (0-100.00%) |
| Done | 10672 | BMS_Bat1_SOH | Battery Port 1 State of Health | U16 | % | 0.01 | 0-10000 (0-100.00%) |
| Done | 10674 | BMS_Bat1_Voltage | Battery Port 1 DC Voltage | U16 | V | 0.1 | |
| Done | 10675 | BMS_Bat1_Current | Battery Port 1 DC Current | S16 | A | 0.1 | +discharge / -charge |
| Done | 10676 | BMS_Bat1_Temperature | Battery Port 1 Temperature | S16 | °C | 0.1 | |
| Done | 10677 | BMS_Bat1_Charge_Energy | Battery Port 1 Total Charge Energy | U32_BE | kWh | 0.1 | 2 registers |
| Done | 10679 | BMS_Bat1_Discharge_Energy | Battery Port 1 Total Discharge Energy | U32_BE | kWh | 0.1 | 2 registers |
| Done | 10681 | BMS_Bat1_Status | Battery Port 1 Status Word | U32_BE | 1 | 2 registers | |
| Done | 10683 | BMS_Bat1_Cycle_Count | Battery Port 1 Cycle Count | U32_BE | 1 | 2 registers | |
| P1 | 10685 | BMS_Bat1_Power | Battery Port 1 Active Power | S16 | kW | 0.01 | +discharge / -charge. Direct readout avoids V×I calculation errors.Ref: SMA 30775, Fronius StorCtl_W |
| P1 | 10686 | BMS_Bat1_Max_Charge_Power | BMS Allowed Max Charge Power | U16 | kW | 0.01 | RO. BMS-reported maximum allowable charge power (may differ from EMS setting due to temp/SOC).Ref: Solinteg 41020, Sigenergy BMS limits |
| P1 | 10687 | BMS_Bat1_Max_Discharge_Power | BMS Allowed Max Discharge Power | U16 | kW | 0.01 | RO. BMS-reported maximum allowable discharge power (may be lower at low temp/SOC). |
| P2 | 10688 | BMS_Bat1_Rated_Capacity | Battery Port 1 Rated Capacity | U16 | kWh | 0.01 | RO. Rated total capacity. For VPP dispatch planning. |
| P2 | 10689 | BMS_Bat1_Available_Energy | Battery Port 1 Available Energy | U16 | kWh | 0.01 | RO. Usable energy remaining (SOC x usable capacity). |
| Done | 10693 | BMS_Bat1_Alarm | Battery Port 1 Alarm Code | U32_BE | 1 | 2 registers | |
| Done | 10695 | BMS_Bat1_Warning | Battery Port 1 Warning Code | U32_BE | 1 | 2 registers | |
| Done | 10697 | BMS_Bat1_Force_Charge | Battery Port 1 Force Charge Status | U16 | 1 | ||
| Done | 10725 | BMS_Bat2_SOC | Battery Port 2 State of Charge | U16 | % | 0.01 | 0-10000 (0-100.00%) |
| Done | 10726 | BMS_Bat2_SOH | Battery Port 2 State of Health | U16 | % | 0.01 | 0-10000 (0-100.00%) |
| Done | 10728 | BMS_Bat2_Voltage | Battery Port 2 DC Voltage | U16 | V | 0.1 | |
| Done | 10729 | BMS_Bat2_Current | Battery Port 2 DC Current | S16 | A | 0.1 | +discharge / -charge |
| Done | 10730 | BMS_Bat2_Temperature | Battery Port 2 Temperature | S16 | °C | 0.1 | |
| Done | 10731 | BMS_Bat2_Charge_Energy | Battery Port 2 Total Charge Energy | U32_BE | kWh | 0.1 | 2 registers |
| Done | 10733 | BMS_Bat2_Discharge_Energy | Battery Port 2 Total Discharge Energy | U32_BE | kWh | 0.1 | 2 registers |
| P1 | 10739 | BMS_Bat2_Power | Battery Port 2 Active Power | S16 | kW | 0.01 | +discharge / -charge. Direct readout. |
| P1 | 10740 | BMS_Bat2_Max_Charge_Power | BMS2 Allowed Max Charge Power | U16 | kW | 0.01 | RO. BMS-reported maximum allowable charge power. |
| P1 | 10741 | BMS_Bat2_Max_Discharge_Power | BMS2 Allowed Max Discharge Power | U16 | kW | 0.01 | RO. BMS-reported maximum allowable discharge power. |
| Done | 10747 | BMS_Bat2_Alarm | Battery Port 2 Alarm Code | U32_BE | 1 | 2 registers | |
| Done | 10749 | BMS_Bat2_Warning | Battery Port 2 Warning Code | U32_BE | 1 | 2 registers | |
| 6. External Meter | |||||||
| Done | 10752 | Meter_Voltage_A | Meter Voltage Phase A (RMS) | S16 | V | 0.1 | |
| Done | 10753 | Meter_Voltage_B | Meter Voltage Phase B (RMS) | S16 | V | 0.1 | |
| Done | 10754 | Meter_Voltage_C | Meter Voltage Phase C (RMS) | S16 | V | 0.1 | |
| Done | 10758 | Meter_Current_A | Meter Current Phase A (RMS) | S16 | A | 0.1 | |
| Done | 10759 | Meter_Current_B | Meter Current Phase B (RMS) | S16 | A | 0.1 | |
| Done | 10760 | Meter_Current_C | Meter Current Phase C (RMS) | S16 | A | 0.1 | |
| Done | 10762 | Meter_Active_Power_A | Meter Phase A Active Power | S16 | kW | 0.01 | +import / -export |
| Done | 10763 | Meter_Active_Power_B | Meter Phase B Active Power | S16 | kW | 0.01 | +import / -export |
| Done | 10764 | Meter_Active_Power_C | Meter Phase C Active Power | S16 | kW | 0.01 | +import / -export |
| Done | 10765 | Meter_Active_Power_Total | Meter Total Active Power | S16 | kW | 0.01 | +import / -export |
| Done | 10769 | Meter_Reactive_Power_Total | Meter Total Reactive Power | S16 | kVar | 0.01 | |
| Done | 10777 | Meter_Power_Factor_Total | Meter Total Power Factor | S16 | 0.01 | -1.00 to 1.00 | |
| Done | 10778 | Meter_Frequency | Grid Frequency at PCC | S16 | Hz | 0.01 | |
| Done | 10781 | Meter_Energy_Import | Total Import Energy | U32_BE | kWh | 0.1 | 2 registers |
| Done | 10783 | Meter_Energy_Export | Total Export Energy | U32_BE | kWh | 0.1 | 2 registers |
| 7. Energy Counters | |||||||
| Done | 10785 | PV_To_Grid_Energy_Today | Today PV to Grid Energy | U16 | kWh | 0.1 | |
| Done | 10786 | PV_To_Load_Energy_Today | Today PV to Load Energy | S16 | kWh | 0.1 | Note: S16 limits range to 3276.7 kWh/day. Future: migrate to U16. |
| Done | 10787 | Battery_To_Grid_Today | Today Battery to Grid Energy | S16 | kWh | 0.1 | Note: S16 limits range to 3276.7 kWh/day. Future: migrate to U16. |
| Done | 10788 | Battery_To_Load_Energy_Today | Today Battery to Load Energy | U16 | kWh | 0.1 | |
| Done | 10789 | PV_To_Grid_Energy_Total | Total PV to Grid Energy | U32_BE | kWh | 0.1 | 2 registers |
| Done | 10791 | PV_To_Load_Energy_Total | Total PV to Load Energy | U32_BE | kWh | 0.1 | 2 registers |
| Done | 10793 | Battery_To_Grid_Total | Total Battery to Grid Energy | U32_BE | kWh | 0.1 | 2 registers |
| Done | 10795 | Battery_To_Load_Energy_Total | Total Battery to Load Energy | U32_BE | kWh | 0.1 | 2 registers |
| P1 | 10797 | PV_Energy_Today | Total PV Production Today | U16 | kWh | 0.1 | Sum of all MPPT tracker production regardless of destination.Ref: SMA 30535 (Day yield), Fronius DAY_ENERGY |
| P1 | 10798–10799 | PV_Energy_Total | Lifetime PV Production | U32_BE | kWh | 0.1 | 2 registers. Cumulative total PV energy.Ref: SMA 30529 (Total yield), SolarEdge E_Total |
| 8. Inverter Temperature | |||||||
| Done | 10103 | Ambient_Temp_External | External Ambient Temperature | S16 | °C | 0.1 | |
| Done | 10104 | Ambient_Temp_Internal | Internal Ambient Temperature | S16 | °C | 0.1 | |
| Done | 10105 | Radiator_Temp | Heatsink Temperature | S16 | °C | 0.1 | |
| 9. Load Power Monitoring P1 NEW | |||||||
| P1 | 10800–10801 | Load_Power_Total | Total Load Active Power | S32_BE | kW | 0.01 | 2 registers. EMS needs load data for optimization.Ref: Solinteg 40020, SMA 30775 |
| P1 | 10802 | Load_Power_A | Phase A Load Power | S16 | kW | 0.01 | Per-phase load for unbalanced optimization |
| P1 | 10803 | Load_Power_B | Phase B Load Power | S16 | kW | 0.01 | |
| P1 | 10804 | Load_Power_C | Phase C Load Power | S16 | kW | 0.01 | |
| 10. System Key Performance Indicators P1 IMPORTANT | |||||||
| P1 | 10810 | Self_Consumption_Rate | Self-Consumption Rate | U16 | % | 0.1 | 0-100.0%. (PV self-consumed / PV total) × 100. Core dashboard KPI for residential EMS.Ref: SMA Home Manager, Fronius Solar.web, SolarEdge monitoring |
| P1 | 10812 | Autarky_Rate | Self-Sufficiency (Autarky) Rate | U16 | % | 0.1 | 0-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.
Settings Registers
READ/WRITE| Status | Register | Name | Description | Type | Unit | Scale | Default | Min | Max | Values / Notes |
|---|---|---|---|---|---|---|---|---|---|---|
| 1. System Time | ||||||||||
| Done | 11000 | RTC_Year_Set | RTC Year | S16 | 1 | 0 | 2000 | 2099 | ||
| Done | 11001 | RTC_Month_Set | RTC Month | S16 | 1 | 0 | 1 | 12 | ||
| Done | 11002 | RTC_Day_Set | RTC Day | S16 | 1 | 0 | 1 | 31 | ||
| Done | 11003 | RTC_Hour_Set | RTC Hour | S16 | 1 | 0 | 0 | 23 | ||
| Done | 11004 | RTC_Minute_Set | RTC Minute | S16 | 1 | 0 | 0 | 59 | ||
| Done | 11005 | RTC_Second_Set | RTC Second | S16 | 1 | 0 | 0 | 59 | ||
| Done | 11006 | Time_Zone_Set | Time Zone | S16 | 1 | 1 | 1 | 650 | Lookup 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 | ||||||||||
| Done | 11060 | EMS_Switch_Set | External EMS Dispatch Enable | S16 | 1 | 0 | 0 | 1 | 0: Disabled 1: Enabled MUST enable before EMS commands take effect | |
| 3. Active Power Control | ||||||||||
| Done | 11045 | Active_Power_Set | Active Power Setpoint (Total) | S16 | kW | 0.01 | 2500 | -6000 | 6000 | Positive = 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. |
| P1 | 11046 | Per_Phase_Power_Enable | Enable Per-Phase Power Control | S16 | 1 | 0 | 0 | 1 | 0: Total only 1: Per-phaseRef: Sigenergy, Solinteg 50202-50206 | |
| P1 | 11047 | Active_Power_Set_A | Phase A Power Setpoint | S16 | kW | 0.01 | 0 | -6000 | 6000 | Requires Per_Phase_Power_Enable=1 |
| P1 | 11048 | Active_Power_Set_B | Phase B Power Setpoint | S16 | kW | 0.01 | 0 | -6000 | 6000 | |
| P1 | 11049 | Active_Power_Set_C | Phase C Power Setpoint | S16 | kW | 0.01 | 0 | -6000 | 6000 | |
| 4. Working Mode | ||||||||||
| Done | 11151 | Run_Mode_Set | Operating Mode | S16 | 1 | 0 | 0 | 20 | 0: General (Self-Consumption) 1: Grid Balancing 2: Economic (Peak Shaving) 3: UPS / Backup Priority 4: TOU (Time-of-Use) 5: Forecast 6: Off-Grid | |
| P2 | 11155 | Safety_Code_Set | Safety Regulation Code | U16 | 1 | 0 | 0 | 99 | Country-specific grid code selectorRef: SMA 40163, SolarEdge F142 | |
| 5. SOC Limits | ||||||||||
| Done | 11108 | Grid_tied_SOC_MIN | Grid-tied Minimum SOC | S16 | % | 0.01 | 1000 | 0 | 10000 | Do not discharge below this SOC |
| Done | 11109 | Grid_tied_SOC_MAX | Grid-tied Maximum SOC | S16 | % | 0.01 | 10000 | 0 | 10000 | Do not charge above this SOC |
| Done | 11110 | Off_grid_SOC_MIN | Off-grid Minimum SOC | S16 | % | 0.01 | 1000 | 0 | 10000 | Backup reserve SOC |
| Done | 11111 | Off_grid_SOC_MAX | Off-grid Maximum SOC | S16 | % | 0.01 | 10000 | 0 | 10000 | |
| Done | 11162 | Gen_Mode_Min_SOC_Set | General Mode Min SOC | S16 | % | 0.01 | 1000 | 0 | 10000 | |
| Done | 11163 | Gen_Mode_Max_SOC_Set | General Mode Max SOC | S16 | % | 0.01 | 8000 | 0 | 10000 | |
| 6. Grid Export / Import Limits | ||||||||||
| Done | 11106 | Import_Limits_Switch | Import Power Limit Enable | S16 | 1 | 0 | 0 | 1 | 0: Disabled 1: Enabled | |
| Done | 11107 | Export_Limits_Switch | Export Power Limit Enable | S16 | 1 | 0 | 0 | 1 | 0: Disabled 1: Enabled | |
| Done | 11112 | Import_Limits | Max Import Power | S16 | kW | 0.01 | 2500 | 0 | 5000 | |
| Done | 11113 | Export_Limits | Max Export Power | S16 | kW | 0.01 | 2500 | 0 | 2500 | Set to 0 for zero-export |
| Done | 11164 | Gen_Mode_Max_Buy_Power | General Mode Max Import | S16 | kW | 0.01 | 2500 | 0 | 5000 | |
| Done | 11165 | Gen_Mode_Max_Sell_Power | General Mode Max Export | S16 | kW | 0.01 | 2500 | 0 | 5000 | |
| P1 | 11116 | Export_On_Comms_Loss | Export Control on Comms Loss | S16 | 1 | 0 | 0 | 2 | 0: Keep last setting 1: Zero export 2: Limit to value in 11117Ref: SunSpec DERCtl Model 701 timeout action | |
| P1 | 11117 | Export_Limit_On_Comms_Loss | Export Limit When Comms Lost | S16 | kW | 0.01 | 0 | 0 | 2500 | Used when 11116 = 2 |
| 7. Battery Power Limits | ||||||||||
| Done | 11057 | Bat_1_Enable | Battery Port 1 Enable | S16 | 1 | 1 | 0 | 1 | 0: Disabled 1: Enabled | |
| Done | 11058 | Bat_2_Enable | Battery Port 2 Enable | S16 | 1 | 0 | 0 | 1 | 0: Disabled 1: Enabled | |
| Done | 11068 | Bat1_Max_Chg_Power | Battery Port 1 Max Charge Power | S16 | kW | 0.01 | 2500 | 0 | 3200 | |
| Done | 11069 | Bat1_Max_Dchg_Power | Battery Port 1 Max Discharge Power | S16 | kW | 0.01 | 2500 | 0 | 3200 | |
| Done | 11076 | Bat2_Max_Chg_Power | Battery Port 2 Max Charge Power | S16 | kW | 0.01 | 2500 | 0 | 3200 | |
| Done | 11077 | Bat2_Max_Dchg_Power | Battery Port 2 Max Discharge Power | S16 | kW | 0.01 | 2500 | 0 | 3200 | |
| 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) | ||||||||||
| Done | 11190 | Eco_Start1_Set | TOU Period 1 Start Time | S16 | min | 1 | 0 | 0 | 1440 | |
| Done | 11191 | Eco_End1_Set | TOU Period 1 End Time | S16 | min | 1 | 0 | 0 | 1440 | |
| Done | 11192 | Eco_Peak_Sel1_Set | TOU Period 1 Mode | S16 | 1 | 0 | 0 | 2 | 0: Flat 1: Peak 2: Valley | |
| Done | 11193–11195 | Eco_Period2 | TOU Period 2 (Start/End/Mode) | S16 | min | 1 | 0 | 0 | 1440 | Same structure as Period 1 |
| Done | 11196–11198 | Eco_Period3 | TOU Period 3 (Start/End/Mode) | S16 | min | 1 | 0 | 0 | 1440 | |
| Done | 11199–11201 | Eco_Period4 | TOU Period 4 (Start/End/Mode) | S16 | min | 1 | 0 | 0 | 1440 | |
| Done | 11202–11204 | Eco_Period5 | TOU Period 5 (Start/End/Mode) | S16 | min | 1 | 0 | 0 | 1440 | |
| Done | 11205–11207 | Eco_Period6 | TOU Period 6 (Start/End/Mode) | S16 | min | 1 | 0 | 0 | 1440 | |
| Done | 11208–11210 | Eco_Period7 | TOU Period 7 (Start/End/Mode) | S16 | min | 1 | 0 | 0 | 1440 | |
| Done | 11211–11213 | Eco_Period8 | TOU Period 8 (Start/End/Mode) | S16 | min | 1 | 0 | 0 | 1440 | |
| Done | 11214–11216 | Eco_Period9 | TOU Period 9 (Start/End/Mode) | S16 | min | 1 | 0 | 0 | 1440 | |
| Done | 11217–11219 | Eco_Period10 | TOU Period 10 (Start/End/Mode) | S16 | min | 1 | 0 | 0 | 1440 | |
| Periods 11-20 follow same pattern (registers 11220-11249) | ||||||||||
| P1 | 11250 | Eco_Charge_Power_Set | TOU Charge Power Limit | S16 | kW | 0.01 | 2500 | 0 | 6000 | Max charge power during Valley periodsRef: Solinteg 50204, SMA WChaMax |
| P1 | 11251 | Eco_Discharge_Power_Set | TOU Discharge Power Limit | S16 | kW | 0.01 | 2500 | 0 | 6000 | Max discharge power during Peak periods |
| P1 | 11252 | Eco_Target_SOC_Set | TOU Target SOC | S16 | % | 0.01 | 10000 | 0 | 10000 | Valley charging target SOC |
| P1 | 11253 | Eco_Min_SOC_Set | TOU Minimum SOC | S16 | % | 0.01 | 1000 | 0 | 10000 | Min SOC during Peak discharge |
| P0 | 11254 | TOU_Day_Of_Week_Mask | TOU Day-of-Week Schedule Mask | U16 | 1 | 127 | 0 | 127 | Bitmap: 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 | ||||||||||
| Done | 11137 | Backup_Switch | Backup Output Enable | S16 | 1 | 1 | 0 | 1 | 0: Disabled 1: Enabled | |
| Done | 11041 | On_Off_Grid_Switch_Enable | Planned Off-Grid Enable | S16 | 1 | 1 | 0 | 1 | 0: Disabled 1: Enabled | |
| 10. Reactive Power Control P0 MUST-HAVE | ||||||||||
| P0 | 11300 | Reactive_Power_Mode | Reactive Power Control Mode | S16 | 1 | 0 | 0 | 4 | 0: 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 | |
| P0 | 11301 | Reactive_Power_PF_Set | Power Factor Setpoint | S16 | 0.001 | 1000 | -1000 | 1000 | -1.000 to +1.000 (mode 1) | |
| P0 | 11302 | Reactive_Power_Q_Set | Reactive Power Setpoint | S16 | kVar | 0.01 | 0 | -2500 | 2500 | +leading/-lagging (mode 2) |
| P0 | 11303 | Reactive_Power_Q_Max | Max Reactive Power | S16 | kVar | 0.01 | 2500 | 0 | 2500 | Rated reactive capacity |
| Cosφ(P) Curve Points — 4 points defining PF as function of active power % | ||||||||||
| P0 | 11304 | CosPhiP_P1 | Curve Point 1 Power % | S16 | % | 1 | 20 | 0 | 100 | |
| P0 | 11305 | CosPhiP_Cos1 | Curve Point 1 Cosφ | S16 | 0.001 | 1000 | -1000 | 1000 | ||
| P0 | 11306 | CosPhiP_P2 | Curve Point 2 Power % | S16 | % | 1 | 50 | 0 | 100 | |
| P0 | 11307 | CosPhiP_Cos2 | Curve Point 2 Cosφ | S16 | 0.001 | 1000 | -1000 | 1000 | ||
| P0 | 11308 | CosPhiP_P3 | Curve Point 3 Power % | S16 | % | 1 | 80 | 0 | 100 | |
| P0 | 11309 | CosPhiP_Cos3 | Curve Point 3 Cosφ | S16 | 0.001 | 950 | -1000 | 1000 | ||
| P0 | 11310 | CosPhiP_P4 | Curve Point 4 Power % | S16 | % | 1 | 100 | 0 | 100 | |
| P0 | 11311 | CosPhiP_Cos4 | Curve Point 4 Cosφ | S16 | 0.001 | 900 | -1000 | 1000 | ||
| Q(U) Curve Points — 4 points defining reactive power % as function of voltage % | ||||||||||
| P0 | 11312 | QU_V1 | Curve Point 1 Voltage % | S16 | % | 1 | 92 | 80 | 120 | |
| P0 | 11313 | QU_Q1 | Curve Point 1 Q % | S16 | % | 1 | 100 | -100 | 100 | |
| P0 | 11314 | QU_V2 | Curve Point 2 Voltage % | S16 | % | 1 | 98 | 80 | 120 | |
| P0 | 11315 | QU_Q2 | Curve Point 2 Q % | S16 | % | 1 | 0 | -100 | 100 | |
| P0 | 11316 | QU_V3 | Curve Point 3 Voltage % | S16 | % | 1 | 102 | 80 | 120 | |
| P0 | 11317 | QU_Q3 | Curve Point 3 Q % | S16 | % | 1 | 0 | -100 | 100 | |
| P0 | 11318 | QU_V4 | Curve Point 4 Voltage % | S16 | % | 1 | 108 | 80 | 120 | |
| P0 | 11319 | QU_Q4 | Curve Point 4 Q % | S16 | % | 1 | -100 | -100 | 100 | |
| 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. | ||||||||||
| P0 | 11320 | Active_Power_Limit_Pct_Enable | Enable Percentage Power Limit | S16 | 1 | 0 | 0 | 1 | 0: Use absolute kW (11045) 1: Use % limit (11321)Ref: SMA 40016, SunSpec 123 WMaxLimPct_Ena | |
| P0 | 11321 | Active_Power_Limit_Pct | Active Power Limit as % of Pn | U16 | % | 0.1 | 1000 | 0 | 1000 | 0-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. | ||||||||||
| P0 | 11322 | Power_Ramp_Rate_Up | Power Increase Ramp Rate | U16 | %Pn/s | 0.1 | 100 | 1 | 1000 | Rate of active power increase. 100 = 10.0%/s.Ref: SMA WGra 40236, SunSpec 124 WGra |
| P0 | 11323 | Power_Ramp_Rate_Down | Power Decrease Ramp Rate | U16 | %Pn/s | 0.1 | 1000 | 1 | 1000 | Rate of active power decrease. Typically faster than ramp-up. |
| P0 | 11324 | Reconnect_Ramp_Rate | Post-Reconnection Ramp Rate | U16 | %Pn/min | 0.1 | 100 | 10 | 1000 | Ramp 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. | ||||||||||
| P0 | 11330 | Freq_Watt_Enable | Frequency-Watt Response Enable | S16 | 1 | 0 | 0 | 1 | 0: Disabled 1: EnabledRef: SunSpec 712 Ena, SMA P(f) settings | |
| P0 | 11331 | Freq_Watt_HzStr | Over-Freq Start Point | U16 | Hz | 0.01 | 5020 | 4500 | 5500 | Start reducing power. Default 50.20Hz (EU). |
| P0 | 11332 | Freq_Watt_HzStop | Over-Freq Stop Point | U16 | Hz | 0.01 | 5150 | 4500 | 5500 | Zero power output. Default 51.50Hz. |
| P0 | 11333 | Freq_Watt_WGra | Over-Freq Droop Gradient | U16 | %Pn/Hz | 0.1 | 400 | 10 | 1000 | Power reduction per Hz. 400 = 40.0%/Hz (5% droop). |
| P0 | 11334 | Freq_Watt_HzStrLow | Under-Freq Start Point | U16 | Hz | 0.01 | 4980 | 4500 | 5500 | Start increasing power. Default 49.80Hz (LFSM-U). |
| P0 | 11335 | Freq_Watt_HzStopLow | Under-Freq Stop Point | U16 | Hz | 0.01 | 4850 | 4500 | 5500 | Maximum power increase point. |
| P0 | 11336 | Freq_Watt_WGraLow | Under-Freq Response Gradient | U16 | %Pn/Hz | 0.1 | 400 | 10 | 1000 | Power increase per Hz below start point. |
| 14. Grid Service / VPP Mode P2 PLANNED | ||||||||||
| P2 | 11340 | Grid_Service_Mode | Grid Service Participation Mode | S16 | 1 | 0 | 0 | 4 | 0: 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. | ||||||||||
| P0 | 11350 | Demand_Power_Limit | Site Demand Power Limit (MDI) | U16 | kW | 0.01 | 0 | 0 | 65535 | Maximum 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. | ||||||||||
| P1 | 11355 | SG_Ready_Mode | SG-Ready Heat Pump Signal | U16 | 1 | 1 | 1 | 4 | 1: 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.
Control Commands
WRITE ONLY| Status | Register | Name | Description | Type | Default | Min | Max | Values / Notes |
|---|---|---|---|---|---|---|---|---|
| Done | 12000 | PowerOn_Switch | Power On / Off | S16 | 0 | 0 | 1 | 0: Power Off 1: Power On |
| Done | 12001 | Malfunction_Clear | Fault Clear | S16 | 0 | 0 | 1 | 0: No action 1: Clear faults |
| Done | 12002 | Remote_Mode | Remote Control Mode | S16 | 0 | 0 | 1 | 0: Local 1: Remote Set to 1 for EMS control |
| Done | 12003 | Restore_Factory_Setting | Restore Factory Settings | S16 | 0 | 0 | 1 | 0: No action 1: Restore USE WITH CAUTION |
| Done | 12004 | Communication_Reset | Communication Module Reset | S16 | 0 | 0 | 1 | 0: No action 1: Reset |
| Done | 12005 | Device_Restart | Device Restart | S16 | 0 | 0 | 1 | 0: No action 1: Restart |
| Done | 12006 | Device_Self_Check | Device Self-Check | S16 | 0 | 0 | 1 | 0: 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. | ||||||||
| P0 | 12008 | Safety_Unlock | Safety Unlock for Destructive Commands | U16 | 0 | 0 | 65535 | Write 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 |
| P0 | 12007 | Emergency_Stop | Emergency Stop | S16 | 0 | 0 | 1 | 1: 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 | ||||||||
| P0 | 12010 | Comms_Watchdog_Enable | Watchdog Timer Enable | S16 | 0 | 0 | 1 | 0: Disabled 1: Enabled Enable for safety: auto-failsafe on comms lossRef: SMA 41193-41197, Solinteg 20085 |
| P0 | 12011 | Comms_Watchdog_Timeout | Watchdog Timeout | S16 | 60 | 10 | 600 | Seconds before failsafe action triggers |
| P0 | 12012 | Comms_Heartbeat | Heartbeat Counter | U16 | 0 | 0 | 65535 | Write any value to reset watchdog timer |
| P0 | 12013 | Comms_Timeout_Action | Timeout Action | S16 | 0 | 0 | 3 | 0: 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.
Device Registers
READ-ONLY| Status | Register | Name | Description | Type | Unit | Scale | Notes |
|---|---|---|---|---|---|---|---|
| Done | 13000 | Device_SN | Device Serial Number | ASCII32 | 1 | 16 registers (32 bytes) | |
| Done | 13016 | Device_Info | Device Model Code | U16 | 1 | See model table below | |
| Done | 13017 | DSP_AC_App_Version | DSP AC App Version | U16 | 1 | ||
| Done | 13019 | DC_App_V_Ver | DSP DC App Version | U16 | 1 | ||
| Done | 13021 | ARM_App_V_Ver | ARM App Version | U16 | 1 | ||
| Done | 13029 | Hardware_Ver | Hardware Version | U16 | 1 | ||
| Done | 13034 | Inverter_Rated_Power | Rated Power | U16 | kW | 0.01 | |
| Done | 13035 | Inverter_Max_Power | Maximum Output Power | U16 | kW | 0.01 | |
| Done | 13120 | Inverter_Total_Working_Time | Total Working Time | U32_BE | h | 0.1 | 2 registers |
| Protocol & Communication Diagnostics P0 / P1 | |||||||
| P0 | 13036 | Protocol_Version | Modbus Protocol Version | U16 | 1 | EMS 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 | |
| P1 | 13040 | Modbus_Msg_Count | Total Modbus Messages Received | U16 | 1 | Rolling counter. Wraps at 65535. Useful for comms health monitoring. | |
| P1 | 13041 | Modbus_Err_Count | Total Modbus Errors | U16 | 1 | CRC errors + timeout errors + exception responses. Non-zero indicates wiring or configuration issues. | |
| P1 | 13042 | Modbus_Last_Err_Code | Last Modbus Error Code | U16 | 1 | 0: None 1: CRC Error 2: Timeout 3: Illegal Function 4: Illegal Address 5: Illegal Value | |
Model Code Reference
INFO| Code | Model | Rated Power | Max Power |
|---|---|---|---|
| 10 | MATIC-10KW-50A | 10.00 kW | 10.00 kW |
| 11 | MATIC-12KW-50A | 12.00 kW | 12.00 kW |
| 12 | MATIC-15KW-50A | 15.00 kW | 15.00 kW |
| 13 | MATIC-20KW-50A | 20.00 kW | 20.00 kW |
| 14 | MATIC-25KW-50A | 25.00 kW | 25.00 kW |
System Status
Current operating state of inverter subsystems. Poll these registers to determine system readiness and operating conditions.
Status Registers
READ-ONLY| Status | Register | Name | Description | Type | Values / States |
|---|---|---|---|---|---|
| Done | 14000 | PCS_Working_Mode | PCS Working Mode | U16 | 0: General 1: Grid Balancing 2: Economic 3: UPS 4: TOU 5: Forecast 6: Off-Grid |
| Done | 14001 | TOU_Submode | TOU Sub-Mode | U16 | 0: Self-Use 1: Charge 2: Discharge 3: Peak Shaving 4: Battery Off |
| Done | 14002 | PCS_Working_Status | PCS Running Status | U16 | 0: 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) |
| Done | 14003 | Battery1_Working_Status | Battery Port 1 Status | U16 | 0: Stop 1: Charging 2: Discharging 3: Not Connected 4: Abnormal |
| Done | 14004 | Battery2_Working_Status | Battery Port 2 Status | U16 | Same as Battery Port 1 |
| Done | 14005 | PV1_Working_Status | PV1 Status | U16 | 0: Stop 1: Running 2: Not Connected 3: Abnormal 4: No Interface |
| Done | 14006 | PV2_Working_Status | PV2 Status | U16 | Same as PV1 |
| Done | 14007 | PV3_Working_Status | PV3 Status | U16 | Same as PV1 |
| Done | 14008 | PV4_Working_Status | PV4 Status | U16 | Same as PV1 |
| Done | 14009 | PV5_Working_Status | PV5 Status | U16 | Same as PV1 |
| Done | 14010 | Grid_Working_Status | Grid Connection Status | U16 | 0: On-Grid 1: Off-Grid (Islanded) |
| Done | 14011 | Diesel_Working_Status | Diesel Generator Status | U16 | 0: Stop 1: Running 3: Not Connected 4: Abnormal |
| Done | 14027 | AC_Power_On_Off_Status | AC Side On/Off | U16 | 0: Off 1: On |
| Done | 14028 | DC_Power_On_Off_Status | DC Side On/Off | U16 | 0: Off 1: On |
| Done | 14029 | AC_Run_Status | AC Running State | U16 | 0: Stop 1-7: Starting 8: Running |
| Done | 14030 | DC_Run_Status | DC Running State | U16 | 0: Stop 1: Soft-Start 2-3: Running |
| Communications Watchdog Status P0 MUST-HAVE | |||||
| P0 | 14031 | Comms_Watchdog_Status | Watchdog State | U16 | 0: Inactive 1: Active (OK) 2: Timed Out |
| P0 | 14032 | Comms_Heartbeat_Age | Last Heartbeat Age | U16 | Seconds 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. | |||||
| P0 | 14033 | Applied_Power_Limit_Pct | Applied Power Limit (%) | U16 | 0-1000 (0-100.0%). Current active power limit as % of Pn. RO readback.Ref: SMA 30231, Fronius WMaxLimPct_RB |
| P0 | 14034 | Applied_Power_Limit_Abs | Applied Power Limit (kW) | S16 | Scale 0.01 kW. Actual max power after all limits applied (EMS + grid code + derating). |
| P0 | 14035 | Active_Power_Limit_Source | Power Limit Source | U16 | Bitmap: bit0=EMS, bit1=Grid Code, bit2=Thermal Derating, bit3=Frequency Response, bit4=External Signal |
| Derating Feedback P1 IMPORTANT | |||||
| P1 | 14036 | Derating_Status | Current Derating Source | U16 | Bitmap: bit0=Temperature, bit1=Voltage, bit2=Overload, bit3=BMS Limit, bit4=Grid Code. 0 = no derating.Ref: SMA 30233 derating source |
| P1 | 14037 | Derating_Power_Limit | Derated Max Power | U16 | Scale 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 | |||||
| P2 | 14038 | Active_Safety_Code | Active Grid Code | U16 | Currently 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. | |||||
| P0 | 14039–14040 | Last_Cmd_Timestamp | Last Command Execution Timestamp | U32_BE | Unix 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 |
| P1 | 14043 | Comms_RSSI | Communication Signal Quality | S16 | WiFi/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. | |||||
| P0 | 14044 | EMS_Control_Active | EMS Control Active Status | U16 | 0: 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 |
| P0 | 14045 | Remote_Mode_Status | Remote Mode Active Status | U16 | 0: Local mode 1: Remote mode active Readback of Remote_Mode(12002). If user overrides from panel, this returns 0. |
| P1 | 14046 | Mode_Change_Reason | Last Mode Change Reason | U16 | Bit 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.
| Bit | Name | Description |
|---|---|---|
| 14022:0 | AC_External_Temp_Derating | AC External Ambient Temp Derating |
| 14022:1 | AC_Internal_Temp_Derating | AC Internal Ambient Temp Derating |
| 14022:2 | AC_Bus_Derating | AC Bus Derating |
| 14022:4 | Grid_Voltage_Derating | Grid Voltage Derating |
| 14022:5 | AC_Overload_Derating | AC Overload Derating |
| 14022:6 | AC_Radiator_Temp_Derating | AC Heatsink Temp Derating |
| Bit | Name | Description |
|---|---|---|
| 14023:3 | DC_External_Temp_Derating | DC External Ambient Temp Derating |
| 14023:4 | DC_Inside_Temp_Derating | DC Internal Ambient Temp Derating |
| 14023:5 | Battery_Bus_Voltage_Derating | Battery Bus Voltage Derating |
| 14023:6 | PV_Bus_Voltage_Derating | PV Bus Voltage Derating |
| 14023:8 | BMS1_Charge_Prohibited | BMS1 Charge Prohibited |
| 14023:9 | BMS1_Discharge_Prohibited | BMS1 Discharge Prohibited |
| 14023:10 | BMS2_Charge_Prohibited | BMS2 Charge Prohibited |
| 14023:11 | BMS2_Discharge_Prohibited | BMS2 Discharge Prohibited |
| Bit | Name | Description |
|---|---|---|
| 14100:0 | Grid_LV1_OV_A | Grid Level-1 Over-Voltage Phase A |
| 14100:3 | Grid_LV2_OV_A | Grid Level-2 Over-Voltage Phase A |
| 14100:6 | Grid_LV3_OV_A | Grid Level-3 Over-Voltage Phase A |
| 14100:9 | Grid_10min_OV_A | Grid 10-min Over-Voltage Phase A |
| 14100:12 | Grid_LV1_UV_A | Grid Level-1 Under-Voltage Phase A |
| 14101:6 | Grid_LV1_OF | Grid Level-1 Over-Frequency |
| 14101:7 | Grid_LV2_OF | Grid Level-2 Over-Frequency |
| 14101:8 | Grid_LV1_UF | Grid Level-1 Under-Frequency |
| 14101:9 | Grid_LV2_UF | Grid Level-2 Under-Frequency |
| 14101:10 | Grid_Phase_Sequence | Grid Phase Sequence Error |
| 14101:12 | Grid_Phase_Loss_A | Grid Phase Loss A |
| 14101:15 | ROCOF | Rate of Change of Frequency |
| Register | Name | Description |
|---|---|---|
| 14109:0-14 | Grid_Voltage_Faults | Grid Over/Under Voltage Faults (3 levels, 3 phases) |
| 14110:0-5 | Grid_VF_Faults_LV2_3 | Grid Level-2/3 Under-Voltage + Frequency Faults |
| 14110:10-14 | Grid_Phase_Faults | Grid Phase Sequence / Loss / Lock Faults |
| 14112:0-2 | BackUp_Overcurrent | Backup Output Overcurrent (A/B/C) |
| 14112:3-5 | BackUp_Overload_Timeout | Backup Overload Timeout (A/B/C) |
| 14113:3-6 | Inv_HW_Overcurrent | Inverter Hardware Overcurrent |
| 14113:7-9 | Inv_SW_Overcurrent | Inverter Software Overcurrent (A/B/C) |
| 14115:0-2 | Inv_Short_Circuit | Inverter Short Circuit (A/B/C) |
| 14104:0 | Anti_Islanding | Anti-Islanding Protection Triggered |
| 14104:1 | Heatsink_Overtemp | Inverter Heatsink Over-Temperature |
| 14104:3 | CT_Reverse | CT Reverse Connection Detected (Alarm) |
| 14104:7 | Anti_Backfeed_Failure | Anti-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.
Connect & Identify
- Establish Modbus RTU/TCP connection (default: address
1, baud9600) - Read
Device_SN (13000)andDevice_Info (13016)to verify device identity - Read
Inverter_Rated_Power (13034)to determine rated capacity
Enable EMS Control
- Write
Remote_Mode (12002) = 1to enable remote control - Write
EMS_Switch_Set (11060) = 1to enable external EMS dispatch - P0 Write
Comms_Watchdog_Enable (12010) = 1to enable safety watchdog - P0 Write
Comms_Watchdog_Timeout (12011) = 60(60s timeout) - P0 Write
Comms_Timeout_Action (12013) = 1(self-consumption on timeout)
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)xBMS_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+
Send Heartbeat (Loop)
P0 Write to heartbeat register every 15-30 seconds:
Interval: every 15-30 seconds
If Comms_Watchdog_Status (14031) = 2 → comms lost, check connection
Control Operations
- Set Working Mode: Write
Run_Mode_Set (11151)= 0 / 2 / 4 - Set Active Power: Write
Active_Power_Set (11045)
Write -1000 = charge 10.00 kW from grid
- Export Limit (Zero Export): Write
Export_Limits_Switch (11107) = 1, WriteExport_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
Safety & Error Handling
- Monitor fault registers periodically
- Use
Malfunction_Clear (12001) = 1to clear recoverable faults - P0 Emergency: Write
Safety_Unlock (12008) = 0x55AA, thenEmergency_Stop (12007) = 1within 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.
Real-Time Monitoring Dashboard
RTU + TCPfrom 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)
}
}
Zero-Export Control
RTU + TCPfrom 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)
}
}
TOU Schedule Programming
TCPfrom 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])
}
}
Peak Shaving / Demand Management
RTU + TCPfrom 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)
}
}
Self-Consumption Optimization
RTU + TCPfrom 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)
}
}
VPP (Virtual Power Plant) Remote Dispatch
TCPfrom 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
}
Emergency Stop & Safety Sequence
RTUfrom 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
}
Multi-Battery Port Monitoring
RTU + TCPfrom 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)
}
SG-Ready Heat Pump Coordination
RTUfrom 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)
}
}
Grid Code Compliance Setup (EN 50549 / VDE-AR-N 4105)
TCPfrom 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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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).
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.
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.
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.
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.
Communication Diagnostics
Remote diagnostics of Modbus communication quality. Essential for troubleshooting wiring issues, baud rate mismatches, and WiFi/4G signal degradation in field installations.