diff --git a/custom_components/solax_modbus/plugin_growatt.py b/custom_components/solax_modbus/plugin_growatt.py index f87e90730..6bbc27f7e 100644 --- a/custom_components/solax_modbus/plugin_growatt.py +++ b/custom_components/solax_modbus/plugin_growatt.py @@ -1,5 +1,5 @@ import logging -from dataclasses import dataclass +from dataclasses import dataclass, replace from typing import Any from homeassistant.components.number import NumberDeviceClass @@ -94,6 +94,12 @@ MPPT10 = 0x400000 ALL_MPPT_GROUP = MPPT3 | MPPT4 | MPPT6 | MPPT8 | MPPT10 +# DLP MID 30KTL3-XH units expose BMS1 module 1 through the APX input-register block. +APX_BMS_INPUT = 0x800000 +ALL_APX_BMS_REGISTER_GROUP = APX_BMS_INPUT + +APX_BMS_INPUT_SERIAL_PREFIXES = ["DLP"] + ALLDEFAULT = 0 # should be equivalent to HYBRID | AC | GEN | GEN2 | GEN3 | GEN4 | X1 | X3 # SPF models known to expose only one PV input / MPPT even if the generic SPF block defines PV2 sensors. @@ -6695,6 +6701,7 @@ def value_function_inverter_module(initval: int, descr: Any, datadict: dict[str, scale=value_function_module_status, allowedtypes=HYBRID | GEN4, entity_registry_enabled_default=False, + blacklist=APX_BMS_INPUT_SERIAL_PREFIXES, icon="mdi:battery", ), GrowattModbusSensorEntityDescription( @@ -6716,10 +6723,11 @@ def value_function_inverter_module(initval: int, descr: Any, datadict: dict[str, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, register=5882, - register_type=REG_HOLDING, ### HOLDING!!! + register_type=REG_HOLDING, register_data_type=REGISTER_U16, allowedtypes=HYBRID | GEN4, entity_registry_enabled_default=False, + blacklist=APX_BMS_INPUT_SERIAL_PREFIXES, icon="mdi:battery-heart", ), GrowattModbusSensorEntityDescription( @@ -6729,11 +6737,12 @@ def value_function_inverter_module(initval: int, descr: Any, datadict: dict[str, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, register=5883, - register_type=REG_HOLDING, ### HOLDING!!! + register_type=REG_HOLDING, register_data_type=REGISTER_U16, scale=0.1, allowedtypes=GEN4 | HYBRID, entity_registry_enabled_default=False, + blacklist=APX_BMS_INPUT_SERIAL_PREFIXES, icon="mdi:battery", ), GrowattModbusSensorEntityDescription( @@ -6743,11 +6752,12 @@ def value_function_inverter_module(initval: int, descr: Any, datadict: dict[str, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, register=5884, - register_type=REG_HOLDING, ### HOLDING!!! + register_type=REG_HOLDING, register_data_type=REGISTER_U16, scale=value_function_bms_module_combined_current, allowedtypes=GEN4 | HYBRID, entity_registry_enabled_default=False, + blacklist=APX_BMS_INPUT_SERIAL_PREFIXES, icon="mdi:battery", ), GrowattModbusSensorEntityDescription( @@ -6762,6 +6772,7 @@ def value_function_inverter_module(initval: int, descr: Any, datadict: dict[str, register_data_type=REGISTER_U16, entity_registry_enabled_default=True, allowedtypes=GEN4 | HYBRID, + blacklist=APX_BMS_INPUT_SERIAL_PREFIXES, icon="mdi:battery", ), GrowattModbusSensorEntityDescription( @@ -6771,11 +6782,12 @@ def value_function_inverter_module(initval: int, descr: Any, datadict: dict[str, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, register=5887, # maybe 5886 and U32 - register_type=REG_HOLDING, ### HOLDING!!! + register_type=REG_HOLDING, register_data_type=REGISTER_U16, # maybe U32 but then change register to 5886 scale=0.1, allowedtypes=GEN4 | HYBRID, entity_registry_enabled_default=False, + blacklist=APX_BMS_INPUT_SERIAL_PREFIXES, icon="mdi:battery", ), GrowattModbusSensorEntityDescription( @@ -6790,6 +6802,7 @@ def value_function_inverter_module(initval: int, descr: Any, datadict: dict[str, scale=0.1, allowedtypes=GEN4 | HYBRID, entity_registry_enabled_default=False, + blacklist=APX_BMS_INPUT_SERIAL_PREFIXES, icon="mdi:battery", ), GrowattModbusSensorEntityDescription( @@ -6804,6 +6817,7 @@ def value_function_inverter_module(initval: int, descr: Any, datadict: dict[str, scale=0.1, allowedtypes=GEN4 | HYBRID, entity_registry_enabled_default=False, + blacklist=APX_BMS_INPUT_SERIAL_PREFIXES, icon="mdi:battery", ), GrowattModbusSensorEntityDescription( @@ -6815,6 +6829,7 @@ def value_function_inverter_module(initval: int, descr: Any, datadict: dict[str, scale=value_function_module_warning_text, allowedtypes=HYBRID | GEN4, entity_registry_enabled_default=False, + blacklist=APX_BMS_INPUT_SERIAL_PREFIXES, icon="mdi:battery", ), GrowattModbusSensorEntityDescription( @@ -6825,6 +6840,7 @@ def value_function_inverter_module(initval: int, descr: Any, datadict: dict[str, register_data_type=REGISTER_U16, allowedtypes=GEN4 | HYBRID, entity_registry_enabled_default=False, + blacklist=APX_BMS_INPUT_SERIAL_PREFIXES, icon="mdi:battery", ), GrowattModbusSensorEntityDescription( @@ -9174,6 +9190,33 @@ def value_function_inverter_module(initval: int, descr: Any, datadict: dict[str, ), ] +APX_BMS1_MODULE1_INPUT_REGISTERS = { + "bms_1_module_1_status": 5080, + "bms_1_module_1_soh": 5082, + "bms_1_module_1_volt": 5083, + "bms_1_module_1_combined_current": 5084, + "bms_1_module_1_combined_power": 5085, + "bms_1_module_1_toe": 5087, + "bms_1_module_1_max_cell_temp": 5090, + "bms_1_module_1_min_cell_temp": 5091, + "bms_1_module_1_warning_text": 5098, + "bms_1_module_1_charge_cycles": 5108, +} + +# Keep the established 588x holding-register descriptions for other Growatt models, +# and create a DLP-only 508x input-register variant from the same metadata. +SENSOR_TYPES.extend( + replace( + description, + register=APX_BMS1_MODULE1_INPUT_REGISTERS[description.key], + register_type=REG_INPUT, + allowedtypes=description.allowedtypes | APX_BMS_INPUT, + blacklist=None, + ) + for description in tuple(SENSOR_TYPES) + if description.key in APX_BMS1_MODULE1_INPUT_REGISTERS +) + TIME_TYPES = [ GrowattModbusTimeEntityDescription( @@ -9684,7 +9727,7 @@ def value_function_inverter_module(initval: int, descr: Any, datadict: dict[str, "KMN": HYBRID | GEN4 | X3, # MID 17000 TL3-XH Hybrid, 2 MPPT "KNN": HYBRID | GEN4 | X3 | MPPT3, # MID 25000 TL3-XH Hybrid, 3 MPPT "RKM": HYBRID | GEN4 | X3 | MPPT3, # MID 30000 TL3-XH Hybrid, 3 MPPT - "DLP": HYBRID | GEN4 | X3 | MPPT3, # MID 30000 TL3-XH Hybrid, 3 MPPT + "DLP": HYBRID | GEN4 | X3 | MPPT3 | APX_BMS_INPUT, # MID 30000 TL3-XH Hybrid, 3 MPPT # MOD BP hybrid "FMP": HYBRID | GEN4 | X3, # MOD 5000 TL3-XH (BP) Hybrid, 2 MPPT "FPP": HYBRID | GEN4 | X3, # MOD 7000 TL3-XH (BP) Hybrid, 2 MPPT @@ -9825,12 +9868,13 @@ def matchInverterWithMask( epsmatch = ((inverterspec & entitymask & ALL_EPS_GROUP) != 0) or (entitymask & ALL_EPS_GROUP == 0) dcbmatch = ((inverterspec & entitymask & ALL_DCB_GROUP) != 0) or (entitymask & ALL_DCB_GROUP == 0) mpptmatch = ((inverterspec & entitymask & ALL_MPPT_GROUP) != 0) or (entitymask & ALL_MPPT_GROUP == 0) + apx_bms_register_match = ((inverterspec & entitymask & ALL_APX_BMS_REGISTER_GROUP) != 0) or (entitymask & ALL_APX_BMS_REGISTER_GROUP == 0) blacklisted = False if blacklist: for start in blacklist: if serialnumber.startswith(start): blacklisted = True - return (genmatch and xmatch and hybmatch and epsmatch and dcbmatch and mpptmatch) and not blacklisted + return (genmatch and xmatch and hybmatch and epsmatch and dcbmatch and mpptmatch and apx_bms_register_match) and not blacklisted ENERGY_DASHBOARD_MAPPING = EnergyDashboardMapping( diff --git a/tests/unit/test_growatt_apx_module_registers.py b/tests/unit/test_growatt_apx_module_registers.py new file mode 100644 index 000000000..8f2df7347 --- /dev/null +++ b/tests/unit/test_growatt_apx_module_registers.py @@ -0,0 +1,62 @@ +from custom_components.solax_modbus.const import REG_HOLDING, REG_INPUT +from custom_components.solax_modbus.plugin_growatt import ( + SENSOR_TYPES, + SERIAL_PREFIX_TYPES, + GrowattModbusSensorEntityDescription, + plugin_instance, +) + + +def _matching_module1_descriptions(serial_number: str) -> dict[str, GrowattModbusSensorEntityDescription]: + inverter_type = SERIAL_PREFIX_TYPES[serial_number[:3]] + descriptions: dict[str, GrowattModbusSensorEntityDescription] = {} + for description in SENSOR_TYPES: + if not description.key.startswith("bms_1_module_1_") or not plugin_instance.matchInverterWithMask( + inverter_type, description.allowedtypes, serial_number, description.blacklist + ): + continue + assert description.key not in descriptions, f"duplicate matching description for {serial_number}: {description.key}" + descriptions[description.key] = description + return descriptions + + +def test_dlp_bms1_module1_uses_apx_input_registers() -> None: + expected_input_registers = { + "bms_1_module_1_status": 5080, + "bms_1_module_1_soh": 5082, + "bms_1_module_1_volt": 5083, + "bms_1_module_1_combined_current": 5084, + "bms_1_module_1_combined_power": 5085, + "bms_1_module_1_toe": 5087, + "bms_1_module_1_max_cell_temp": 5090, + "bms_1_module_1_min_cell_temp": 5091, + "bms_1_module_1_warning_text": 5098, + "bms_1_module_1_charge_cycles": 5108, + } + descriptions = _matching_module1_descriptions("DLP1234567") + + for key, register in expected_input_registers.items(): + description = descriptions[key] + assert description.register == register + assert description.register_type == REG_INPUT + + +def test_jcm_bms1_module1_keeps_holding_registers() -> None: + expected_holding_registers = { + "bms_1_module_1_status": 5880, + "bms_1_module_1_soh": 5882, + "bms_1_module_1_volt": 5883, + "bms_1_module_1_combined_current": 5884, + "bms_1_module_1_combined_power": 5885, + "bms_1_module_1_toe": 5887, + "bms_1_module_1_max_cell_temp": 5890, + "bms_1_module_1_min_cell_temp": 5891, + "bms_1_module_1_warning_text": 5898, + "bms_1_module_1_charge_cycles": 5908, + } + descriptions = _matching_module1_descriptions("JCM0D12345") + + for key, register in expected_holding_registers.items(): + description = descriptions[key] + assert description.register == register + assert description.register_type == REG_HOLDING