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:
Michael Hansen 2023-01-23 21:38:41 -06:00 committed by GitHub
parent 6d811d3fdb
commit ea95abcb30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 236 additions and 134 deletions

View File

@ -294,7 +294,7 @@ class AlmondAgent(conversation.AbstractConversationAgent):
context: Context, context: Context,
conversation_id: str | None = None, conversation_id: str | None = None,
language: str | None = None, language: str | None = None,
) -> conversation.ConversationResult | None: ) -> conversation.ConversationResult:
"""Process a sentence.""" """Process a sentence."""
response = await self.api.async_converse_text(text, conversation_id) response = await self.api.async_converse_text(text, conversation_id)
language = language or self.hass.config.language language = language or self.hass.config.language

View File

@ -242,44 +242,5 @@ async def async_converse(
if language is None: if language is None:
language = hass.config.language 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) 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
)
return result return result

View File

@ -47,7 +47,7 @@ class AbstractConversationAgent(ABC):
context: Context, context: Context,
conversation_id: str | None = None, conversation_id: str | None = None,
language: str | None = None, language: str | None = None,
) -> ConversationResult | None: ) -> ConversationResult:
"""Process a sentence.""" """Process a sentence."""
async def async_reload(self, language: str | None = None): async def async_reload(self, language: str | None = None):

View File

@ -8,31 +8,40 @@ from dataclasses import dataclass
import logging import logging
from pathlib import Path from pathlib import Path
import re 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.recognize import recognize
from hassil.util import merge_dict from hassil.util import merge_dict
from home_assistant_intents import get_intents from home_assistant_intents import get_intents
import yaml import yaml
from homeassistant import core, setup 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 .agent import AbstractConversationAgent, ConversationResult
from .const import DOMAIN from .const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that"
REGEX_TYPE = type(re.compile("")) 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 @dataclass
class LanguageIntents: class LanguageIntents:
"""Loaded intents for a language.""" """Loaded intents for a language."""
intents: Intents intents: Intents
intents_dict: dict[str, Any] intents_dict: dict[str, Any]
intent_responses: dict[str, Any]
error_responses: dict[str, Any]
loaded_components: set[str] loaded_components: set[str]
@ -78,7 +87,7 @@ class DefaultAgent(AbstractConversationAgent):
context: core.Context, context: core.Context,
conversation_id: str | None = None, conversation_id: str | None = None,
language: str | None = None, language: str | None = None,
) -> ConversationResult | None: ) -> ConversationResult:
"""Process a sentence.""" """Process a sentence."""
language = language or self.hass.config.language language = language or self.hass.config.language
lang_intents = self._lang_intents.get(language) lang_intents = self._lang_intents.get(language)
@ -93,7 +102,12 @@ class DefaultAgent(AbstractConversationAgent):
if lang_intents is None: if lang_intents is None:
# No intents loaded # No intents loaded
_LOGGER.warning("No intents were loaded for language: %s", language) _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] = { slot_lists: dict[str, SlotList] = {
"area": self._make_areas_list(), "area": self._make_areas_list(),
@ -102,17 +116,65 @@ class DefaultAgent(AbstractConversationAgent):
result = recognize(text, lang_intents.intents, slot_lists=slot_lists) result = recognize(text, lang_intents.intents, slot_lists=slot_lists)
if result is None: 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,
)
try:
intent_response = await intent.async_handle( intent_response = await intent.async_handle(
self.hass, self.hass,
DOMAIN, DOMAIN,
result.intent.name, result.intent.name,
{entity.name: {"value": entity.value} for entity in result.entities_list}, {
entity.name: {"value": entity.value}
for entity in result.entities_list
},
text, text,
context, context,
language, 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( return ConversationResult(
response=intent_response, conversation_id=conversation_id response=intent_response, conversation_id=conversation_id
@ -168,7 +230,9 @@ class DefaultAgent(AbstractConversationAgent):
# Check for intents for this component with the target language. # Check for intents for this component with the target language.
# Try en-US, en, etc. # Try en-US, en, etc.
for language_variation in _get_language_variations(language): 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: if component_intents:
# Merge sentences into existing dictionary # Merge sentences into existing dictionary
merge_dict(intents_dict, component_intents) merge_dict(intents_dict, component_intents)
@ -230,11 +294,24 @@ class DefaultAgent(AbstractConversationAgent):
# components with sentences are often being loaded. # components with sentences are often being loaded.
intents = Intents.from_dict(intents_dict) 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: 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 self._lang_intents[language] = lang_intents
else: else:
lang_intents.intents = intents lang_intents.intents = intents
lang_intents.intent_responses = intent_responses
lang_intents.error_responses = error_responses
return lang_intents return lang_intents
@ -256,6 +333,9 @@ class DefaultAgent(AbstractConversationAgent):
registry = entity_registry.async_get(self.hass) registry = entity_registry.async_get(self.hass)
names = [] names = []
for state in states: for state in states:
domain = state.entity_id.split(".", maxsplit=1)[0]
context = {"domain": domain}
entry = registry.async_get(state.entity_id) entry = registry.async_get(state.entity_id)
if entry is not None: if entry is not None:
if entry.entity_category: if entry.entity_category:
@ -264,9 +344,30 @@ class DefaultAgent(AbstractConversationAgent):
if entry.aliases: if entry.aliases:
for alias in entry.aliases: for alias in entry.aliases:
names.append((alias, state.entity_id)) names.append((alias, state.entity_id, context))
# Default name # Default name
names.append((state.name, state.entity_id)) names.append((state.name, state.entity_id, context))
return TextSlotList.from_tuples(names) 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)

View File

@ -2,7 +2,7 @@
"domain": "conversation", "domain": "conversation",
"name": "Conversation", "name": "Conversation",
"documentation": "https://www.home-assistant.io/integrations/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"], "dependencies": ["http"],
"codeowners": ["@home-assistant/core"], "codeowners": ["@home-assistant/core"],
"quality_scale": "internal", "quality_scale": "internal",

View File

@ -131,7 +131,7 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent):
context: Context, context: Context,
conversation_id: str | None = None, conversation_id: str | None = None,
language: str | None = None, language: str | None = None,
) -> conversation.ConversationResult | None: ) -> conversation.ConversationResult:
"""Process a sentence.""" """Process a sentence."""
if self.session: if self.session:
session = self.session session = self.session

