mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 09:23:17 -04:00
Include disabled entities in search/related when requested (#178024)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
parent
ae3fae9879
commit
74639e3063
@@ -63,6 +63,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
probatio.Required("type"): "search/related",
|
||||
probatio.Required("item_type"): probatio.Coerce(ItemType),
|
||||
probatio.Required("item_id"): str,
|
||||
probatio.Optional("include_disabled_entities", default=False): bool,
|
||||
}
|
||||
)
|
||||
@callback
|
||||
@@ -72,7 +73,11 @@ def websocket_search_related(
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Handle search."""
|
||||
searcher = Searcher(hass, get_entity_sources(hass))
|
||||
searcher = Searcher(
|
||||
hass,
|
||||
get_entity_sources(hass),
|
||||
include_disabled_entities=msg["include_disabled_entities"],
|
||||
)
|
||||
connection.send_result(
|
||||
msg["id"], searcher.async_search(msg["item_type"], msg["item_id"])
|
||||
)
|
||||
@@ -87,6 +92,8 @@ class Searcher:
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entity_sources: dict[str, EntityInfo],
|
||||
*,
|
||||
include_disabled_entities: bool = False,
|
||||
) -> None:
|
||||
"""Search results."""
|
||||
self.hass = hass
|
||||
@@ -94,6 +101,7 @@ class Searcher:
|
||||
self._device_registry = dr.async_get(hass)
|
||||
self._entity_registry = er.async_get(hass)
|
||||
self._entity_sources = entity_sources
|
||||
self._include_disabled_entities = include_disabled_entities
|
||||
self.results: defaultdict[ItemType, set[str]] = defaultdict(set)
|
||||
|
||||
@callback
|
||||
@@ -154,7 +162,9 @@ class Searcher:
|
||||
|
||||
# Entities of this device
|
||||
for entity_entry in er.async_entries_for_device(
|
||||
self._entity_registry, device.id
|
||||
self._entity_registry,
|
||||
device.id,
|
||||
include_disabled_entities=self._include_disabled_entities,
|
||||
):
|
||||
# Skip the entity if it's in a different area
|
||||
if entity_entry.area_id is not None:
|
||||
@@ -326,7 +336,9 @@ class Searcher:
|
||||
|
||||
# Entities of this device
|
||||
for entity_entry in er.async_entries_for_device(
|
||||
self._entity_registry, device_id
|
||||
self._entity_registry,
|
||||
device_id,
|
||||
include_disabled_entities=self._include_disabled_entities,
|
||||
):
|
||||
self._add(ItemType.ENTITY, entity_entry.entity_id)
|
||||
# Add all entity information as well
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Test fixtures for the search integration."""
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.search import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
floor_registry as fr,
|
||||
)
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture(name="search_item_ids")
|
||||
async def search_item_ids_fixture(
|
||||
hass: HomeAssistant,
|
||||
area_registry: ar.AreaRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
floor_registry: fr.FloorRegistry,
|
||||
) -> dict[str, str]:
|
||||
"""Set up a device and a child device, each owning enabled and disabled entities.
|
||||
|
||||
The child device inherits the area from its parent, so it is reached by an area
|
||||
or floor search without having an area of its own.
|
||||
|
||||
Returns the item id to search by, per key.
|
||||
"""
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
|
||||
floor = floor_registry.async_create("First floor")
|
||||
area = area_registry.async_create("Kitchen", floor_id=floor.floor_id)
|
||||
|
||||
config_entry = MockConfigEntry(domain="test")
|
||||
config_entry.add_to_hass(hass)
|
||||
device = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id, identifiers={("test", "1")}
|
||||
)
|
||||
device_registry.async_update_device(device.id, area_id=area.id)
|
||||
|
||||
child_device = device_registry.async_get_or_create_child(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
identifiers={("test", "1-child")},
|
||||
parent_device_id=device.id,
|
||||
name="Child",
|
||||
)
|
||||
|
||||
entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"enabled",
|
||||
suggested_object_id="enabled",
|
||||
config_entry=config_entry,
|
||||
device_id=device.id,
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"disabled",
|
||||
suggested_object_id="disabled",
|
||||
config_entry=config_entry,
|
||||
device_id=device.id,
|
||||
disabled_by=er.RegistryEntryDisabler.USER,
|
||||
)
|
||||
# A disabled entity that overrides its area instead of inheriting it from the
|
||||
# device is reached through the area index, which has no disabled filter.
|
||||
disabled_area_override_entity = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"disabled_area_override",
|
||||
suggested_object_id="disabled_area_override",
|
||||
config_entry=config_entry,
|
||||
device_id=device.id,
|
||||
disabled_by=er.RegistryEntryDisabler.USER,
|
||||
)
|
||||
entity_registry.async_update_entity(
|
||||
disabled_area_override_entity.entity_id, area_id=area.id
|
||||
)
|
||||
|
||||
entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"child_enabled",
|
||||
suggested_object_id="child_enabled",
|
||||
config_entry=config_entry,
|
||||
device_id=child_device.id,
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"child_disabled",
|
||||
suggested_object_id="child_disabled",
|
||||
config_entry=config_entry,
|
||||
device_id=child_device.id,
|
||||
disabled_by=er.RegistryEntryDisabler.USER,
|
||||
)
|
||||
|
||||
return {
|
||||
"floor": floor.floor_id,
|
||||
"area": area.id,
|
||||
"device": device.id,
|
||||
"child_device": child_device.id,
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Tests for Search integration."""
|
||||
|
||||
import attr
|
||||
import pytest
|
||||
from pytest_unordered import unordered
|
||||
|
||||
from homeassistant.components.search import DOMAIN, ItemType, Searcher
|
||||
@@ -1371,3 +1372,95 @@ async def test_search_child_devices(
|
||||
ItemType.CONFIG_ENTRY: {config_entry.entry_id},
|
||||
ItemType.INTEGRATION: {"test"},
|
||||
}
|
||||
|
||||
|
||||
ALL_ENTITIES = {
|
||||
"light.enabled",
|
||||
"light.disabled",
|
||||
"light.disabled_area_override",
|
||||
"light.child_enabled",
|
||||
"light.child_disabled",
|
||||
}
|
||||
PARENT_DEVICE_DEFAULT = {"light.enabled", "light.child_enabled"}
|
||||
AREA_DEFAULT = {"light.enabled", "light.disabled_area_override", "light.child_enabled"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("item_type", "item_key", "expected_default", "expected_included"),
|
||||
[
|
||||
pytest.param(
|
||||
ItemType.DEVICE, "device", PARENT_DEVICE_DEFAULT, ALL_ENTITIES, id="device"
|
||||
),
|
||||
pytest.param(
|
||||
ItemType.DEVICE,
|
||||
"child_device",
|
||||
{"light.child_enabled"},
|
||||
{"light.child_enabled", "light.child_disabled"},
|
||||
id="child_device",
|
||||
),
|
||||
pytest.param(ItemType.AREA, "area", AREA_DEFAULT, ALL_ENTITIES, id="area"),
|
||||
pytest.param(ItemType.FLOOR, "floor", AREA_DEFAULT, ALL_ENTITIES, id="floor"),
|
||||
],
|
||||
)
|
||||
async def test_search_include_disabled_entities(
|
||||
hass: HomeAssistant,
|
||||
search_item_ids: dict[str, str],
|
||||
item_type: ItemType,
|
||||
item_key: str,
|
||||
expected_default: set[str],
|
||||
expected_included: set[str],
|
||||
) -> None:
|
||||
"""Test device-inherited disabled entities are only returned when requested.
|
||||
|
||||
A child device is searched both directly and through the area and floor it
|
||||
inherits from its parent. Searching the child does not return the parent's
|
||||
entities, because the parent is only resolved up.
|
||||
"""
|
||||
item_id = search_item_ids[item_key]
|
||||
|
||||
searcher = Searcher(hass, {})
|
||||
assert (
|
||||
searcher.async_search(item_type, item_id)[ItemType.ENTITY] == expected_default
|
||||
)
|
||||
|
||||
searcher = Searcher(hass, {}, include_disabled_entities=True)
|
||||
assert (
|
||||
searcher.async_search(item_type, item_id)[ItemType.ENTITY] == expected_included
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("extra_msg", "expected"),
|
||||
[
|
||||
pytest.param({}, PARENT_DEVICE_DEFAULT, id="key_omitted"),
|
||||
pytest.param(
|
||||
{"include_disabled_entities": False},
|
||||
PARENT_DEVICE_DEFAULT,
|
||||
id="explicit_false",
|
||||
),
|
||||
pytest.param(
|
||||
{"include_disabled_entities": True}, ALL_ENTITIES, id="explicit_true"
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_search_related_include_disabled_entities_websocket(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
search_item_ids: dict[str, str],
|
||||
extra_msg: dict[str, bool],
|
||||
expected: set[str],
|
||||
) -> None:
|
||||
"""Test the websocket command accepts the new option, and defaults it to False."""
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "search/related",
|
||||
"item_type": "device",
|
||||
"item_id": search_item_ids["device"],
|
||||
}
|
||||
| extra_msg
|
||||
)
|
||||
response = await client.receive_json()
|
||||
|
||||
assert response["success"]
|
||||
assert response["result"][ItemType.ENTITY] == unordered(list(expected))
|
||||
|
||||
Reference in New Issue
Block a user