Quellcode durchsuchen

Add migrations for entities renamed in this release.

In testing this, a divide-by-zero error was found in the unit tests,
which could potentially have come from a config missing required dps.
A new test was added for this, and for convenience, a new all_entities
method added to the device_config helper to return primary and
secondary entities together to avoid needing to process them
separately. Other tests were updated to use this too.

Some unmigrated "countdown" timers were also found and fixed in this
migration.
Jason Rumney vor 1 Jahr
Ursprung
Commit
73833402f7

+ 67 - 0
custom_components/tuya_local/__init__.py

@@ -507,6 +507,73 @@ async def async_migrate_entry(hass, entry: ConfigEntry):
             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
 
 

+ 1 - 1
custom_components/tuya_local/config_flow.py

@@ -73,7 +73,7 @@ HUB_CATEGORIES = [
 
 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 = {}

+ 1 - 7
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

+ 0 - 1
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:

+ 0 - 1
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

+ 0 - 1
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:

+ 6 - 4
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

+ 1 - 2
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

+ 6 - 0
custom_components/tuya_local/helpers/device_config.py

@@ -135,6 +135,12 @@ class TuyaDeviceConfig:
         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()
 

+ 1 - 5
tests/devices/base_device_tests.py

@@ -73,12 +73,8 @@ class TuyaDeviceTestCase(IsolatedAsyncioTestCase):
 
         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
 

+ 1 - 1
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):

+ 1 - 1
tests/test_device.py

@@ -91,7 +91,7 @@ class TestDevice(IsolatedAsyncioTestCase):
         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):

+ 15 - 0
tests/test_device_config.py

@@ -521,6 +521,21 @@ class TestDeviceConfig(IsolatedAsyncioTestCase):
                         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.
 

+ 1 - 5
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)