From 7d9ae0784ead986480cd9910bb513ddb20799e77 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 29 Aug 2022 13:59:00 -0400 Subject: [PATCH] Allow searching for person (#77339) --- homeassistant/components/person/__init__.py | 41 ++++++++++++++- homeassistant/components/search/__init__.py | 17 ++++++- tests/components/person/test_init.py | 56 +++++++++++++++++++++ tests/components/search/test_init.py | 30 +++++++++++ 4 files changed, 140 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 2eb80ed69cc..09851d70384 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -125,6 +125,38 @@ async def async_add_user_device_tracker( break +@callback +def persons_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: + """Return all persons that reference the entity.""" + if ( + DOMAIN not in hass.data + or split_entity_id(entity_id)[0] != DEVICE_TRACKER_DOMAIN + ): + return [] + + component: EntityComponent = hass.data[DOMAIN][2] + + return [ + person_entity.entity_id + for person_entity in component.entities + if entity_id in cast(Person, person_entity).device_trackers + ] + + +@callback +def entities_in_person(hass: HomeAssistant, entity_id: str) -> list[str]: + """Return all entities belonging to a person.""" + if DOMAIN not in hass.data: + return [] + + component: EntityComponent = hass.data[DOMAIN][2] + + if (person_entity := component.get_entity(entity_id)) is None: + return [] + + return cast(Person, person_entity).device_trackers + + CREATE_FIELDS = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_USER_ID): vol.Any(str, None), @@ -318,7 +350,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) await storage_collection.async_load() - hass.data[DOMAIN] = (yaml_collection, storage_collection) + hass.data[DOMAIN] = (yaml_collection, storage_collection, entity_component) collection.StorageCollectionWebsocket( storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS @@ -412,6 +444,11 @@ class Person(RestoreEntity): """Return a unique ID for the person.""" return self._config[CONF_ID] + @property + def device_trackers(self): + """Return the device trackers for the person.""" + return self._config[CONF_DEVICE_TRACKERS] + async def async_added_to_hass(self): """Register device trackers.""" await super().async_added_to_hass() @@ -506,7 +543,7 @@ def ws_list_person( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg ): """List persons.""" - yaml, storage = hass.data[DOMAIN] + yaml, storage, _ = hass.data[DOMAIN] connection.send_result( msg[ATTR_ID], {"storage": storage.async_items(), "config": yaml.async_items()} ) diff --git a/homeassistant/components/search/__init__.py b/homeassistant/components/search/__init__.py index c951122d195..bfde6f38a73 100644 --- a/homeassistant/components/search/__init__.py +++ b/homeassistant/components/search/__init__.py @@ -6,7 +6,7 @@ import logging import voluptuous as vol -from homeassistant.components import automation, group, script, websocket_api +from homeassistant.components import automation, group, person, script, websocket_api from homeassistant.components.homeassistant import scene from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.helpers import device_registry, entity_registry @@ -36,6 +36,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: "group", "scene", "script", + "person", ) ), vol.Required("item_id"): str, @@ -67,7 +68,7 @@ class Searcher: # These types won't be further explored. Config entries + Output types. DONT_RESOLVE = {"scene", "automation", "script", "group", "config_entry", "area"} # These types exist as an entity and so need cleanup in results - EXIST_AS_ENTITY = {"script", "scene", "automation", "group"} + EXIST_AS_ENTITY = {"script", "scene", "automation", "group", "person"} def __init__( self, @@ -183,6 +184,9 @@ class Searcher: for entity in script.scripts_with_entity(self.hass, entity_id): self._add_or_resolve("entity", entity) + for entity in person.persons_with_entity(self.hass, entity_id): + self._add_or_resolve("entity", entity) + # Find devices entity_entry = self._entity_reg.async_get(entity_id) if entity_entry is not None: @@ -251,6 +255,15 @@ class Searcher: for entity in scene.entities_in_scene(self.hass, scene_entity_id): self._add_or_resolve("entity", entity) + @callback + def _resolve_person(self, person_entity_id) -> None: + """Resolve a person. + + Will only be called if person is an entry point. + """ + for entity in person.entities_in_person(self.hass, person_entity_id): + self._add_or_resolve("entity", entity) + @callback def _resolve_config_entry(self, config_entry_id) -> None: """Resolve a config entry. diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index 4e6793c07bb..981343ea3a5 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -783,3 +783,59 @@ async def test_person_storage_fixing_device_trackers(storage_collection): await storage_collection.async_load() assert storage_collection.data["bla"]["device_trackers"] == [] + + +async def test_persons_with_entity(hass): + """Test finding persons with an entity.""" + assert await async_setup_component( + hass, + "person", + { + "person": [ + { + "id": "abcd", + "name": "Paulus", + "device_trackers": [ + "device_tracker.paulus_iphone", + "device_tracker.paulus_ipad", + ], + }, + { + "id": "efgh", + "name": "Anne Therese", + "device_trackers": [ + "device_tracker.at_pixel", + ], + }, + ] + }, + ) + + assert person.persons_with_entity(hass, "device_tracker.paulus_iphone") == [ + "person.paulus" + ] + + +async def test_entities_in_person(hass): + """Test finding entities tracked by person.""" + assert await async_setup_component( + hass, + "person", + { + "person": [ + { + "id": "abcd", + "name": "Paulus", + "device_trackers": [ + "device_tracker.paulus_iphone", + "device_tracker.paulus_ipad", + ], + } + ] + }, + ) + + assert person.entities_in_person(hass, "person.paulus") == [ + "device_tracker.paulus_iphone", + "device_tracker.paulus_ipad", + ] diff --git a/tests/components/search/test_init.py b/tests/components/search/test_init.py index e9d320aa9ef..a728ef9b8c4 100644 --- a/tests/components/search/test_init.py +++ b/tests/components/search/test_init.py @@ -368,6 +368,36 @@ async def test_area_lookup(hass): } +async def test_person_lookup(hass): + """Test searching persons.""" + assert await async_setup_component( + hass, + "person", + { + "person": [ + { + "id": "abcd", + "name": "Paulus", + "device_trackers": ["device_tracker.paulus_iphone"], + } + ] + }, + ) + + device_reg = dr.async_get(hass) + entity_reg = er.async_get(hass) + + searcher = search.Searcher(hass, device_reg, entity_reg, MOCK_ENTITY_SOURCES) + assert searcher.async_search("entity", "device_tracker.paulus_iphone") == { + "person": {"person.paulus"}, + } + + searcher = search.Searcher(hass, device_reg, entity_reg, MOCK_ENTITY_SOURCES) + assert searcher.async_search("entity", "person.paulus") == { + "entity": {"device_tracker.paulus_iphone"}, + } + + async def test_ws_api(hass, hass_ws_client): """Test WS API.""" assert await async_setup_component(hass, "search", {})