Skip to content

Commit fd7c700

Browse files
committed
Update profiles and integration from dev branch + add MOD battery_power support
Merged changes from origin/dev: - Updated MOD profile with corrected battery power registers (31126-31127) - Updated SPH TL3 profile with AC register aliases for compatibility - Updated integration logic (growatt_modbus.py, device_profiles.py, manifest.json) Key MOD profile changes: - Fixed battery power registers: moved from 3178-3181 (broken) to 31126-31127 (working) - Renamed battery energy registers to match SPH TL3 naming convention: - battery_discharge_today_* → discharge_energy_today_* - battery_charge_today_* → charge_energy_today_* - Added 'alias' fields to AC registers for generic compatibility Emulator changes: - Added battery_power_high/low handler for MOD series (register 31126-31127) - Supports signed 32-bit values (positive=charging, negative=discharging) - Properly handles two's complement for negative values This ensures the emulator correctly simulates the MOD series with the updated, working battery power registers confirmed by Growatt.
1 parent 7700737 commit fd7c700

6 files changed

Lines changed: 184 additions & 60 deletions

File tree

custom_components/growatt_modbus/device_profiles.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,10 @@
5959
}
6060

6161
THREE_PHASE_SENSORS: Set[str] = {
62+
"ac_voltage_r", "ac_voltage_s", "ac_voltage_t", # Phase voltages
6263
"ac_voltage_rs", "ac_voltage_st", "ac_voltage_tr", # Line-to-line voltages
63-
"ac_current_r", "ac_current_s", "ac_current_t",
64-
"ac_power_r", "ac_power_s", "ac_power_t",
64+
"ac_current_r", "ac_current_s", "ac_current_t", # Phase currents
65+
"ac_power_r", "ac_power_s", "ac_power_t", # Phase powers
6566
"ac_frequency",
6667
}
6768

custom_components/growatt_modbus/growatt_modbus.py

Lines changed: 132 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,27 @@ class GrowattData:
7272
pv3_power: float = 0.0 # W
7373
pv_total_power: float = 0.0 # W
7474

75-
# AC Output
75+
# AC Output (generic - usually Phase R for 3-phase)
7676
ac_voltage: float = 0.0 # V
7777
ac_current: float = 0.0 # A
7878
ac_power: float = 0.0 # W
7979
ac_frequency: float = 0.0 # Hz
80+
81+
# Three-Phase AC Output (individual phases)
82+
ac_voltage_r: float = 0.0 # V (Phase R/L1)
83+
ac_current_r: float = 0.0 # A (Phase R/L1)
84+
ac_power_r: float = 0.0 # W (Phase R/L1)
85+
ac_voltage_s: float = 0.0 # V (Phase S/L2)
86+
ac_current_s: float = 0.0 # A (Phase S/L2)
87+
ac_power_s: float = 0.0 # W (Phase S/L2)
88+
ac_voltage_t: float = 0.0 # V (Phase T/L3)
89+
ac_current_t: float = 0.0 # A (Phase T/L3)
90+
ac_power_t: float = 0.0 # W (Phase T/L3)
91+
92+
# Line-to-Line Voltages (3-phase only)
93+
ac_voltage_rs: float = 0.0 # V
94+
ac_voltage_st: float = 0.0 # V
95+
ac_voltage_tr: float = 0.0 # V
8096

8197
# Power Flow (storage/hybrid models)
8298
power_to_user: float = 0.0 # W
@@ -410,6 +426,7 @@ def read_all_data(self) -> Optional[GrowattData]:
410426
has_base_range = any(0 <= addr < 1000 for addr in addresses)
411427
has_storage_range = any(1000 <= addr < 2000 for addr in addresses)
412428
has_3000_range = any(3000 <= addr < 4000 for addr in addresses)
429+
has_31000_range = any(31000 <= addr < 32000 for addr in addresses)
413430

