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
11 changes: 6 additions & 5 deletions src/pooldose/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions src/pooldose/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 70 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
}
}
}
79 changes: 79 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)