View File

@ -31,21 +31,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
intent.async_register( intent.async_register(
hass, hass,
OnOffIntentHandler( OnOffIntentHandler(intent.INTENT_TURN_ON, HA_DOMAIN, SERVICE_TURN_ON),
intent.INTENT_TURN_ON, HA_DOMAIN, SERVICE_TURN_ON, "Turned {} on"
),
) )
intent.async_register( intent.async_register(
hass, hass,
OnOffIntentHandler( OnOffIntentHandler(intent.INTENT_TURN_OFF, HA_DOMAIN, SERVICE_TURN_OFF),
intent.INTENT_TURN_OFF, HA_DOMAIN, SERVICE_TURN_OFF, "Turned {} off"
),
) )
intent.async_register( intent.async_register(
hass, hass,
intent.ServiceIntentHandler( intent.ServiceIntentHandler(intent.INTENT_TOGGLE, HA_DOMAIN, SERVICE_TOGGLE),
intent.INTENT_TOGGLE, HA_DOMAIN, SERVICE_TOGGLE, "Toggled {}"
),
) )
return True return True

View File

@ -47,7 +47,6 @@ class SetIntentHandler(intent.IntentHandler):
"""Handle the hass intent.""" """Handle the hass intent."""
hass = intent_obj.hass hass = intent_obj.hass
service_data: dict[str, Any] = {} service_data: dict[str, Any] = {}
speech_parts: list[str] = []
slots = self.async_validate_slots(intent_obj.slots) slots = self.async_validate_slots(intent_obj.slots)
name: str | None = slots.get("name", {}).get("value") name: str | None = slots.get("name", {}).get("value")
@ -92,13 +91,9 @@ class SetIntentHandler(intent.IntentHandler):
if "color" in slots: if "color" in slots:
service_data[ATTR_RGB_COLOR] = slots["color"]["value"] 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: if "brightness" in slots:
service_data[ATTR_BRIGHTNESS_PCT] = slots["brightness"]["value"] service_data[ATTR_BRIGHTNESS_PCT] = slots["brightness"]["value"]
speech_parts.append(f"{slots['brightness']['value']}% brightness")
response = intent_obj.create_response() response = intent_obj.create_response()
needs_brightness = ATTR_BRIGHTNESS_PCT in service_data needs_brightness = ATTR_BRIGHTNESS_PCT in service_data
@ -116,9 +111,6 @@ class SetIntentHandler(intent.IntentHandler):
id=area.id, id=area.id,
) )
) )
speech_name = area.name
else:
speech_name = states[0].name
for state in states: for state in states:
target = intent.IntentResponseTarget( target = intent.IntentResponseTarget(
@ -152,19 +144,4 @@ class SetIntentHandler(intent.IntentHandler):
success_results=success_results, failed_results=failed_results 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 return response

View File

@ -286,7 +286,7 @@ class ServiceIntentHandler(IntentHandler):
} }
def __init__( 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: ) -> None:
"""Create Service Intent Handler.""" """Create Service Intent Handler."""
self.intent_type = intent_type self.intent_type = intent_type
@ -382,6 +382,8 @@ class ServiceIntentHandler(IntentHandler):
response.async_set_results( response.async_set_results(
success_results=success_results, success_results=success_results,
) )
if self.speech is not None:
response.async_set_speech(self.speech.format(speech_name)) response.async_set_speech(self.speech.format(speech_name))
return response return response

