Skip to content

Commit

Permalink
Add migrations for entities renamed in this release.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
make-all committed Aug 25, 2024
1 parent b270a14 commit 7383340
Show file tree
Hide file tree
Showing 14 changed files with 101 additions and 29 deletions.
67 changes: 67 additions & 0 deletions custom_components/tuya_local/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
2 changes: 1 addition & 1 deletion custom_components/tuya_local/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down
8 changes: 1 addition & 7 deletions custom_components/tuya_local/devices/atorch_s1wp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ primary_entity:
secondary_entities:
- entity: number
category: config
name: Countdown
translation_key: timer
dps:
- id: 9
Expand All @@ -28,7 +27,6 @@ secondary_entities:
step: 60
- entity: sensor
class: current
name: Current
dps:
- id: 18
name: sensor
Expand All @@ -39,7 +37,6 @@ secondary_entities:
- scale: 1000
- entity: sensor
class: power
name: Power
dps:
- id: 19
name: sensor
Expand All @@ -50,7 +47,6 @@ secondary_entities:
- scale: 100
- entity: sensor
class: voltage
name: Voltage
dps:
- id: 20
name: sensor
Expand Down Expand Up @@ -318,7 +314,7 @@ secondary_entities:
mapping:
- scale: 1000
- entity: sensor
name: Countdown timer
translation_key: time_remaining
category: diagnostic
class: duration
dps:
Expand Down Expand Up @@ -462,7 +458,6 @@ secondary_entities:
value: Countdown
- entity: sensor
class: frequency
name: Frequency
dps:
- id: 133
name: sensor
Expand All @@ -472,7 +467,6 @@ secondary_entities:
mapping:
- scale: 100
- entity: sensor
name: Power factor
class: power_factor
dps:
- id: 134
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ primary_entity:
value: closed
secondary_entities:
- entity: number
name: Countdown
translation_key: timer
category: config
dps:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ secondary_entities:
value: LED
- entity: number
category: config
name: Countdown
translation_key: timer
dps:
- id: 6
Expand Down
1 change: 0 additions & 1 deletion custom_components/tuya_local/devices/loratap_relay.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ primary_entity:
optional: true
secondary_entities:
- entity: number
name: Countdown
translation_key: timer
category: config
dps:
Expand Down
10 changes: 6 additions & 4 deletions custom_components/tuya_local/devices/simple_gate_opener.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions custom_components/tuya_local/helpers/device_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
6 changes: 1 addition & 5 deletions tests/devices/base_device_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion tests/devices/test_gx_aroma_diffuser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
15 changes: 15 additions & 0 deletions tests/test_device_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 1 addition & 5 deletions tests/test_translations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit 7383340

Please sign in to comment.