-
-
Notifications
You must be signed in to change notification settings - Fork 37.8k
Add search integration #30511
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Add search integration #30511
Changes from 2 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
77102bc
Add search integration
balloob 3c49862
Add scenes and config entry support
balloob f6a655e
Update comments
balloob f97d2fb
Add support for groups
balloob bc5149f
Allow querying config entry
balloob 6b754c8
Update manifest
balloob 61c7cc3
Fix scene tests
balloob File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| """The Search integration.""" | ||
| from collections import defaultdict | ||
|
|
||
| import voluptuous as vol | ||
|
|
||
| from homeassistant.components import websocket_api | ||
| from homeassistant.components.homeassistant import scene | ||
| from homeassistant.core import HomeAssistant, split_entity_id | ||
| from homeassistant.helpers import device_registry, entity_registry | ||
|
|
||
| DOMAIN = "search" | ||
|
|
||
|
|
||
| async def async_setup(hass: HomeAssistant, config: dict): | ||
| """Set up the Search component.""" | ||
| websocket_api.async_register_command(hass, websocket_search) | ||
| return True | ||
|
|
||
|
|
||
| @websocket_api.async_response | ||
| @websocket_api.websocket_command( | ||
| { | ||
| vol.Required("type"): "search", | ||
| vol.Required("item_type"): vol.In( | ||
| ("area", "device", "entity", "script", "scene", "automation") | ||
| ), | ||
| vol.Required("item_id"): str, | ||
| } | ||
| ) | ||
| async def websocket_search(hass, connection, msg): | ||
| """Handle search.""" | ||
| searcher = Searcher( | ||
| hass, | ||
| await device_registry.async_get_registry(hass), | ||
| await entity_registry.async_get_registry(hass), | ||
| ) | ||
| connection.send_result( | ||
| msg["id"], await searcher.search(msg["item_type"], msg["item_id"]) | ||
| ) | ||
|
|
||
|
|
||
| class Searcher: | ||
| """Find related things. | ||
|
|
||
| Few rules: | ||
| Scenes, scripts, automations and config entries will only be expanded if they are | ||
| the entry point. They won't be expanded if we process them. This is because they | ||
| turn the results into garbage. | ||
| """ | ||
|
|
||
| # These types won't be further explored | ||
| DONT_RESOLVE = {"scene", "automation", "script", "config_entry"} | ||
|
|
||
| def __init__( | ||
| self, | ||
| hass: HomeAssistant, | ||
| device_reg: device_registry.DeviceRegistry, | ||
| entity_reg: entity_registry.EntityRegistry, | ||
| ): | ||
| """Search results.""" | ||
| self.hass = hass | ||
| self._device_reg = device_reg | ||
| self._entity_reg = entity_reg | ||
| self.results = defaultdict(set) | ||
| self._to_resolve = set() | ||
|
|
||
| async def search(self, item_type, item_id): | ||
|
balloob marked this conversation as resolved.
Outdated
|
||
| """Find results.""" | ||
| self.results[item_type].add(item_id) | ||
| self._to_resolve.add((item_type, item_id)) | ||
|
|
||
| while self._to_resolve: | ||
| search_type, search_id = self._to_resolve.pop() | ||
| await getattr(self, f"_resolve_{search_type}")(search_id) | ||
|
balloob marked this conversation as resolved.
Outdated
|
||
|
|
||
| # Clean up entity_id items, from the general "entity" type result, | ||
| # that are also found in the specific entity domain type. | ||
| self.results["entity"] -= self.results["script"] | ||
| self.results["entity"] -= self.results["scene"] | ||
| self.results["entity"] -= self.results["automation"] | ||
|
|
||
| # Remove entry into graph from search results. | ||
| self.results[item_type].remove(item_id) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can keep it in vertices if we return a graph |
||
|
|
||
| # Filter out empty sets. | ||
| return {key: val for key, val in self.results.items() if val} | ||
|
|
||
| def _add_or_resolve(self, item_type, item_id): | ||
| """Add an item to explore.""" | ||
| if item_id in self.results[item_type]: | ||
| return | ||
|
|
||
| self.results[item_type].add(item_id) | ||
|
|
||
| if item_type not in self.DONT_RESOLVE: | ||
| self._to_resolve.add((item_type, item_id)) | ||
|
|
||
| async def _resolve_area(self, area_id) -> None: | ||
| """Resolve an area.""" | ||
| for device in device_registry.async_entries_for_area(self._device_reg, area_id): | ||
| self._add_or_resolve("device", device.id) | ||
|
|
||
| async def _resolve_device(self, device_id) -> None: | ||
| """Resolve a device.""" | ||
| device_entry = self._device_reg.async_get(device_id) | ||
| # Unlikely entry doesn't exist, but let's guard for bad data. | ||
| if device_entry is not None: | ||
| if device_entry.area_id: | ||
| self._add_or_resolve("area", device_entry.area_id) | ||
|
|
||
| for config_entry_id in device_entry.config_entries: | ||
| self._add_or_resolve("config_entry", config_entry_id) | ||
|
|
||
| # We do not resolve device_entry.via_device_id because that | ||
| # device is not related data-wise inside HA. | ||
|
|
||
| for entity_entry in entity_registry.async_entries_for_device( | ||
| self._entity_reg, device_id | ||
| ): | ||
| self._add_or_resolve("entity", entity_entry.entity_id) | ||
|
|
||
| # Extra: Find automations that reference this device | ||
|
|
||
| async def _resolve_entity(self, entity_id) -> None: | ||
| """Resolve an entity.""" | ||
| # Extra: Find automations and scripts that reference this entity. | ||
|
|
||
| for entity in scene.scenes_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: | ||
| if entity_entry.device_id: | ||
| self._add_or_resolve("device", entity_entry.device_id) | ||
|
|
||
| if entity_entry.config_entry_id is not None: | ||
| self._add_or_resolve("config_entry", entity_entry.config_entry_id) | ||
|
|
||
| domain = split_entity_id(entity_id)[0] | ||
|
|
||
| if domain in ("scene", "automation", "script"): | ||
| self._add_or_resolve(domain, entity_id) | ||
|
|
||
| async def _resolve_automation(self, automation_entity_id) -> None: | ||
| """Resolve an automation. | ||
|
|
||
| Will only be called if automation is an entry point. | ||
| """ | ||
| # Extra: Check with automation integration what entities/devices they reference | ||
|
|
||
| async def _resolve_script(self, script_entity_id) -> None: | ||
| """Resolve a script. | ||
|
|
||
| Will only be called if script is an entry point. | ||
| """ | ||
| # Extra: Check with script integration what entities/devices they reference | ||
|
|
||
| async def _resolve_scene(self, scene_entity_id) -> None: | ||
| """Resolve a scene. | ||
|
|
||
| Will only be called if scene is an entry point. | ||
| """ | ||
| for entity in scene.entities_in_scene(self.hass, scene_entity_id): | ||
| self._add_or_resolve("entity", entity) | ||
|
|
||
| async def _resolve_config_entry(self, config_entry_id) -> None: | ||
| """Resolve a config entry. | ||
|
|
||
| Will only be called if config entry is an entry point. | ||
| """ | ||
| for device_entry in device_registry.async_entries_for_config_entry( | ||
| self._device_reg, config_entry_id | ||
| ): | ||
| self._add_or_resolve("device", device_entry.id) | ||
|
|
||
| for entity_entry in entity_registry.async_entries_for_config_entry( | ||
| self._entity_reg, config_entry_id | ||
| ): | ||
| self._add_or_resolve("entity", entity_entry.entity_id) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "domain": "search", | ||
| "name": "Search", | ||
| "documentation": "https://www.home-assistant.io/integrations/search", | ||
| "requirements": [], | ||
| "ssdp": [], | ||
| "zeroconf": [], | ||
| "homekit": {}, | ||
| "dependencies": ["websocket_api"], | ||
| "codeowners": ["@home-assistant/core"] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """Tests for the Search integration.""" |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.