mirror of
https://github.com/Electric-Special/ha-core.git
synced 2026-03-21 02:03:27 +01:00
Validate conversation_command in start timer intent (#149915)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
This commit is contained in:
@@ -30,6 +30,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
TIMER_NOT_FOUND_RESPONSE = "timer_not_found"
|
||||
MULTIPLE_TIMERS_MATCHED_RESPONSE = "multiple_timers_matched"
|
||||
NO_TIMER_SUPPORT_RESPONSE = "no_timer_support"
|
||||
NO_TIMER_COMMAND_RESPONSE = "no_timer_command"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -192,6 +193,17 @@ class MultipleTimersMatchedError(intent.IntentHandleError):
|
||||
super().__init__("Multiple timers matched", MULTIPLE_TIMERS_MATCHED_RESPONSE)
|
||||
|
||||
|
||||
class NoTimerCommandError(intent.IntentHandleError):
|
||||
"""Error when a conversation command does not match any intent."""
|
||||
|
||||
def __init__(self, command: str) -> None:
|
||||
"""Initialize error."""
|
||||
super().__init__(
|
||||
f"Intent not recognized: {command}",
|
||||
NO_TIMER_COMMAND_RESPONSE,
|
||||
)
|
||||
|
||||
|
||||
class TimersNotSupportedError(intent.IntentHandleError):
|
||||
"""Error when a timer intent is used from a device that isn't registered to handle timer events."""
|
||||
|
||||
@@ -836,6 +848,12 @@ class StartTimerIntentHandler(intent.IntentHandler):
|
||||
# Fail early if this is not a delayed command
|
||||
raise TimersNotSupportedError(intent_obj.device_id)
|
||||
|
||||
# Validate conversation command if provided
|
||||
if conversation_command and not await self._validate_conversation_command(
|
||||
intent_obj, conversation_command
|
||||
):
|
||||
raise NoTimerCommandError(conversation_command)
|
||||
|
||||
name: str | None = None
|
||||
if "name" in slots:
|
||||
name = slots["name"]["value"]
|
||||
@@ -865,6 +883,48 @@ class StartTimerIntentHandler(intent.IntentHandler):
|
||||
|
||||
return intent_obj.create_response()
|
||||
|
||||
async def _validate_conversation_command(
|
||||
self, intent_obj: intent.Intent, conversation_command: str
|
||||
) -> bool:
|
||||
"""Validate that a conversation command can be executed."""
|
||||
from homeassistant.components.conversation import ( # noqa: PLC0415
|
||||
ConversationInput,
|
||||
async_get_agent,
|
||||
default_agent,
|
||||
)
|
||||
|
||||
# Only validate if using the default agent
|
||||
conversation_agent = async_get_agent(
|
||||
intent_obj.hass, intent_obj.conversation_agent_id
|
||||
)
|
||||
|
||||
if conversation_agent is None or not isinstance(
|
||||
conversation_agent, default_agent.DefaultAgent
|
||||
):
|
||||
return True # Skip validation
|
||||
|
||||
test_input = ConversationInput(
|
||||
text=conversation_command,
|
||||
context=intent_obj.context,
|
||||
conversation_id=None,
|
||||
device_id=intent_obj.device_id,
|
||||
satellite_id=intent_obj.satellite_id,
|
||||
language=intent_obj.language,
|
||||
agent_id=conversation_agent.entity_id,
|
||||
)
|
||||
|
||||
# check for sentence trigger
|
||||
if (
|
||||
await conversation_agent.async_recognize_sentence_trigger(test_input)
|
||||
) is not None:
|
||||
return True
|
||||
|
||||
# check for intent
|
||||
if (await conversation_agent.async_recognize_intent(test_input)) is not None:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class CancelTimerIntentHandler(intent.IntentHandler):
|
||||
"""Intent handler for cancelling a timer."""
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
"""Tests for intent timers."""
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import conversation
|
||||
from homeassistant.components.intent.timers import (
|
||||
TIMER_DATA,
|
||||
MultipleTimersMatchedError,
|
||||
NoTimerCommandError,
|
||||
TimerEventType,
|
||||
TimerInfo,
|
||||
TimerManager,
|
||||
@@ -32,6 +35,8 @@ from tests.common import MockConfigEntry
|
||||
@pytest.fixture
|
||||
async def init_components(hass: HomeAssistant) -> None:
|
||||
"""Initialize required components for tests."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
assert await async_setup_component(hass, "conversation", {})
|
||||
assert await async_setup_component(hass, "intent", {})
|
||||
|
||||
|
||||
@@ -1463,6 +1468,106 @@ async def test_start_timer_with_conversation_command(
|
||||
assert mock_converse.call_args.args[1] == test_command
|
||||
|
||||
|
||||
async def test_start_timer_with_sentence_trigger_validation(
|
||||
hass: HomeAssistant, init_components
|
||||
) -> None:
|
||||
"""Test starting a timer with a conversation command validates against sentence triggers."""
|
||||
device_id = "test_device"
|
||||
timer_name = "test timer"
|
||||
test_command = "turn on the lights"
|
||||
agent_id = None # Default agent
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.conversation.async_get_agent"
|
||||
) as mock_get_agent:
|
||||
mock_agent = MagicMock(spec=conversation.default_agent.DefaultAgent)
|
||||
mock_agent.async_recognize_sentence_trigger = AsyncMock(
|
||||
return_value=MagicMock(),
|
||||
)
|
||||
mock_agent.async_recognize_intent = AsyncMock(return_value=None)
|
||||
mock_get_agent.return_value = mock_agent
|
||||
|
||||
result = await intent.async_handle(
|
||||
hass,
|
||||
"test",
|
||||
intent.INTENT_START_TIMER,
|
||||
{
|
||||
"name": {"value": timer_name},
|
||||
"seconds": {"value": 5},
|
||||
"conversation_command": {"value": test_command},
|
||||
},
|
||||
device_id=device_id,
|
||||
conversation_agent_id=agent_id,
|
||||
)
|
||||
|
||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
|
||||
# Verify the sentence trigger was checked
|
||||
mock_agent.async_recognize_sentence_trigger.assert_called_once()
|
||||
|
||||
# Verify timer was created successfully
|
||||
timer_manager = hass.data[TIMER_DATA]
|
||||
assert len(timer_manager.timers) == 1
|
||||
|
||||
|
||||
async def test_start_timer_with_invalid_conversation_command(
|
||||
hass: HomeAssistant, init_components
|
||||
) -> None:
|
||||
"""Test starting a timer with an invalid conversation command fails validation."""
|
||||
device_id = "test_device"
|
||||
timer_name = "test timer"
|
||||
invalid_command = "invalid command that does not exist"
|
||||
agent_id = None # Default agent
|
||||
|
||||
with pytest.raises(NoTimerCommandError):
|
||||
await intent.async_handle(
|
||||
hass,
|
||||
"test",
|
||||
intent.INTENT_START_TIMER,
|
||||
{
|
||||
"name": {"value": timer_name},
|
||||
"seconds": {"value": 5},
|
||||
"conversation_command": {"value": invalid_command},
|
||||
},
|
||||
device_id=device_id,
|
||||
conversation_agent_id=agent_id,
|
||||
)
|
||||
|
||||
# Verify no timer was created
|
||||
timer_manager = hass.data[TIMER_DATA]
|
||||
assert len(timer_manager.timers) == 0
|
||||
|
||||
|
||||
async def test_start_timer_with_conversation_command_skip_validation(
|
||||
hass: HomeAssistant, init_components
|
||||
) -> None:
|
||||
"""Test starting a timer with a conversation command skips validation for non-default agents."""
|
||||
device_id = "test_device"
|
||||
timer_name = "test timer"
|
||||
invalid_command = "invalid command that does not exist"
|
||||
agent_id = "conversation.test_llm_agent"
|
||||
|
||||
# This should NOT raise an error because validation is skipped for all non-default agents
|
||||
result = await intent.async_handle(
|
||||
hass,
|
||||
"test",
|
||||
intent.INTENT_START_TIMER,
|
||||
{
|
||||
"name": {"value": timer_name},
|
||||
"seconds": {"value": 5},
|
||||
"conversation_command": {"value": invalid_command},
|
||||
},
|
||||
device_id=device_id,
|
||||
conversation_agent_id=agent_id,
|
||||
)
|
||||
|
||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
|
||||
# Verify timer was created successfully despite invalid command
|
||||
timer_manager = hass.data[TIMER_DATA]
|
||||
assert len(timer_manager.timers) == 1
|
||||
|
||||
|
||||
async def test_pause_unpause_timer_disambiguate(
|
||||
hass: HomeAssistant, init_components
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user