Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 51 additions & 7 deletions custom_components/solax_modbus/plugin_growatt.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
62 changes: 62 additions & 0 deletions tests/unit/test_growatt_apx_module_registers.py
Original file line number Diff line number Diff line change
@@ -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
Loading