From a8c14835b9e4a1d79145089957f0b6ee38d66ef1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 18 Jan 2022 22:47:46 +0100 Subject: [PATCH] Add entity options to entity registry (#64350) * Initial commit for entity options * Tweak broadlink tests * Add async_update_entity_options + test --- homeassistant/helpers/entity_registry.py | 37 ++++++++++++++++++++--- tests/components/broadlink/test_remote.py | 12 ++++---- tests/helpers/test_entity_registry.py | 32 +++++++++++++++++++- 3 files changed, 70 insertions(+), 11 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 200eb789061..87c89cfdec9 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -57,7 +57,7 @@ SAVE_DELAY = 10 _LOGGER = logging.getLogger(__name__) STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 4 +STORAGE_VERSION_MINOR = 5 STORAGE_KEY = "core.entity_registry" # Attributes relevant to describing entity @@ -109,6 +109,9 @@ class RegistryEntry: icon: str | None = attr.ib(default=None) id: str = attr.ib(factory=uuid_util.random_uuid_hex) name: str | None = attr.ib(default=None) + options: Mapping[str, Mapping[str, Any]] = attr.ib( + default=None, converter=attr.converters.default_if_none(factory=dict) # type: ignore[misc] + ) # As set by integration original_device_class: str | None = attr.ib(default=None) original_icon: str | None = attr.ib(default=None) @@ -560,6 +563,25 @@ class EntityRegistry: return new + @callback + def async_update_entity_options( + self, entity_id: str, domain: str, options: dict[str, Any] + ) -> None: + """Update entity options.""" + old = self.entities[entity_id] + new_options: Mapping[str, Mapping[str, Any]] = {**old.options, domain: options} + self.entities[entity_id] = attr.evolve(old, options=new_options) + + self.async_schedule_save() + + data: dict[str, str | dict[str, Any]] = { + "action": "update", + "entity_id": entity_id, + "changes": {"options": old.options}, + } + + self.hass.bus.async_fire(EVENT_ENTITY_REGISTRY_UPDATED, data) + async def async_load(self) -> None: """Load the entity registry.""" async_setup_entity_restore(self.hass, self) @@ -595,6 +617,7 @@ class EntityRegistry: icon=entity["icon"], id=entity["id"], name=entity["name"], + options=entity["options"], original_device_class=entity["original_device_class"], original_icon=entity["original_icon"], original_name=entity["original_name"], @@ -629,6 +652,7 @@ class EntityRegistry: "icon": entry.icon, "id": entry.id, "name": entry.name, + "options": entry.options, "original_device_class": entry.original_device_class, "original_icon": entry.original_icon, "original_name": entry.original_name, @@ -749,7 +773,7 @@ async def _async_migrate( old_major_version: int, old_minor_version: int, data: dict ) -> dict: """Migrate to the new version.""" - if old_major_version < 2 and old_minor_version < 2: + if old_major_version == 1 and old_minor_version < 2: # From version 1.1 for entity in data["entities"]: # Populate all keys @@ -768,18 +792,23 @@ async def _async_migrate( entity["supported_features"] = entity.get("supported_features", 0) entity["unit_of_measurement"] = entity.get("unit_of_measurement") - if old_major_version < 2 and old_minor_version < 3: + if old_major_version == 1 and old_minor_version < 3: # Version 1.3 adds original_device_class for entity in data["entities"]: # Move device_class to original_device_class entity["original_device_class"] = entity["device_class"] entity["device_class"] = None - if old_major_version < 2 and old_minor_version < 4: + if old_major_version == 1 and old_minor_version < 4: # Version 1.4 adds id for entity in data["entities"]: entity["id"] = uuid_util.random_uuid_hex() + if old_major_version == 1 and old_minor_version < 5: + # Version 1.5 adds entity options + for entity in data["entities"]: + entity["options"] = {} + if old_major_version > 1: raise NotImplementedError return data diff --git a/tests/components/broadlink/test_remote.py b/tests/components/broadlink/test_remote.py index 1ee48063613..3c97f8ea47a 100644 --- a/tests/components/broadlink/test_remote.py +++ b/tests/components/broadlink/test_remote.py @@ -34,10 +34,10 @@ async def test_remote_setup_works(hass): {(DOMAIN, mock_setup.entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) - remotes = {entry for entry in entries if entry.domain == Platform.REMOTE} + remotes = [entry for entry in entries if entry.domain == Platform.REMOTE] assert len(remotes) == 1 - remote = remotes.pop() + remote = remotes[0] assert remote.original_name == f"{device.name} Remote" assert hass.states.get(remote.entity_id).state == STATE_ON assert mock_setup.api.auth.call_count == 1 @@ -54,10 +54,10 @@ async def test_remote_send_command(hass): {(DOMAIN, mock_setup.entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) - remotes = {entry for entry in entries if entry.domain == Platform.REMOTE} + remotes = [entry for entry in entries if entry.domain == Platform.REMOTE] assert len(remotes) == 1 - remote = remotes.pop() + remote = remotes[0] await hass.services.async_call( Platform.REMOTE, SERVICE_SEND_COMMAND, @@ -81,10 +81,10 @@ async def test_remote_turn_off_turn_on(hass): {(DOMAIN, mock_setup.entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) - remotes = {entry for entry in entries if entry.domain == Platform.REMOTE} + remotes = [entry for entry in entries if entry.domain == Platform.REMOTE] assert len(remotes) == 1 - remote = remotes.pop() + remote = remotes[0] await hass.services.async_call( Platform.REMOTE, SERVICE_TURN_OFF, diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 568d82ebd4b..f299177a08e 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -196,12 +196,16 @@ async def test_loading_saving_data(hass, registry): supported_features=5, unit_of_measurement="initial-unit_of_measurement", ) - orig_entry2 = registry.async_update_entity( + registry.async_update_entity( orig_entry2.entity_id, device_class="user-class", name="User Name", icon="hass:user-icon", ) + registry.async_update_entity_options( + orig_entry2.entity_id, "light", {"minimum_brightness": 20} + ) + orig_entry2 = registry.async_get(orig_entry2.entity_id) assert len(registry.entities) == 2 @@ -227,6 +231,7 @@ async def test_loading_saving_data(hass, registry): assert new_entry2.entity_category == "config" assert new_entry2.icon == "hass:user-icon" assert new_entry2.name == "User Name" + assert new_entry2.options == {"light": {"minimum_brightness": 20}} assert new_entry2.original_device_class == "mock-device-class" assert new_entry2.original_icon == "hass:original-icon" assert new_entry2.original_name == "Original Name" @@ -570,6 +575,31 @@ async def test_update_entity(registry): entry = updated_entry +async def test_update_entity_options(registry): + """Test updating entity.""" + mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") + entry = registry.async_get_or_create( + "light", "hue", "5678", config_entry=mock_config + ) + + registry.async_update_entity_options( + entry.entity_id, "light", {"minimum_brightness": 20} + ) + new_entry_1 = registry.async_get(entry.entity_id) + + assert entry.options == {} + assert new_entry_1.options == {"light": {"minimum_brightness": 20}} + + registry.async_update_entity_options( + entry.entity_id, "light", {"minimum_brightness": 30} + ) + new_entry_2 = registry.async_get(entry.entity_id) + + assert entry.options == {} + assert new_entry_1.options == {"light": {"minimum_brightness": 20}} + assert new_entry_2.options == {"light": {"minimum_brightness": 30}} + + async def test_disabled_by(registry): """Test that we can disable an entry when we create it.""" entry = registry.async_get_or_create(