diff --git a/custom_components/tuya_local/__init__.py b/custom_components/tuya_local/__init__.py index 64ec0f7d94..0224478e96 100644 --- a/custom_components/tuya_local/__init__.py +++ b/custom_components/tuya_local/__init__.py @@ -507,6 +507,73 @@ def update_unique_id13_3(entity_entry): options={**entry.options}, minor_version=4, ) + + if entry.version == 13 and entry.minor_version < 5: + # Migrate unique ids of existing entities to new id taking into + # account translation_key, and standardising naming + device_id = entry.unique_id + conf_file = await hass.async_add_executor_job( + get_config, + entry.data[CONF_TYPE], + ) + if conf_file is None: + _LOGGER.error( + NOT_FOUND, + entry.data[CONF_TYPE], + ) + return False + + @callback + def update_unique_id13_5(entity_entry): + """Update the unique id of an entity entry.""" + old_id = entity_entry.unique_id + platform = entity_entry.entity_id.split(".", 1)[0] + # Standardistion of entity naming to use translation_key + replacements = { + "number_countdown": "number_timer", + "select_countdown": "select_timer", + "sensor_countdown": "sensor_time_remaining", + "sensor_countdown_timer": "sensor_time_remaining", + "fan": "fan_aroma_diffuser", + } + for suffix, new_suffix in replacements.items(): + if old_id.endswith(suffix): + e = conf_file.primary_entity + new_id = e.unique_id(device_id) + if ( + e.entity != platform + or e.name + or not new_id.endswith(new_suffix) + ): + for e in conf_file.secondary_entities(): + new_id = e.unique_id(device_id) + if ( + e.entity == platform + and not e.name + and new_id.endswith(new_suffix) + ): + break + if ( + e.entity == platform + and not e.name + and new_id.endswith(new_suffix) + ): + _LOGGER.info( + "Migrating %s unique_id %s to %s", + e.entity, + old_id, + new_id, + ) + return { + "new_unique_id": entity_entry.unique_id.replace( + old_id, + new_id, + ) + } + + await async_migrate_entries(hass, entry.entry_id, update_unique_id13_5) + hass.config_entries.async_update_entry(entry, minor_version=5) + return True diff --git a/custom_components/tuya_local/config_flow.py b/custom_components/tuya_local/config_flow.py index 5dedc377d2..3cf400e57e 100644 --- a/custom_components/tuya_local/config_flow.py +++ b/custom_components/tuya_local/config_flow.py @@ -73,7 +73,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 13 - MINOR_VERSION = 4 + MINOR_VERSION = 5 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH device = None data = {} diff --git a/custom_components/tuya_local/devices/atorch_s1wp.yaml b/custom_components/tuya_local/devices/atorch_s1wp.yaml index d6dd77d0ed..415317fa1d 100644 --- a/custom_components/tuya_local/devices/atorch_s1wp.yaml +++ b/custom_components/tuya_local/devices/atorch_s1wp.yaml @@ -13,7 +13,6 @@ primary_entity: secondary_entities: - entity: number category: config - name: Countdown translation_key: timer dps: - id: 9 @@ -28,7 +27,6 @@ secondary_entities: step: 60 - entity: sensor class: current - name: Current dps: - id: 18 name: sensor @@ -39,7 +37,6 @@ secondary_entities: - scale: 1000 - entity: sensor class: power - name: Power dps: - id: 19 name: sensor @@ -50,7 +47,6 @@ secondary_entities: - scale: 100 - entity: sensor class: voltage - name: Voltage dps: - id: 20 name: sensor @@ -318,7 +314,7 @@ secondary_entities: mapping: - scale: 1000 - entity: sensor - name: Countdown timer + translation_key: time_remaining category: diagnostic class: duration dps: @@ -462,7 +458,6 @@ secondary_entities: value: Countdown - entity: sensor class: frequency - name: Frequency dps: - id: 133 name: sensor @@ -472,7 +467,6 @@ secondary_entities: mapping: - scale: 100 - entity: sensor - name: Power factor class: power_factor dps: - id: 134 diff --git a/custom_components/tuya_local/devices/dongguan_garage_door_opener.yaml b/custom_components/tuya_local/devices/dongguan_garage_door_opener.yaml index f1b72615ee..b8a4285a3f 100644 --- a/custom_components/tuya_local/devices/dongguan_garage_door_opener.yaml +++ b/custom_components/tuya_local/devices/dongguan_garage_door_opener.yaml @@ -29,7 +29,6 @@ primary_entity: value: closed secondary_entities: - entity: number - name: Countdown translation_key: timer category: config dps: diff --git a/custom_components/tuya_local/devices/ge_jasco_ultra_pro_toggle_dimmer_v2.yaml b/custom_components/tuya_local/devices/ge_jasco_ultra_pro_toggle_dimmer_v2.yaml index e1e00c7600..1e00bac23a 100644 --- a/custom_components/tuya_local/devices/ge_jasco_ultra_pro_toggle_dimmer_v2.yaml +++ b/custom_components/tuya_local/devices/ge_jasco_ultra_pro_toggle_dimmer_v2.yaml @@ -34,7 +34,6 @@ secondary_entities: value: LED - entity: number category: config - name: Countdown translation_key: timer dps: - id: 6 diff --git a/custom_components/tuya_local/devices/loratap_relay.yaml b/custom_components/tuya_local/devices/loratap_relay.yaml index b874dfbdca..1d6ac61c18 100644 --- a/custom_components/tuya_local/devices/loratap_relay.yaml +++ b/custom_components/tuya_local/devices/loratap_relay.yaml @@ -22,7 +22,6 @@ primary_entity: optional: true secondary_entities: - entity: number - name: Countdown translation_key: timer category: config dps: diff --git a/custom_components/tuya_local/devices/simple_gate_opener.yaml b/custom_components/tuya_local/devices/simple_gate_opener.yaml index ad1aa5b52d..551a07e354 100644 --- a/custom_components/tuya_local/devices/simple_gate_opener.yaml +++ b/custom_components/tuya_local/devices/simple_gate_opener.yaml @@ -21,16 +21,18 @@ primary_entity: type: boolean secondary_entities: - entity: number - name: Countdown category: config translation_key: timer - mode: box dps: - id: 7 type: integer + optional: true name: value - unit: sec + unit: min range: min: 0 max: 86400 - optional: true + mapping: + - scale: 60 + step: 60 + - dps_val: null diff --git a/custom_components/tuya_local/devices/tontine_039-WIFI_blanket.yaml b/custom_components/tuya_local/devices/tontine_039-WIFI_blanket.yaml index 80fe994810..979f8a8199 100644 --- a/custom_components/tuya_local/devices/tontine_039-WIFI_blanket.yaml +++ b/custom_components/tuya_local/devices/tontine_039-WIFI_blanket.yaml @@ -38,9 +38,8 @@ secondary_entities: min: 1 max: 10 - entity: sensor - name: Countdown class: duration - translation_key: timer + translation_key: time_remaining dps: - id: 10 name: sensor diff --git a/custom_components/tuya_local/helpers/device_config.py b/custom_components/tuya_local/helpers/device_config.py index e2b7945be4..b00f2ac849 100644 --- a/custom_components/tuya_local/helpers/device_config.py +++ b/custom_components/tuya_local/helpers/device_config.py @@ -135,6 +135,12 @@ def secondary_entities(self): for conf in self._config.get("secondary_entities", {}): yield TuyaEntityConfig(self, conf) + def all_entities(self): + """Iterate through all entities for this device.""" + yield self.primary_entity + for e in self.secondary_entities(): + yield e + def matches(self, dps): required_dps = self._get_required_dps() diff --git a/tests/devices/base_device_tests.py b/tests/devices/base_device_tests.py index 4672050ba8..026d7768b7 100644 --- a/tests/devices/base_device_tests.py +++ b/tests/devices/base_device_tests.py @@ -73,12 +73,8 @@ def setUpForConfig(self, config_file, payload): self.entities = {} self.secondary_category = [] - self.primary_entity = cfg.primary_entity.config_id - self.entities[self.primary_entity] = self.create_entity(cfg.primary_entity) - self.names = {} - self.names[cfg.primary_entity.config_id] = cfg.primary_entity.name - for e in cfg.secondary_entities(): + for e in cfg.all_entities(): self.entities[e.config_id] = self.create_entity(e) self.names[e.config_id] = e.name diff --git a/tests/devices/test_gx_aroma_diffuser.py b/tests/devices/test_gx_aroma_diffuser.py index 843db98a89..9a39b2b46a 100644 --- a/tests/devices/test_gx_aroma_diffuser.py +++ b/tests/devices/test_gx_aroma_diffuser.py @@ -19,7 +19,7 @@ class TestAromaDiffuser(TuyaDeviceTestCase): def setUp(self): self.setUpForConfig("yym_805SW_aroma_nightlight.yaml", GX_AROMA_PAYLOAD) - self.subject = self.entities["fan"] + self.subject = self.entities["fan_aroma_diffuser"] self.mark_secondary(["select_timer"]) def test_speed_step(self): diff --git a/tests/test_device.py b/tests/test_device.py index 1a1651b4a2..4a9cfbb14b 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -91,7 +91,7 @@ async def test_refreshes_state_if_no_cached_state_exists(self): self.subject.async_refresh.assert_awaited() async def test_detection_returns_none_when_device_type_not_detected(self): - self.subject._cached_state = {"2": False, "updated_at": time()} + self.subject._cached_state = {"192": False, "updated_at": time()} self.assertEqual(await self.subject.async_inferred_type(), None) async def test_refreshes_when_there_is_no_pending_reset(self): diff --git a/tests/test_device_config.py b/tests/test_device_config.py index 88d8de6e71..a3257cda5b 100644 --- a/tests/test_device_config.py +++ b/tests/test_device_config.py @@ -521,6 +521,21 @@ def test_config_files_parse(self): f"misspelled secondary_entities in {cfg}", ) + def test_configs_can_be_matched(self): + """Test that the config files can be matched to a device.""" + required_dps = 0 + for cfg in available_configs(): + parsed = TuyaDeviceConfig(cfg) + for entity in parsed.all_entities(): + for dp in entity.dps(): + if not dp.optional: + required_dps += 1 + self.assertGreater( + required_dps, + 0, + msg=f"No required dps found in {cfg}", + ) + # Most of the device_config functionality is exercised during testing of # the various supported devices. These tests concentrate only on the gaps. diff --git a/tests/test_translations.py b/tests/test_translations.py index 93cd32aeb2..ba58782042 100644 --- a/tests/test_translations.py +++ b/tests/test_translations.py @@ -52,10 +52,6 @@ def get_devices(): # @pytest.mark.parametrize("device", get_devices()) # def test_device_covered(device): -# entity = device.primary_entity -# if entity.deprecated: -# subtest_entity_covered(entity) - -# for entity in device.secondary_entities(): +# for entity in device.all_entities(): # if entity.deprecated: # subtest_entity_covered(entity)