414431
# Read base range (0-124) if needed - SPH models
415432
if has_base_range:
@@ -435,18 +452,31 @@ def read_all_data(self) -> Optional[GrowattData]:
435452
for i, value in enumerate(registers):
436453
self._register_cache[1000 + i] = value
437454

438-
# Read 3000 range if needed - MIN models
455+
# Read 3000 range if needed - MIN/MOD models
439456
if has_3000_range:
440-
logger.debug("Reading 3000 range (3000-3110)")
441-
registers = self.read_input_registers(3000, 111)
457+
logger.debug("Reading 3000 range (3000-3250)")
458+
# Extended range to cover MOD battery registers up to 3250
459+
registers = self.read_input_registers(3000, 251)
442460
if registers is None:
443461
logger.error("Failed to read main input register block")
444462
return None
445-
463+
446464
# Populate cache
447465
for i, value in enumerate(registers):
448466
self._register_cache[3000 + i] = value
449-
467+
468+
# Read 31000 range if needed - MOD extended battery/BMS range
469+
if has_31000_range:
470+
logger.debug("Reading 31000 range (31100-31300)")
471+
registers = self.read_input_registers(31100, 201)
472+
if registers is None:
473+
logger.warning("Failed to read extended battery register block (31100+)")
474+
# Don't return None - continue with what we have
475+
else:
476+
# Populate cache
477+
for i, value in enumerate(registers):
478+
self._register_cache[31100 + i] = value
479+
450480
# Now extract values using the register map
451481
try:
452482
# Status
@@ -496,12 +526,12 @@ def read_all_data(self) -> Optional[GrowattData]:
496526
# Calculate from strings if not available
497527
data.pv_total_power = data.pv1_power + data.pv2_power + data.pv3_power
498528

499-
# AC Output
529+
# AC Output (generic - will use Phase R via alias for 3-phase)
500530
ac_voltage_addr = self._find_register_by_name('ac_voltage')
501531
ac_current_addr = self._find_register_by_name('ac_current')
502532
ac_power_addr = self._find_register_by_name('ac_power_low')
503533
ac_freq_addr = self._find_register_by_name('ac_frequency')
504-
534+
505535
if ac_voltage_addr:
506536
data.ac_voltage = self._get_register_value(ac_voltage_addr) or 0.0
507537
if ac_current_addr:
@@ -510,6 +540,51 @@ def read_all_data(self) -> Optional[GrowattData]:
510540
data.ac_power = self._get_register_value(ac_power_addr) or 0.0
511541
if ac_freq_addr:
512542
data.ac_frequency = self._get_register_value(ac_freq_addr) or 0.0
543+
544+
# Three-Phase AC Output (individual phases)
545+
# Phase R
546+
ac_voltage_r_addr = self._find_register_by_name('ac_voltage_r')
547+
ac_current_r_addr = self._find_register_by_name('ac_current_r')
548+
ac_power_r_addr = self._find_register_by_name('ac_power_r_low')
549+
if ac_voltage_r_addr:
550+
data.ac_voltage_r = self._get_register_value(ac_voltage_r_addr) or 0.0
551+
if ac_current_r_addr:
552+
data.ac_current_r = self._get_register_value(ac_current_r_addr) or 0.0
553+
if ac_power_r_addr:
554+
data.ac_power_r = self._get_register_value(ac_power_r_addr) or 0.0
555+
556+
# Phase S
557+
ac_voltage_s_addr = self._find_register_by_name('ac_voltage_s')
558+
ac_current_s_addr = self._find_register_by_name('ac_current_s')
559+
ac_power_s_addr = self._find_register_by_name('ac_power_s_low')
560+
if ac_voltage_s_addr:
561+
data.ac_voltage_s = self._get_register_value(ac_voltage_s_addr) or 0.0
562+
if ac_current_s_addr:
563+
data.ac_current_s = self._get_register_value(ac_current_s_addr) or 0.0
564+
if ac_power_s_addr:
565+
data.ac_power_s = self._get_register_value(ac_power_s_addr) or 0.0
566+
567+
# Phase T
568+
ac_voltage_t_addr = self._find_register_by_name('ac_voltage_t')
569+
ac_current_t_addr = self._find_register_by_name('ac_current_t')
570+
ac_power_t_addr = self._find_register_by_name('ac_power_t_low')
571+
if ac_voltage_t_addr:
572+
data.ac_voltage_t = self._get_register_value(ac_voltage_t_addr) or 0.0
573+
if ac_current_t_addr:
574+
data.ac_current_t = self._get_register_value(ac_current_t_addr) or 0.0
575+
if ac_power_t_addr:
576+
data.ac_power_t = self._get_register_value(ac_power_t_addr) or 0.0
577+
578+
# Line-to-Line Voltages
579+
ac_voltage_rs_addr = self._find_register_by_name('line_voltage_rs')
580+
ac_voltage_st_addr = self._find_register_by_name('line_voltage_st')
581+
ac_voltage_tr_addr = self._find_register_by_name('line_voltage_tr')
582+
if ac_voltage_rs_addr:
583+
data.ac_voltage_rs = self._get_register_value(ac_voltage_rs_addr) or 0.0
584+
if ac_voltage_st_addr:
585+
data.ac_voltage_st = self._get_register_value(ac_voltage_st_addr) or 0.0
586+
if ac_voltage_tr_addr:
587+
data.ac_voltage_tr = self._get_register_value(ac_voltage_tr_addr) or 0.0
513588

