diff --git a/homeassistant/components/conversation/agent_manager.py b/homeassistant/components/conversation/agent_manager.py index aa8b7644900..8202b9a0ed4 100644 --- a/homeassistant/components/conversation/agent_manager.py +++ b/homeassistant/components/conversation/agent_manager.py @@ -96,6 +96,7 @@ async def async_converse( conversation_id=conversation_id, device_id=device_id, language=language, + agent_id=agent_id, ) with async_conversation_trace() as trace: trace.add_event( diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 2fe016351d6..2366722e929 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -354,6 +354,7 @@ class DefaultAgent(ConversationEntity): language, assistant=DOMAIN, device_id=user_input.device_id, + conversation_agent_id=user_input.agent_id, ) except intent.MatchFailedError as match_error: # Intent was valid, but no entities matched the constraints. diff --git a/homeassistant/components/conversation/http.py b/homeassistant/components/conversation/http.py index 866a910a4a7..e0821e14738 100644 --- a/homeassistant/components/conversation/http.py +++ b/homeassistant/components/conversation/http.py @@ -188,6 +188,7 @@ async def websocket_hass_agent_debug( conversation_id=None, device_id=msg.get("device_id"), language=msg.get("language", hass.config.language), + agent_id=None, ) ) for sentence in msg["sentences"] diff --git a/homeassistant/components/conversation/models.py b/homeassistant/components/conversation/models.py index 3fd24152698..902b52483e0 100644 --- a/homeassistant/components/conversation/models.py +++ b/homeassistant/components/conversation/models.py @@ -27,6 +27,7 @@ class ConversationInput: conversation_id: str | None device_id: str | None language: str + agent_id: str | None = None @dataclass(slots=True) diff --git a/homeassistant/components/intent/timers.py b/homeassistant/components/intent/timers.py index 167f37ed6fc..f93b9a0e2b8 100644 --- a/homeassistant/components/intent/timers.py +++ b/homeassistant/components/intent/timers.py @@ -14,7 +14,7 @@ from typing import Any import voluptuous as vol from homeassistant.const import ATTR_DEVICE_ID, ATTR_ID, ATTR_NAME -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Context, HomeAssistant, callback from homeassistant.helpers import ( area_registry as ar, config_validation as cv, @@ -78,6 +78,18 @@ class TimerInfo: floor_id: str | None = None """Id of floor that the device's area belongs to.""" + conversation_command: str | None = None + """Text of conversation command to execute when timer is finished. + + This command must be in the language used to set the timer. + """ + + conversation_agent_id: str | None = None + """Id of the conversation agent used to set the timer. + + This agent will be used to execute the conversation command. + """ + @property def seconds_left(self) -> int: """Return number of seconds left on the timer.""" @@ -207,6 +219,8 @@ class TimerManager: seconds: int | None, language: str, name: str | None = None, + conversation_command: str | None = None, + conversation_agent_id: str | None = None, ) -> str: """Start a timer.""" if not self.is_timer_device(device_id): @@ -235,6 +249,8 @@ class TimerManager: device_id=device_id, created_at=created_at, updated_at=created_at, + conversation_command=conversation_command, + conversation_agent_id=conversation_agent_id, ) # Fill in area/floor info @@ -410,6 +426,23 @@ class TimerManager: timer.device_id, ) + if timer.conversation_command: + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.conversation import async_converse + + self.hass.async_create_background_task( + async_converse( + self.hass, + timer.conversation_command, + conversation_id=None, + context=Context(), + language=timer.language, + agent_id=timer.conversation_agent_id, + device_id=timer.device_id, + ), + "timer assist command", + ) + def is_timer_device(self, device_id: str) -> bool: """Return True if device has been registered to handle timer events.""" return device_id in self.handlers @@ -742,6 +775,7 @@ class StartTimerIntentHandler(intent.IntentHandler): slot_schema = { vol.Required(vol.Any("hours", "minutes", "seconds")): cv.positive_int, vol.Optional("name"): cv.string, + vol.Optional("conversation_command"): cv.string, } async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: @@ -772,6 +806,10 @@ class StartTimerIntentHandler(intent.IntentHandler): if "seconds" in slots: seconds = int(slots["seconds"]["value"]) + conversation_command: str | None = None + if "conversation_command" in slots: + conversation_command = slots["conversation_command"]["value"] + timer_manager.start_timer( intent_obj.device_id, hours, @@ -779,6 +817,8 @@ class StartTimerIntentHandler(intent.IntentHandler): seconds, language=intent_obj.language, name=name, + conversation_command=conversation_command, + conversation_agent_id=intent_obj.conversation_agent_id, ) return intent_obj.create_response() diff --git a/homeassistant/components/wyoming/manifest.json b/homeassistant/components/wyoming/manifest.json index 70768329e60..30104a88dce 100644 --- a/homeassistant/components/wyoming/manifest.json +++ b/homeassistant/components/wyoming/manifest.json @@ -3,7 +3,7 @@ "name": "Wyoming Protocol", "codeowners": ["@balloob", "@synesthesiam"], "config_flow": true, - "dependencies": ["assist_pipeline", "intent"], + "dependencies": ["assist_pipeline", "intent", "conversation"], "documentation": "https://www.home-assistant.io/integrations/wyoming", "integration_type": "service", "iot_class": "local_push", diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 986bcd33484..ccef934d6ad 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -104,6 +104,7 @@ async def async_handle( language: str | None = None, assistant: str | None = None, device_id: str | None = None, + conversation_agent_id: str | None = None, ) -> IntentResponse: """Handle an intent.""" handler = hass.data.get(DATA_KEY, {}).get(intent_type) @@ -127,6 +128,7 @@ async def async_handle( language=language, assistant=assistant, device_id=device_id, + conversation_agent_id=conversation_agent_id, ) try: @@ -1156,6 +1158,7 @@ class Intent: "category", "assistant", "device_id", + "conversation_agent_id", ] def __init__( @@ -1170,6 +1173,7 @@ class Intent: category: IntentCategory | None = None, assistant: str | None = None, device_id: str | None = None, + conversation_agent_id: str | None = None, ) -> None: """Initialize an intent.""" self.hass = hass @@ -1182,6 +1186,7 @@ class Intent: self.category = category self.assistant = assistant self.device_id = device_id + self.conversation_agent_id = conversation_agent_id @callback def create_response(self) -> IntentResponse: diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 5b117c1ac70..64832761364 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -927,6 +927,7 @@ async def test_non_default_response(hass: HomeAssistant, init_components) -> Non conversation_id=None, device_id=None, language=hass.config.language, + agent_id=None, ) ) assert len(calls) == 1 diff --git a/tests/components/conversation/test_trigger.py b/tests/components/conversation/test_trigger.py index 83f4e97c853..fe1181e48c4 100644 --- a/tests/components/conversation/test_trigger.py +++ b/tests/components/conversation/test_trigger.py @@ -555,6 +555,7 @@ async def test_trigger_with_device_id(hass: HomeAssistant) -> None: conversation_id=None, device_id="my_device", language=hass.config.language, + agent_id=None, ) ) assert result.response.speech["plain"]["speech"] == "my_device" diff --git a/tests/components/intent/test_timers.py b/tests/components/intent/test_timers.py index 273fe0d3be6..f014bb5880c 100644 --- a/tests/components/intent/test_timers.py +++ b/tests/components/intent/test_timers.py @@ -1421,6 +1421,48 @@ def test_round_time() -> None: assert _round_time(0, 0, 35) == (0, 0, 30) +async def test_start_timer_with_conversation_command( + hass: HomeAssistant, init_components +) -> None: + """Test starting a timer with an conversation command and having it finish.""" + device_id = "test_device" + timer_name = "test timer" + test_command = "turn on the lights" + agent_id = "test_agent" + finished_event = asyncio.Event() + + @callback + def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None: + if event_type == TimerEventType.FINISHED: + assert timer.conversation_command == test_command + assert timer.conversation_agent_id == agent_id + finished_event.set() + + async_register_timer_handler(hass, device_id, handle_timer) + + with patch("homeassistant.components.conversation.async_converse") as mock_converse: + result = await intent.async_handle( + hass, + "test", + intent.INTENT_START_TIMER, + { + "name": {"value": timer_name}, + "seconds": {"value": 0}, + "conversation_command": {"value": test_command}, + }, + device_id=device_id, + conversation_agent_id=agent_id, + ) + + assert result.response_type == intent.IntentResponseType.ACTION_DONE + + async with asyncio.timeout(1): + await finished_event.wait() + + mock_converse.assert_called_once() + assert mock_converse.call_args.args[1] == test_command + + async def test_pause_unpause_timer_disambiguate( hass: HomeAssistant, init_components ) -> None: