core/tests/components/conversation/test_init.py

1495 lines
46 KiB
Python

"""The tests for the Conversation component."""
from http import HTTPStatus
import os
import tempfile
from typing import Any
from unittest.mock import patch
import pytest
from syrupy.assertion import SnapshotAssertion
import voluptuous as vol
import yaml
from homeassistant.components import conversation
from homeassistant.components.conversation import default_agent
from homeassistant.components.conversation.models import ConversationInput
from homeassistant.components.cover import SERVICE_OPEN_COVER
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_ON
from homeassistant.core import Context, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
area_registry as ar,
device_registry as dr,
entity_registry as er,
intent,
)
from homeassistant.setup import async_setup_component
from . import MockAgent, expose_entity, expose_new
from tests.common import (
MockConfigEntry,
MockUser,
async_mock_service,
setup_test_component_platform,
)
from tests.components.light.common import MockLight
from tests.typing import ClientSessionGenerator, WebSocketGenerator
AGENT_ID_OPTIONS = [
None,
# Old value of conversation.HOME_ASSISTANT_AGENT,
"homeassistant",
# Current value of conversation.HOME_ASSISTANT_AGENT,
"conversation.home_assistant",
]
class OrderBeerIntentHandler(intent.IntentHandler):
"""Handle OrderBeer intent."""
intent_type = "OrderBeer"
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
"""Return speech response."""
beer_style = intent_obj.slots["beer_style"]["value"]
response = intent_obj.create_response()
response.async_set_speech(f"You ordered a {beer_style}")
return response
@pytest.mark.parametrize("agent_id", AGENT_ID_OPTIONS)
async def test_http_processing_intent(
hass: HomeAssistant,
init_components,
hass_client: ClientSessionGenerator,
hass_admin_user: MockUser,
agent_id,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test processing intent via HTTP API."""
# Add an alias
entity_registry.async_get_or_create(
"light", "demo", "1234", suggested_object_id="kitchen"
)
entity_registry.async_update_entity("light.kitchen", aliases={"my cool light"})
hass.states.async_set("light.kitchen", "off")
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
client = await hass_client()
data: dict[str, Any] = {"text": "turn on my cool light"}
if agent_id:
data["agent_id"] = agent_id
resp = await client.post("/api/conversation/process", json=data)
assert resp.status == HTTPStatus.OK
assert len(calls) == 1
data = await resp.json()
assert data == snapshot
async def test_http_processing_intent_target_ha_agent(
hass: HomeAssistant,
init_components,
hass_client: ClientSessionGenerator,
hass_admin_user: MockUser,
mock_conversation_agent: MockAgent,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test processing intent can be processed via HTTP API with picking agent."""
# Add an alias
entity_registry.async_get_or_create(
"light", "demo", "1234", suggested_object_id="kitchen"
)
entity_registry.async_update_entity("light.kitchen", aliases={"my cool light"})
hass.states.async_set("light.kitchen", "off")
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
client = await hass_client()
resp = await client.post(
"/api/conversation/process",
json={"text": "turn on my cool light", "agent_id": "homeassistant"},
)
assert resp.status == HTTPStatus.OK
assert len(calls) == 1
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
async def test_http_processing_intent_entity_added_removed(
hass: HomeAssistant,
init_components,
hass_client: ClientSessionGenerator,
hass_admin_user: MockUser,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test processing intent via HTTP API with entities added later.
We want to ensure that adding an entity later busts the cache
so that the new entity is available as well as any aliases.
"""
entity_registry.async_get_or_create(
"light", "demo", "1234", suggested_object_id="kitchen"
)
entity_registry.async_update_entity("light.kitchen", aliases={"my cool light"})
hass.states.async_set("light.kitchen", "off")
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on my cool light"}
)
assert resp.status == HTTPStatus.OK
assert len(calls) == 1
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
# Add an entity
entity_registry.async_get_or_create(
"light", "demo", "5678", suggested_object_id="late"
)
hass.states.async_set("light.late", "off", {"friendly_name": "friendly light"})
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on friendly light"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
# Now add an alias
entity_registry.async_update_entity("light.late", aliases={"late added light"})
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on late added light"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
# Now delete the entity
hass.states.async_remove("light.late")
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on late added light"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "error"
async def test_http_processing_intent_alias_added_removed(
hass: HomeAssistant,
init_components,
hass_client: ClientSessionGenerator,
hass_admin_user: MockUser,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test processing intent via HTTP API with aliases added later.
We want to ensure that adding an alias later busts the cache
so that the new alias is available.
"""
entity_registry.async_get_or_create(
"light", "demo", "1234", suggested_object_id="kitchen"
)
hass.states.async_set("light.kitchen", "off", {"friendly_name": "kitchen light"})
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on kitchen light"}
)
assert resp.status == HTTPStatus.OK
assert len(calls) == 1
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
# Add an alias
entity_registry.async_update_entity("light.kitchen", aliases={"late added alias"})
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on late added alias"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
# Now remove the alieas
entity_registry.async_update_entity("light.kitchen", aliases={})
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on late added alias"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "error"
async def test_http_processing_intent_entity_renamed(
hass: HomeAssistant,
init_components,
hass_client: ClientSessionGenerator,
hass_admin_user: MockUser,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test processing intent via HTTP API with entities renamed later.
We want to ensure that renaming an entity later busts the cache
so that the new name is used.
"""
entity = MockLight("kitchen light", STATE_ON)
entity._attr_unique_id = "1234"
entity.entity_id = "light.kitchen"
setup_test_component_platform(hass, LIGHT_DOMAIN, [entity])
assert await async_setup_component(
hass,
LIGHT_DOMAIN,
{LIGHT_DOMAIN: [{"platform": "test"}]},
)
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on kitchen light"}
)
assert resp.status == HTTPStatus.OK
assert len(calls) == 1
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
# Rename the entity
entity_registry.async_update_entity("light.kitchen", name="renamed light")
await hass.async_block_till_done()
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on renamed light"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on kitchen light"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "error"
# Now clear the custom name
entity_registry.async_update_entity("light.kitchen", name=None)
await hass.async_block_till_done()
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on kitchen light"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on renamed light"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "error"
async def test_http_processing_intent_entity_exposed(
hass: HomeAssistant,
init_components,
hass_client: ClientSessionGenerator,
hass_admin_user: MockUser,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test processing intent via HTTP API with manual expose.
We want to ensure that manually exposing an entity later busts the cache
so that the new setting is used.
"""
entity = MockLight("kitchen light", STATE_ON)
entity._attr_unique_id = "1234"
entity.entity_id = "light.kitchen"
setup_test_component_platform(hass, LIGHT_DOMAIN, [entity])
assert await async_setup_component(
hass,
LIGHT_DOMAIN,
{LIGHT_DOMAIN: [{"platform": "test"}]},
)
await hass.async_block_till_done()
entity_registry.async_update_entity("light.kitchen", aliases={"my cool light"})
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on kitchen light"}
)
assert resp.status == HTTPStatus.OK
assert len(calls) == 1
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on my cool light"}
)
assert resp.status == HTTPStatus.OK
assert len(calls) == 1
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
# Unexpose the entity
expose_entity(hass, "light.kitchen", False)
await hass.async_block_till_done()
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on kitchen light"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "error"
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on my cool light"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "error"
# Now expose the entity
expose_entity(hass, "light.kitchen", True)
await hass.async_block_till_done()
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on kitchen light"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on my cool light"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
async def test_http_processing_intent_conversion_not_expose_new(
hass: HomeAssistant,
init_components,
hass_client: ClientSessionGenerator,
hass_admin_user: MockUser,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test processing intent via HTTP API when not exposing new entities."""
# Disable exposing new entities to the default agent
expose_new(hass, False)
entity = MockLight("kitchen light", STATE_ON)
entity._attr_unique_id = "1234"
entity.entity_id = "light.kitchen"
setup_test_component_platform(hass, LIGHT_DOMAIN, [entity])
assert await async_setup_component(
hass,
LIGHT_DOMAIN,
{LIGHT_DOMAIN: [{"platform": "test"}]},
)
await hass.async_block_till_done()
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on kitchen light"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "error"
# Expose the entity
expose_entity(hass, "light.kitchen", True)
await hass.async_block_till_done()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on kitchen light"}
)
assert resp.status == HTTPStatus.OK
assert len(calls) == 1
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
@pytest.mark.parametrize("agent_id", AGENT_ID_OPTIONS)
@pytest.mark.parametrize("sentence", ["turn on kitchen", "turn kitchen on"])
@pytest.mark.parametrize("conversation_id", ["my_new_conversation", None])
async def test_turn_on_intent(
hass: HomeAssistant,
init_components,
conversation_id,
sentence,
agent_id,
snapshot: SnapshotAssertion,
) -> None:
"""Test calling the turn on intent."""
hass.states.async_set("light.kitchen", "off")
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
data = {conversation.ATTR_TEXT: sentence}
if agent_id is not None:
data[conversation.ATTR_AGENT_ID] = agent_id
if conversation_id is not None:
data[conversation.ATTR_CONVERSATION_ID] = conversation_id
result = await hass.services.async_call(
"conversation",
"process",
data,
blocking=True,
return_response=True,
)
assert len(calls) == 1
call = calls[0]
assert call.domain == LIGHT_DOMAIN
assert call.service == "turn_on"
assert call.data == {"entity_id": ["light.kitchen"]}
assert result == snapshot
async def test_service_fails(hass: HomeAssistant, init_components) -> None:
"""Test calling the turn on intent."""
with (
pytest.raises(HomeAssistantError),
patch(
"homeassistant.components.conversation.async_converse",
side_effect=intent.IntentHandleError,
),
):
await hass.services.async_call(
"conversation",
"process",
{"text": "bla"},
blocking=True,
)
@pytest.mark.parametrize("sentence", ["turn off kitchen", "turn kitchen off"])
async def test_turn_off_intent(hass: HomeAssistant, init_components, sentence) -> None:
"""Test calling the turn on intent."""
hass.states.async_set("light.kitchen", "on")
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_off")
await hass.services.async_call(
"conversation", "process", {conversation.ATTR_TEXT: sentence}
)
await hass.async_block_till_done()
assert len(calls) == 1
call = calls[0]
assert call.domain == LIGHT_DOMAIN
assert call.service == "turn_off"
assert call.data == {"entity_id": ["light.kitchen"]}
async def test_http_api_no_match(
hass: HomeAssistant,
init_components,
hass_client: ClientSessionGenerator,
snapshot: SnapshotAssertion,
) -> None:
"""Test the HTTP conversation API with an intent match failure."""
client = await hass_client()
# Shouldn't match any intents
resp = await client.post("/api/conversation/process", json={"text": "do something"})
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "error"
assert data["response"]["data"]["code"] == "no_intent_match"
async def test_http_api_handle_failure(
hass: HomeAssistant,
init_components,
hass_client: ClientSessionGenerator,
snapshot: SnapshotAssertion,
) -> None:
"""Test the HTTP conversation API with an error during handling."""
client = await hass_client()
hass.states.async_set("light.kitchen", "off")
# Raise an error during intent handling
def async_handle_error(*args, **kwargs):
raise intent.IntentHandleError
with patch("homeassistant.helpers.intent.async_handle", new=async_handle_error):
resp = await client.post(
"/api/conversation/process", json={"text": "turn on the kitchen"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "error"
assert data["response"]["data"]["code"] == "failed_to_handle"
async def test_http_api_unexpected_failure(
hass: HomeAssistant,
init_components,
hass_client: ClientSessionGenerator,
snapshot: SnapshotAssertion,
) -> None:
"""Test the HTTP conversation API with an unexpected error during handling."""
client = await hass_client()
hass.states.async_set("light.kitchen", "off")
# Raise an "unexpected" error during intent handling
def async_handle_error(*args, **kwargs):
raise intent.IntentUnexpectedError
with patch("homeassistant.helpers.intent.async_handle", new=async_handle_error):
resp = await client.post(
"/api/conversation/process", json={"text": "turn on the kitchen"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "error"
assert data["response"]["data"]["code"] == "unknown"
async def test_http_api_wrong_data(
hass: HomeAssistant, init_components, hass_client: ClientSessionGenerator
) -> None:
"""Test the HTTP conversation API."""
client = await hass_client()
resp = await client.post("/api/conversation/process", json={"text": 123})
assert resp.status == HTTPStatus.BAD_REQUEST
resp = await client.post("/api/conversation/process", json={})
assert resp.status == HTTPStatus.BAD_REQUEST
async def test_custom_agent(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
hass_admin_user: MockUser,
mock_conversation_agent: MockAgent,
snapshot: SnapshotAssertion,
) -> None:
"""Test a custom conversation agent."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "conversation", {})
assert await async_setup_component(hass, "intent", {})
client = await hass_client()
data = {
"text": "Test Text",
"conversation_id": "test-conv-id",
"language": "test-language",
"agent_id": mock_conversation_agent.agent_id,
}
resp = await client.post("/api/conversation/process", json=data)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
assert data["response"]["speech"]["plain"]["speech"] == "Test response"
assert data["conversation_id"] == "test-conv-id"
assert len(mock_conversation_agent.calls) == 1
assert mock_conversation_agent.calls[0].text == "Test Text"
assert mock_conversation_agent.calls[0].context.user_id == hass_admin_user.id
assert mock_conversation_agent.calls[0].conversation_id == "test-conv-id"
assert mock_conversation_agent.calls[0].language == "test-language"
conversation.async_unset_agent(
hass, hass.config_entries.async_get_entry(mock_conversation_agent.agent_id)
)
@pytest.mark.parametrize(
"payload",
[
{
"text": "Test Text",
},
{
"text": "Test Text",
"language": "test-language",
},
{
"text": "Test Text",
"conversation_id": "test-conv-id",
},
{
"text": "Test Text",
"conversation_id": None,
},
{
"text": "Test Text",
"conversation_id": "test-conv-id",
"language": "test-language",
},
{
"text": "Test Text",
"agent_id": "homeassistant",
},
],
)
async def test_ws_api(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
payload,
snapshot: SnapshotAssertion,
) -> None:
"""Test the Websocket conversation API."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "conversation", {})
client = await hass_ws_client(hass)
await client.send_json_auto_id({"type": "conversation/process", **payload})
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] == snapshot
assert msg["result"]["response"]["data"]["code"] == "no_intent_match"
@pytest.mark.parametrize("agent_id", AGENT_ID_OPTIONS)
async def test_ws_prepare(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, agent_id
) -> None:
"""Test the Websocket prepare conversation API."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "conversation", {})
agent = default_agent.async_get_default_agent(hass)
assert isinstance(agent, default_agent.DefaultAgent)
# No intents should be loaded yet
assert not agent._lang_intents.get(hass.config.language)
client = await hass_ws_client(hass)
msg = {"type": "conversation/prepare"}
if agent_id is not None:
msg["agent_id"] = agent_id
await client.send_json_auto_id(msg)
msg = await client.receive_json()
assert msg["success"]
# Intents should now be load
assert agent._lang_intents.get(hass.config.language)
async def test_custom_sentences(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
hass_admin_user: MockUser,
snapshot: SnapshotAssertion,
) -> None:
"""Test custom sentences with a custom intent."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "conversation", {})
assert await async_setup_component(hass, "intent", {})
# Expecting testing_config/custom_sentences/en/beer.yaml
intent.async_register(hass, OrderBeerIntentHandler())
# Don't use "en" to test loading custom sentences with language variants.
language = "en-us"
# Invoke intent via HTTP API
client = await hass_client()
for beer_style in ("stout", "lager"):
resp = await client.post(
"/api/conversation/process",
json={
"text": f"I'd like to order a {beer_style}, please",
"language": language,
},
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
assert (
data["response"]["speech"]["plain"]["speech"]
== f"You ordered a {beer_style}"
)
async def test_custom_sentences_config(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
hass_admin_user: MockUser,
snapshot: SnapshotAssertion,
) -> None:
"""Test custom sentences with a custom intent in config."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(
hass,
"conversation",
{"conversation": {"intents": {"StealthMode": ["engage stealth mode"]}}},
)
assert await async_setup_component(hass, "intent", {})
assert await async_setup_component(
hass,
"intent_script",
{
"intent_script": {
"StealthMode": {"speech": {"text": "Stealth mode engaged"}}
}
},
)
# Invoke intent via HTTP API
client = await hass_client()
resp = await client.post(
"/api/conversation/process",
json={"text": "engage stealth mode"},
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
assert data["response"]["speech"]["plain"]["speech"] == "Stealth mode engaged"
async def test_prepare_reload(hass: HomeAssistant) -> None:
"""Test calling the reload service."""
language = hass.config.language
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "conversation", {})
# Load intents
agent = default_agent.async_get_default_agent(hass)
assert isinstance(agent, default_agent.DefaultAgent)
await agent.async_prepare(language)
# Confirm intents are loaded
assert agent._lang_intents.get(language)
# Try to clear for a different language
await hass.services.async_call("conversation", "reload", {"language": "elvish"})
await hass.async_block_till_done()
# Confirm intents are still loaded
assert agent._lang_intents.get(language)
# Clear cache for all languages
await hass.services.async_call("conversation", "reload", {})
await hass.async_block_till_done()
# Confirm intent cache is cleared
assert not agent._lang_intents.get(language)
async def test_prepare_fail(hass: HomeAssistant) -> None:
"""Test calling prepare with a non-existent language."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "conversation", {})
# Load intents
agent = default_agent.async_get_default_agent(hass)
assert isinstance(agent, default_agent.DefaultAgent)
await agent.async_prepare("not-a-language")
# Confirm no intents were loaded
assert not agent._lang_intents.get("not-a-language")
async def test_language_region(hass: HomeAssistant, init_components) -> None:
"""Test calling the turn on intent."""
hass.states.async_set("light.kitchen", "off")
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
# Add fake region
language = f"{hass.config.language}-YZ"
await hass.services.async_call(
"conversation",
"process",
{
conversation.ATTR_TEXT: "turn on the kitchen",
conversation.ATTR_LANGUAGE: language,
},
)
await hass.async_block_till_done()
assert len(calls) == 1
call = calls[0]
assert call.domain == LIGHT_DOMAIN
assert call.service == "turn_on"
assert call.data == {"entity_id": ["light.kitchen"]}
async def test_non_default_response(hass: HomeAssistant, init_components) -> None:
"""Test intent response that is not the default."""
hass.states.async_set("cover.front_door", "closed")
calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER)
agent = default_agent.async_get_default_agent(hass)
assert isinstance(agent, default_agent.DefaultAgent)
result = await agent.async_process(
ConversationInput(
text="open the front door",
context=Context(),
conversation_id=None,
device_id=None,
language=hass.config.language,
agent_id=None,
)
)
assert len(calls) == 1
assert result.response.speech["plain"]["speech"] == "Opened"
async def test_turn_on_area(
hass: HomeAssistant,
init_components,
area_registry: ar.AreaRegistry,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test turning on an area."""
entry = MockConfigEntry(domain="test")
entry.add_to_hass(hass)
device = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
kitchen_area = area_registry.async_create("kitchen")
device_registry.async_update_device(device.id, area_id=kitchen_area.id)
entity_registry.async_get_or_create(
"light", "demo", "1234", suggested_object_id="stove"
)
entity_registry.async_update_entity(
"light.stove", aliases={"my stove light"}, area_id=kitchen_area.id
)
hass.states.async_set("light.stove", "off")
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
await hass.services.async_call(
"conversation",
"process",
{conversation.ATTR_TEXT: "turn on lights in the kitchen"},
)
await hass.async_block_till_done()
assert len(calls) == 1
call = calls[0]
assert call.domain == LIGHT_DOMAIN
assert call.service == "turn_on"
assert call.data == {"entity_id": ["light.stove"]}
basement_area = area_registry.async_create("basement")
device_registry.async_update_device(device.id, area_id=basement_area.id)
entity_registry.async_update_entity("light.stove", area_id=basement_area.id)
calls.clear()
# Test that the area is updated
await hass.services.async_call(
"conversation",
"process",
{conversation.ATTR_TEXT: "turn on lights in the kitchen"},
)
await hass.async_block_till_done()
assert len(calls) == 0
# Test the new area works
await hass.services.async_call(
"conversation",
"process",
{conversation.ATTR_TEXT: "turn on lights in the basement"},
)
await hass.async_block_till_done()
assert len(calls) == 1
call = calls[0]
assert call.domain == LIGHT_DOMAIN
assert call.service == "turn_on"
assert call.data == {"entity_id": ["light.stove"]}
async def test_light_area_same_name(
hass: HomeAssistant,
init_components,
area_registry: ar.AreaRegistry,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test turning on a light with the same name as an area."""
entry = MockConfigEntry(domain="test")
entry.add_to_hass(hass)
device = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
kitchen_area = area_registry.async_create("kitchen")
device_registry.async_update_device(device.id, area_id=kitchen_area.id)
kitchen_light = entity_registry.async_get_or_create(
"light", "demo", "1234", original_name="kitchen light"
)
entity_registry.async_update_entity(
kitchen_light.entity_id, area_id=kitchen_area.id
)
hass.states.async_set(
kitchen_light.entity_id, "off", attributes={ATTR_FRIENDLY_NAME: "kitchen light"}
)
ceiling_light = entity_registry.async_get_or_create(
"light", "demo", "5678", original_name="ceiling light"
)
entity_registry.async_update_entity(
ceiling_light.entity_id, area_id=kitchen_area.id
)
hass.states.async_set(
ceiling_light.entity_id, "off", attributes={ATTR_FRIENDLY_NAME: "ceiling light"}
)
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
await hass.services.async_call(
"conversation",
"process",
{conversation.ATTR_TEXT: "turn on kitchen light"},
)
await hass.async_block_till_done()
# Should only turn on one light instead of all lights in the kitchen
assert len(calls) == 1
call = calls[0]
assert call.domain == LIGHT_DOMAIN
assert call.service == "turn_on"
assert call.data == {"entity_id": [kitchen_light.entity_id]}
async def test_agent_id_validator_invalid_agent(
hass: HomeAssistant, init_components
) -> None:
"""Test validating agent id."""
with pytest.raises(vol.Invalid):
conversation.agent_id_validator("invalid_agent")
conversation.agent_id_validator(conversation.HOME_ASSISTANT_AGENT)
conversation.agent_id_validator("conversation.home_assistant")
async def test_get_agent_list(
hass: HomeAssistant,
init_components,
mock_conversation_agent: MockAgent,
mock_agent_support_all: MockAgent,
hass_ws_client: WebSocketGenerator,
snapshot: SnapshotAssertion,
) -> None:
"""Test getting agent info."""
client = await hass_ws_client(hass)
await client.send_json_auto_id({"type": "conversation/agent/list"})
msg = await client.receive_json()
assert msg["type"] == "result"
assert msg["success"]
assert msg["result"] == snapshot
await client.send_json_auto_id(
{"type": "conversation/agent/list", "language": "smurfish"}
)
msg = await client.receive_json()
assert msg["type"] == "result"
assert msg["success"]
assert msg["result"] == snapshot
await client.send_json_auto_id(
{"type": "conversation/agent/list", "language": "en"}
)
msg = await client.receive_json()
assert msg["type"] == "result"
assert msg["success"]
assert msg["result"] == snapshot
await client.send_json_auto_id(
{"type": "conversation/agent/list", "language": "en-UK"}
)
msg = await client.receive_json()
assert msg["type"] == "result"
assert msg["success"]
assert msg["result"] == snapshot
await client.send_json_auto_id(
{"type": "conversation/agent/list", "language": "de"}
)
msg = await client.receive_json()
assert msg["type"] == "result"
assert msg["success"]
assert msg["result"] == snapshot
await client.send_json_auto_id(
{"type": "conversation/agent/list", "language": "de", "country": "ch"}
)
msg = await client.receive_json()
assert msg["type"] == "result"
assert msg["success"]
assert msg["result"] == snapshot
async def test_get_agent_info(
hass: HomeAssistant,
init_components,
mock_conversation_agent: MockAgent,
snapshot: SnapshotAssertion,
) -> None:
"""Test get agent info."""
agent_info = conversation.async_get_agent_info(hass)
# Test it's the default
assert conversation.async_get_agent_info(hass, "homeassistant") == agent_info
assert conversation.async_get_agent_info(hass, "homeassistant") == snapshot
assert (
conversation.async_get_agent_info(hass, mock_conversation_agent.agent_id)
== snapshot
)
assert conversation.async_get_agent_info(hass, "not exist") is None
# Test the name when config entry title is empty
agent_entry = hass.config_entries.async_get_entry("mock-entry")
hass.config_entries.async_update_entry(agent_entry, title="")
agent_info = conversation.async_get_agent_info(hass)
assert agent_info == snapshot
async def test_ws_hass_agent_debug(
hass: HomeAssistant,
init_components,
hass_ws_client: WebSocketGenerator,
area_registry: ar.AreaRegistry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test homeassistant agent debug websocket command."""
client = await hass_ws_client(hass)
kitchen_area = area_registry.async_create("kitchen")
entity_registry.async_get_or_create(
"light", "demo", "1234", suggested_object_id="kitchen"
)
entity_registry.async_update_entity(
"light.kitchen",
aliases={"my cool light"},
area_id=kitchen_area.id,
)
hass.states.async_set("light.kitchen", "off")
on_calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
off_calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_off")
await client.send_json_auto_id(
{
"type": "conversation/agent/homeassistant/debug",
"sentences": [
"turn on my cool light",
"turn my cool light off",
"turn on all lights in the kitchen",
"how many lights are on in the kitchen?",
"this will not match anything", # None in results
],
}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] == snapshot
# Last sentence should be a failed match
assert msg["result"]["results"][-1] is None
# Light state should not have been changed
assert len(on_calls) == 0
assert len(off_calls) == 0
async def test_ws_hass_agent_debug_null_result(
hass: HomeAssistant,
init_components,
hass_ws_client: WebSocketGenerator,
snapshot: SnapshotAssertion,
) -> None:
"""Test homeassistant agent debug websocket command with a null result."""
client = await hass_ws_client(hass)
async def async_recognize(self, user_input, *args, **kwargs):
if user_input.text == "bad sentence":
return None
return await self.async_recognize(user_input, *args, **kwargs)
with patch(
"homeassistant.components.conversation.default_agent.DefaultAgent.async_recognize",
async_recognize,
):
await client.send_json_auto_id(
{
"type": "conversation/agent/homeassistant/debug",
"sentences": [
"bad sentence",
],
}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] == snapshot
assert msg["result"]["results"] == [None]
async def test_ws_hass_agent_debug_out_of_range(
hass: HomeAssistant,
init_components,
hass_ws_client: WebSocketGenerator,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Test homeassistant agent debug websocket command with an out of range entity."""
test_light = entity_registry.async_get_or_create("light", "demo", "1234")
hass.states.async_set(
test_light.entity_id, "off", attributes={ATTR_FRIENDLY_NAME: "test light"}
)
client = await hass_ws_client(hass)
# Brightness is in range (0-100)
await client.send_json_auto_id(
{
"type": "conversation/agent/homeassistant/debug",
"sentences": [
"set test light brightness to 100%",
],
}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] == snapshot
results = msg["result"]["results"]
assert len(results) == 1
assert results[0]["match"]
# Brightness is out of range
await client.send_json_auto_id(
{
"type": "conversation/agent/homeassistant/debug",
"sentences": [
"set test light brightness to 1001%",
],
}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] == snapshot
results = msg["result"]["results"]
assert len(results) == 1
assert not results[0]["match"]
# Name matched, but brightness didn't
assert results[0]["slots"] == {"name": "test light"}
assert results[0]["unmatched_slots"] == {"brightness": 1001}
async def test_ws_hass_agent_debug_custom_sentence(
hass: HomeAssistant,
init_components,
hass_ws_client: WebSocketGenerator,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Test homeassistant agent debug websocket command with a custom sentence."""
# Expecting testing_config/custom_sentences/en/beer.yaml
intent.async_register(hass, OrderBeerIntentHandler())
client = await hass_ws_client(hass)
# Brightness is in range (0-100)
await client.send_json_auto_id(
{
"type": "conversation/agent/homeassistant/debug",
"sentences": [
"I'd like to order a lager, please.",
],
}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] == snapshot
debug_results = msg["result"].get("results", [])
assert len(debug_results) == 1
assert debug_results[0].get("match")
assert debug_results[0].get("source") == "custom"
assert debug_results[0].get("file") == "en/beer.yaml"
async def test_ws_hass_agent_debug_sentence_trigger(
hass: HomeAssistant,
init_components,
hass_ws_client: WebSocketGenerator,
snapshot: SnapshotAssertion,
) -> None:
"""Test homeassistant agent debug websocket command with a sentence trigger."""
calls = async_mock_service(hass, "test", "automation")
assert await async_setup_component(
hass,
"automation",
{
"automation": {
"trigger": {
"platform": "conversation",
"command": ["hello", "hello[ world]"],
},
"action": {
"service": "test.automation",
"data_template": {"data": "{{ trigger }}"},
},
}
},
)
client = await hass_ws_client(hass)
# Use trigger sentence
await client.send_json_auto_id(
{
"type": "conversation/agent/homeassistant/debug",
"sentences": ["hello world"],
}
)
await hass.async_block_till_done()
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] == snapshot
debug_results = msg["result"].get("results", [])
assert len(debug_results) == 1
assert debug_results[0].get("match")
assert debug_results[0].get("source") == "trigger"
assert debug_results[0].get("sentence_template") == "hello[ world]"
# Trigger should not have been executed
assert len(calls) == 0
async def test_custom_sentences_priority(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
hass_admin_user: MockUser,
snapshot: SnapshotAssertion,
) -> None:
"""Test that user intents from custom_sentences have priority over builtin intents/sentences."""
with tempfile.NamedTemporaryFile(
mode="w+",
encoding="utf-8",
suffix=".yaml",
dir=os.path.join(hass.config.config_dir, "custom_sentences", "en"),
) as custom_sentences_file:
# Add a custom sentence that would match a builtin sentence.
# Custom sentences have priority.
yaml.dump(
{
"language": "en",
"intents": {
"CustomIntent": {"data": [{"sentences": ["turn on the lamp"]}]}
},
},
custom_sentences_file,
)
custom_sentences_file.flush()
custom_sentences_file.seek(0)
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "conversation", {})
assert await async_setup_component(hass, "light", {})
assert await async_setup_component(hass, "intent", {})
assert await async_setup_component(
hass,
"intent_script",
{
"intent_script": {
"CustomIntent": {"speech": {"text": "custom response"}}
}
},
)
# Ensure that a "lamp" exists so that we can verify the custom intent
# overrides the builtin sentence.
hass.states.async_set("light.lamp", "off")
client = await hass_client()
resp = await client.post(
"/api/conversation/process",
json={
"text": "turn on the lamp",
"language": hass.config.language,
},
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data["response"]["response_type"] == "action_done"
assert data["response"]["speech"]["plain"]["speech"] == "custom response"
async def test_config_sentences_priority(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
hass_admin_user: MockUser,
snapshot: SnapshotAssertion,
) -> None:
"""Test that user intents from configuration.yaml have priority over builtin intents/sentences."""
# Add a custom sentence that would match a builtin sentence.
# Custom sentences have priority.
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "intent", {})
assert await async_setup_component(
hass,
"conversation",
{"conversation": {"intents": {"CustomIntent": ["turn on the lamp"]}}},
)
assert await async_setup_component(hass, "light", {})
assert await async_setup_component(
hass,
"intent_script",
{"intent_script": {"CustomIntent": {"speech": {"text": "custom response"}}}},
)
# Ensure that a "lamp" exists so that we can verify the custom intent
# overrides the builtin sentence.
hass.states.async_set("light.lamp", "off")
client = await hass_client()
resp = await client.post(
"/api/conversation/process",
json={
"text": "turn on the lamp",
"language": hass.config.language,
},
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data["response"]["response_type"] == "action_done"
assert data["response"]["speech"]["plain"]["speech"] == "custom response"