From b0a0c4c80fad40ada55a8921049f175a788e1021 Mon Sep 17 00:00:00 2001 From: Ronald van der Meer Date: Wed, 11 Mar 2026 13:00:30 +0100 Subject: [PATCH 1/2] Add mapping for POOLDOSE DOUBLE SPA (PDPR1H04AW100_FW539292) Add device mapping file for SEKO POOLDOSE DOUBLE SPA with product code PDPR1H04AW100 and firmware code 539292. This is a significantly more capable device than the single pH+ORP models, featuring: - pH, ORP and Chlorine measurement sensors - CL dosing controls (setpoint, type, peristaltic, time on/off) - CL calibration sensors (type, offset, slope) - Extended alarm binary sensors (OFA2 for pH/ORP/CL, water temperature, concentration, system standby) - Delay timers (power-on and flow delay with enable/disable) - Device configuration sensor (pH/ORP vs pH/ORP/Chlorine mode) - Temperature unit selection Entity counts: 26 sensors, 23 binary_sensors, 11 numbers, 3 switches, 1 select (65 total). Mapping created from debug data provided in issue report. Added 8 tests validating loading, entity types and counts. All 131 tests pass. --- .../model_PDPR1H04AW100_FW539292.json | 330 ++++++++++++++++++ tests/test_mapping_info.py | 132 +++++++ 2 files changed, 462 insertions(+) create mode 100644 src/pooldose/mappings/model_PDPR1H04AW100_FW539292.json diff --git a/src/pooldose/mappings/model_PDPR1H04AW100_FW539292.json b/src/pooldose/mappings/model_PDPR1H04AW100_FW539292.json new file mode 100644 index 0000000..17d46df --- /dev/null +++ b/src/pooldose/mappings/model_PDPR1H04AW100_FW539292.json @@ -0,0 +1,330 @@ +{ + "temperature": { + "key": "w_1eommf39k", + "type": "sensor" + }, + "ph": { + "key": "w_1ekeigkin", + "type": "sensor" + }, + "orp": { + "key": "w_1eklenb23", + "type": "sensor" + }, + "cl": { + "key": "w_1eo03t46k", + "type": "sensor" + }, + "ph_type_dosing": { + "key": "w_1eklg44ro", + "type": "sensor", + "conversion": { + "|PDPR1H04AW100_FW539292_LABEL_w_1eklg44ro_ALCALYNE|": "alcalyne", + "|PDPR1H04AW100_FW539292_LABEL_w_1eklg44ro_ACID|": "acid" + } + }, + "peristaltic_ph_dosing": { + "key": "w_1eklj6euj", + "type": "sensor", + "conversion": { + "|PDPR1H04AW100_FW539292_LABEL_w_1eklj6euj_OFF|": "off", + "|PDPR1H04AW100_FW539292_LABEL_w_1eklj6euj_PROPORTIONAL|": "proportional", + "|PDPR1H04AW100_FW539292_LABEL_w_1eklj6euj_ON_OFF|": "on / off", + "|PDPR1H04AW100_FW539292_LABEL_w_1eklj6euj_TIMED|": "timed", + "|PDPR1H04AW100_FW539292_LABEL_w_1eklj6euj_CYCLE|": "cycle" + } + }, + "orp_type_dosing": { + "key": "w_1eklgnolb", + "type": "sensor", + "conversion": { + "|PDPR1H04AW100_FW539292_LABEL_w_1eklgnolb_LOW|": "low", + "|PDPR1H04AW100_FW539292_LABEL_w_1eklgnolb_HIGH|": "high" + } + }, + "peristaltic_orp_dosing": { + "key": "w_1eo1s18s8", + "type": "sensor", + "conversion": { + "|PDPR1H04AW100_FW539292_LABEL_w_1eo1s18s8_OFF|": "off", + "|PDPR1H04AW100_FW539292_LABEL_w_1eo1s18s8_PROPORTIONAL|": "proportional", + "|PDPR1H04AW100_FW539292_LABEL_w_1eo1s18s8_ON_OFF|": "on / off", + "|PDPR1H04AW100_FW539292_LABEL_w_1eo1s18s8_TIMED|": "timed", + "|PDPR1H04AW100_FW539292_LABEL_w_1eo1s18s8_CYCLE|": "cycle" + } + }, + "peristaltic_cl_dosing": { + "key": "w_1eo1sf1m6", + "type": "sensor", + "conversion": { + "|PDPR1H04AW100_FW539292_LABEL_w_1eo1sf1m6_OFF|": "off", + "|PDPR1H04AW100_FW539292_LABEL_w_1eo1sf1m6_PROPORTIONAL|": "proportional", + "|PDPR1H04AW100_FW539292_LABEL_w_1eo1sf1m6_ON_OFF|": "on / off", + "|PDPR1H04AW100_FW539292_LABEL_w_1eo1sf1m6_TIMED|": "timed", + "|PDPR1H04AW100_FW539292_LABEL_w_1eo1sf1m6_CYCLE|": "cycle" + } + }, + "ph_calibration_type": { + "key": "w_1eklh8gb7", + "type": "sensor", + "conversion": { + "|PDPR1H04AW100_FW539292_LABEL_w_1eklh8gb7_OFF|": "off", + "|PDPR1H04AW100_FW539292_LABEL_w_1eklh8gb7_REFERENCE|": "reference", + "|PDPR1H04AW100_FW539292_LABEL_w_1eklh8gb7_1_POINT|": "1_point", + "|PDPR1H04AW100_FW539292_LABEL_w_1eklh8gb7_2_POINTS|": "2_points" + } + }, + "ph_calibration_offset": { + "key": "w_1eklhs3b4", + "type": "sensor" + }, + "ph_calibration_slope": { + "key": "w_1eklhs65u", + "type": "sensor" + }, + "orp_calibration_type": { + "key": "w_1eklh8i5t", + "type": "sensor", + "conversion": { + "|PDPR1H04AW100_FW539292_LABEL_w_1eklh8i5t_OFF|": "off", + "|PDPR1H04AW100_FW539292_LABEL_w_1eklh8i5t_REFERENCE|": "reference", + "|PDPR1H04AW100_FW539292_LABEL_w_1eklh8i5t_1_POINT|": "1_point" + } + }, + "orp_calibration_offset": { + "key": "w_1eklhs8r3", + "type": "sensor" + }, + "orp_calibration_slope": { + "key": "w_1eklhsase", + "type": "sensor" + }, + "cl_calibration_type": { + "key": "w_1eo1umse8", + "type": "sensor", + "conversion": { + "|PDPR1H04AW100_FW539292_LABEL_w_1eo1umse8_OFF|": "off", + "|PDPR1H04AW100_FW539292_LABEL_w_1eo1umse8_2_POINT_CAL|": "2_point_cal" + } + }, + "cl_calibration_offset": { + "key": "w_1eo1uno9u", + "type": "sensor" + }, + "cl_calibration_slope": { + "key": "w_1eo1unpqb", + "type": "sensor" + }, + "circulation_pump_status": { + "key": "w_1eo2fpohe", + "type": "sensor", + "conversion": { + "|PDPR1H04AW100_FW539292_LABEL_w_1eo2fpohe_DISABLED_|": "disabled", + "|PDPR1H04AW100_FW539292_LABEL_w_1eo2fpohe_ENABLED|": "enabled" + } + }, + "ofa_ph_time": { + "key": "w_1eo1ttmft", + "type": "sensor" + }, + "ofa_orp_time": { + "key": "w_1eo1tui1d", + "type": "sensor" + }, + "ofa_cl_time": { + "key": "w_1eo1u082o", + "type": "sensor" + }, + "pump_alarm": { + "key": "w_1ekga097n", + "type": "binary_sensor" + }, + "ph_level_alarm": { + "key": "w_1eklf77pm", + "type": "binary_sensor" + }, + "orp_level_alarm": { + "key": "w_1eo04bcr2", + "type": "binary_sensor" + }, + "cl_level_alarm": { + "key": "w_1eo04dnhs", + "type": "binary_sensor" + }, + "flow_rate_alarm": { + "key": "w_1eo04nc5n", + "type": "binary_sensor" + }, + "relay_alarm": { + "key": "w_1eklffdl0", + "type": "binary_sensor" + }, + "relay_aux1": { + "key": "w_1eoi2rv4h", + "type": "binary_sensor" + }, + "relay_aux2": { + "key": "w_1eoi2s16b", + "type": "binary_sensor" + }, + "alarm_ofa_ph": { + "key": "w_1eklfb73r", + "type": "binary_sensor" + }, + "alarm_ofa2_ph": { + "key": "w_1eklfb8ob", + "type": "binary_sensor" + }, + "alarm_ofa_orp": { + "key": "w_1eo1pabt6", + "type": "binary_sensor" + }, + "alarm_ofa2_orp": { + "key": "w_1eo1paejq", + "type": "binary_sensor" + }, + "alarm_ofa_cl": { + "key": "w_1eo1pju7i", + "type": "binary_sensor" + }, + "alarm_ofa2_cl": { + "key": "w_1eo1pk0kl", + "type": "binary_sensor" + }, + "alarm_water_too_cold": { + "key": "w_1fahufq7q", + "type": "binary_sensor" + }, + "alarm_water_too_hot": { + "key": "w_1fahufrqe", + "type": "binary_sensor" + }, + "alarm_ph_too_low": { + "key": "w_1fahuft7o", + "type": "binary_sensor" + }, + "alarm_ph_too_high": { + "key": "w_1fahufum0", + "type": "binary_sensor" + }, + "alarm_cl_too_low_orp": { + "key": "w_1fahug0lo", + "type": "binary_sensor" + }, + "alarm_cl_too_high_orp": { + "key": "w_1fahug414", + "type": "binary_sensor" + }, + "alarm_cl_too_low": { + "key": "w_1fahvmoal", + "type": "binary_sensor" + }, + "alarm_cl_too_high": { + "key": "w_1fahvms2n", + "type": "binary_sensor" + }, + "alarm_system_standby": { + "key": "w_1fai1n09b", + "type": "binary_sensor" + }, + "ph_target": { + "key": "w_1ekeiqfat", + "type": "number" + }, + "orp_target": { + "key": "w_1eklgnjk2", + "type": "number" + }, + "cl_target": { + "key": "w_1eo1sejba", + "type": "number" + }, + "time_on_ph_dosing": { + "key": "w_1eklj2r1a", + "type": "number" + }, + "time_off_ph_dosing": { + "key": "w_1eklj30b7", + "type": "number" + }, + "time_on_orp_dosing": { + "key": "w_1eo1v2uji", + "type": "number" + }, + "time_off_orp_dosing": { + "key": "w_1eo1v30n4", + "type": "number" + }, + "time_on_cl_dosing": { + "key": "w_1eo20i95c", + "type": "number" + }, + "time_off_cl_dosing": { + "key": "w_1eo20ibtm", + "type": "number" + }, + "power_on_delay_timer": { + "key": "w_1ffng5qhk", + "type": "number" + }, + "flow_delay_timer": { + "key": "w_1ffng5spg", + "type": "number" + }, + "pause_dosing": { + "key": "w_1emtltkel", + "type": "switch" + }, + "pump_monitoring": { + "key": "w_1eklft47q", + "type": "switch" + }, + "frequency_input": { + "key": "w_1eklft5qt", + "type": "switch" + }, + "water_meter_unit": { + "key": "w_1eklinki6", + "type": "select", + "options": { + "0": "PDPR1H04AW100_FW539292_COMBO_w_1eklinki6_M_", + "1": "PDPR1H04AW100_FW539292_COMBO_w_1eklinki6_LITER" + }, + "conversion": { + "PDPR1H04AW100_FW539292_COMBO_w_1eklinki6_M_": "m3", + "PDPR1H04AW100_FW539292_COMBO_w_1eklinki6_LITER": "L" + } + }, + "device_config": { + "key": "w_1eu60qapu", + "type": "sensor", + "conversion": { + "|PDPR1H04AW100_FW539292_LABEL_w_1eu60qapu_PH_ORP|": "ph_orp", + "|PDPR1H04AW100_FW539292_LABEL_w_1eu60qapu_PH_ORP_CHLORINE|": "ph_orp_chlorine" + } + }, + "temperature_unit": { + "key": "w_1fkjtkd4g", + "type": "sensor", + "conversion": { + "|PDPR1H04AW100_FW539292_LABEL_w_1fkjtkd4g__C|": "celsius", + "|PDPR1H04AW100_FW539292_LABEL_w_1fkjtkd4g__F|": "fahrenheit" + } + }, + "power_on_delay_status": { + "key": "w_1fhb0hqu2", + "type": "sensor", + "conversion": { + "|PDPR1H04AW100_FW539292_LABEL_w_1fhb0hqu2_DISABLE|": "disabled", + "|PDPR1H04AW100_FW539292_LABEL_w_1fhb0hqu2_ENABLE|": "enabled" + } + }, + "flow_delay_status": { + "key": "w_1fhb0ht7t", + "type": "sensor", + "conversion": { + "|PDPR1H04AW100_FW539292_LABEL_w_1fhb0ht7t_DISABLE|": "disabled", + "|PDPR1H04AW100_FW539292_LABEL_w_1fhb0ht7t_ENABLE|": "enabled" + } + } +} diff --git a/tests/test_mapping_info.py b/tests/test_mapping_info.py index 017c69b..aa18b7a 100644 --- a/tests/test_mapping_info.py +++ b/tests/test_mapping_info.py @@ -59,3 +59,135 @@ def test_available_selects_missing_fields(): mapping_info = MappingInfo(mapping=fake_mapping, status=RequestStatus.SUCCESS) with pytest.raises(KeyError): _ = mapping_info.available_selects() + + +class TestDoubleSpaMapping: + """Tests for the POOLDOSE DOUBLE SPA mapping file (PDPR1H04AW100_FW539292).""" + + @pytest.mark.asyncio + async def test_load_double_spa_mapping(self): + """Test that the DOUBLE SPA mapping file loads successfully.""" + mapping_info = await MappingInfo.load("PDPR1H04AW100", "539292") + assert mapping_info.status == RequestStatus.SUCCESS + assert mapping_info.mapping is not None + + @pytest.mark.asyncio + async def test_double_spa_sensors(self): + """Test that expected sensors are present in the DOUBLE SPA mapping.""" + mapping_info = await MappingInfo.load("PDPR1H04AW100", "539292") + sensors = mapping_info.available_sensors() + + # Core measurement sensors + expected_sensors = [ + "temperature", "ph", "orp", "cl", + "ph_type_dosing", "peristaltic_ph_dosing", + "orp_type_dosing", "peristaltic_orp_dosing", + "peristaltic_cl_dosing", + "ph_calibration_type", "ph_calibration_offset", "ph_calibration_slope", + "orp_calibration_type", "orp_calibration_offset", "orp_calibration_slope", + "cl_calibration_type", "cl_calibration_offset", "cl_calibration_slope", + "circulation_pump_status", + "ofa_ph_time", "ofa_orp_time", "ofa_cl_time", + "device_config", "temperature_unit", + "power_on_delay_status", "flow_delay_status", + ] + for name in expected_sensors: + assert name in sensors, f"Missing sensor: {name}" + + @pytest.mark.asyncio + async def test_double_spa_sensor_conversions(self): + """Test that sensors with conversions have the correct label prefix.""" + mapping_info = await MappingInfo.load("PDPR1H04AW100", "539292") + sensors = mapping_info.available_sensors() + + # pH type dosing should have conversion with PDPR1H04AW100_FW539292 prefix + ph_type = sensors["ph_type_dosing"] + assert ph_type.conversion is not None + assert "|PDPR1H04AW100_FW539292_LABEL_w_1eklg44ro_ACID|" in ph_type.conversion + assert ph_type.conversion["|PDPR1H04AW100_FW539292_LABEL_w_1eklg44ro_ACID|"] == "acid" + + # Peristaltic CL dosing - unique to DOUBLE SPA + cl_dosing = sensors["peristaltic_cl_dosing"] + assert cl_dosing.conversion is not None + assert len(cl_dosing.conversion) == 5 # off, proportional, on/off, timed, cycle + + @pytest.mark.asyncio + async def test_double_spa_binary_sensors(self): + """Test that expected binary sensors are present, including DOUBLE SPA extras.""" + mapping_info = await MappingInfo.load("PDPR1H04AW100", "539292") + binary_sensors = mapping_info.available_binary_sensors() + + # Standard alarms (shared with PDPR1H1HAW100) + standard_alarms = [ + "pump_alarm", "ph_level_alarm", "orp_level_alarm", + "flow_rate_alarm", "relay_alarm", "relay_aux1", "relay_aux2", + "alarm_ofa_ph", "alarm_ofa_orp", + ] + for name in standard_alarms: + assert name in binary_sensors, f"Missing binary_sensor: {name}" + + # DOUBLE SPA-specific alarms + double_spa_alarms = [ + "cl_level_alarm", + "alarm_ofa2_ph", "alarm_ofa2_orp", + "alarm_ofa_cl", "alarm_ofa2_cl", + "alarm_water_too_cold", "alarm_water_too_hot", + "alarm_ph_too_low", "alarm_ph_too_high", + "alarm_cl_too_low_orp", "alarm_cl_too_high_orp", + "alarm_cl_too_low", "alarm_cl_too_high", + "alarm_system_standby", + ] + for name in double_spa_alarms: + assert name in binary_sensors, f"Missing DOUBLE SPA binary_sensor: {name}" + + @pytest.mark.asyncio + async def test_double_spa_numbers(self): + """Test that expected number entities are present.""" + mapping_info = await MappingInfo.load("PDPR1H04AW100", "539292") + numbers = mapping_info.available_numbers() + + expected_numbers = [ + "ph_target", "orp_target", "cl_target", + "time_on_ph_dosing", "time_off_ph_dosing", + "time_on_orp_dosing", "time_off_orp_dosing", + "time_on_cl_dosing", "time_off_cl_dosing", + "power_on_delay_timer", "flow_delay_timer", + ] + for name in expected_numbers: + assert name in numbers, f"Missing number: {name}" + + @pytest.mark.asyncio + async def test_double_spa_switches(self): + """Test that expected switch entities are present.""" + mapping_info = await MappingInfo.load("PDPR1H04AW100", "539292") + switches = mapping_info.available_switches() + + expected_switches = ["pause_dosing", "pump_monitoring", "frequency_input"] + for name in expected_switches: + assert name in switches, f"Missing switch: {name}" + + @pytest.mark.asyncio + async def test_double_spa_selects(self): + """Test that select entities are present with correct options.""" + mapping_info = await MappingInfo.load("PDPR1H04AW100", "539292") + selects = mapping_info.available_selects() + + assert "water_meter_unit" in selects + wmu = selects["water_meter_unit"] + assert wmu.key == "w_1eklinki6" + assert "0" in wmu.options + assert "1" in wmu.options + assert "PDPR1H04AW100_FW539292_COMBO_w_1eklinki6_M_" in wmu.conversion + assert wmu.conversion["PDPR1H04AW100_FW539292_COMBO_w_1eklinki6_M_"] == "m3" + + @pytest.mark.asyncio + async def test_double_spa_entity_counts(self): + """Test the total entity counts for the DOUBLE SPA mapping.""" + mapping_info = await MappingInfo.load("PDPR1H04AW100", "539292") + types = mapping_info.available_types() + + assert len(types.get("sensor", [])) == 26 + assert len(types.get("binary_sensor", [])) == 23 + assert len(types.get("number", [])) == 11 + assert len(types.get("switch", [])) == 3 + assert len(types.get("select", [])) == 1 From da71e4a1a1977e3e82ac4158b03d8ed65d19fe9a Mon Sep 17 00:00:00 2001 From: Ronald van der Meer Date: Sun, 15 Mar 2026 18:06:21 +0100 Subject: [PATCH 2/2] Convert enabled/disabled sensors to binary_sensor type in DOUBLE SPA mapping --- .../mappings/model_PDPR1H04AW100_FW539292.json | 18 +++++++++--------- tests/test_mapping_info.py | 6 ++---- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/pooldose/mappings/model_PDPR1H04AW100_FW539292.json b/src/pooldose/mappings/model_PDPR1H04AW100_FW539292.json index 17d46df..2cb23d8 100644 --- a/src/pooldose/mappings/model_PDPR1H04AW100_FW539292.json +++ b/src/pooldose/mappings/model_PDPR1H04AW100_FW539292.json @@ -117,10 +117,10 @@ }, "circulation_pump_status": { "key": "w_1eo2fpohe", - "type": "sensor", + "type": "binary_sensor", "conversion": { - "|PDPR1H04AW100_FW539292_LABEL_w_1eo2fpohe_DISABLED_|": "disabled", - "|PDPR1H04AW100_FW539292_LABEL_w_1eo2fpohe_ENABLED|": "enabled" + "|PDPR1H04AW100_FW539292_LABEL_w_1eo2fpohe_DISABLED_|": false, + "|PDPR1H04AW100_FW539292_LABEL_w_1eo2fpohe_ENABLED|": true } }, "ofa_ph_time": { @@ -313,18 +313,18 @@ }, "power_on_delay_status": { "key": "w_1fhb0hqu2", - "type": "sensor", + "type": "binary_sensor", "conversion": { - "|PDPR1H04AW100_FW539292_LABEL_w_1fhb0hqu2_DISABLE|": "disabled", - "|PDPR1H04AW100_FW539292_LABEL_w_1fhb0hqu2_ENABLE|": "enabled" + "|PDPR1H04AW100_FW539292_LABEL_w_1fhb0hqu2_DISABLE|": false, + "|PDPR1H04AW100_FW539292_LABEL_w_1fhb0hqu2_ENABLE|": true } }, "flow_delay_status": { "key": "w_1fhb0ht7t", - "type": "sensor", + "type": "binary_sensor", "conversion": { - "|PDPR1H04AW100_FW539292_LABEL_w_1fhb0ht7t_DISABLE|": "disabled", - "|PDPR1H04AW100_FW539292_LABEL_w_1fhb0ht7t_ENABLE|": "enabled" + "|PDPR1H04AW100_FW539292_LABEL_w_1fhb0ht7t_DISABLE|": false, + "|PDPR1H04AW100_FW539292_LABEL_w_1fhb0ht7t_ENABLE|": true } } } diff --git a/tests/test_mapping_info.py b/tests/test_mapping_info.py index 09f1efa..d187fe0 100644 --- a/tests/test_mapping_info.py +++ b/tests/test_mapping_info.py @@ -86,10 +86,8 @@ async def test_double_spa_sensors(self): "ph_calibration_type", "ph_calibration_offset", "ph_calibration_slope", "orp_calibration_type", "orp_calibration_offset", "orp_calibration_slope", "cl_calibration_type", "cl_calibration_offset", "cl_calibration_slope", - "circulation_pump_status", "ofa_ph_time", "ofa_orp_time", "ofa_cl_time", "device_config", "temperature_unit", - "power_on_delay_status", "flow_delay_status", ] for name in expected_sensors: assert name in sensors, f"Missing sensor: {name}" @@ -189,8 +187,8 @@ async def test_double_spa_entity_counts(self): mapping_info = await MappingInfo.load("PDPR1H04AW100", "539292") types = mapping_info.available_types() - assert len(types.get("sensor", [])) == 26 - assert len(types.get("binary_sensor", [])) == 23 + assert len(types.get("sensor", [])) == 23 + assert len(types.get("binary_sensor", [])) == 26 assert len(types.get("number", [])) == 11 assert len(types.get("switch", [])) == 3 assert len(types.get("select", [])) == 1