diff --git a/.gitignore b/.gitignore index 75ab19881ac..7a0cb29bc2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ config/* +config2/* tests/testing_config/deps tests/testing_config/home-assistant.log diff --git a/homeassistant/components/websocket_api/permissions.py b/homeassistant/components/websocket_api/permissions.py index b98b21d184e..f1849fda539 100644 --- a/homeassistant/components/websocket_api/permissions.py +++ b/homeassistant/components/websocket_api/permissions.py @@ -10,6 +10,9 @@ from homeassistant.const import ( EVENT_THEMES_UPDATED) from homeassistant.components.persistent_notification import ( EVENT_PERSISTENT_NOTIFICATIONS_UPDATED) +from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED +from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED +from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED # These are events that do not contain any sensitive data # Except for state_changed, which is handled accordingly. @@ -20,4 +23,7 @@ SUBSCRIBE_WHITELIST = { EVENT_SERVICE_REMOVED, EVENT_STATE_CHANGED, EVENT_THEMES_UPDATED, + EVENT_AREA_REGISTRY_UPDATED, + EVENT_DEVICE_REGISTRY_UPDATED, + EVENT_ENTITY_REGISTRY_UPDATED, } diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index adf5410516d..4476d526987 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -16,7 +16,7 @@ from .typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) DATA_REGISTRY = 'area_registry' - +EVENT_AREA_REGISTRY_UPDATED = 'area_registry_updated' STORAGE_KEY = 'core.area_registry' STORAGE_VERSION = 1 SAVE_DELAY = 10 @@ -58,7 +58,14 @@ class AreaRegistry: area = AreaEntry() self.areas[area.id] = area - return self.async_update(area.id, name=name) + created = self._async_update(area.id, name=name) + + self.hass.bus.async_fire(EVENT_AREA_REGISTRY_UPDATED, { + 'action': 'create', + 'area_id': created.id, + }) + + return created async def async_delete(self, area_id: str) -> None: """Delete area.""" @@ -68,10 +75,25 @@ class AreaRegistry: del self.areas[area_id] + self.hass.bus.async_fire(EVENT_AREA_REGISTRY_UPDATED, { + 'action': 'remove', + 'area_id': area_id, + }) + self.async_schedule_save() @callback def async_update(self, area_id: str, name: str) -> AreaEntry: + """Update name of area.""" + updated = self._async_update(area_id, name) + self.hass.bus.async_fire(EVENT_AREA_REGISTRY_UPDATED, { + 'action': 'update', + 'area_id': area_id, + }) + return updated + + @callback + def _async_update(self, area_id: str, name: str) -> AreaEntry: """Update name of area.""" old = self.areas[area_id] diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 596bc84b6f9..5c066967437 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__) _UNDEF = object() DATA_REGISTRY = 'device_registry' - +EVENT_DEVICE_REGISTRY_UPDATED = 'device_registry_updated' STORAGE_KEY = 'core.device_registry' STORAGE_VERSION = 1 SAVE_DELAY = 10 @@ -42,6 +42,8 @@ class DeviceEntry: area_id = attr.ib(type=str, default=None) name_by_user = attr.ib(type=str, default=None) id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) + # This value is not stored, just used to keep track of events to fire. + is_new = attr.ib(type=bool, default=False) def format_mac(mac): @@ -111,7 +113,7 @@ class DeviceRegistry: device = self.async_get_device(identifiers, connections) if device is None: - device = DeviceEntry() + device = DeviceEntry(is_new=True) self.devices[device.id] = device if via_hub is not None: @@ -201,11 +203,20 @@ class DeviceRegistry: name_by_user != old.name_by_user): changes['name_by_user'] = name_by_user + if old.is_new: + changes['is_new'] = False + if not changes: return old new = self.devices[device_id] = attr.evolve(old, **changes) self.async_schedule_save() + + self.hass.bus.async_fire(EVENT_DEVICE_REGISTRY_UPDATED, { + 'action': 'create' if 'is_new' in changes else 'update', + 'device_id': new.id, + }) + return new async def async_load(self): diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 64064ffde7b..0a0c441b9cf 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -25,6 +25,7 @@ from .typing import HomeAssistantType PATH_REGISTRY = 'entity_registry.yaml' DATA_REGISTRY = 'entity_registry' +EVENT_ENTITY_REGISTRY_UPDATED = 'entity_registry_updated' SAVE_DELAY = 10 _LOGGER = logging.getLogger(__name__) _UNDEF = object() @@ -150,12 +151,22 @@ class EntityRegistry: _LOGGER.info('Registered new %s.%s entity: %s', domain, platform, entity_id) self.async_schedule_save() + + self.hass.bus.async_fire(EVENT_ENTITY_REGISTRY_UPDATED, { + 'action': 'create', + 'entity_id': entity_id + }) + return entity @callback def async_remove(self, entity_id): """Remove an entity from registry.""" self.entities.pop(entity_id) + self.hass.bus.async_fire(EVENT_ENTITY_REGISTRY_UPDATED, { + 'action': 'remove', + 'entity_id': entity_id + }) self.async_schedule_save() @callback @@ -234,6 +245,11 @@ class EntityRegistry: self.async_schedule_save() + self.hass.bus.async_fire(EVENT_ENTITY_REGISTRY_UPDATED, { + 'action': 'update', + 'entity_id': entity_id + }) + return new async def async_load(self): diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py index 284cb2b3dbe..b4b9c4a58cb 100644 --- a/tests/helpers/test_area_registry.py +++ b/tests/helpers/test_area_registry.py @@ -4,6 +4,7 @@ import asyncio import asynctest import pytest +from homeassistant.core import callback from homeassistant.helpers import area_registry from tests.common import mock_area_registry, flush_store @@ -14,6 +15,21 @@ def registry(hass): return mock_area_registry(hass) +@pytest.fixture +def update_events(hass): + """Capture update events.""" + events = [] + + @callback + def async_capture(event): + events.append(event.data) + + hass.bus.async_listen(area_registry.EVENT_AREA_REGISTRY_UPDATED, + async_capture) + + return events + + async def test_list_areas(registry): """Make sure that we can read areas.""" registry.async_create('mock') @@ -23,15 +39,22 @@ async def test_list_areas(registry): assert len(areas) == len(registry.areas) -async def test_create_area(registry): +async def test_create_area(hass, registry, update_events): """Make sure that we can create an area.""" area = registry.async_create('mock') assert area.name == 'mock' assert len(registry.areas) == 1 + await hass.async_block_till_done() -async def test_create_area_with_name_already_in_use(registry): + assert len(update_events) == 1 + assert update_events[0]['action'] == 'create' + assert update_events[0]['area_id'] == area.id + + +async def test_create_area_with_name_already_in_use(hass, registry, + update_events): """Make sure that we can't create an area with a name already in use.""" area1 = registry.async_create('mock') @@ -40,10 +63,13 @@ async def test_create_area_with_name_already_in_use(registry): assert area1 != area2 assert e_info == "Name is already in use" + await hass.async_block_till_done() + assert len(registry.areas) == 1 + assert len(update_events) == 1 -async def test_delete_area(registry): +async def test_delete_area(hass, registry, update_events): """Make sure that we can delete an area.""" area = registry.async_create('mock') @@ -51,6 +77,14 @@ async def test_delete_area(registry): assert not registry.areas + await hass.async_block_till_done() + + assert len(update_events) == 2 + assert update_events[0]['action'] == 'create' + assert update_events[0]['area_id'] == area.id + assert update_events[1]['action'] == 'remove' + assert update_events[1]['area_id'] == area.id + async def test_delete_non_existing_area(registry): """Make sure that we can't delete an area that doesn't exist.""" @@ -62,7 +96,7 @@ async def test_delete_non_existing_area(registry): assert len(registry.areas) == 1 -async def test_update_area(registry): +async def test_update_area(hass, registry, update_events): """Make sure that we can read areas.""" area = registry.async_create('mock') @@ -72,6 +106,14 @@ async def test_update_area(registry): assert updated_area.name == 'mock1' assert len(registry.areas) == 1 + await hass.async_block_till_done() + + assert len(update_events) == 2 + assert update_events[0]['action'] == 'create' + assert update_events[0]['area_id'] == area.id + assert update_events[1]['action'] == 'update' + assert update_events[1]['area_id'] == area.id + async def test_update_area_with_same_name(registry): """Make sure that we can reapply the same name to the area.""" diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 8c874a9837b..4b08bf960bf 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -5,6 +5,7 @@ from unittest.mock import patch import asynctest import pytest +from homeassistant.core import callback from homeassistant.helpers import device_registry from tests.common import mock_device_registry, flush_store @@ -15,7 +16,22 @@ def registry(hass): return mock_device_registry(hass) -async def test_get_or_create_returns_same_entry(registry): +@pytest.fixture +def update_events(hass): + """Capture update events.""" + events = [] + + @callback + def async_capture(event): + events.append(event.data) + + hass.bus.async_listen(device_registry.EVENT_DEVICE_REGISTRY_UPDATED, + async_capture) + + return events + + +async def test_get_or_create_returns_same_entry(hass, registry, update_events): """Make sure we do not duplicate entries.""" entry = registry.async_get_or_create( config_entry_id='1234', @@ -51,6 +67,15 @@ async def test_get_or_create_returns_same_entry(registry): assert entry3.name == 'name' assert entry3.sw_version == 'sw-version' + await hass.async_block_till_done() + + # Only 2 update events. The third entry did not generate any changes. + assert len(update_events) == 2 + assert update_events[0]['action'] == 'create' + assert update_events[0]['device_id'] == entry.id + assert update_events[1]['action'] == 'update' + assert update_events[1]['device_id'] == entry.id + async def test_requirement_for_identifier_or_connection(registry): """Make sure we do require some descriptor of device.""" @@ -155,7 +180,7 @@ async def test_loading_from_storage(hass, hass_storage): assert isinstance(entry.config_entries, set) -async def test_removing_config_entries(registry): +async def test_removing_config_entries(hass, registry, update_events): """Make sure we do not get duplicate entries.""" entry = registry.async_get_or_create( config_entry_id='123', @@ -191,6 +216,20 @@ async def test_removing_config_entries(registry): assert entry.config_entries == {'456'} assert entry3.config_entries == set() + await hass.async_block_till_done() + + assert len(update_events) == 5 + assert update_events[0]['action'] == 'create' + assert update_events[0]['device_id'] == entry.id + assert update_events[1]['action'] == 'update' + assert update_events[1]['device_id'] == entry2.id + assert update_events[2]['action'] == 'create' + assert update_events[2]['device_id'] == entry3.id + assert update_events[3]['action'] == 'update' + assert update_events[3]['device_id'] == entry.id + assert update_events[4]['action'] == 'update' + assert update_events[4]['device_id'] == entry3.id + async def test_removing_area_id(registry): """Make sure we can clear area id.""" diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 529b03160ca..624adbb8ea3 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -5,7 +5,7 @@ from unittest.mock import patch import asynctest import pytest -from homeassistant.core import valid_entity_id +from homeassistant.core import valid_entity_id, callback from homeassistant.helpers import entity_registry from tests.common import mock_registry, flush_store @@ -20,14 +20,34 @@ def registry(hass): return mock_registry(hass) -def test_get_or_create_returns_same_entry(registry): +@pytest.fixture +def update_events(hass): + """Capture update events.""" + events = [] + + @callback + def async_capture(event): + events.append(event.data) + + hass.bus.async_listen(entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, + async_capture) + + return events + + +async def test_get_or_create_returns_same_entry(hass, registry, update_events): """Make sure we do not duplicate entries.""" entry = registry.async_get_or_create('light', 'hue', '1234') entry2 = registry.async_get_or_create('light', 'hue', '1234') + await hass.async_block_till_done() + assert len(registry.entities) == 1 assert entry is entry2 assert entry.entity_id == 'light.hue_1234' + assert len(update_events) == 1 + assert update_events[0]['action'] == 'create' + assert update_events[0]['entity_id'] == entry.entity_id def test_get_or_create_suggested_object_id(registry): @@ -168,7 +188,7 @@ def test_async_get_entity_id(registry): assert registry.async_get_entity_id('light', 'hue', '123') is None -def test_updating_config_entry_id(registry): +async def test_updating_config_entry_id(hass, registry, update_events): """Test that we update config entry id in registry.""" entry = registry.async_get_or_create( 'light', 'hue', '5678', config_entry_id='mock-id-1') @@ -177,8 +197,16 @@ def test_updating_config_entry_id(registry): assert entry.entity_id == entry2.entity_id assert entry2.config_entry_id == 'mock-id-2' + await hass.async_block_till_done() -def test_removing_config_entry_id(registry): + assert len(update_events) == 2 + assert update_events[0]['action'] == 'create' + assert update_events[0]['entity_id'] == entry.entity_id + assert update_events[1]['action'] == 'update' + assert update_events[1]['entity_id'] == entry.entity_id + + +async def test_removing_config_entry_id(hass, registry, update_events): """Test that we update config entry id in registry.""" entry = registry.async_get_or_create( 'light', 'hue', '5678', config_entry_id='mock-id-1') @@ -188,6 +216,14 @@ def test_removing_config_entry_id(registry): entry = registry.entities[entry.entity_id] assert entry.config_entry_id is None + await hass.async_block_till_done() + + assert len(update_events) == 2 + assert update_events[0]['action'] == 'create' + assert update_events[0]['entity_id'] == entry.entity_id + assert update_events[1]['action'] == 'update' + assert update_events[1]['entity_id'] == entry.entity_id + async def test_migration(hass): """Test migration from old data to new."""