Skip unexposed entities in intent handlers (#92415)

* Filter intent handler entities by exposure

* Add test for skipping unexposed entities
This commit is contained in:
Michael Hansen 2023-05-03 11:18:31 -05:00 committed by GitHub
parent 2ae3e90238
commit 74560ab139
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 76 additions and 2 deletions

View File

@ -201,6 +201,7 @@ class DefaultAgent(AbstractConversationAgent):
user_input.text,
user_input.context,
language,
assistant=DOMAIN,
)
except intent.IntentHandleError:
_LOGGER.exception("Intent handling error")

View File

@ -140,16 +140,18 @@ class GetStateIntentHandler(intent.IntentHandler):
area=area,
domains=domains,
device_classes=device_classes,
assistant=intent_obj.assistant,
)
)
_LOGGER.debug(
"Found %s state(s) that matched: name=%s, area=%s, domains=%s, device_classes=%s",
"Found %s state(s) that matched: name=%s, area=%s, domains=%s, device_classes=%s, assistant=%s",
len(states),
name,
area,
domains,
device_classes,
intent_obj.assistant,
)
# Create response

View File

@ -11,6 +11,7 @@ from typing import Any, TypeVar
import voluptuous as vol
from homeassistant.components.homeassistant.exposed_entities import async_should_expose
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
@ -65,6 +66,7 @@ async def async_handle(
text_input: str | None = None,
context: Context | None = None,
language: str | None = None,
assistant: str | None = None,
) -> IntentResponse:
"""Handle an intent."""
handler: IntentHandler = hass.data.get(DATA_KEY, {}).get(intent_type)
@ -79,7 +81,14 @@ async def async_handle(
language = hass.config.language
intent = Intent(
hass, platform, intent_type, slots or {}, text_input, context, language
hass,
platform=platform,
intent_type=intent_type,
slots=slots or {},
text_input=text_input,
context=context,
language=language,
assistant=assistant,
)
try:
@ -208,6 +217,7 @@ def async_match_states(
entities: entity_registry.EntityRegistry | None = None,
areas: area_registry.AreaRegistry | None = None,
devices: device_registry.DeviceRegistry | None = None,
assistant: str | None = None,
) -> Iterable[State]:
"""Find states that match the constraints."""
if states is None:
@ -258,6 +268,14 @@ def async_match_states(
states_and_entities = list(_filter_by_area(states_and_entities, area, devices))
if assistant is not None:
# Filter by exposure
states_and_entities = [
(state, entity)
for state, entity in states_and_entities
if async_should_expose(hass, assistant, state.entity_id)
]
if name is not None:
if devices is None:
devices = device_registry.async_get(hass)
@ -387,6 +405,7 @@ class ServiceIntentHandler(IntentHandler):
area=area,
domains=domains,
device_classes=device_classes,
assistant=intent_obj.assistant,
)
)
@ -496,6 +515,7 @@ class Intent:
"context",
"language",
"category",
"assistant",
]
def __init__(
@ -508,6 +528,7 @@ class Intent:
context: Context,
language: str,
category: IntentCategory | None = None,
assistant: str | None = None,
) -> None:
"""Initialize an intent."""
self.hass = hass
@ -518,6 +539,7 @@ class Intent:
self.context = context
self.language = language
self.category = category
self.assistant = assistant
@callback
def create_response(self) -> IntentResponse:

View File

@ -174,3 +174,52 @@ async def test_expose_flag_automatically_set(
new_light: {"should_expose": True},
test.entity_id: {"should_expose": False},
}
async def test_unexposed_entities_skipped(
hass: HomeAssistant,
init_components,
area_registry: ar.AreaRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that unexposed entities are skipped in exposed areas."""
area_kitchen = area_registry.async_get_or_create("kitchen")
# Both lights are in the kitchen
exposed_light = entity_registry.async_get_or_create("light", "demo", "1234")
entity_registry.async_update_entity(
exposed_light.entity_id,
area_id=area_kitchen.id,
)
hass.states.async_set(exposed_light.entity_id, "off")
unexposed_light = entity_registry.async_get_or_create("light", "demo", "5678")
entity_registry.async_update_entity(
unexposed_light.entity_id,
area_id=area_kitchen.id,
)
hass.states.async_set(unexposed_light.entity_id, "off")
# On light is exposed, the other is not
expose_entity(hass, exposed_light.entity_id, True)
expose_entity(hass, unexposed_light.entity_id, False)
# Only one light should be turned on
calls = async_mock_service(hass, "light", "turn_on")
result = await conversation.async_converse(
hass, "turn on kitchen lights", None, Context(), None
)
assert len(calls) == 1
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
# Only one light should be returned
hass.states.async_set(exposed_light.entity_id, "on")
hass.states.async_set(unexposed_light.entity_id, "on")
result = await conversation.async_converse(
hass, "how many lights are on in the kitchen", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER
assert len(result.response.matched_states) == 1
assert result.response.matched_states[0].entity_id == exposed_light.entity_id