Skip to content

Commit ff30822

Browse files
0xAHAclaude
andcommitted
Bump version to v1.1.0 — SPE 8000-12000 ES grid-tie controls (#322)
- Add SPE grid export energy sensors: reg 45 (today, single 16-bit per Protocol V0.26), regs 46/47 (total, 32-bit pair). Fixes earlier incorrect assumption that reg 44 was the high word of today's export — reg 44 = DTC per Off-Grid Protocol V0.26. - Add coordinator fallback to find standalone energy_to_grid_today register (no _low suffix) so SPE's single-register reading is picked up correctly. - Add SPE grid-tie holding register controls (115-124): grid export enable, output priority, feed range (region), battery export enable, export power limit, battery export current, battery voltage loss/back thresholds, SOC loss/back points. - Valid ranges taken from Off-Grid Protocol V0.26: reg 120=0-400A, reg 123=5-90%, reg 124=15-100% (corrected from nicauswu's ZIP which had all three as 0-100). - Add only_profiles/not_profiles filter to WRITABLE_REGISTERS; applied in number.py and select.py to prevent reg 123 cross-contamination between SPH (export_limit_power) and SPE (export_min_soc) which share the same register address. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c42ff81 commit ff30822

8 files changed

Lines changed: 237 additions & 5 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Growatt Modbus Integration for Home Assistant ☀️
44

55
![HACS Badge](https://img.shields.io/badge/HACS-Custom-orange.svg)
6-
![Version](https://img.shields.io/badge/Version-1.0.9-blue.svg)
6+
![Version](https://img.shields.io/badge/Version-1.1.0-blue.svg)
77
[![GitHub Issues](https://img.shields.io/github/issues/0xAHA/Growatt_ModbusTCP.svg)](https://github.com/0xAHA/Growatt_ModbusTCP/issues)
88
[![GitHub Stars](https://img.shields.io/github/stars/0xAHA/Growatt_ModbusTCP.svg?style=social)](https://github.com/0xAHA/Growatt_ModbusTCP)
99

RELEASENOTES.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,42 @@
44

55
---
66

7+
## v1.1.0
8+
9+
Issues: #322
10+
11+
- **New: SPE 8000-12000 ES grid-tie export sensors (#322):**
12+
Added grid export energy sensors for the SPE 8000-12000 ES single-phase hybrid inverter:
13+
- **Energy to Grid Today** — input reg 45, single 16-bit register, 0.1 kWh resolution
14+
(confirmed per Off-Grid Protocol V0.26; prior implementation incorrectly used a 32-bit pair)
15+
- **Energy to Grid Total** — input regs 46/47, 32-bit pair, 0.1 kWh resolution
16+
17+
- **New: SPE 8000-12000 ES grid-tie controls (#322):**
18+
Added ten writable holding registers for SPE grid-tied export configuration:
19+
| Register | Name | Description |
20+
|----------|------|-------------|
21+
| 115 | Grid Export Enable | Enable/disable grid feed |
22+
| 116 | Output Priority | Charge First / Load First / Feed First |
23+
| 117 | Feed Range | Grid compliance region (Asia/Europe/S.America/S.Africa) |
24+
| 118 | Battery Export Enable | Enable battery-to-grid export |
25+
| 119 | Export Power Limit | Max export power (0–12 kW) |
26+
| 120 | Battery Export Max Current | Max battery current for export (0–400 A) |
27+
| 121 | Battery Feed Voltage Loss | Voltage at which export stops (42–54V) |
28+
| 122 | Battery Feed Voltage Back | Voltage to resume export (44–56V) |
29+
| 123 | Export Min SOC | Minimum SOC to allow export (5–90%) |
30+
| 124 | Export Back SOC | SOC to resume export after stopping (15–100%) |
31+
32+
All controls are SPE-specific and will not appear on other profiles.
33+
Valid ranges are taken from Off-Grid Protocol V0.26; some differ from nicauswu's original
34+
implementation (regs 120, 123, 124) and are pending field validation.
35+
36+
- **Fix: profile-specific writable register filtering (#322):**
37+
Register 123 is `export_limit_power` on SPH/XH but `export_min_soc` on SPE. A new
38+
`only_profiles` / `not_profiles` filter on `WRITABLE_REGISTERS` entries prevents
39+
cross-profile contamination. The filter is applied in both `number.py` and `select.py`.
40+
41+
---
42+
743
## v1.0.9
844

945
Issues: #351

custom_components/growatt_modbus/const.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@
149149
},
150150
'export_limit_power': {
151151
'register': 123,
152+
'not_profiles': ['SPE_8000_12000_ES'], # SPE reg 123 = export_min_soc (different semantic)
152153
'scale': 0.1, # Store as 0-1000, display as 0-100.0%
153154
'valid_range': (0, 1000), # 0 = 0%, 1000 = 100%
154155
'unit': '%'
@@ -343,6 +344,93 @@
343344
'battery_dependent': True
344345
},
345346

347+
# SPE 8000-12000 ES Grid-Tie Export Controls (confirmed working via nicauswu field data, Issue #322)
348+
# These registers are SPE-only (only_profiles guard prevents cross-profile contamination).
349+
'spe_grid_export_enable': {
350+
'register': 115,
351+
'only_profiles': ['SPE_8000_12000_ES'],
352+
'scale': 1,
353+
'valid_range': (0, 1),
354+
'options': {0: 'Disabled', 1: 'Enabled'},
355+
'desc': 'Grid export enable/disable',
356+
},
357+
'spe_battery_export_enable': {
358+
'register': 118,
359+
'only_profiles': ['SPE_8000_12000_ES'],
360+
'scale': 1,
361+
'valid_range': (0, 1),
362+
'options': {0: 'Disabled', 1: 'Enabled'},
363+
'desc': 'Battery-to-grid export enable/disable',
364+
},
365+
'spe_export_limit_power': {
366+
'register': 119,
367+
'only_profiles': ['SPE_8000_12000_ES'],
368+
'scale': 0.1,
369+
'valid_range': (0, 120),
370+
'unit': 'kW',
371+
'desc': 'Grid export power limit (0-12kW, stored as 0-120 × 0.1 kW)',
372+
},
373+
'spe_output_priority': {
374+
'register': 116,
375+
'only_profiles': ['SPE_8000_12000_ES'],
376+
'scale': 1,
377+
'valid_range': (0, 2),
378+
'options': {0: 'Charge First', 1: 'Load First', 2: 'Feed First'},
379+
'desc': 'Output priority (uwLoadFirst): charge first / load first / feed first',
380+
},
381+
'spe_feed_range': {
382+
'register': 117,
383+
'only_profiles': ['SPE_8000_12000_ES'],
384+
'scale': 1,
385+
'valid_range': (0, 3),
386+
'options': {0: 'Asia', 1: 'Europe', 2: 'South America', 3: 'South Africa'},
387+
'desc': 'Grid compliance region (uwFeedRange)',
388+
},
389+
'spe_battery_export_max_current': {
390+
'register': 120,
391+
'only_profiles': ['SPE_8000_12000_ES'],
392+
'scale': 1,
393+
'valid_range': (0, 400),
394+
'unit': 'A',
395+
'desc': 'Max battery current for grid export (uwBatFeedCurr): 0-400 A per Protocol V0.26',
396+
},
397+
'spe_bat_feed_vloss': {
398+
'register': 121,
399+
'only_profiles': ['SPE_8000_12000_ES'],
400+
'scale': 0.1,
401+
'valid_range': (420, 540),
402+
'unit': 'V',
403+
'desc': 'Battery voltage loss point to stop export (uwBatFeedVLoss): raw 420-540 = 42-54V',
404+
},
405+
'spe_bat_feed_vback': {
406+
'register': 122,
407+
'only_profiles': ['SPE_8000_12000_ES'],
408+
'scale': 0.1,
409+
'valid_range': (440, 560),
410+
'unit': 'V',
411+
'desc': 'Battery voltage back point to resume export (uwBatFeedVBack): raw 440-560 = 44-56V',
412+
},
413+
# SPE reg 123 = export min SOC. Separate from SPH reg 123 = export_limit_power (%).
414+
# The not_profiles guard on export_limit_power prevents cross-contamination.
415+
# Protocol V0.26 valid range is 5-90, not 0-100.
416+
'spe_export_min_soc': {
417+
'register': 123,
418+
'only_profiles': ['SPE_8000_12000_ES'],
419+
'scale': 1,
420+
'valid_range': (5, 90),
421+
'unit': '%',
422+
'desc': 'Min battery SOC to allow export (uwBatFeedSocLoss): 5-90% per Protocol V0.26',
423+
},
424+
# Protocol V0.26 valid range is 15-100.
425+
'spe_export_back_soc': {
426+
'register': 124,
427+
'only_profiles': ['SPE_8000_12000_ES'],
428+
'scale': 1,
429+
'valid_range': (15, 100),
430+
'unit': '%',
431+
'desc': 'SOC back point to resume export (uwBatFeedSocBack): 15-100% per Protocol V0.26',
432+
},
433+
346434
'discharge_power_rate': {
347435
'register': 1070,
348436
'scale': 1,

custom_components/growatt_modbus/growatt_modbus.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2498,10 +2498,13 @@ def _read_energy_breakdown(self, data: GrowattData) -> None:
24982498
data.energy_to_user_total = self._get_register_value(addr) or 0.0
24992499

25002500
# Energy to grid
2501-
addr = self._find_register_by_name('energy_to_grid_today_low')
2501+
# Prefer 32-bit pair (_low suffix); fall back to standalone single register.
2502+
# SPE uses a single 16-bit register for today (Protocol V0.26 reg 45).
2503+
addr = self._find_register_by_name('energy_to_grid_today_low') or \
2504+
self._find_register_by_name('energy_to_grid_today')
25022505
if addr:
25032506
data.energy_to_grid_today = self._get_register_value(addr) or 0.0
2504-
2507+
25052508
addr = self._find_register_by_name('energy_to_grid_total_low')
25062509
if addr:
25072510
data.energy_to_grid_total = self._get_register_value(addr) or 0.0

custom_components/growatt_modbus/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
"pymodbus>=3.0.0",
1313
"pyserial>=3.4"
1414
],
15-
"version": "1.0.9"
15+
"version": "1.1.0"
1616
}

custom_components/growatt_modbus/number.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ async def async_setup_entry(
102102
if register_num not in holding_registers:
103103
continue # Skip if register not in this profile
104104

105+
# Profile-specific filter: only_profiles restricts to named maps; not_profiles excludes them
106+
_only = control_config.get('only_profiles')
107+
if _only and register_map_name not in _only:
108+
continue
109+
_not = control_config.get('not_profiles')
110+
if _not and register_map_name in _not:
111+
continue
112+
105113
# VPP export limit requires live confirmation that the inverter responds to 30200-30201
106114
if control_name == 'vpp_export_limit_power_rate':
107115
if coordinator.data is None or not coordinator.data.vpp_export_limit_available:

custom_components/growatt_modbus/profiles/spe.py

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
* Regs 92-97: Generator registers — SPE has no generator input, all zero
2626
- offgrid_protocol flag prevents reading VPP registers (30000+) which return garbage on this firmware
2727
28+
Grid-Tie Export Controls (confirmed via nicauswu field data, Issue #322):
29+
- Holding regs 115-124 control grid-tied export behaviour (not present on SPF)
30+
- Cross-referenced against Off-Grid Protocol V0.26 (docs/developer/protocol-offgrid.md)
31+
2832
DTC Identification:
2933
- This device returned DTC 64541 (unknown, not in standard mapping) in the Issue #212 scan
3034
- Auto-detection falls back to legacy range analysis for this device
@@ -56,6 +60,28 @@
5660
for _addr in (92, 93, 94, 95, 96, 97):
5761
_spe_input_regs.pop(_addr, None)
5862

63+
# ── Add grid export energy registers (SPE grid-tied, not present in SPF) ─────
64+
# Reg 44 = DTC (Device Type Code) per Off-Grid Protocol V0.26 — NOT overridden here.
65+
# Reg 45 = Export to Grid Today — single 16-bit register (scale 0.1 kWh) per V0.26.
66+
# Nicauswu's implementation also reads this as a standalone register. Max 6553.5 kWh
67+
# is sufficient for daily totals.
68+
# Regs 46/47 = Export to Grid Total — 32-bit pair, combined scale 0.1 kWh per V0.26.
69+
_spe_input_regs.update({
70+
45: {
71+
'name': 'energy_to_grid_today', 'scale': 0.1, 'unit': 'kWh',
72+
'desc': 'Grid export energy today (single 16-bit register). Protocol V0.26 reg 45.',
73+
},
74+
46: {
75+
'name': 'energy_to_grid_total_high', 'scale': 1, 'unit': '', 'pair': 47,
76+
'desc': 'Grid export energy total (HIGH word). Protocol V0.26 reg 46.',
77+
},
78+
47: {
79+
'name': 'energy_to_grid_total_low', 'scale': 1, 'unit': '', 'pair': 46,
80+
'combined_scale': 0.1, 'combined_unit': 'kWh',
81+
'desc': 'Grid export energy total (LOW word). Protocol V0.26 reg 47.',
82+
},
83+
})
84+
5985
# ── Remap energy registers: SPF names are semantically wrong for SPE ──────────
6086

6187
# Regs 64/65: SPF labels these "AC discharge energy today" (battery-to-load via inverter).
@@ -123,10 +149,13 @@
123149
'description': 'Single-phase hybrid inverter with battery storage (8-12kW)',
124150
'notes': (
125151
'Uses SPF-compatible 0-97 register range with key remapping. '
152+
'Reg 45 = grid export energy today (single 16-bit, 0.1 kWh). '
153+
'Regs 46/47 = grid export energy total (32-bit pair). '
126154
'Regs 64/65 = grid import energy (not AC discharge), '
127155
'Regs 85-88 = load energy today/total (not operational discharge). '
128156
'Regs 36/37 (ac_input_power) excluded — produces 429GW overflow. '
129-
'No generator input. PV register validation pending daytime scan.'
157+
'No generator input. '
158+
'Grid-tie controls at holding regs 115-124 (confirmed via Issue #322, Protocol V0.26).'
130159
),
131160
# NOTE: offgrid_protocol refers to the REGISTER LAYOUT (SPF-style 0-97),
132161
# not the inverter's grid capability. The SPE supports grid-tied operation
@@ -139,6 +168,66 @@
139168
# entity values seen in Issue #212 scan (charge_current, battery_type,
140169
# ac_input_mode, output_config, charge_config all reading correctly).
141170
**SPF_3000_6000_ES_PLUS['holding_registers'],
171+
172+
# ── SPE grid-tie export controls ──────────────────────────────────────
173+
# All confirmed via nicauswu field data (Issue #322).
174+
# Cross-referenced against Off-Grid Protocol V0.26 holding register table.
175+
# Ranges and names from V0.26 unless noted.
176+
177+
# 115: uwFeedEn — grid feed enable
178+
115: {'name': 'spe_grid_export_enable', 'scale': 1, 'unit': '', 'access': 'RW',
179+
'desc': 'Grid export enable (uwFeedEn): 0=Disabled, 1=Enabled',
180+
'values': {0: 'Disabled', 1: 'Enabled'}},
181+
182+
# 116: uwLoadFirst — output priority: charge first / load first / feed first
183+
116: {'name': 'spe_output_priority', 'scale': 1, 'unit': '', 'access': 'RW',
184+
'desc': 'Output priority (uwLoadFirst): 0=Charge first, 1=Load first, 2=Feed first',
185+
'values': {0: 'Charge First', 1: 'Load First', 2: 'Feed First'}},
186+
187+
# 117: uwFeedRange — grid compliance region
188+
117: {'name': 'spe_feed_range', 'scale': 1, 'unit': '', 'access': 'RW',
189+
'desc': 'Grid compliance region (uwFeedRange): 0=Asia, 1=Europe, 2=S.America, 3=S.Africa',
190+
'values': {0: 'Asia', 1: 'Europe', 2: 'South America', 3: 'South Africa'}},
191+
192+
# 118: uwBatFeedEn — battery-to-grid export enable
193+
118: {'name': 'spe_battery_export_enable', 'scale': 1, 'unit': '', 'access': 'RW',
194+
'desc': 'Battery-to-grid export enable (uwBatFeedEn): 0=Disabled, 1=Enabled',
195+
'values': {0: 'Disabled', 1: 'Enabled'}},
196+
197+
# 119: uwFeedPow — feed power limit (0-120 raw = 0-12.0 kW)
198+
119: {'name': 'spe_export_limit_power', 'scale': 0.1, 'unit': 'kW', 'access': 'RW',
199+
'valid_range': (0, 120),
200+
'desc': 'Grid export power limit (uwFeedPow): 0-12 kW (raw 0-120, scale 0.1)'},
201+
202+
# 120: uwBatFeedCurr — max battery current for grid export (0-400 A per V0.26)
203+
# NOTE: nicauswu had this as 0-100; protocol V0.26 states 0-400.
204+
# Pending validation from nicauswu register scan.
205+
120: {'name': 'spe_battery_export_max_current', 'scale': 1, 'unit': 'A', 'access': 'RW',
206+
'valid_range': (0, 400),
207+
'desc': 'Max battery current for grid export (uwBatFeedCurr): 0-400 A (Protocol V0.26)'},
208+
209+
# 121: uwBatFeedVLoss — battery voltage at which export stops (420-540, units: 0.1V = 42-54V)
210+
121: {'name': 'spe_bat_feed_vloss', 'scale': 0.1, 'unit': 'V', 'access': 'RW',
211+
'valid_range': (420, 540),
212+
'desc': 'Battery voltage loss point to stop export (uwBatFeedVLoss): 42-54V (raw 420-540)'},
213+
214+
# 122: uwBatFeedVBack — battery voltage at which export resumes (440-560, units: 0.1V = 44-56V)
215+
122: {'name': 'spe_bat_feed_vback', 'scale': 0.1, 'unit': 'V', 'access': 'RW',
216+
'valid_range': (440, 560),
217+
'desc': 'Battery voltage back point to resume export (uwBatFeedVBack): 44-56V (raw 440-560)'},
218+
219+
# 123: uwBatFeedSocLoss — minimum SOC to allow export (5-90 %)
220+
# NOTE: SPH reg 123 = export_limit_power (%). WRITABLE_REGISTERS carries
221+
# not_profiles=['SPE_8000_12000_ES'] on that entry to prevent cross-contamination.
222+
# Protocol V0.26 valid range is 5-90 (not 0-100 as nicauswu had).
223+
123: {'name': 'spe_export_min_soc', 'scale': 1, 'unit': '%', 'access': 'RW',
224+
'valid_range': (5, 90),
225+
'desc': 'Min battery SOC to allow export (uwBatFeedSocLoss): 5-90% (Protocol V0.26)'},
226+
227+
# 124: uwBatFeedSocBack — SOC hysteresis to re-enable export (15-100 %)
228+
124: {'name': 'spe_export_back_soc', 'scale': 1, 'unit': '%', 'access': 'RW',
229+
'valid_range': (15, 100),
230+
'desc': 'SOC back point to resume export (uwBatFeedSocBack): 15-100% (Protocol V0.26)'},
142231
},
143232
}
144233

custom_components/growatt_modbus/select.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ async def async_setup_entry(
8686
if register_num not in holding_registers:
8787
continue # Skip if register not in this profile
8888

89+
# Profile-specific filter: only_profiles restricts to named maps; not_profiles excludes them
90+
_only = control_config.get('only_profiles')
91+
if _only and register_map_name not in _only:
92+
continue
93+
_not = control_config.get('not_profiles')
94+
if _not and register_map_name in _not:
95+
continue
96+
8997
# allow_grid_charge is handled by GrowattModAllowGridChargeSelect below
9098
if control_name == 'allow_grid_charge':
9199
continue

0 commit comments

Comments
 (0)