View File

@ -21,10 +21,10 @@ cryptography==39.0.0
dbus-fast==1.84.0 dbus-fast==1.84.0
fnvhash==0.1.0 fnvhash==0.1.0
hass-nabucasa==0.61.0 hass-nabucasa==0.61.0
hassil==0.2.3 hassil==0.2.5
home-assistant-bluetooth==1.9.2 home-assistant-bluetooth==1.9.2
home-assistant-frontend==20230110.0 home-assistant-frontend==20230110.0
home-assistant-intents==0.0.1 home-assistant-intents==2022.1.23
httpx==0.23.2 httpx==0.23.2
ifaddr==0.1.7 ifaddr==0.1.7
janus==1.0.0 janus==1.0.0

View File

@ -877,7 +877,7 @@ hass-nabucasa==0.61.0
hass_splunk==0.1.1 hass_splunk==0.1.1
# homeassistant.components.conversation # homeassistant.components.conversation
hassil==0.2.3 hassil==0.2.5
# homeassistant.components.tasmota # homeassistant.components.tasmota
hatasmota==0.6.3 hatasmota==0.6.3
@ -913,7 +913,7 @@ holidays==0.18.0
home-assistant-frontend==20230110.0 home-assistant-frontend==20230110.0
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==0.0.1 home-assistant-intents==2022.1.23
# homeassistant.components.home_connect # homeassistant.components.home_connect
homeconnect==0.7.2 homeconnect==0.7.2

View File

@ -669,7 +669,7 @@ habitipy==0.2.0
hass-nabucasa==0.61.0 hass-nabucasa==0.61.0
# homeassistant.components.conversation # homeassistant.components.conversation
hassil==0.2.3 hassil==0.2.5
# homeassistant.components.tasmota # homeassistant.components.tasmota
hatasmota==0.6.3 hatasmota==0.6.3
@ -696,7 +696,7 @@ holidays==0.18.0
home-assistant-frontend==20230110.0 home-assistant-frontend==20230110.0
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==0.0.1 home-assistant-intents==2022.1.23
# homeassistant.components.home_connect # homeassistant.components.home_connect
homeconnect==0.7.2 homeconnect==0.7.2

View File

@ -20,7 +20,7 @@ class MockAgent(conversation.AbstractConversationAgent):
context: Context, context: Context,
conversation_id: str | None = None, conversation_id: str | None = None,
language: str | None = None, language: str | None = None,
) -> conversation.ConversationResult | None: ) -> conversation.ConversationResult:
"""Process some text.""" """Process some text."""
self.calls.append((text, context, conversation_id, language)) self.calls.append((text, context, conversation_id, language))
response = intent.IntentResponse(language=language) response = intent.IntentResponse(language=language)

View File

