From 30076d184335647f7becbc27094c444aa500e428 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 5 Jan 2020 11:16:37 +0100 Subject: [PATCH] Add person reload service (#30493) --- homeassistant/components/person/__init__.py | 28 ++++++++- homeassistant/components/person/services.yaml | 2 + homeassistant/helpers/collection.py | 17 +++++- homeassistant/helpers/entity_component.py | 5 +- tests/components/person/test_init.py | 60 ++++++++++++++++++- tests/helpers/test_collection.py | 17 ++++++ 6 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/person/services.yaml diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 17262c29887..f37de3ad567 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -19,13 +19,26 @@ from homeassistant.const import ( CONF_ID, CONF_NAME, EVENT_HOMEASSISTANT_START, + SERVICE_RELOAD, STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, State, callback, split_entity_id -from homeassistant.helpers import collection, config_validation as cv, entity_registry +from homeassistant.core import ( + Event, + HomeAssistant, + ServiceCall, + State, + callback, + split_entity_id, +) +from homeassistant.helpers import ( + collection, + config_validation as cv, + entity_registry, + service, +) from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.restore_state import RestoreEntity @@ -303,6 +316,17 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): hass.bus.async_listen(EVENT_USER_REMOVED, _handle_user_removed) + async def async_reload_yaml(call: ServiceCall): + """Reload YAML.""" + conf = await entity_component.async_prepare_reload(skip_reset=True) + if conf is None: + return + await yaml_collection.async_load(await filter_yaml_data(hass, conf[DOMAIN])) + + service.async_register_admin_service( + hass, DOMAIN, SERVICE_RELOAD, async_reload_yaml + ) + return True diff --git a/homeassistant/components/person/services.yaml b/homeassistant/components/person/services.yaml new file mode 100644 index 00000000000..0af934f56b8 --- /dev/null +++ b/homeassistant/components/person/services.yaml @@ -0,0 +1,2 @@ +reload: + description: Reload the person configuration. diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 80401fcb30f..dd0edbd09b9 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -115,16 +115,27 @@ class YamlCollection(ObservableCollection): """Offer a fake CRUD interface on top of static YAML.""" async def async_load(self, data: List[dict]) -> None: - """Load the storage Manager.""" + """Load the YAML collection. Overrides existing data.""" + old_ids = set(self.data) + for item in data: item_id = item[CONF_ID] - if self.id_manager.has_id(item_id): + if item_id in old_ids: + old_ids.remove(item_id) + event = CHANGE_UPDATED + elif self.id_manager.has_id(item_id): self.logger.warning("Duplicate ID '%s' detected, skipping", item_id) continue + else: + event = CHANGE_ADDED self.data[item_id] = item - await self.notify_change(CHANGE_ADDED, item[CONF_ID], item) + await self.notify_change(event, item[CONF_ID], item) + + for item_id in old_ids: + self.data.pop(item_id) + await self.notify_change(CHANGE_REMOVED, item_id, None) class StorageCollection(ObservableCollection): diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 84aa8becafd..c4815474745 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -290,7 +290,7 @@ class EntityComponent: if entity_id in platform.entities: await platform.async_remove_entity(entity_id) - async def async_prepare_reload(self): + async def async_prepare_reload(self, *, skip_reset=False): """Prepare reloading this entity component. This method must be run in the event loop. @@ -310,7 +310,8 @@ class EntityComponent: if conf is None: return None - await self._async_reset() + if not skip_reset: + await self._async_reset() return conf def _async_init_entity_platform( diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index 3bfd061257e..5eaec6d5bf1 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -1,5 +1,6 @@ """The tests for the person component.""" import logging +from unittest.mock import patch import pytest @@ -16,9 +17,10 @@ from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, EVENT_HOMEASSISTANT_START, + SERVICE_RELOAD, STATE_UNKNOWN, ) -from homeassistant.core import CoreState, State +from homeassistant.core import Context, CoreState, State from homeassistant.helpers import collection, entity_registry from homeassistant.setup import async_setup_component @@ -703,3 +705,59 @@ async def test_add_user_device_tracker(hass, storage_setup, hass_read_only_user) "device_tracker.on_create", "device_tracker.added", ] + + +async def test_reload(hass, hass_admin_user): + """Test reloading the YAML config.""" + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: [ + {"name": "Person 1", "id": "id-1"}, + {"name": "Person 2", "id": "id-2"}, + ] + }, + ) + + assert len(hass.states.async_entity_ids()) == 2 + + state_1 = hass.states.get("person.person_1") + state_2 = hass.states.get("person.person_2") + state_3 = hass.states.get("person.person_3") + + assert state_1 is not None + assert state_1.name == "Person 1" + assert state_2 is not None + assert state_2.name == "Person 2" + assert state_3 is None + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={ + DOMAIN: [ + {"name": "Person 1-updated", "id": "id-1"}, + {"name": "Person 3", "id": "id-3"}, + ] + }, + ), patch("homeassistant.config.find_config_file", return_value=""): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids()) == 2 + + state_1 = hass.states.get("person.person_1") + state_2 = hass.states.get("person.person_2") + state_3 = hass.states.get("person.person_3") + + assert state_1 is not None + assert state_1.name == "Person 1-updated" + assert state_2 is None + assert state_3 is not None + assert state_3.name == "Person 3" diff --git a/tests/helpers/test_collection.py b/tests/helpers/test_collection.py index 29eeca67f3f..1cc600c3f01 100644 --- a/tests/helpers/test_collection.py +++ b/tests/helpers/test_collection.py @@ -118,6 +118,23 @@ async def test_yaml_collection(): {"id": "mock-2", "name": "Mock 2"}, ) + # Test loading new data. Mock 1 is updated, 2 removed, 3 added. + await coll.async_load( + [{"id": "mock-1", "name": "Mock 1-updated"}, {"id": "mock-3", "name": "Mock 3"}] + ) + assert len(changes) == 5 + assert changes[2] == ( + collection.CHANGE_UPDATED, + "mock-1", + {"id": "mock-1", "name": "Mock 1-updated"}, + ) + assert changes[3] == ( + collection.CHANGE_ADDED, + "mock-3", + {"id": "mock-3", "name": "Mock 3"}, + ) + assert changes[4] == (collection.CHANGE_REMOVED, "mock-2", None,) + async def test_yaml_collection_skipping_duplicate_ids(): """Test YAML collection skipping duplicate IDs."""