core/tests/components/conversation/test_default_agent.py

1086 lines
41 KiB
Python

"""Test for the default agent."""
from collections import defaultdict
from unittest.mock import AsyncMock, patch
from hassil.recognize import Intent, IntentData, MatchEntity, RecognizeResult
import pytest
from homeassistant.components import conversation
from homeassistant.components.homeassistant.exposed_entities import (
async_get_assistant_settings,
)
from homeassistant.const import ATTR_FRIENDLY_NAME
from homeassistant.core import DOMAIN as HASS_DOMAIN, Context, HomeAssistant
from homeassistant.helpers import (
area_registry as ar,
device_registry as dr,
entity,
entity_registry as er,
floor_registry as fr,
intent,
)
from homeassistant.setup import async_setup_component
from . import expose_entity
from tests.common import MockConfigEntry, async_mock_service
@pytest.fixture
async def init_components(hass):
"""Initialize relevant components with empty configs."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "conversation", {})
assert await async_setup_component(hass, "intent", {})
@pytest.mark.parametrize(
"er_kwargs",
[
{"hidden_by": er.RegistryEntryHider.USER},
{"hidden_by": er.RegistryEntryHider.INTEGRATION},
{"entity_category": entity.EntityCategory.CONFIG},
{"entity_category": entity.EntityCategory.DIAGNOSTIC},
],
)
async def test_hidden_entities_skipped(
hass: HomeAssistant, init_components, er_kwargs, entity_registry: er.EntityRegistry
) -> None:
"""Test we skip hidden entities."""
entity_registry.async_get_or_create(
"light", "demo", "1234", suggested_object_id="Test light", **er_kwargs
)
hass.states.async_set("light.test_light", "off")
calls = async_mock_service(hass, HASS_DOMAIN, "turn_on")
result = await conversation.async_converse(
hass, "turn on test light", None, Context(), None
)
assert len(calls) == 0
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
async def test_exposed_domains(hass: HomeAssistant, init_components) -> None:
"""Test that we can't interact with entities that aren't exposed."""
hass.states.async_set(
"media_player.test", "off", attributes={ATTR_FRIENDLY_NAME: "Test Media Player"}
)
result = await conversation.async_converse(
hass, "turn on test media player", None, Context(), None
)
# This is a match failure instead of a handle failure because the media
# player domain is not exposed.
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
async def test_exposed_areas(
hass: HomeAssistant,
init_components,
area_registry: ar.AreaRegistry,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that all areas are exposed."""
area_kitchen = area_registry.async_get_or_create("kitchen_id")
area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
area_bedroom = area_registry.async_get_or_create("bedroom_id")
area_bedroom = area_registry.async_update(area_bedroom.id, name="bedroom")
entry = MockConfigEntry()
entry.add_to_hass(hass)
kitchen_device = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections=set(),
identifiers={("demo", "id-1234")},
)
device_registry.async_update_device(kitchen_device.id, area_id=area_kitchen.id)
kitchen_light = entity_registry.async_get_or_create("light", "demo", "1234")
kitchen_light = entity_registry.async_update_entity(
kitchen_light.entity_id, device_id=kitchen_device.id
)
hass.states.async_set(
kitchen_light.entity_id, "on", attributes={ATTR_FRIENDLY_NAME: "kitchen light"}
)
bedroom_light = entity_registry.async_get_or_create("light", "demo", "5678")
bedroom_light = entity_registry.async_update_entity(
bedroom_light.entity_id, area_id=area_bedroom.id
)
hass.states.async_set(
bedroom_light.entity_id, "on", attributes={ATTR_FRIENDLY_NAME: "bedroom light"}
)
# Hide the bedroom light
expose_entity(hass, bedroom_light.entity_id, False)
result = await conversation.async_converse(
hass, "turn on lights in the kitchen", None, Context(), None
)
# All is well for the exposed kitchen light
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert result.response.intent.slots["area"]["value"] == area_kitchen.id
assert result.response.intent.slots["area"]["text"] == area_kitchen.normalized_name
# Bedroom has no exposed entities
result = await conversation.async_converse(
hass, "turn on lights in the bedroom", None, Context(), None
)
# This should be an error because the lights in that area are not exposed
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
# But we can still ask questions about the bedroom, even with no exposed entities
result = await conversation.async_converse(
hass, "how many lights are on in the bedroom?", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER
async def test_conversation_agent(
hass: HomeAssistant,
init_components,
) -> None:
"""Test DefaultAgent."""
agent = await conversation._get_agent_manager(hass).async_get_agent(
conversation.HOME_ASSISTANT_AGENT
)
with patch(
"homeassistant.components.conversation.default_agent.get_languages",
return_value=["dwarvish", "elvish", "entish"],
):
assert agent.supported_languages == ["dwarvish", "elvish", "entish"]
async def test_expose_flag_automatically_set(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
) -> None:
"""Test DefaultAgent sets the expose flag on all entities automatically."""
assert await async_setup_component(hass, "homeassistant", {})
light = entity_registry.async_get_or_create("light", "demo", "1234")
test = entity_registry.async_get_or_create("test", "demo", "1234")
assert async_get_assistant_settings(hass, conversation.DOMAIN) == {}
assert await async_setup_component(hass, "conversation", {})
await hass.async_block_till_done()
with patch("homeassistant.components.http.start_http_server_and_save_config"):
await hass.async_start()
# After setting up conversation, the expose flag should now be set on all entities
assert async_get_assistant_settings(hass, conversation.DOMAIN) == {
light.entity_id: {"should_expose": True},
test.entity_id: {"should_expose": False},
}
# New entities will automatically have the expose flag set
new_light = "light.demo_2345"
hass.states.async_set(new_light, "test")
await hass.async_block_till_done()
assert async_get_assistant_settings(hass, conversation.DOMAIN) == {
light.entity_id: {"should_expose": True},
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_id")
area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
# Both lights are in the kitchen
exposed_light = entity_registry.async_get_or_create("light", "demo", "1234")
exposed_light = 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")
unexposed_light = 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
assert result.response.intent is not None
assert result.response.intent.slots["area"]["value"] == area_kitchen.id
assert result.response.intent.slots["area"]["text"] == area_kitchen.normalized_name
# 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
async def test_trigger_sentences(hass: HomeAssistant, init_components) -> None:
"""Test registering/unregistering/matching a few trigger sentences."""
trigger_sentences = ["It's party time", "It is time to party"]
trigger_response = "Cowabunga!"
agent = await conversation._get_agent_manager(hass).async_get_agent(
conversation.HOME_ASSISTANT_AGENT
)
assert isinstance(agent, conversation.DefaultAgent)
callback = AsyncMock(return_value=trigger_response)
unregister = agent.register_trigger(trigger_sentences, callback)
result = await conversation.async_converse(hass, "Not the trigger", None, Context())
assert result.response.response_type == intent.IntentResponseType.ERROR
# Using different case and including punctuation
test_sentences = ["it's party time!", "IT IS TIME TO PARTY."]
for sentence in test_sentences:
callback.reset_mock()
result = await conversation.async_converse(hass, sentence, None, Context())
assert callback.call_count == 1
assert callback.call_args[0][0] == sentence
assert (
result.response.response_type == intent.IntentResponseType.ACTION_DONE
), sentence
assert result.response.speech == {
"plain": {"speech": trigger_response, "extra_data": None}
}
unregister()
# Should produce errors now
callback.reset_mock()
for sentence in test_sentences:
result = await conversation.async_converse(hass, sentence, None, Context())
assert (
result.response.response_type == intent.IntentResponseType.ERROR
), sentence
assert len(callback.mock_calls) == 0
async def test_shopping_list_add_item(
hass: HomeAssistant, init_components, sl_setup
) -> None:
"""Test adding an item to the shopping list through the default agent."""
result = await conversation.async_converse(
hass, "add apples to my shopping list", None, Context()
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.speech == {
"plain": {"speech": "Added apples", "extra_data": None}
}
async def test_nevermind_item(hass: HomeAssistant, init_components) -> None:
"""Test HassNevermind intent through the default agent."""
result = await conversation.async_converse(hass, "nevermind", None, Context())
assert result.response.intent is not None
assert result.response.intent.intent_type == intent.INTENT_NEVERMIND
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert not result.response.speech
async def test_device_area_context(
hass: HomeAssistant,
init_components,
area_registry: ar.AreaRegistry,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that including a device_id will target a specific area."""
turn_on_calls = async_mock_service(hass, "light", "turn_on")
turn_off_calls = async_mock_service(hass, "light", "turn_off")
area_kitchen = area_registry.async_get_or_create("kitchen_id")
area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
area_bedroom = area_registry.async_get_or_create("bedroom_id")
area_bedroom = area_registry.async_update(area_bedroom.id, name="bedroom")
# Create 2 lights in each area
area_lights = defaultdict(list)
all_lights = []
for area in (area_kitchen, area_bedroom):
for i in range(2):
light_entity = entity_registry.async_get_or_create(
"light", "demo", f"{area.name}-light-{i}"
)
light_entity = entity_registry.async_update_entity(
light_entity.entity_id, area_id=area.id
)
hass.states.async_set(
light_entity.entity_id,
"off",
attributes={ATTR_FRIENDLY_NAME: f"{area.name} light {i}"},
)
area_lights[area.id].append(light_entity)
all_lights.append(light_entity)
# Create voice satellites in each area
entry = MockConfigEntry()
entry.add_to_hass(hass)
kitchen_satellite = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections=set(),
identifiers={("demo", "id-satellite-kitchen")},
)
device_registry.async_update_device(kitchen_satellite.id, area_id=area_kitchen.id)
bedroom_satellite = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections=set(),
identifiers={("demo", "id-satellite-bedroom")},
)
device_registry.async_update_device(bedroom_satellite.id, area_id=area_bedroom.id)
# Turn on lights in the area of a device
result = await conversation.async_converse(
hass,
"turn on the lights",
None,
Context(),
None,
device_id=kitchen_satellite.id,
)
await hass.async_block_till_done()
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert result.response.intent.slots["area"]["value"] == area_kitchen.id
assert result.response.intent.slots["area"]["text"] == area_kitchen.normalized_name
# Verify only kitchen lights were targeted
assert {s.entity_id for s in result.response.matched_states} == {
e.entity_id for e in area_lights[area_kitchen.id]
}
assert {c.data["entity_id"][0] for c in turn_on_calls} == {
e.entity_id for e in area_lights[area_kitchen.id]
}
turn_on_calls.clear()
# Ensure we can still target other areas by name
result = await conversation.async_converse(
hass,
"turn on lights in the bedroom",
None,
Context(),
None,
device_id=kitchen_satellite.id,
)
await hass.async_block_till_done()
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert result.response.intent.slots["area"]["value"] == area_bedroom.id
assert result.response.intent.slots["area"]["text"] == area_bedroom.normalized_name
# Verify only bedroom lights were targeted
assert {s.entity_id for s in result.response.matched_states} == {
e.entity_id for e in area_lights[area_bedroom.id]
}
assert {c.data["entity_id"][0] for c in turn_on_calls} == {
e.entity_id for e in area_lights[area_bedroom.id]
}
turn_on_calls.clear()
# Turn off all lights in the area of the other device
result = await conversation.async_converse(
hass,
"turn lights off",
None,
Context(),
None,
device_id=bedroom_satellite.id,
)
await hass.async_block_till_done()
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert result.response.intent.slots["area"]["value"] == area_bedroom.id
assert result.response.intent.slots["area"]["text"] == area_bedroom.normalized_name
# Verify only bedroom lights were targeted
assert {s.entity_id for s in result.response.matched_states} == {
e.entity_id for e in area_lights[area_bedroom.id]
}
assert {c.data["entity_id"][0] for c in turn_off_calls} == {
e.entity_id for e in area_lights[area_bedroom.id]
}
turn_off_calls.clear()
# Turn on/off all lights also works
for command in ("on", "off"):
result = await conversation.async_converse(
hass, f"turn {command} all lights", None, Context(), None
)
await hass.async_block_till_done()
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
# All lights should have been targeted
assert {s.entity_id for s in result.response.matched_states} == {
e.entity_id for e in all_lights
}
async def test_error_no_device(hass: HomeAssistant, init_components) -> None:
"""Test error message when device/entity is missing."""
result = await conversation.async_converse(
hass, "turn on missing entity", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
== "Sorry, I am not aware of any device called missing entity"
)
async def test_error_no_area(hass: HomeAssistant, init_components) -> None:
"""Test error message when area is missing."""
result = await conversation.async_converse(
hass, "turn on the lights in missing area", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
== "Sorry, I am not aware of any area called missing area"
)
async def test_error_no_floor(hass: HomeAssistant, init_components) -> None:
"""Test error message when floor is missing."""
result = await conversation.async_converse(
hass, "turn on all the lights on missing floor", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
== "Sorry, I am not aware of any floor called missing"
)
async def test_error_no_device_in_area(
hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry
) -> None:
"""Test error message when area is missing a device/entity."""
area_kitchen = area_registry.async_get_or_create("kitchen_id")
area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
result = await conversation.async_converse(
hass, "turn on missing entity in the kitchen", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
== "Sorry, I am not aware of any device called missing entity in the kitchen area"
)
async def test_error_no_domain(
hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry
) -> None:
"""Test error message when no devices/entities exist for a domain."""
# We don't have a sentence for turning on all fans
fan_domain = MatchEntity(name="domain", value="fan", text="fans")
recognize_result = RecognizeResult(
intent=Intent("HassTurnOn"),
intent_data=IntentData([]),
entities={"domain": fan_domain},
entities_list=[fan_domain],
)
with patch(
"homeassistant.components.conversation.default_agent.recognize_all",
return_value=[recognize_result],
):
result = await conversation.async_converse(
hass, "turn on the fans", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert (
result.response.error_code
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
)
assert (
result.response.speech["plain"]["speech"]
== "Sorry, I am not aware of any fan"
)
async def test_error_no_domain_in_area(
hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry
) -> None:
"""Test error message when no devices/entities for a domain exist in an area."""
area_kitchen = area_registry.async_get_or_create("kitchen_id")
area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
result = await conversation.async_converse(
hass, "turn on the lights in the kitchen", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
== "Sorry, I am not aware of any light in the kitchen area"
)
async def test_error_no_domain_in_floor(
hass: HomeAssistant,
init_components,
area_registry: ar.AreaRegistry,
floor_registry: fr.FloorRegistry,
) -> None:
"""Test error message when no devices/entities for a domain exist on a floor."""
floor_ground = floor_registry.async_create("ground")
area_kitchen = area_registry.async_get_or_create("kitchen_id")
area_kitchen = area_registry.async_update(
area_kitchen.id, name="kitchen", floor_id=floor_ground.floor_id
)
result = await conversation.async_converse(
hass, "turn on all lights on the ground floor", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
== "Sorry, I am not aware of any light on the ground floor"
)
# Add a new floor/area to trigger registry event handlers
floor_upstairs = floor_registry.async_create("upstairs")
area_bedroom = area_registry.async_get_or_create("bedroom_id")
area_bedroom = area_registry.async_update(
area_bedroom.id, name="bedroom", floor_id=floor_upstairs.floor_id
)
result = await conversation.async_converse(
hass, "turn on all lights upstairs", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
== "Sorry, I am not aware of any light on the upstairs floor"
)
async def test_error_no_device_class(hass: HomeAssistant, init_components) -> None:
"""Test error message when no entities of a device class exist."""
# We don't have a sentence for opening all windows
window_class = MatchEntity(name="device_class", value="window", text="windows")
recognize_result = RecognizeResult(
intent=Intent("HassTurnOn"),
intent_data=IntentData([]),
entities={"device_class": window_class},
entities_list=[window_class],
)
with patch(
"homeassistant.components.conversation.default_agent.recognize_all",
return_value=[recognize_result],
):
result = await conversation.async_converse(
hass, "open the windows", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert (
result.response.error_code
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
)
assert (
result.response.speech["plain"]["speech"]
== "Sorry, I am not aware of any window"
)
async def test_error_no_device_class_in_area(
hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry
) -> None:
"""Test error message when no entities of a device class exist in an area."""
area_bedroom = area_registry.async_get_or_create("bedroom_id")
area_bedroom = area_registry.async_update(area_bedroom.id, name="bedroom")
result = await conversation.async_converse(
hass, "open bedroom windows", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
== "Sorry, I am not aware of any window in the bedroom area"
)
async def test_error_no_intent(hass: HomeAssistant, init_components) -> None:
"""Test response with an intent match failure."""
with patch(
"homeassistant.components.conversation.default_agent.recognize_all",
return_value=[],
):
result = await conversation.async_converse(
hass, "do something", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert (
result.response.error_code == intent.IntentResponseErrorCode.NO_INTENT_MATCH
)
assert (
result.response.speech["plain"]["speech"]
== "Sorry, I couldn't understand that"
)
async def test_error_duplicate_names(
hass: HomeAssistant, init_components, entity_registry: er.EntityRegistry
) -> None:
"""Test error message when multiple devices have the same name (or alias)."""
kitchen_light_1 = entity_registry.async_get_or_create("light", "demo", "1234")
kitchen_light_2 = entity_registry.async_get_or_create("light", "demo", "5678")
# Same name and alias
for light in (kitchen_light_1, kitchen_light_2):
light = entity_registry.async_update_entity(
light.entity_id,
name="kitchen light",
aliases={"overhead light"},
)
hass.states.async_set(
light.entity_id,
"off",
attributes={ATTR_FRIENDLY_NAME: light.name},
)
# Check name and alias
for name in ("kitchen light", "overhead light"):
# command
result = await conversation.async_converse(
hass, f"turn on {name}", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert (
result.response.error_code
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
)
assert (
result.response.speech["plain"]["speech"]
== f"Sorry, there are multiple devices called {name}"
)
# question
result = await conversation.async_converse(
hass, f"is {name} on?", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert (
result.response.error_code
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
)
assert (
result.response.speech["plain"]["speech"]
== f"Sorry, there are multiple devices called {name}"
)
async def test_error_duplicate_names_in_area(
hass: HomeAssistant,
init_components,
area_registry: ar.AreaRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test error message when multiple devices have the same name (or alias)."""
area_kitchen = area_registry.async_get_or_create("kitchen_id")
area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
kitchen_light_1 = entity_registry.async_get_or_create("light", "demo", "1234")
kitchen_light_2 = entity_registry.async_get_or_create("light", "demo", "5678")
# Same name and alias
for light in (kitchen_light_1, kitchen_light_2):
light = entity_registry.async_update_entity(
light.entity_id,
name="kitchen light",
area_id=area_kitchen.id,
aliases={"overhead light"},
)
hass.states.async_set(
light.entity_id,
"off",
attributes={ATTR_FRIENDLY_NAME: light.name},
)
# Check name and alias
for name in ("kitchen light", "overhead light"):
# command
result = await conversation.async_converse(
hass, f"turn on {name} in {area_kitchen.name}", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert (
result.response.error_code
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
)
assert (
result.response.speech["plain"]["speech"]
== f"Sorry, there are multiple devices called {name} in the {area_kitchen.name} area"
)
# question
result = await conversation.async_converse(
hass, f"is {name} on in the {area_kitchen.name}?", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert (
result.response.error_code
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
)
assert (
result.response.speech["plain"]["speech"]
== f"Sorry, there are multiple devices called {name} in the {area_kitchen.name} area"
)
async def test_no_states_matched_default_error(
hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry
) -> None:
"""Test default response when no states match and slots are missing."""
area_kitchen = area_registry.async_get_or_create("kitchen_id")
area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
with patch(
"homeassistant.components.conversation.default_agent.intent.async_handle",
side_effect=intent.NoStatesMatchedError(),
):
result = await conversation.async_converse(
hass, "turn on lights in the kitchen", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert (
result.response.error_code
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
)
assert (
result.response.speech["plain"]["speech"]
== "Sorry, I couldn't understand that"
)
async def test_empty_aliases(
hass: HomeAssistant,
init_components,
area_registry: ar.AreaRegistry,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
floor_registry: fr.FloorRegistry,
) -> None:
"""Test that empty aliases are not added to slot lists."""
floor_1 = floor_registry.async_create("first floor", aliases={" "})
area_kitchen = area_registry.async_get_or_create("kitchen_id")
area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
area_kitchen = area_registry.async_update(
area_kitchen.id, aliases={" "}, floor_id=floor_1
)
entry = MockConfigEntry()
entry.add_to_hass(hass)
kitchen_device = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections=set(),
identifiers={("demo", "id-1234")},
)
device_registry.async_update_device(kitchen_device.id, area_id=area_kitchen.id)
kitchen_light = entity_registry.async_get_or_create("light", "demo", "1234")
kitchen_light = entity_registry.async_update_entity(
kitchen_light.entity_id,
device_id=kitchen_device.id,
name="kitchen light",
aliases={" "},
)
hass.states.async_set(
kitchen_light.entity_id,
"on",
attributes={ATTR_FRIENDLY_NAME: kitchen_light.name},
)
with patch(
"homeassistant.components.conversation.DefaultAgent._recognize",
return_value=None,
) as mock_recognize_all:
await conversation.async_converse(
hass, "turn on lights in the kitchen", None, Context(), None
)
assert mock_recognize_all.call_count > 0
slot_lists = mock_recognize_all.call_args[0][2]
# Slot lists should only contain non-empty text
assert slot_lists.keys() == {"area", "name", "floor"}
areas = slot_lists["area"]
assert len(areas.values) == 1
assert areas.values[0].value_out == area_kitchen.id
assert areas.values[0].text_in.text == area_kitchen.normalized_name
names = slot_lists["name"]
assert len(names.values) == 1
assert names.values[0].value_out == kitchen_light.name
assert names.values[0].text_in.text == kitchen_light.name
floors = slot_lists["floor"]
assert len(floors.values) == 1
assert floors.values[0].value_out == floor_1.floor_id
assert floors.values[0].text_in.text == floor_1.name
async def test_all_domains_loaded(hass: HomeAssistant, init_components) -> None:
"""Test that sentences for all domains are always loaded."""
# light domain is not loaded
assert "light" not in hass.config.components
result = await conversation.async_converse(
hass, "set brightness of test light to 100%", None, Context(), None
)
# Invalid target vs. no intent recognized
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
== "Sorry, I am not aware of any device called test light"
)
async def test_same_named_entities_in_different_areas(
hass: HomeAssistant,
init_components,
area_registry: ar.AreaRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that entities with the same name in different areas can be targeted."""
area_kitchen = area_registry.async_get_or_create("kitchen_id")
area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
area_bedroom = area_registry.async_get_or_create("bedroom_id")
area_bedroom = area_registry.async_update(area_bedroom.id, name="bedroom")
# Both lights have the same name, but are in different areas
kitchen_light = entity_registry.async_get_or_create("light", "demo", "1234")
kitchen_light = entity_registry.async_update_entity(
kitchen_light.entity_id,
area_id=area_kitchen.id,
name="overhead light",
)
hass.states.async_set(
kitchen_light.entity_id,
"off",
attributes={ATTR_FRIENDLY_NAME: kitchen_light.name},
)
bedroom_light = entity_registry.async_get_or_create("light", "demo", "5678")
bedroom_light = entity_registry.async_update_entity(
bedroom_light.entity_id,
area_id=area_bedroom.id,
name="overhead light",
)
hass.states.async_set(
bedroom_light.entity_id,
"off",
attributes={ATTR_FRIENDLY_NAME: bedroom_light.name},
)
# Target kitchen light
calls = async_mock_service(hass, "light", "turn_on")
result = await conversation.async_converse(
hass, "turn on overhead light in the kitchen", None, Context(), None
)
await hass.async_block_till_done()
assert len(calls) == 1
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert (
result.response.intent.slots.get("name", {}).get("value") == kitchen_light.name
)
assert (
result.response.intent.slots.get("name", {}).get("text") == kitchen_light.name
)
assert len(result.response.matched_states) == 1
assert result.response.matched_states[0].entity_id == kitchen_light.entity_id
assert calls[0].data.get("entity_id") == [kitchen_light.entity_id]
# Target bedroom light
calls.clear()
result = await conversation.async_converse(
hass, "turn on overhead light in the bedroom", None, Context(), None
)
await hass.async_block_till_done()
assert len(calls) == 1
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert (
result.response.intent.slots.get("name", {}).get("value") == bedroom_light.name
)
assert (
result.response.intent.slots.get("name", {}).get("text") == bedroom_light.name
)
assert len(result.response.matched_states) == 1
assert result.response.matched_states[0].entity_id == bedroom_light.entity_id
assert calls[0].data.get("entity_id") == [bedroom_light.entity_id]
# Targeting a duplicate name should fail
result = await conversation.async_converse(
hass, "turn on overhead light", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
# Querying a duplicate name should also fail
result = await conversation.async_converse(
hass, "is the overhead light on?", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
# But we can still ask questions that don't rely on the name
result = await conversation.async_converse(
hass, "how many lights are on?", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER
async def test_same_aliased_entities_in_different_areas(
hass: HomeAssistant,
init_components,
area_registry: ar.AreaRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that entities with the same alias (but different names) in different areas can be targeted."""
area_kitchen = area_registry.async_get_or_create("kitchen_id")
area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
area_bedroom = area_registry.async_get_or_create("bedroom_id")
area_bedroom = area_registry.async_update(area_bedroom.id, name="bedroom")
# Both lights have the same alias, but are in different areas
kitchen_light = entity_registry.async_get_or_create("light", "demo", "1234")
kitchen_light = entity_registry.async_update_entity(
kitchen_light.entity_id,
area_id=area_kitchen.id,
name="kitchen overhead light",
aliases={"overhead light"},
)
hass.states.async_set(
kitchen_light.entity_id,
"off",
attributes={ATTR_FRIENDLY_NAME: kitchen_light.name},
)
bedroom_light = entity_registry.async_get_or_create("light", "demo", "5678")
bedroom_light = entity_registry.async_update_entity(
bedroom_light.entity_id,
area_id=area_bedroom.id,
name="bedroom overhead light",
aliases={"overhead light"},
)
hass.states.async_set(
bedroom_light.entity_id,
"off",
attributes={ATTR_FRIENDLY_NAME: bedroom_light.name},
)
# Target kitchen light
calls = async_mock_service(hass, "light", "turn_on")
result = await conversation.async_converse(
hass, "turn on overhead light in the kitchen", None, Context(), None
)
await hass.async_block_till_done()
assert len(calls) == 1
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert result.response.intent.slots.get("name", {}).get("value") == "overhead light"
assert result.response.intent.slots.get("name", {}).get("text") == "overhead light"
assert len(result.response.matched_states) == 1
assert result.response.matched_states[0].entity_id == kitchen_light.entity_id
assert calls[0].data.get("entity_id") == [kitchen_light.entity_id]
# Target bedroom light
calls.clear()
result = await conversation.async_converse(
hass, "turn on overhead light in the bedroom", None, Context(), None
)
await hass.async_block_till_done()
assert len(calls) == 1
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert result.response.intent.slots.get("name", {}).get("value") == "overhead light"
assert result.response.intent.slots.get("name", {}).get("text") == "overhead light"
assert len(result.response.matched_states) == 1
assert result.response.matched_states[0].entity_id == bedroom_light.entity_id
assert calls[0].data.get("entity_id") == [bedroom_light.entity_id]
# Targeting a duplicate alias should fail
result = await conversation.async_converse(
hass, "turn on overhead light", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
# Querying a duplicate alias should also fail
result = await conversation.async_converse(
hass, "is the overhead light on?", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
# But we can still ask questions that don't rely on the alias
result = await conversation.async_converse(
hass, "how many lights are on?", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER