Use intent responses from home-assistant-intents (#86484)
* Use intent responses from home_assistant_intents * Use error responses from home_assistant_intents * Remove speech checks for intent tests (set by conversation now) * Bump hassil and home-assistant-intents versions * Use Home Assistant JSON reader when loading intents * Remove speech checks for light tests (done in agent) * Add more tests for code coverage * Add test for reloading on new component * Add test for non-default response
This commit is contained in:
parent
6d811d3fdb
commit
ea95abcb30
16 changed files with 236 additions and 134 deletions
|
@ -294,7 +294,7 @@ class AlmondAgent(conversation.AbstractConversationAgent):
|
|||
context: Context,
|
||||
conversation_id: str | None = None,
|
||||
language: str | None = None,
|
||||
) -> conversation.ConversationResult | None:
|
||||
) -> conversation.ConversationResult:
|
||||
"""Process a sentence."""
|
||||
response = await self.api.async_converse_text(text, conversation_id)
|
||||
language = language or self.hass.config.language
|
||||
|
|
|
@ -242,44 +242,5 @@ async def async_converse(
|
|||
if language is None:
|
||||
language = hass.config.language
|
||||
|
||||
result: ConversationResult | None = None
|
||||
intent_response: intent.IntentResponse | None = None
|
||||
|
||||
try:
|
||||
result = await agent.async_process(text, context, conversation_id, language)
|
||||
except intent.IntentHandleError as err:
|
||||
# Match was successful, but target(s) were invalid
|
||||
intent_response = intent.IntentResponse(language=language)
|
||||
intent_response.async_set_error(
|
||||
intent.IntentResponseErrorCode.NO_VALID_TARGETS,
|
||||
str(err),
|
||||
)
|
||||
except intent.IntentUnexpectedError as err:
|
||||
# Match was successful, but an error occurred while handling intent
|
||||
intent_response = intent.IntentResponse(language=language)
|
||||
intent_response.async_set_error(
|
||||
intent.IntentResponseErrorCode.FAILED_TO_HANDLE,
|
||||
str(err),
|
||||
)
|
||||
except intent.IntentError as err:
|
||||
# Unknown error
|
||||
intent_response = intent.IntentResponse(language=language)
|
||||
intent_response.async_set_error(
|
||||
intent.IntentResponseErrorCode.UNKNOWN,
|
||||
str(err),
|
||||
)
|
||||
|
||||
if result is None:
|
||||
if intent_response is None:
|
||||
# Match was not successful
|
||||
intent_response = intent.IntentResponse(language=language)
|
||||
intent_response.async_set_error(
|
||||
intent.IntentResponseErrorCode.NO_INTENT_MATCH,
|
||||
"Sorry, I didn't understand that",
|
||||
)
|
||||
|
||||
result = ConversationResult(
|
||||
response=intent_response, conversation_id=conversation_id
|
||||
)
|
||||
|
||||
result = await agent.async_process(text, context, conversation_id, language)
|
||||
return result
|
||||
|
|
|
@ -47,7 +47,7 @@ class AbstractConversationAgent(ABC):
|
|||
context: Context,
|
||||
conversation_id: str | None = None,
|
||||
language: str | None = None,
|
||||
) -> ConversationResult | None:
|
||||
) -> ConversationResult:
|
||||
"""Process a sentence."""
|
||||
|
||||
async def async_reload(self, language: str | None = None):
|
||||
|
|
|
@ -8,31 +8,40 @@ from dataclasses import dataclass
|
|||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any
|
||||
from typing import IO, Any
|
||||
|
||||
from hassil.intents import Intents, SlotList, TextSlotList
|
||||
from hassil.intents import Intents, ResponseType, SlotList, TextSlotList
|
||||
from hassil.recognize import recognize
|
||||
from hassil.util import merge_dict
|
||||
from home_assistant_intents import get_intents
|
||||
import yaml
|
||||
|
||||
from homeassistant import core, setup
|
||||
from homeassistant.helpers import area_registry, entity_registry, intent
|
||||
from homeassistant.helpers import area_registry, entity_registry, intent, template
|
||||
from homeassistant.helpers.json import json_loads
|
||||
|
||||
from .agent import AbstractConversationAgent, ConversationResult
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that"
|
||||
|
||||
REGEX_TYPE = type(re.compile(""))
|
||||
|
||||
|
||||
def json_load(fp: IO[str]) -> dict[str, Any]:
|
||||
"""Wrap json_loads for get_intents."""
|
||||
return json_loads(fp.read())
|
||||
|
||||
|
||||
@dataclass
|
||||
class LanguageIntents:
|
||||
"""Loaded intents for a language."""
|
||||
|
||||
intents: Intents
|
||||
intents_dict: dict[str, Any]
|
||||
intent_responses: dict[str, Any]
|
||||
error_responses: dict[str, Any]
|
||||
loaded_components: set[str]
|
||||
|
||||
|
||||
|
@ -78,7 +87,7 @@ class DefaultAgent(AbstractConversationAgent):
|
|||
context: core.Context,
|
||||
conversation_id: str | None = None,
|
||||
language: str | None = None,
|
||||
) -> ConversationResult | None:
|
||||
) -> ConversationResult:
|
||||
"""Process a sentence."""
|
||||
language = language or self.hass.config.language
|
||||
lang_intents = self._lang_intents.get(language)
|
||||
|
@ -93,7 +102,12 @@ class DefaultAgent(AbstractConversationAgent):
|
|||
if lang_intents is None:
|
||||
# No intents loaded
|
||||
_LOGGER.warning("No intents were loaded for language: %s", language)
|
||||
return None
|
||||
return _make_error_result(
|
||||
language,
|
||||
intent.IntentResponseErrorCode.NO_INTENT_MATCH,
|
||||
_DEFAULT_ERROR_TEXT,
|
||||
conversation_id,
|
||||
)
|
||||
|
||||
slot_lists: dict[str, SlotList] = {
|
||||
"area": self._make_areas_list(),
|
||||
|
@ -102,17 +116,65 @@ class DefaultAgent(AbstractConversationAgent):
|
|||
|
||||
result = recognize(text, lang_intents.intents, slot_lists=slot_lists)
|
||||
if result is None:
|
||||
return None
|
||||
_LOGGER.debug("No intent was matched for '%s'", text)
|
||||
return _make_error_result(
|
||||
language,
|
||||
intent.IntentResponseErrorCode.NO_INTENT_MATCH,
|
||||
self._get_error_text(ResponseType.NO_INTENT, lang_intents),
|
||||
conversation_id,
|
||||
)
|
||||
|
||||
intent_response = await intent.async_handle(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
result.intent.name,
|
||||
{entity.name: {"value": entity.value} for entity in result.entities_list},
|
||||
text,
|
||||
context,
|
||||
language,
|
||||
)
|
||||
try:
|
||||
intent_response = await intent.async_handle(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
result.intent.name,
|
||||
{
|
||||
entity.name: {"value": entity.value}
|
||||
for entity in result.entities_list
|
||||
},
|
||||
text,
|
||||
context,
|
||||
language,
|
||||
)
|
||||
except intent.IntentHandleError:
|
||||
_LOGGER.exception("Intent handling error")
|
||||
return _make_error_result(
|
||||
language,
|
||||
intent.IntentResponseErrorCode.FAILED_TO_HANDLE,
|
||||
self._get_error_text(ResponseType.HANDLE_ERROR, lang_intents),
|
||||
conversation_id,
|
||||
)
|
||||
except intent.IntentUnexpectedError:
|
||||
_LOGGER.exception("Unexpected intent error")
|
||||
return _make_error_result(
|
||||
language,
|
||||
intent.IntentResponseErrorCode.UNKNOWN,
|
||||
self._get_error_text(ResponseType.HANDLE_ERROR, lang_intents),
|
||||
conversation_id,
|
||||
)
|
||||
|
||||
if (
|
||||
(not intent_response.speech)
|
||||
and (intent_response.intent is not None)
|
||||
and (response_key := result.response)
|
||||
):
|
||||
# Use response template, if available
|
||||
response_str = lang_intents.intent_responses.get(
|
||||
result.intent.name, {}
|
||||
).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()
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
return ConversationResult(
|
||||
response=intent_response, conversation_id=conversation_id
|
||||
|
@ -168,7 +230,9 @@ class DefaultAgent(AbstractConversationAgent):
|
|||
# Check for intents for this component with the target language.
|
||||
# Try en-US, en, etc.
|
||||
for language_variation in _get_language_variations(language):
|
||||
component_intents = get_intents(component, language_variation)
|
||||
component_intents = get_intents(
|
||||
component, language_variation, json_load=json_load
|
||||
)
|
||||
if component_intents:
|
||||
# Merge sentences into existing dictionary
|
||||
merge_dict(intents_dict, component_intents)
|
||||
|
@ -230,11 +294,24 @@ class DefaultAgent(AbstractConversationAgent):
|
|||
# components with sentences are often being loaded.
|
||||
intents = Intents.from_dict(intents_dict)
|
||||
|
||||
# Load responses
|
||||
responses_dict = intents_dict.get("responses", {})
|
||||
intent_responses = responses_dict.get("intents", {})
|
||||
error_responses = responses_dict.get("errors", {})
|
||||
|
||||
if lang_intents is None:
|
||||
lang_intents = LanguageIntents(intents, intents_dict, loaded_components)
|
||||
lang_intents = LanguageIntents(
|
||||
intents,
|
||||
intents_dict,
|
||||
intent_responses,
|
||||
error_responses,
|
||||
loaded_components,
|
||||
)
|
||||
self._lang_intents[language] = lang_intents
|
||||
else:
|
||||
lang_intents.intents = intents
|
||||
lang_intents.intent_responses = intent_responses
|
||||
lang_intents.error_responses = error_responses
|
||||
|
||||
return lang_intents
|
||||
|
||||
|
@ -256,6 +333,9 @@ class DefaultAgent(AbstractConversationAgent):
|
|||
registry = entity_registry.async_get(self.hass)
|
||||
names = []
|
||||
for state in states:
|
||||
domain = state.entity_id.split(".", maxsplit=1)[0]
|
||||
context = {"domain": domain}
|
||||
|
||||
entry = registry.async_get(state.entity_id)
|
||||
if entry is not None:
|
||||
if entry.entity_category:
|
||||
|
@ -264,9 +344,30 @@ class DefaultAgent(AbstractConversationAgent):
|
|||
|
||||
if entry.aliases:
|
||||
for alias in entry.aliases:
|
||||
names.append((alias, state.entity_id))
|
||||
names.append((alias, state.entity_id, context))
|
||||
|
||||
# Default name
|
||||
names.append((state.name, state.entity_id))
|
||||
names.append((state.name, state.entity_id, context))
|
||||
|
||||
return TextSlotList.from_tuples(names)
|
||||
|
||||
def _get_error_text(
|
||||
self, response_type: ResponseType, lang_intents: LanguageIntents
|
||||
) -> str:
|
||||
"""Get response error text by type."""
|
||||
response_key = response_type.value
|
||||
response_str = lang_intents.error_responses.get(response_key)
|
||||
return response_str or _DEFAULT_ERROR_TEXT
|
||||
|
||||
|
||||
def _make_error_result(
|
||||
language: str,
|
||||
error_code: intent.IntentResponseErrorCode,
|
||||
response_text: str,
|
||||
conversation_id: str | None = None,
|
||||
) -> ConversationResult:
|
||||
"""Create conversation result with error code and text."""
|
||||
response = intent.IntentResponse(language=language)
|
||||
response.async_set_error(error_code, response_text)
|
||||
|
||||
return ConversationResult(response, conversation_id)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"domain": "conversation",
|
||||
"name": "Conversation",
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"requirements": ["hassil==0.2.3", "home-assistant-intents==0.0.1"],
|
||||
"requirements": ["hassil==0.2.5", "home-assistant-intents==2022.1.23"],
|
||||
"dependencies": ["http"],
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"quality_scale": "internal",
|
||||
|
|
|
@ -131,7 +131,7 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent):
|
|||
context: Context,
|
||||
conversation_id: str | None = None,
|
||||
language: str | None = None,
|
||||
) -> conversation.ConversationResult | None:
|
||||
) -> conversation.ConversationResult:
|
||||
"""Process a sentence."""
|
||||
if self.session:
|
||||
session = self.session
|
||||
|
|
|
@ -31,21 +31,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||
|
||||
intent.async_register(
|
||||
hass,
|
||||
OnOffIntentHandler(
|
||||
intent.INTENT_TURN_ON, HA_DOMAIN, SERVICE_TURN_ON, "Turned {} on"
|
||||
),
|
||||
OnOffIntentHandler(intent.INTENT_TURN_ON, HA_DOMAIN, SERVICE_TURN_ON),
|
||||
)
|
||||
intent.async_register(
|
||||
hass,
|
||||
OnOffIntentHandler(
|
||||
intent.INTENT_TURN_OFF, HA_DOMAIN, SERVICE_TURN_OFF, "Turned {} off"
|
||||
),
|
||||
OnOffIntentHandler(intent.INTENT_TURN_OFF, HA_DOMAIN, SERVICE_TURN_OFF),
|
||||
)
|
||||
intent.async_register(
|
||||
hass,
|
||||
intent.ServiceIntentHandler(
|
||||
intent.INTENT_TOGGLE, HA_DOMAIN, SERVICE_TOGGLE, "Toggled {}"
|
||||
),
|
||||
intent.ServiceIntentHandler(intent.INTENT_TOGGLE, HA_DOMAIN, SERVICE_TOGGLE),
|
||||
)
|
||||
|
||||
return True
|
||||
|
|
|
@ -47,7 +47,6 @@ class SetIntentHandler(intent.IntentHandler):
|
|||
"""Handle the hass intent."""
|
||||
hass = intent_obj.hass
|
||||
service_data: dict[str, Any] = {}
|
||||
speech_parts: list[str] = []
|
||||
slots = self.async_validate_slots(intent_obj.slots)
|
||||
|
||||
name: str | None = slots.get("name", {}).get("value")
|
||||
|
@ -92,13 +91,9 @@ class SetIntentHandler(intent.IntentHandler):
|
|||
|
||||
if "color" in slots:
|
||||
service_data[ATTR_RGB_COLOR] = slots["color"]["value"]
|
||||
# Use original passed in value of the color because we don't have
|
||||
# human readable names for that internally.
|
||||
speech_parts.append(f"the color {intent_obj.slots['color']['value']}")
|
||||
|
||||
if "brightness" in slots:
|
||||
service_data[ATTR_BRIGHTNESS_PCT] = slots["brightness"]["value"]
|
||||
speech_parts.append(f"{slots['brightness']['value']}% brightness")
|
||||
|
||||
response = intent_obj.create_response()
|
||||
needs_brightness = ATTR_BRIGHTNESS_PCT in service_data
|
||||
|
@ -116,9 +111,6 @@ class SetIntentHandler(intent.IntentHandler):
|
|||
id=area.id,
|
||||
)
|
||||
)
|
||||
speech_name = area.name
|
||||
else:
|
||||
speech_name = states[0].name
|
||||
|
||||
for state in states:
|
||||
target = intent.IntentResponseTarget(
|
||||
|
@ -152,19 +144,4 @@ class SetIntentHandler(intent.IntentHandler):
|
|||
success_results=success_results, failed_results=failed_results
|
||||
)
|
||||
|
||||
if not speech_parts: # No attributes changed
|
||||
speech = f"Turned on {speech_name}"
|
||||
else:
|
||||
parts = [f"Changed {speech_name} to"]
|
||||
for index, part in enumerate(speech_parts):
|
||||
if index == 0:
|
||||
parts.append(f" {part}")
|
||||
elif index != len(speech_parts) - 1:
|
||||
parts.append(f", {part}")
|
||||
else:
|
||||
parts.append(f" and {part}")
|
||||
speech = "".join(parts)
|
||||
|
||||
response.async_set_speech(speech)
|
||||
|
||||
return response
|
||||
|
|
|
@ -286,7 +286,7 @@ class ServiceIntentHandler(IntentHandler):
|
|||
}
|
||||
|
||||
def __init__(
|
||||
self, intent_type: str, domain: str, service: str, speech: str
|
||||
self, intent_type: str, domain: str, service: str, speech: str | None = None
|
||||
) -> None:
|
||||
"""Create Service Intent Handler."""
|
||||
self.intent_type = intent_type
|
||||
|
@ -382,7 +382,9 @@ class ServiceIntentHandler(IntentHandler):
|
|||
response.async_set_results(
|
||||
success_results=success_results,
|
||||
)
|
||||
response.async_set_speech(self.speech.format(speech_name))
|
||||
|
||||
if self.speech is not None:
|
||||
response.async_set_speech(self.speech.format(speech_name))
|
||||
|
||||
return response
|
||||
|
||||
|
|
|
@ -21,10 +21,10 @@ cryptography==39.0.0
|
|||
dbus-fast==1.84.0
|
||||
fnvhash==0.1.0
|
||||
hass-nabucasa==0.61.0
|
||||
hassil==0.2.3
|
||||
hassil==0.2.5
|
||||
home-assistant-bluetooth==1.9.2
|
||||
home-assistant-frontend==20230110.0
|
||||
home-assistant-intents==0.0.1
|
||||
home-assistant-intents==2022.1.23
|
||||
httpx==0.23.2
|
||||
ifaddr==0.1.7
|
||||
janus==1.0.0
|
||||
|
|
|
@ -877,7 +877,7 @@ hass-nabucasa==0.61.0
|
|||
hass_splunk==0.1.1
|
||||
|
||||
# homeassistant.components.conversation
|
||||
hassil==0.2.3
|
||||
hassil==0.2.5
|
||||
|
||||
# homeassistant.components.tasmota
|
||||
hatasmota==0.6.3
|
||||
|
@ -913,7 +913,7 @@ holidays==0.18.0
|
|||
home-assistant-frontend==20230110.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==0.0.1
|
||||
home-assistant-intents==2022.1.23
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
|
|
|
@ -669,7 +669,7 @@ habitipy==0.2.0
|
|||
hass-nabucasa==0.61.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
hassil==0.2.3
|
||||
hassil==0.2.5
|
||||
|
||||
# homeassistant.components.tasmota
|
||||
hatasmota==0.6.3
|
||||
|
@ -696,7 +696,7 @@ holidays==0.18.0
|
|||
home-assistant-frontend==20230110.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==0.0.1
|
||||
home-assistant-intents==2022.1.23
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
|
|
|
@ -20,7 +20,7 @@ class MockAgent(conversation.AbstractConversationAgent):
|
|||
context: Context,
|
||||
conversation_id: str | None = None,
|
||||
language: str | None = None,
|
||||
) -> conversation.ConversationResult | None:
|
||||
) -> conversation.ConversationResult:
|
||||
"""Process some text."""
|
||||
self.calls.append((text, context, conversation_id, language))
|
||||
response = intent.IntentResponse(language=language)
|
||||
|
|
|
@ -5,8 +5,9 @@ from unittest.mock import ANY, patch
|
|||
import pytest
|
||||
|
||||
from homeassistant.components import conversation
|
||||
from homeassistant.core import DOMAIN as HASS_DOMAIN
|
||||
from homeassistant.helpers import intent
|
||||
from homeassistant.components.cover import SERVICE_OPEN_COVER
|
||||
from homeassistant.core import DOMAIN as HASS_DOMAIN, Context
|
||||
from homeassistant.helpers import entity_registry, intent
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import async_mock_service
|
||||
|
@ -37,10 +38,15 @@ async def test_http_processing_intent(
|
|||
hass, init_components, hass_client, hass_admin_user
|
||||
):
|
||||
"""Test processing intent via HTTP API."""
|
||||
hass.states.async_set("light.kitchen", "on")
|
||||
# Add an alias
|
||||
entities = entity_registry.async_get(hass)
|
||||
entities.async_get_or_create("light", "demo", "1234", suggested_object_id="kitchen")
|
||||
entities.async_update_entity("light.kitchen", aliases={"my cool light"})
|
||||
hass.states.async_set("light.kitchen", "off")
|
||||
|
||||
client = await hass_client()
|
||||
resp = await client.post(
|
||||
"/api/conversation/process", json={"text": "turn on kitchen"}
|
||||
"/api/conversation/process", json={"text": "turn on my cool light"}
|
||||
)
|
||||
|
||||
assert resp.status == HTTPStatus.OK
|
||||
|
@ -53,7 +59,7 @@ async def test_http_processing_intent(
|
|||
"speech": {
|
||||
"plain": {
|
||||
"extra_data": None,
|
||||
"speech": "Turned kitchen on",
|
||||
"speech": "Turned on my cool light",
|
||||
}
|
||||
},
|
||||
"language": hass.config.language,
|
||||
|
@ -120,7 +126,7 @@ async def test_http_api(hass, init_components, hass_client):
|
|||
assert data == {
|
||||
"response": {
|
||||
"card": {},
|
||||
"speech": {"plain": {"extra_data": None, "speech": "Turned kitchen on"}},
|
||||
"speech": {"plain": {"extra_data": None, "speech": "Turned on kitchen"}},
|
||||
"language": hass.config.language,
|
||||
"response_type": "action_done",
|
||||
"data": {
|
||||
|
@ -161,7 +167,7 @@ async def test_http_api_no_match(hass, init_components, hass_client):
|
|||
"card": {},
|
||||
"speech": {
|
||||
"plain": {
|
||||
"speech": "Sorry, I didn't understand that",
|
||||
"speech": "Sorry, I couldn't understand that",
|
||||
"extra_data": None,
|
||||
},
|
||||
},
|
||||
|
@ -178,11 +184,9 @@ async def test_http_api_handle_failure(hass, init_components, hass_client):
|
|||
|
||||
hass.states.async_set("light.kitchen", "off")
|
||||
|
||||
# Raise an "unexpected" error during intent handling
|
||||
# Raise an error during intent handling
|
||||
def async_handle_error(*args, **kwargs):
|
||||
raise intent.IntentUnexpectedError(
|
||||
"Unexpected error turning on the kitchen light"
|
||||
)
|
||||
raise intent.IntentHandleError()
|
||||
|
||||
with patch("homeassistant.helpers.intent.async_handle", new=async_handle_error):
|
||||
resp = await client.post(
|
||||
|
@ -199,7 +203,7 @@ async def test_http_api_handle_failure(hass, init_components, hass_client):
|
|||
"speech": {
|
||||
"plain": {
|
||||
"extra_data": None,
|
||||
"speech": "Unexpected error turning on the kitchen light",
|
||||
"speech": "An unexpected error occurred while handling the intent",
|
||||
}
|
||||
},
|
||||
"language": hass.config.language,
|
||||
|
@ -211,6 +215,43 @@ async def test_http_api_handle_failure(hass, init_components, hass_client):
|
|||
}
|
||||
|
||||
|
||||
async def test_http_api_unexpected_failure(hass, init_components, hass_client):
|
||||
"""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 == {
|
||||
"response": {
|
||||
"response_type": "error",
|
||||
"card": {},
|
||||
"speech": {
|
||||
"plain": {
|
||||
"extra_data": None,
|
||||
"speech": "An unexpected error occurred while handling the intent",
|
||||
}
|
||||
},
|
||||
"language": hass.config.language,
|
||||
"data": {
|
||||
"code": "unknown",
|
||||
},
|
||||
},
|
||||
"conversation_id": None,
|
||||
}
|
||||
|
||||
|
||||
async def test_http_api_wrong_data(hass, init_components, hass_client):
|
||||
"""Test the HTTP conversation API."""
|
||||
client = await hass_client()
|
||||
|
@ -302,7 +343,7 @@ async def test_ws_api(hass, hass_ws_client, payload):
|
|||
"speech": {
|
||||
"plain": {
|
||||
"extra_data": None,
|
||||
"speech": "Sorry, I didn't understand that",
|
||||
"speech": "Sorry, I couldn't understand that",
|
||||
}
|
||||
},
|
||||
"language": payload.get("language", hass.config.language),
|
||||
|
@ -484,3 +525,40 @@ async def test_language_region(hass, init_components):
|
|||
assert call.domain == HASS_DOMAIN
|
||||
assert call.service == "turn_on"
|
||||
assert call.data == {"entity_id": "light.kitchen"}
|
||||
|
||||
|
||||
async def test_reload_on_new_component(hass):
|
||||
"""Test intents being reloaded when a new component is loaded."""
|
||||
language = hass.config.language
|
||||
assert await async_setup_component(hass, "conversation", {})
|
||||
|
||||
# Load intents
|
||||
agent = await conversation._get_agent(hass)
|
||||
assert isinstance(agent, conversation.DefaultAgent)
|
||||
await agent.async_prepare()
|
||||
|
||||
lang_intents = agent._lang_intents.get(language)
|
||||
assert lang_intents is not None
|
||||
loaded_components = set(lang_intents.loaded_components)
|
||||
|
||||
# Load another component
|
||||
assert await async_setup_component(hass, "light", {})
|
||||
|
||||
# Intents should reload
|
||||
await agent.async_prepare()
|
||||
lang_intents = agent._lang_intents.get(language)
|
||||
assert lang_intents is not None
|
||||
|
||||
assert {"light"} == (lang_intents.loaded_components - loaded_components)
|
||||
|
||||
|
||||
async def test_non_default_response(hass, init_components):
|
||||
"""Test intent response that is not the default."""
|
||||
hass.states.async_set("cover.front_door", "closed")
|
||||
async_mock_service(hass, "cover", SERVICE_OPEN_COVER)
|
||||
|
||||
agent = await conversation._get_agent(hass)
|
||||
assert isinstance(agent, conversation.DefaultAgent)
|
||||
|
||||
result = await agent.async_process("open the front door", Context())
|
||||
assert result.response.speech["plain"]["speech"] == "Opened front door"
|
||||
|
|
|
@ -96,12 +96,11 @@ async def test_turn_on_intent(hass):
|
|||
hass.states.async_set("light.test_light", "off")
|
||||
calls = async_mock_service(hass, "light", SERVICE_TURN_ON)
|
||||
|
||||
response = await intent.async_handle(
|
||||
await intent.async_handle(
|
||||
hass, "test", "HassTurnOn", {"name": {"value": "test light"}}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert response.speech["plain"]["speech"] == "Turned test light on"
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
assert call.domain == "light"
|
||||
|
@ -118,12 +117,11 @@ async def test_turn_off_intent(hass):
|
|||
hass.states.async_set("light.test_light", "on")
|
||||
calls = async_mock_service(hass, "light", SERVICE_TURN_OFF)
|
||||
|
||||
response = await intent.async_handle(
|
||||
await intent.async_handle(
|
||||
hass, "test", "HassTurnOff", {"name": {"value": "test light"}}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert response.speech["plain"]["speech"] == "Turned test light off"
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
assert call.domain == "light"
|
||||
|
@ -140,12 +138,11 @@ async def test_toggle_intent(hass):
|
|||
hass.states.async_set("light.test_light", "off")
|
||||
calls = async_mock_service(hass, "light", SERVICE_TOGGLE)
|
||||
|
||||
response = await intent.async_handle(
|
||||
await intent.async_handle(
|
||||
hass, "test", "HassToggle", {"name": {"value": "test light"}}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert response.speech["plain"]["speech"] == "Toggled test light"
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
assert call.domain == "light"
|
||||
|
@ -167,12 +164,11 @@ async def test_turn_on_multiple_intent(hass):
|
|||
hass.states.async_set("light.test_lighter", "off")
|
||||
calls = async_mock_service(hass, "light", SERVICE_TURN_ON)
|
||||
|
||||
response = await intent.async_handle(
|
||||
await intent.async_handle(
|
||||
hass, "test", "HassTurnOn", {"name": {"value": "test lights 2"}}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert response.speech["plain"]["speech"] == "Turned test lights 2 on"
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
assert call.domain == "light"
|
||||
|
|
|
@ -16,7 +16,7 @@ async def test_intent_set_color(hass):
|
|||
calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON)
|
||||
await intent.async_setup_intents(hass)
|
||||
|
||||
result = await async_handle(
|
||||
await async_handle(
|
||||
hass,
|
||||
"test",
|
||||
intent.INTENT_SET,
|
||||
|
@ -24,8 +24,6 @@ async def test_intent_set_color(hass):
|
|||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result.speech["plain"]["speech"] == "Changed hello 2 to the color blue"
|
||||
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
assert call.domain == light.DOMAIN
|
||||
|
@ -62,7 +60,7 @@ async def test_intent_set_color_and_brightness(hass):
|
|||
calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON)
|
||||
await intent.async_setup_intents(hass)
|
||||
|
||||
result = await async_handle(
|
||||
await async_handle(
|
||||
hass,
|
||||
"test",
|
||||
intent.INTENT_SET,
|
||||
|
@ -74,11 +72,6 @@ async def test_intent_set_color_and_brightness(hass):
|
|||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
result.speech["plain"]["speech"]
|
||||
== "Changed hello 2 to the color blue and 20% brightness"
|
||||
)
|
||||
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
assert call.domain == light.DOMAIN
|
||||
|
|
Loading…
Add table
Reference in a new issue