514589
# Power Flow (if available - storage/hybrid models)
515590
power_to_user_addr = self._find_register_by_name('power_to_user_low')
@@ -611,11 +686,15 @@ def write_register(self, register: int, value: int) -> bool:
611686

612687

613688
def _find_register_by_name(self, name: str) -> Optional[int]:
614-
"""Find register address by its name"""
689+
"""Find register address by its name or alias"""
615690
input_regs = self.register_map['input_registers']
616691
for addr, reg_info in input_regs.items():
692+
# Check exact name match
617693
if reg_info['name'] == name:
618694
return addr
695+
# Check alias match (for 3-phase compatibility)
696+
if reg_info.get('alias') == name:
697+
return addr
619698
return None
620699

621700
def _read_energy_breakdown(self, data: GrowattData) -> None:
@@ -678,31 +757,53 @@ def _read_battery_data(self, data: GrowattData) -> None:
678757
data.battery_temp = self._get_register_value(addr) or 0.0
679758
logger.debug(f"Battery temp from reg {addr}: {data.battery_temp}°C")
680759

681-
# Charge power (try to read from registers first)
682-
addr = self._find_register_by_name('charge_power_low')
760+
# Battery power (signed: positive=charging, negative=discharging)
761+
# Try new signed battery_power register first (MOD series @ 31126)
762+
addr = self._find_register_by_name('battery_power_low')
683763
if addr:
684764
raw_low = self._register_cache.get(addr, 0)
685-
pair_addr = self._find_register_by_name('charge_power_high')
765+
pair_addr = self._find_register_by_name('battery_power_high')
686766
raw_high = self._register_cache.get(pair_addr, 0) if pair_addr else 0
687-
data.charge_power = self._get_register_value(addr) or 0.0
688-
logger.debug(f"Charge power: HIGH={raw_high} (reg {pair_addr}), LOW={raw_low} (reg {addr}) → {data.charge_power}W")
689-
elif data.battery_voltage > 0 and data.battery_current < 0:
690-
# Fallback: Calculate from V×I when charging (negative current)
691-
data.charge_power = data.battery_voltage * abs(data.battery_current)
692-
logger.debug(f"Charge power (calculated): {data.battery_voltage}V × {abs(data.battery_current)}A = {data.charge_power}W")
693-
694-
# Discharge power (try to read from registers first)
695-
addr = self._find_register_by_name('discharge_power_low')
696-
if addr:
697-
raw_low = self._register_cache.get(addr, 0)
698-
pair_addr = self._find_register_by_name('discharge_power_high')
699-
raw_high = self._register_cache.get(pair_addr, 0) if pair_addr else 0
700-
data.discharge_power = self._get_register_value(addr) or 0.0
701-
logger.debug(f"Discharge power: HIGH={raw_high} (reg {pair_addr}), LOW={raw_low} (reg {addr}) → {data.discharge_power}W")
702-
elif data.battery_voltage > 0 and data.battery_current > 0:
703-
# Fallback: Calculate from V×I when discharging (positive current)
704-
data.discharge_power = data.battery_voltage * data.battery_current
705-
logger.debug(f"Discharge power (calculated): {data.battery_voltage}V × {data.battery_current}A = {data.discharge_power}W")
767+
battery_power = self._get_register_value(addr) or 0.0
768+
logger.debug(f"Battery power (signed): HIGH={raw_high} (reg {pair_addr}), LOW={raw_low} (reg {addr}) → {battery_power}W")
769+
770+
# Split into charge/discharge based on sign
771+
if battery_power > 0:
772+
data.charge_power = battery_power
773+
data.discharge_power = 0.0
774+
logger.debug(f" → Charging: {data.charge_power}W")
775+
elif battery_power < 0:
776+
data.charge_power = 0.0
777+
data.discharge_power = abs(battery_power)
778+
logger.debug(f" → Discharging: {data.discharge_power}W")
779+
else:
780+
data.charge_power = 0.0
781+
data.discharge_power = 0.0
782+
else:
783+
# Fallback: Try old separate charge/discharge registers (SPH series)
784+
addr = self._find_register_by_name('charge_power_low')
785+
if addr:
786+
raw_low = self._register_cache.get(addr, 0)
787+
pair_addr = self._find_register_by_name('charge_power_high')
788+
raw_high = self._register_cache.get(pair_addr, 0) if pair_addr else 0
789+
data.charge_power = self._get_register_value(addr) or 0.0
790+
logger.debug(f"Charge power: HIGH={raw_high} (reg {pair_addr}), LOW={raw_low} (reg {addr}) → {data.charge_power}W")
791+
elif data.battery_voltage > 0 and data.battery_current < 0:
792+
# Fallback: Calculate from V×I when charging (negative current)
793+
data.charge_power = data.battery_voltage * abs(data.battery_current)
794+
logger.debug(f"Charge power (calculated): {data.battery_voltage}V × {abs(data.battery_current)}A = {data.charge_power}W")
795+
796+
addr = self._find_register_by_name('discharge_power_low')
797+
if addr:
798+
raw_low = self._register_cache.get(addr, 0)
799+
pair_addr = self._find_register_by_name('discharge_power_high')
800+
raw_high = self._register_cache.get(pair_addr, 0) if pair_addr else 0
801+
data.discharge_power = self._get_register_value(addr) or 0.0
802+
logger.debug(f"Discharge power: HIGH={raw_high} (reg {pair_addr}), LOW={raw_low} (reg {addr}) → {data.discharge_power}W")
803+
elif data.battery_voltage > 0 and data.battery_current > 0:
804+
# Fallback: Calculate from V×I when discharging (positive current)
805+
data.discharge_power = data.battery_voltage * data.battery_current
806+
logger.debug(f"Discharge power (calculated): {data.battery_voltage}V × {data.battery_current}A = {data.discharge_power}W")
706807

707808
# Charge energy today
708809
addr = self._find_register_by_name('charge_energy_today_low')

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": "0.0.7-beta.1"
15+
"version": "0.0.7-beta.2"
1616
}

custom_components/growatt_modbus/profiles/mod.py

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,13 @@
3737
# === AC OUTPUT - THREE PHASE ===
3838
# Grid Frequency (shared across all phases)
3939
37: {'name': 'ac_frequency', 'scale': 0.01, 'unit': 'Hz', 'desc': 'AC output frequency'},
40-
41-
# Phase R (L1) - AC Output
42-
38: {'name': 'ac_voltage_r', 'scale': 0.1, 'unit': 'V', 'desc': 'Phase R AC voltage'},
43-
39: {'name': 'ac_current_r', 'scale': 0.1, 'unit': 'A', 'desc': 'Phase R AC current'},
44-
40: {'name': 'ac_power_r_high', 'scale': 1, 'unit': '', 'pair': 41},
45-
41: {'name': 'ac_power_r_low', 'scale': 1, 'unit': '', 'pair': 40, 'combined_scale': 0.1, 'combined_unit': 'VA'},
40+
41+
# Generic AC aliases (point to Phase R for compatibility with generic code)
42+
# These allow the standard ac_voltage/current/power fields to work
43+
38: {'name': 'ac_voltage_r', 'scale': 0.1, 'unit': 'V', 'desc': 'Phase R AC voltage', 'alias': 'ac_voltage'},
44+
39: {'name': 'ac_current_r', 'scale': 0.1, 'unit': 'A', 'desc': 'Phase R AC current', 'alias': 'ac_current'},
45+
40: {'name': 'ac_power_r_high', 'scale': 1, 'unit': '', 'pair': 41, 'alias': 'ac_power_high'},
46+
41: {'name': 'ac_power_r_low', 'scale': 1, 'unit': '', 'pair': 40, 'combined_scale': 0.1, 'combined_unit': 'VA', 'alias': 'ac_power_low'},
4647

