Skip to content

Commit

Permalink
Use device area id in intent matching (home-assistant#86678)
Browse files Browse the repository at this point in the history
* Use device area id when matching

* Normalize whitespace in response

* Add extra test entity
  • Loading branch information
synesthesiam authored and balloob committed Jan 31, 2023
1 parent 0702314 commit c7b944c
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 11 deletions.
18 changes: 10 additions & 8 deletions homeassistant/components/conversation/default_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,17 +180,19 @@ async def async_process(self, user_input: ConversationInput) -> ConversationResu
).get(response_key)
if response_str:
response_template = template.Template(response_str, self.hass)
intent_response.async_set_speech(
response_template.async_render(
{
"slots": {
entity_name: entity_value.text or entity_value.value
for entity_name, entity_value in result.entities.items()
}
speech = response_template.async_render(
{
"slots": {
entity_name: entity_value.text or entity_value.value
for entity_name, entity_value in result.entities.items()
}
)
}
)

# Normalize whitespace
speech = " ".join(speech.strip().split())
intent_response.async_set_speech(speech)

return ConversationResult(
response=intent_response, conversation_id=conversation_id
)
Expand Down
22 changes: 20 additions & 2 deletions homeassistant/helpers/intent.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import bind_hass

from . import area_registry, config_validation as cv, entity_registry
from . import area_registry, config_validation as cv, device_registry, entity_registry

_LOGGER = logging.getLogger(__name__)
_SlotsType = dict[str, Any]
Expand Down Expand Up @@ -159,6 +159,7 @@ def async_match_states(
states: Iterable[State] | None = None,
entities: entity_registry.EntityRegistry | None = None,
areas: area_registry.AreaRegistry | None = None,
devices: device_registry.DeviceRegistry | None = None,
) -> Iterable[State]:
"""Find states that match the constraints."""
if states is None:
Expand Down Expand Up @@ -206,11 +207,28 @@ def async_match_states(
assert area is not None, f"No area named {area_name}"

if area is not None:
if devices is None:
devices = device_registry.async_get(hass)

entity_area_ids: dict[str, str | None] = {}
for _state, entity in states_and_entities:
if entity is None:
continue

if entity.area_id:
# Use entity's area id first
entity_area_ids[entity.id] = entity.area_id
elif entity.device_id:
# Fall back to device area if not set on entity
device = devices.async_get(entity.device_id)
if device is not None:
entity_area_ids[entity.id] = device.area_id

# Filter by area
states_and_entities = [
(state, entity)
for state, entity in states_and_entities
if (entity is not None) and (entity.area_id == area.id)
if (entity is not None) and (entity_area_ids.get(entity.id) == area.id)
]

if name is not None:
Expand Down
42 changes: 41 additions & 1 deletion tests/helpers/test_intent.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from homeassistant.helpers import (
area_registry,
config_validation as cv,
device_registry,
entity_registry,
intent,
)
Expand Down Expand Up @@ -41,7 +42,7 @@ async def test_async_match_states(hass):
entities.async_update_entity(state1.entity_id, area_id=area_kitchen.id)

entities.async_get_or_create(
"switch", "demo", "1234", suggested_object_id="bedroom"
"switch", "demo", "5678", suggested_object_id="bedroom"
)
entities.async_update_entity(
state2.entity_id,
Expand Down Expand Up @@ -92,6 +93,45 @@ async def test_async_match_states(hass):
)


async def test_match_device_area(hass):
"""Test async_match_state with a device in an area."""
areas = area_registry.async_get(hass)
area_kitchen = areas.async_get_or_create("kitchen")
area_bedroom = areas.async_get_or_create("bedroom")

devices = device_registry.async_get(hass)
kitchen_device = devices.async_get_or_create(
config_entry_id="1234", connections=set(), identifiers={("demo", "id-1234")}
)
devices.async_update_device(kitchen_device.id, area_id=area_kitchen.id)

state1 = State(
"light.kitchen", "on", attributes={ATTR_FRIENDLY_NAME: "kitchen light"}
)
state2 = State(
"light.bedroom", "on", attributes={ATTR_FRIENDLY_NAME: "bedroom light"}
)
state3 = State(
"light.living_room", "on", attributes={ATTR_FRIENDLY_NAME: "living room light"}
)
entities = entity_registry.async_get(hass)
entities.async_get_or_create("light", "demo", "1234", suggested_object_id="kitchen")
entities.async_update_entity(state1.entity_id, device_id=kitchen_device.id)

entities.async_get_or_create("light", "demo", "5678", suggested_object_id="bedroom")
entities.async_update_entity(state2.entity_id, area_id=area_bedroom.id)

# Match on area/domain
assert [state1] == list(
intent.async_match_states(
hass,
domains={"light"},
area_name="kitchen",
states=[state1, state2, state3],
)
)


def test_async_validate_slots():
"""Test async_validate_slots of IntentHandler."""
handler1 = MockIntentHandler({vol.Required("name"): cv.string})
Expand Down

0 comments on commit c7b944c

Please sign in to comment.