diff --git a/src/pooldose/mappings/model_PDPR1H04AW100_FW539292.json b/src/pooldose/mappings/model_PDPR1H04AW100_FW539292.json new file mode 100644 index 0000000..2cb23d8 --- /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": "binary_sensor", + "conversion": { + "|PDPR1H04AW100_FW539292_LABEL_w_1eo2fpohe_DISABLED_|": false, + "|PDPR1H04AW100_FW539292_LABEL_w_1eo2fpohe_ENABLED|": true + } + }, + "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": "binary_sensor", + "conversion": { + "|PDPR1H04AW100_FW539292_LABEL_w_1fhb0hqu2_DISABLE|": false, + "|PDPR1H04AW100_FW539292_LABEL_w_1fhb0hqu2_ENABLE|": true + } + }, + "flow_delay_status": { + "key": "w_1fhb0ht7t", + "type": "binary_sensor", + "conversion": { + "|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 1acfdd7..d187fe0 100644 --- a/tests/test_mapping_info.py +++ b/tests/test_mapping_info.py @@ -61,88 +61,134 @@ def test_available_selects_missing_fields(): _ = mapping_info.available_selects() -class TestPDPR1H1HAW100FW539187Mapping: - """Tests for the PDPR1H1HAW100 FW539187 mapping file.""" - - MODEL_ID = "PDPR1H1HAW100" - FW_CODE = "539187" - - @pytest.fixture - async def mapping(self): - """Load the mapping file.""" - info = await MappingInfo.load(self.MODEL_ID, self.FW_CODE) - assert info.status == RequestStatus.SUCCESS - assert info.mapping is not None - return info - - async def test_load_success(self, mapping): - """Test mapping file loads successfully.""" - assert mapping.status == RequestStatus.SUCCESS - - async def test_total_entity_count(self, mapping): - """Test total number of mapped entities.""" - assert len(mapping.mapping) == 54 - - async def test_entity_type_counts(self, mapping): - """Test entity count per type.""" - types = mapping.available_types() - assert len(types.get("sensor", [])) == 19 - assert len(types.get("binary_sensor", [])) == 23 - assert len(types.get("number", [])) == 8 - assert len(types.get("switch", [])) == 3 - assert len(types.get("select", [])) == 1 +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", + "ofa_ph_time", "ofa_orp_time", "ofa_cl_time", + "device_config", "temperature_unit", + ] + for name in expected_sensors: + assert name in sensors, f"Missing sensor: {name}" - async def test_new_alarm_binary_sensors(self, mapping): - """Test new alarm binary sensors are present.""" - types = mapping.available_types() - expected_alarms = [ - "alarm_ofa2_ph", - "alarm_ofa2_orp", - "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_high", + @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", "circulation_pump_status", "power_on_delay_status", "flow_delay_status", ] - for alarm in expected_alarms: - assert alarm in types["binary_sensor"], f"Missing binary_sensor: {alarm}" + for name in double_spa_alarms: + assert name in binary_sensors, f"Missing DOUBLE SPA binary_sensor: {name}" - async def test_new_sensors_with_conversions(self, mapping): - """Test new sensors with conversion dictionaries.""" - sensors = mapping.available_sensors() - sensors_with_conversion = [ - "peristaltic_cl_dosing", - "device_config", - "temperature_unit", - ] - for name in sensors_with_conversion: - assert name in sensors, f"Missing sensor: {name}" - assert sensors[name].conversion is not None, f"Missing conversion for {name}" - assert len(sensors[name].conversion) >= 2, f"Conversion too small for {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() - async def test_new_number_entities(self, mapping): - """Test new number entities are present.""" - types = mapping.available_types() expected_numbers = [ - "time_off_ph_dosing", - "time_off_orp_dosing", - "time_off_cl_dosing", - "power_on_delay_timer", - "flow_delay_timer", + "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 number in expected_numbers: - assert number in types["number"], f"Missing number: {number}" + 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() - async def test_select_entity(self, mapping): - """Test select entity with options and conversion.""" - selects = mapping.available_selects() assert "water_meter_unit" in selects - assert len(selects["water_meter_unit"].options) == 2 - assert len(selects["water_meter_unit"].conversion) == 2 + 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", [])) == 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