4748
# Phase S (L2) - AC Output
4849
42: {'name': 'ac_voltage_s', 'scale': 0.1, 'unit': 'V', 'desc': 'Phase S AC voltage'},
@@ -108,14 +109,14 @@
108109
3086: {'name': 'battery_derating_mode', 'scale': 1, 'unit': ''},
109110

110111
# Battery - Discharge/Charge Energy
111-
3125: {'name': 'battery_discharge_today_high', 'scale': 1, 'unit': '', 'pair': 3126},
112-
3126: {'name': 'battery_discharge_today_low', 'scale': 1, 'unit': '', 'pair': 3125, 'combined_scale': 0.1, 'combined_unit': 'kWh'},
113-
3127: {'name': 'battery_discharge_total_high', 'scale': 1, 'unit': '', 'pair': 3128},
114-
3128: {'name': 'battery_discharge_total_low', 'scale': 1, 'unit': '', 'pair': 3127, 'combined_scale': 0.1, 'combined_unit': 'kWh'},
115-
3129: {'name': 'battery_charge_today_high', 'scale': 1, 'unit': '', 'pair': 3130},
116-
3130: {'name': 'battery_charge_today_low', 'scale': 1, 'unit': '', 'pair': 3129, 'combined_scale': 0.1, 'combined_unit': 'kWh'},
117-
3131: {'name': 'battery_charge_total_high', 'scale': 1, 'unit': '', 'pair': 3132},
118-
3132: {'name': 'battery_charge_total_low', 'scale': 1, 'unit': '', 'pair': 3131, 'combined_scale': 0.1, 'combined_unit': 'kWh'},
112+
3125: {'name': 'discharge_energy_today_high', 'scale': 1, 'unit': '', 'pair': 3126},
113+
3126: {'name': 'discharge_energy_today_low', 'scale': 1, 'unit': '', 'pair': 3125, 'combined_scale': 0.1, 'combined_unit': 'kWh'},
114+
3127: {'name': 'discharge_energy_total_high', 'scale': 1, 'unit': '', 'pair': 3128},
115+
3128: {'name': 'discharge_energy_total_low', 'scale': 1, 'unit': '', 'pair': 3127, 'combined_scale': 0.1, 'combined_unit': 'kWh'},
116+
3129: {'name': 'charge_energy_today_high', 'scale': 1, 'unit': '', 'pair': 3130},
117+
3130: {'name': 'charge_energy_today_low', 'scale': 1, 'unit': '', 'pair': 3129, 'combined_scale': 0.1, 'combined_unit': 'kWh'},
118+
3131: {'name': 'charge_energy_total_high', 'scale': 1, 'unit': '', 'pair': 3132},
119+
3132: {'name': 'charge_energy_total_low', 'scale': 1, 'unit': '', 'pair': 3131, 'combined_scale': 0.1, 'combined_unit': 'kWh'},
119120

