diff --git a/src/pooldose/client.py b/src/pooldose/client.py index 9911247..96806f0 100644 --- a/src/pooldose/client.py +++ b/src/pooldose/client.py @@ -10,7 +10,7 @@ from getmac import get_mac_address -from pooldose.constants import get_default_device_info +from pooldose.constants import MODEL_ALIASES, get_default_device_info from pooldose.type_definitions import DeviceInfoDict, StructuredValuesDict, APIVersionResponse from pooldose.mappings.mapping_info import MappingInfo from pooldose.request_handler import RequestHandler @@ -170,7 +170,8 @@ async def _load_device_info(self) -> RequestStatus: # pylint: disable=too-many- model_id = self.device_info.get("MODEL_ID") fw_code = self.device_info.get("FW_CODE") if model_id and fw_code: - self._mapping_info = await MappingInfo.load(str(model_id), str(fw_code)) + resolved_model = MODEL_ALIASES.get(str(model_id), str(model_id)) + self._mapping_info = await MappingInfo.load(resolved_model, str(fw_code)) else: _LOGGER.warning("Missing MODEL_ID or FW_CODE, cannot load mapping") self._mapping_info = MappingInfo(mapping=None, status=RequestStatus.NO_DATA) @@ -262,9 +263,9 @@ async def instant_values(self) -> tuple[RequestStatus, InstantValues | None]: device_raw_data = raw_data.get("devicedata", {}).get(device_id, {}) model_id = str(self.device_info.get("MODEL_ID", "")) fw_code = str(self.device_info.get("FW_CODE", "")) - if model_id == 'PDHC1H1HAR1V1' and fw_code == '539224': - #due to identifier issue in device firmware, use mapping prefix of PDPR1H1HAR1V0 - model_id = 'PDPR1H1HAR1V0' + # Resolve model alias for devices that report a different + # PRODUCT_CODE than the model used in their data keys. + model_id = MODEL_ALIASES.get(model_id, model_id) prefix = f"{model_id}_FW{fw_code}_" return RequestStatus.SUCCESS, InstantValues(device_raw_data, mapping, prefix, device_id, self._request_handler) except (KeyError, TypeError, ValueError) as err: diff --git a/src/pooldose/constants.py b/src/pooldose/constants.py index 5f23f24..b28a4f1 100644 --- a/src/pooldose/constants.py +++ b/src/pooldose/constants.py @@ -2,6 +2,14 @@ from pooldose.type_definitions import DeviceInfoDict +# Model alias mapping: PRODUCT_CODE reported by device → model ID used in data keys/mappings. +# Some devices report a different PRODUCT_CODE than the model ID actually used +# in their data keys and mapping files. +MODEL_ALIASES: dict[str, str] = { + "PDHC1H1HAR1V1": "PDPR1H1HAR1V0", + "PDPR1H1HAW102": "PDPR1H1HAW100", +} + # Default device info structure DEFAULT_DEVICE_INFO: DeviceInfoDict = { "NAME": None, # Device name diff --git a/tests/conftest.py b/tests/conftest.py index 10dacf7..f9c37ec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -242,3 +242,73 @@ def complete_client_setup( # pylint: disable=redefined-outer-name "raw_data": mock_raw_data, "structured_data": mock_structured_data } + + +@pytest.fixture +def mock_debug_config_aliased_model(): + """Create mock debug config with a PRODUCT_CODE that requires alias resolution. + + This simulates a device that reports PDPR1H1HAW102 as its PRODUCT_CODE, + but uses PDPR1H1HAW100 in its actual data keys and mapping files. + """ + return { + "GATEWAY": { + "DID": "TEST456", + "NAME": "Aliased Device", + "FW_REL": "1.7" + }, + "DEVICES": [{ + "DID": "TEST456_DEVICE", + "NAME": "POOLDOSE pH+ORP CF Group Wi-Fi", + "PRODUCT_CODE": "PDPR1H1HAW102", + "FW_REL": "1.7", + "FW_CODE": "539187" + }] + } + + +@pytest.fixture +def mock_device_info_aliased(): + """Create mock device information for an aliased model.""" + return { + "NAME": "Aliased Device", + "SERIAL_NUMBER": "TEST456", + "DEVICE_ID": "TEST456_DEVICE", + "MODEL": "POOLDOSE pH+ORP CF Group Wi-Fi", + "MODEL_ID": "PDPR1H1HAW102", + "OWNERID": "Owner1", + "GROUPNAME": "GroupA", + "FW_VERSION": "1.7", + "SW_VERSION": "3.00", + "API_VERSION": "v1/", + "FW_CODE": "539187", + "MAC": None, + "IP": "192.168.3.158", + "WIFI_SSID": "IoT Wi-Fi", + "WIFI_KEY": None, + "AP_SSID": None, + "AP_KEY": None + } + + +@pytest.fixture +def mock_raw_data_aliased(): + """Create mock raw data using PDPR1H1HAW100 keys (the resolved alias model).""" + return { + "devicedata": { + "TEST456_DEVICE": { + "PDPR1H1HAW100_FW539187_w_1eommf39k": { + "current": 17.5, + "magnitude": ["°C"] + }, + "PDPR1H1HAW100_FW539187_w_1ekeigkin": { + "current": 7.3, + "magnitude": ["pH"] + }, + "PDPR1H1HAW100_FW539187_w_1eklenb23": { + "current": 728, + "magnitude": ["mV"] + } + } + } + } diff --git a/tests/test_client.py b/tests/test_client.py index 05b4b13..e1de868 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -270,3 +270,82 @@ def test_is_connected_property(self): # pylint: disable=protected-access client._connected = True assert client.is_connected is True + + +class TestModelAliases: + """Test MODEL_ALIASES resolution for devices that report a different PRODUCT_CODE.""" + + @pytest.mark.asyncio + async def test_connect_resolves_model_alias( + self, mock_request_handler, mock_debug_config_aliased_model, mock_mapping_info + ): + """Test that connect() resolves PDPR1H1HAW102 to PDPR1H1HAW100 for mapping loading.""" + client = PooldoseClient(host="192.168.3.158") + + mock_request_handler.get_debug_config.return_value = ( + RequestStatus.SUCCESS, mock_debug_config_aliased_model + ) + mock_request_handler.get_wifi_station.return_value = ( + RequestStatus.SUCCESS, {"IP": "192.168.3.158", "SSID": "IoT Wi-Fi"} + ) + mock_request_handler.get_access_point.return_value = (RequestStatus.SUCCESS, {}) + mock_request_handler.get_network_info.return_value = (RequestStatus.SUCCESS, {}) + + with patch('pooldose.client.RequestHandler', return_value=mock_request_handler): + with patch( + 'pooldose.mappings.mapping_info.MappingInfo.load', + return_value=mock_mapping_info + ) as mock_load: + status = await client.connect() + + assert status == RequestStatus.SUCCESS + # Verify MappingInfo.load was called with the resolved alias model ID + mock_load.assert_called_once_with("PDPR1H1HAW100", "539187") + # The original MODEL_ID should still reflect what the device reported + assert client.device_info["MODEL_ID"] == "PDPR1H1HAW102" + + @pytest.mark.asyncio + async def test_instant_values_uses_resolved_prefix( + self, mock_request_handler, mock_device_info_aliased, + mock_mapping_info, mock_raw_data_aliased + ): + """Test that instant_values() uses the resolved model alias for the data prefix.""" + client = PooldoseClient(host="192.168.3.158") + # pylint: disable=protected-access + client._request_handler = mock_request_handler + client.device_info.update(mock_device_info_aliased) + client._mapping_info = mock_mapping_info + + mock_request_handler.get_values_raw.return_value = ( + RequestStatus.SUCCESS, mock_raw_data_aliased + ) + + status, instant_values = await client.instant_values() + + assert status == RequestStatus.SUCCESS + assert isinstance(instant_values, InstantValues) + + @pytest.mark.asyncio + async def test_existing_alias_pdhc1h1har1v1( + self, mock_request_handler, mock_mapping_info + ): + """Test that the existing PDHC1H1HAR1V1 alias still works via MODEL_ALIASES.""" + client = PooldoseClient(host="192.168.1.100") + # pylint: disable=protected-access + client._request_handler = mock_request_handler + client.device_info.update({ + "DEVICE_ID": "TEST_DEVICE", + "MODEL_ID": "PDHC1H1HAR1V1", + "FW_CODE": "539224", + }) + client._mapping_info = mock_mapping_info + + mock_request_handler.get_values_raw.return_value = ( + RequestStatus.SUCCESS, + {"devicedata": {"TEST_DEVICE": {}}} + ) + + status, instant_values = await client.instant_values() + + assert status == RequestStatus.SUCCESS + assert isinstance(instant_values, InstantValues)