mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 02:07:09 +00:00
Specific Assist errors for domain/device class (#107302)
* Specific errors for domain/device class * Don't log exception * Check device class first * Refactor guard clauses * Test default error
This commit is contained in:
parent
20610645fb
commit
4bb2a3ad92
@ -269,7 +269,22 @@ class DefaultAgent(AbstractConversationAgent):
|
|||||||
language,
|
language,
|
||||||
assistant=DOMAIN,
|
assistant=DOMAIN,
|
||||||
)
|
)
|
||||||
|
except intent.NoStatesMatchedError as no_states_error:
|
||||||
|
# Intent was valid, but no entities matched the constraints.
|
||||||
|
error_response_type, error_response_args = _get_no_states_matched_response(
|
||||||
|
no_states_error
|
||||||
|
)
|
||||||
|
return _make_error_result(
|
||||||
|
language,
|
||||||
|
intent.IntentResponseErrorCode.NO_VALID_TARGETS,
|
||||||
|
self._get_error_text(
|
||||||
|
error_response_type, lang_intents, **error_response_args
|
||||||
|
),
|
||||||
|
conversation_id,
|
||||||
|
)
|
||||||
except intent.IntentHandleError:
|
except intent.IntentHandleError:
|
||||||
|
# Intent was valid and entities matched constraints, but an error
|
||||||
|
# occurred during handling.
|
||||||
_LOGGER.exception("Intent handling error")
|
_LOGGER.exception("Intent handling error")
|
||||||
return _make_error_result(
|
return _make_error_result(
|
||||||
language,
|
language,
|
||||||
@ -863,6 +878,40 @@ def _get_unmatched_response(
|
|||||||
return error_response_type, error_response_args
|
return error_response_type, error_response_args
|
||||||
|
|
||||||
|
|
||||||
|
def _get_no_states_matched_response(
|
||||||
|
no_states_error: intent.NoStatesMatchedError,
|
||||||
|
) -> tuple[ResponseType, dict[str, Any]]:
|
||||||
|
"""Return error response type and template arguments for error."""
|
||||||
|
if not (
|
||||||
|
no_states_error.area
|
||||||
|
and (no_states_error.device_classes or no_states_error.domains)
|
||||||
|
):
|
||||||
|
# Device class and domain must be paired with an area for the error
|
||||||
|
# message.
|
||||||
|
return ResponseType.NO_INTENT, {}
|
||||||
|
|
||||||
|
error_response_args: dict[str, Any] = {"area": no_states_error.area}
|
||||||
|
|
||||||
|
# Check device classes first, since it's more specific than domain
|
||||||
|
if no_states_error.device_classes:
|
||||||
|
# No exposed entities of a particular class in an area.
|
||||||
|
# Example: "close the bedroom windows"
|
||||||
|
#
|
||||||
|
# Only use the first device class for the error message
|
||||||
|
error_response_args["device_class"] = next(iter(no_states_error.device_classes))
|
||||||
|
|
||||||
|
return ResponseType.NO_DEVICE_CLASS, error_response_args
|
||||||
|
|
||||||
|
# No exposed entities of a domain in an area.
|
||||||
|
# Example: "turn on lights in kitchen"
|
||||||
|
assert no_states_error.domains
|
||||||
|
#
|
||||||
|
# Only use the first domain for the error message
|
||||||
|
error_response_args["domain"] = next(iter(no_states_error.domains))
|
||||||
|
|
||||||
|
return ResponseType.NO_DOMAIN, error_response_args
|
||||||
|
|
||||||
|
|
||||||
def _collect_list_references(expression: Expression, list_names: set[str]) -> None:
|
def _collect_list_references(expression: Expression, list_names: set[str]) -> None:
|
||||||
"""Collect list reference names recursively."""
|
"""Collect list reference names recursively."""
|
||||||
if isinstance(expression, Sequence):
|
if isinstance(expression, Sequence):
|
||||||
|
@ -109,8 +109,8 @@ async def async_handle(
|
|||||||
except vol.Invalid as err:
|
except vol.Invalid as err:
|
||||||
_LOGGER.warning("Received invalid slot info for %s: %s", intent_type, err)
|
_LOGGER.warning("Received invalid slot info for %s: %s", intent_type, err)
|
||||||
raise InvalidSlotInfo(f"Received invalid slot info for {intent_type}") from err
|
raise InvalidSlotInfo(f"Received invalid slot info for {intent_type}") from err
|
||||||
except IntentHandleError:
|
except IntentError:
|
||||||
raise
|
raise # bubble up intent related errors
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
raise IntentUnexpectedError(f"Error handling {intent_type}") from err
|
raise IntentUnexpectedError(f"Error handling {intent_type}") from err
|
||||||
|
|
||||||
@ -135,6 +135,25 @@ class IntentUnexpectedError(IntentError):
|
|||||||
"""Unexpected error while handling intent."""
|
"""Unexpected error while handling intent."""
|
||||||
|
|
||||||
|
|
||||||
|
class NoStatesMatchedError(IntentError):
|
||||||
|
"""Error when no states match the intent's constraints."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str | None,
|
||||||
|
area: str | None,
|
||||||
|
domains: set[str] | None,
|
||||||
|
device_classes: set[str] | None,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize error."""
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.name = name
|
||||||
|
self.area = area
|
||||||
|
self.domains = domains
|
||||||
|
self.device_classes = device_classes
|
||||||
|
|
||||||
|
|
||||||
def _is_device_class(
|
def _is_device_class(
|
||||||
state: State,
|
state: State,
|
||||||
entity: entity_registry.RegistryEntry | None,
|
entity: entity_registry.RegistryEntry | None,
|
||||||
@ -421,8 +440,12 @@ class ServiceIntentHandler(IntentHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not states:
|
if not states:
|
||||||
raise IntentHandleError(
|
# No states matched constraints
|
||||||
f"No entities matched for: name={name}, area={area}, domains={domains}, device_classes={device_classes}",
|
raise NoStatesMatchedError(
|
||||||
|
name=name,
|
||||||
|
area=area_name,
|
||||||
|
domains=domains,
|
||||||
|
device_classes=device_classes,
|
||||||
)
|
)
|
||||||
|
|
||||||
response = await self.async_handle_states(intent_obj, states, area)
|
response = await self.async_handle_states(intent_obj, states, area)
|
||||||
|
@ -129,7 +129,7 @@ async def test_exposed_areas(
|
|||||||
|
|
||||||
# This should be an error because the lights in that area are not exposed
|
# This should be an error because the lights in that area are not exposed
|
||||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||||
assert result.response.error_code == intent.IntentResponseErrorCode.FAILED_TO_HANDLE
|
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||||
|
|
||||||
# But we can still ask questions about the bedroom, even with no exposed entities
|
# But we can still ask questions about the bedroom, even with no exposed entities
|
||||||
result = await conversation.async_converse(
|
result = await conversation.async_converse(
|
||||||
@ -455,6 +455,38 @@ async def test_error_missing_area(hass: HomeAssistant, init_components) -> None:
|
|||||||
assert result.response.speech["plain"]["speech"] == "No area named missing area"
|
assert result.response.speech["plain"]["speech"] == "No area named missing area"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_error_no_exposed_for_domain(
|
||||||
|
hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry
|
||||||
|
) -> None:
|
||||||
|
"""Test error message when no entities for a domain are exposed in an area."""
|
||||||
|
area_registry.async_get_or_create("kitchen")
|
||||||
|
result = await conversation.async_converse(
|
||||||
|
hass, "turn on the lights in the kitchen", None, Context(), None
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||||
|
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||||
|
assert (
|
||||||
|
result.response.speech["plain"]["speech"] == "kitchen does not contain a light"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_error_no_exposed_for_device_class(
|
||||||
|
hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry
|
||||||
|
) -> None:
|
||||||
|
"""Test error message when no entities of a device class are exposed in an area."""
|
||||||
|
area_registry.async_get_or_create("bedroom")
|
||||||
|
result = await conversation.async_converse(
|
||||||
|
hass, "open bedroom windows", None, Context(), None
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||||
|
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||||
|
assert (
|
||||||
|
result.response.speech["plain"]["speech"] == "bedroom does not contain a window"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_error_match_failure(hass: HomeAssistant, init_components) -> None:
|
async def test_error_match_failure(hass: HomeAssistant, init_components) -> None:
|
||||||
"""Test response with complete match failure."""
|
"""Test response with complete match failure."""
|
||||||
with patch(
|
with patch(
|
||||||
@ -471,6 +503,31 @@ async def test_error_match_failure(hass: HomeAssistant, init_components) -> None
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_no_states_matched_default_error(
|
||||||
|
hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry
|
||||||
|
) -> None:
|
||||||
|
"""Test default response when no states match and slots are missing."""
|
||||||
|
area_registry.async_get_or_create("kitchen")
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.conversation.default_agent.intent.async_handle",
|
||||||
|
side_effect=intent.NoStatesMatchedError(None, None, None, None),
|
||||||
|
):
|
||||||
|
result = await conversation.async_converse(
|
||||||
|
hass, "turn on lights in the kitchen", None, Context(), None
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||||
|
assert (
|
||||||
|
result.response.error_code
|
||||||
|
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
result.response.speech["plain"]["speech"]
|
||||||
|
== "Sorry, I couldn't understand that"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_empty_aliases(
|
async def test_empty_aliases(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
init_components,
|
init_components,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user