120121
# Battery State
121122
3144: {'name': 'priority_mode', 'scale': 1, 'unit': '', 'desc': '0=Load, 1=Battery, 2=Grid'},
@@ -124,13 +125,12 @@
124125
3171: {'name': 'battery_soc', 'scale': 1, 'unit': '%'},
125126
3176: {'name': 'battery_temp', 'scale': 0.1, 'unit': '°C'},
126127

127-
# NOTE: Registers 3178-3180 don't exist on some MOD hardware
128-
# If your inverter doesn't have these, battery power will be calculated from V×I
129-
# Battery Power (32-bit) - OPTIONAL, may not exist on all MOD hardware
130-
3178: {'name': 'discharge_power_high', 'scale': 1, 'unit': '', 'pair': 3179},
131-
3179: {'name': 'discharge_power_low', 'scale': 1, 'unit': '', 'pair': 3178, 'combined_scale': 0.1, 'combined_unit': 'W'},
132-
3180: {'name': 'charge_power_high', 'scale': 1, 'unit': '', 'pair': 3181},
133-
3181: {'name': 'charge_power_low', 'scale': 1, 'unit': '', 'pair': 3180, 'combined_scale': 0.1, 'combined_unit': 'W'},
128+
# NOTE: Old registers 3178-3181 don't work on most MOD hardware (always return 0)
129+
# Growatt confirmed the correct register is 31126 (BDC/BMS Battery Information range)
130+
# Battery Power (32-bit, signed) - Register 31126
131+
# Positive = charging, Negative = discharging
132+
31126: {'name': 'battery_power_high', 'scale': 1, 'unit': '', 'pair': 31127},
133+
31127: {'name': 'battery_power_low', 'scale': 1, 'unit': '', 'pair': 31126, 'combined_scale': 0.1, 'combined_unit': 'W', 'signed': True},
134134
},
135135
'holding_registers': {
136136
# Basic control

custom_components/growatt_modbus/profiles/sph_tl3.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@
3333
# AC Grid Frequency
3434
37: {'name': 'ac_frequency', 'scale': 0.01, 'unit': 'Hz'},
3535

36-
# Three-Phase AC Output - Phase R
37-
38: {'name': 'ac_voltage_r', 'scale': 0.1, 'unit': 'V', 'desc': 'Phase R voltage'},
38-
39: {'name': 'ac_current_r', 'scale': 0.1, 'unit': 'A', 'desc': 'Phase R current'},
39-
40: {'name': 'ac_power_r_high', 'scale': 1, 'unit': '', 'pair': 41},
40-
41: {'name': 'ac_power_r_low', 'scale': 1, 'unit': '', 'pair': 40, 'combined_scale': 0.1, 'combined_unit': 'W'},
36+
# Three-Phase AC Output - Phase R (with generic aliases for compatibility)
37+
38: {'name': 'ac_voltage_r', 'scale': 0.1, 'unit': 'V', 'desc': 'Phase R voltage', 'alias': 'ac_voltage'},
38+
39: {'name': 'ac_current_r', 'scale': 0.1, 'unit': 'A', 'desc': 'Phase R current', 'alias': 'ac_current'},
39+
40: {'name': 'ac_power_r_high', 'scale': 1, 'unit': '', 'pair': 41, 'alias': 'ac_power_high'},
40+
41: {'name': 'ac_power_r_low', 'scale': 1, 'unit': '', 'pair': 40, 'combined_scale': 0.1, 'combined_unit': 'W', 'alias': 'ac_power_low'},
4141

4242
# Three-Phase AC Output - Phase S
4343
42: {'name': 'ac_voltage_s', 'scale': 0.1, 'unit': 'V', 'desc': 'Phase S voltage'},

0 commit comments

Comments
 (0)