Bump intents and add tests for new error messages (#118317)

* Add new error keys

* Bump intents and test new error messages

* Fix response text
This commit is contained in:
Michael Hansen 2024-05-28 11:24:24 -05:00 committed by GitHub
parent 05fc7cfbde
commit 106cb4cfb7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 166 additions and 8 deletions

View File

@ -358,7 +358,7 @@ class DefaultAgent(ConversationEntity):
except intent.MatchFailedError as match_error: except intent.MatchFailedError as match_error:
# Intent was valid, but no entities matched the constraints. # Intent was valid, but no entities matched the constraints.
error_response_type, error_response_args = _get_match_error_response( error_response_type, error_response_args = _get_match_error_response(
match_error self.hass, match_error
) )
return _make_error_result( return _make_error_result(
language, language,
@ -1037,6 +1037,7 @@ def _get_unmatched_response(result: RecognizeResult) -> tuple[ErrorKey, dict[str
def _get_match_error_response( def _get_match_error_response(
hass: core.HomeAssistant,
match_error: intent.MatchFailedError, match_error: intent.MatchFailedError,
) -> tuple[ErrorKey, dict[str, Any]]: ) -> tuple[ErrorKey, dict[str, Any]]:
"""Return key and template arguments for error when target matching fails.""" """Return key and template arguments for error when target matching fails."""
@ -1103,6 +1104,23 @@ def _get_match_error_response(
# Invalid floor name # Invalid floor name
return ErrorKey.NO_FLOOR, {"floor": result.no_match_name} return ErrorKey.NO_FLOOR, {"floor": result.no_match_name}
if reason == intent.MatchFailedReason.FEATURE:
# Feature not supported by entity
return ErrorKey.FEATURE_NOT_SUPPORTED, {}
if reason == intent.MatchFailedReason.STATE:
# Entity is not in correct state
assert match_error.constraints.states
state = next(iter(match_error.constraints.states))
if match_error.constraints.domains:
# Translate if domain is available
domain = next(iter(match_error.constraints.domains))
state = translation.async_translate_state(
hass, state, domain, None, None, None
)
return ErrorKey.ENTITY_WRONG_STATE, {"state": state}
# Default error # Default error
return ErrorKey.NO_INTENT, {} return ErrorKey.NO_INTENT, {}

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation", "documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system", "integration_type": "system",
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["hassil==1.7.1", "home-assistant-intents==2024.4.24"] "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.5.28"]
} }

View File

@ -33,7 +33,7 @@ hass-nabucasa==0.81.0
hassil==1.7.1 hassil==1.7.1
home-assistant-bluetooth==1.12.0 home-assistant-bluetooth==1.12.0
home-assistant-frontend==20240501.1 home-assistant-frontend==20240501.1
home-assistant-intents==2024.4.24 home-assistant-intents==2024.5.28
httpx==0.27.0 httpx==0.27.0
ifaddr==0.2.0 ifaddr==0.2.0
Jinja2==3.1.4 Jinja2==3.1.4

View File

@ -1090,7 +1090,7 @@ holidays==0.49
home-assistant-frontend==20240501.1 home-assistant-frontend==20240501.1
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2024.4.24 home-assistant-intents==2024.5.28
# homeassistant.components.home_connect # homeassistant.components.home_connect
homeconnect==0.7.2 homeconnect==0.7.2

View File

@ -892,7 +892,7 @@ holidays==0.49
home-assistant-frontend==20240501.1 home-assistant-frontend==20240501.1
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2024.4.24 home-assistant-intents==2024.5.28
# homeassistant.components.home_connect # homeassistant.components.home_connect
homeconnect==0.7.2 homeconnect==0.7.2

View File

@ -6,13 +6,18 @@ from unittest.mock import AsyncMock, patch
from hassil.recognize import Intent, IntentData, MatchEntity, RecognizeResult from hassil.recognize import Intent, IntentData, MatchEntity, RecognizeResult
import pytest import pytest
from homeassistant.components import conversation, cover from homeassistant.components import conversation, cover, media_player
from homeassistant.components.conversation import default_agent from homeassistant.components.conversation import default_agent
from homeassistant.components.homeassistant.exposed_entities import ( from homeassistant.components.homeassistant.exposed_entities import (
async_get_assistant_settings, async_get_assistant_settings,
) )
from homeassistant.components.intent import (
TimerEventType,
TimerInfo,
async_register_timer_handler,
)
from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, STATE_CLOSED from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, STATE_CLOSED
from homeassistant.core import DOMAIN as HASS_DOMAIN, Context, HomeAssistant from homeassistant.core import DOMAIN as HASS_DOMAIN, Context, HomeAssistant, callback
from homeassistant.helpers import ( from homeassistant.helpers import (
area_registry as ar, area_registry as ar,
device_registry as dr, device_registry as dr,
@ -792,6 +797,141 @@ async def test_error_duplicate_names_in_area(
) )
async def test_error_wrong_state(hass: HomeAssistant, init_components) -> None:
"""Test error message when no entities are in the correct state."""
assert await async_setup_component(hass, media_player.DOMAIN, {})
hass.states.async_set(
"media_player.test_player",
media_player.STATE_IDLE,
{ATTR_FRIENDLY_NAME: "test player"},
)
expose_entity(hass, "media_player.test_player", True)
result = await conversation.async_converse(
hass, "pause test player", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert result.response.speech["plain"]["speech"] == "Sorry, no device is playing"
async def test_error_feature_not_supported(
hass: HomeAssistant, init_components
) -> None:
"""Test error message when no devices support a required feature."""
assert await async_setup_component(hass, media_player.DOMAIN, {})
hass.states.async_set(
"media_player.test_player",
media_player.STATE_PLAYING,
{ATTR_FRIENDLY_NAME: "test player"},
# missing VOLUME_SET feature
)
expose_entity(hass, "media_player.test_player", True)
result = await conversation.async_converse(
hass, "set test player volume to 100%", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
== "Sorry, no device supports the required features"
)
async def test_error_no_timer_support(hass: HomeAssistant, init_components) -> None:
"""Test error message when a device does not support timers (no handler is registered)."""
device_id = "test_device"
# No timer handler is registered for the device
result = await conversation.async_converse(
hass, "pause timer", None, Context(), None, device_id=device_id
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.FAILED_TO_HANDLE
assert (
result.response.speech["plain"]["speech"]
== "Sorry, timers are not supported on this device"
)
async def test_error_timer_not_found(hass: HomeAssistant, init_components) -> None:
"""Test error message when a timer cannot be matched."""
device_id = "test_device"
@callback
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
pass
# Register a handler so the device "supports" timers
async_register_timer_handler(hass, device_id, handle_timer)
result = await conversation.async_converse(
hass, "pause timer", None, Context(), None, device_id=device_id
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.FAILED_TO_HANDLE
assert (
result.response.speech["plain"]["speech"] == "Sorry, I couldn't find that timer"
)
async def test_error_multiple_timers_matched(
hass: HomeAssistant,
init_components,
area_registry: ar.AreaRegistry,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test error message when an intent would target multiple timers."""
area_kitchen = area_registry.async_create("kitchen")
# Starting a timer requires a device in an area
entry = MockConfigEntry()
entry.add_to_hass(hass)
device_kitchen = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections=set(),
identifiers={("demo", "device-kitchen")},
)
device_registry.async_update_device(device_kitchen.id, area_id=area_kitchen.id)
device_id = device_kitchen.id
@callback
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
pass
# Register a handler so the device "supports" timers
async_register_timer_handler(hass, device_id, handle_timer)
# Create two identical timers from the same device
result = await conversation.async_converse(
hass, "set a timer for 5 minutes", None, Context(), None, device_id=device_id
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
result = await conversation.async_converse(
hass, "set a timer for 5 minutes", None, Context(), None, device_id=device_id
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
# Cannot target multiple timers
result = await conversation.async_converse(
hass, "cancel timer", None, Context(), None, device_id=device_id
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.FAILED_TO_HANDLE
assert (
result.response.speech["plain"]["speech"]
== "Sorry, I am unable to target multiple timers"
)
async def test_no_states_matched_default_error( async def test_no_states_matched_default_error(
hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry
) -> None: ) -> None:

View File

@ -234,7 +234,7 @@ async def test_media_player_intents(
response = result.response response = result.response
assert response.response_type == intent.IntentResponseType.ACTION_DONE assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.speech["plain"]["speech"] == "Unpaused" assert response.speech["plain"]["speech"] == "Resumed"
assert len(calls) == 1 assert len(calls) == 1
call = calls[0] call = calls[0]
assert call.data == {"entity_id": entity_id} assert call.data == {"entity_id": entity_id}