@ -5,8 +5,9 @@ from unittest.mock import ANY, patch
import pytest import pytest
from homeassistant.components import conversation from homeassistant.components import conversation
from homeassistant.core import DOMAIN as HASS_DOMAIN from homeassistant.components.cover import SERVICE_OPEN_COVER
from homeassistant.helpers import intent from homeassistant.core import DOMAIN as HASS_DOMAIN, Context
from homeassistant.helpers import entity_registry, intent
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import async_mock_service 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 hass, init_components, hass_client, hass_admin_user
): ):
"""Test processing intent via HTTP API.""" """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() client = await hass_client()
resp = await client.post( 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 assert resp.status == HTTPStatus.OK
@ -53,7 +59,7 @@ async def test_http_processing_intent(
"speech": { "speech": {
"plain": { "plain": {
"extra_data": None, "extra_data": None,
"speech": "Turned kitchen on", "speech": "Turned on my cool light",
} }
}, },
"language": hass.config.language, "language": hass.config.language,
@ -120,7 +126,7 @@ async def test_http_api(hass, init_components, hass_client):
assert data == { assert data == {
"response": { "response": {
"card": {}, "card": {},
"speech": {"plain": {"extra_data": None, "speech": "Turned kitchen on"}}, "speech": {"plain": {"extra_data": None, "speech": "Turned on kitchen"}},
"language": hass.config.language, "language": hass.config.language,
"response_type": "action_done", "response_type": "action_done",
"data": { "data": {
@ -161,7 +167,7 @@ async def test_http_api_no_match(hass, init_components, hass_client):
"card": {}, "card": {},
"speech": { "speech": {
"plain": { "plain": {
"speech": "Sorry, I didn't understand that", "speech": "Sorry, I couldn't understand that",
"extra_data": None, "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") 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): def async_handle_error(*args, **kwargs):
raise intent.IntentUnexpectedError( raise intent.IntentHandleError()
"Unexpected error turning on the kitchen light"
)
with patch("homeassistant.helpers.intent.async_handle", new=async_handle_error): with patch("homeassistant.helpers.intent.async_handle", new=async_handle_error):
resp = await client.post( resp = await client.post(
@ -199,7 +203,7 @@ async def test_http_api_handle_failure(hass, init_components, hass_client):
"speech": { "speech": {
"plain": { "plain": {
"extra_data": None, "extra_data": None,
"speech": "Unexpected error turning on the kitchen light", "speech": "An unexpected error occurred while handling the intent",
} }
}, },
"language": hass.config.language, "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): async def test_http_api_wrong_data(hass, init_components, hass_client):
"""Test the HTTP conversation API.""" """Test the HTTP conversation API."""
client = await hass_client() client = await hass_client()
@ -302,7 +343,7 @@ async def test_ws_api(hass, hass_ws_client, payload):
"speech": { "speech": {
"plain": { "plain": {
"extra_data": None, "extra_data": None,
"speech": "Sorry, I didn't understand that", "speech": "Sorry, I couldn't understand that",
} }
}, },
"language": payload.get("language", hass.config.language), "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.domain == HASS_DOMAIN
assert call.service == "turn_on" assert call.service == "turn_on"
assert call.data == {"entity_id": "light.kitchen"} 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"

View File

@ -96,12 +96,11 @@ async def test_turn_on_intent(hass):
hass.states.async_set("light.test_light", "off") hass.states.async_set("light.test_light", "off")
calls = async_mock_service(hass, "light", SERVICE_TURN_ON) 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"}} hass, "test", "HassTurnOn", {"name": {"value": "test light"}}
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert response.speech["plain"]["speech"] == "Turned test light on"
assert len(calls) == 1 assert len(calls) == 1
call = calls[0] call = calls[0]
assert call.domain == "light" assert call.domain == "light"
@ -118,12 +117,11 @@ async def test_turn_off_intent(hass):
hass.states.async_set("light.test_light", "on") hass.states.async_set("light.test_light", "on")
calls = async_mock_service(hass, "light", SERVICE_TURN_OFF) 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"}} hass, "test", "HassTurnOff", {"name": {"value": "test light"}}
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert response.speech["plain"]["speech"] == "Turned test light off"
assert len(calls) == 1 assert len(calls) == 1
call = calls[0] call = calls[0]
assert call.domain == "light" assert call.domain == "light"
@ -140,12 +138,11 @@ async def test_toggle_intent(hass):
hass.states.async_set("light.test_light", "off") hass.states.async_set("light.test_light", "off")
calls = async_mock_service(hass, "light", SERVICE_TOGGLE) calls = async_mock_service(hass, "light", SERVICE_TOGGLE)
response = await intent.async_handle( await intent.async_handle(
hass, "test", "HassToggle", {"name": {"value": "test light"}} hass, "test", "HassToggle", {"name": {"value": "test light"}}
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert response.speech["plain"]["speech"] == "Toggled test light"
assert len(calls) == 1 assert len(calls) == 1
call = calls[0] call = calls[0]
assert call.domain == "light" assert call.domain == "light"
@ -167,12 +164,11 @@ async def test_turn_on_multiple_intent(hass):
hass.states.async_set("light.test_lighter", "off") hass.states.async_set("light.test_lighter", "off")
calls = async_mock_service(hass, "light", SERVICE_TURN_ON) 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"}} hass, "test", "HassTurnOn", {"name": {"value": "test lights 2"}}
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert response.speech["plain"]["speech"] == "Turned test lights 2 on"
assert len(calls) == 1 assert len(calls) == 1
call = calls[0] call = calls[0]
assert call.domain == "light" assert call.domain == "light"

View File

@ -16,7 +16,7 @@ async def test_intent_set_color(hass):
calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON)
await intent.async_setup_intents(hass) await intent.async_setup_intents(hass)
result = await async_handle( await async_handle(
hass, hass,
"test", "test",
intent.INTENT_SET, intent.INTENT_SET,
@ -24,8 +24,6 @@ async def test_intent_set_color(hass):
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert result.speech["plain"]["speech"] == "Changed hello 2 to the color blue"
assert len(calls) == 1 assert len(calls) == 1
call = calls[0] call = calls[0]
assert call.domain == light.DOMAIN 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) calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON)
await intent.async_setup_intents(hass) await intent.async_setup_intents(hass)
result = await async_handle( await async_handle(
hass, hass,
"test", "test",
intent.INTENT_SET, intent.INTENT_SET,
@ -74,11 +72,6 @@ async def test_intent_set_color_and_brightness(hass):
) )
await hass.async_block_till_done() 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 assert len(calls) == 1
call = calls[0] call = calls[0]
assert call.domain == light.DOMAIN assert call.domain == light.DOMAIN