mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 16:27:08 +00:00
Add Conversation command to timers (#118325)
* Add Assist command to timers * Rename to conversation_command. Execute in timer code. * Make agent_id optional * Fix arg --------- Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
parent
615a1eda51
commit
d223e1f2ac
@ -96,6 +96,7 @@ async def async_converse(
|
|||||||
conversation_id=conversation_id,
|
conversation_id=conversation_id,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
language=language,
|
language=language,
|
||||||
|
agent_id=agent_id,
|
||||||
)
|
)
|
||||||
with async_conversation_trace() as trace:
|
with async_conversation_trace() as trace:
|
||||||
trace.add_event(
|
trace.add_event(
|
||||||
|
@ -354,6 +354,7 @@ class DefaultAgent(ConversationEntity):
|
|||||||
language,
|
language,
|
||||||
assistant=DOMAIN,
|
assistant=DOMAIN,
|
||||||
device_id=user_input.device_id,
|
device_id=user_input.device_id,
|
||||||
|
conversation_agent_id=user_input.agent_id,
|
||||||
)
|
)
|
||||||
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.
|
||||||
|
@ -188,6 +188,7 @@ async def websocket_hass_agent_debug(
|
|||||||
conversation_id=None,
|
conversation_id=None,
|
||||||
device_id=msg.get("device_id"),
|
device_id=msg.get("device_id"),
|
||||||
language=msg.get("language", hass.config.language),
|
language=msg.get("language", hass.config.language),
|
||||||
|
agent_id=None,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
for sentence in msg["sentences"]
|
for sentence in msg["sentences"]
|
||||||
|
@ -27,6 +27,7 @@ class ConversationInput:
|
|||||||
conversation_id: str | None
|
conversation_id: str | None
|
||||||
device_id: str | None
|
device_id: str | None
|
||||||
language: str
|
language: str
|
||||||
|
agent_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
|
@ -14,7 +14,7 @@ from typing import Any
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import ATTR_DEVICE_ID, ATTR_ID, ATTR_NAME
|
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 (
|
from homeassistant.helpers import (
|
||||||
area_registry as ar,
|
area_registry as ar,
|
||||||
config_validation as cv,
|
config_validation as cv,
|
||||||
@ -78,6 +78,18 @@ class TimerInfo:
|
|||||||
floor_id: str | None = None
|
floor_id: str | None = None
|
||||||
"""Id of floor that the device's area belongs to."""
|
"""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
|
@property
|
||||||
def seconds_left(self) -> int:
|
def seconds_left(self) -> int:
|
||||||
"""Return number of seconds left on the timer."""
|
"""Return number of seconds left on the timer."""
|
||||||
@ -207,6 +219,8 @@ class TimerManager:
|
|||||||
seconds: int | None,
|
seconds: int | None,
|
||||||
language: str,
|
language: str,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
|
conversation_command: str | None = None,
|
||||||
|
conversation_agent_id: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Start a timer."""
|
"""Start a timer."""
|
||||||
if not self.is_timer_device(device_id):
|
if not self.is_timer_device(device_id):
|
||||||
@ -235,6 +249,8 @@ class TimerManager:
|
|||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
created_at=created_at,
|
created_at=created_at,
|
||||||
updated_at=created_at,
|
updated_at=created_at,
|
||||||
|
conversation_command=conversation_command,
|
||||||
|
conversation_agent_id=conversation_agent_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fill in area/floor info
|
# Fill in area/floor info
|
||||||
@ -410,6 +426,23 @@ class TimerManager:
|
|||||||
timer.device_id,
|
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:
|
def is_timer_device(self, device_id: str) -> bool:
|
||||||
"""Return True if device has been registered to handle timer events."""
|
"""Return True if device has been registered to handle timer events."""
|
||||||
return device_id in self.handlers
|
return device_id in self.handlers
|
||||||
@ -742,6 +775,7 @@ class StartTimerIntentHandler(intent.IntentHandler):
|
|||||||
slot_schema = {
|
slot_schema = {
|
||||||
vol.Required(vol.Any("hours", "minutes", "seconds")): cv.positive_int,
|
vol.Required(vol.Any("hours", "minutes", "seconds")): cv.positive_int,
|
||||||
vol.Optional("name"): cv.string,
|
vol.Optional("name"): cv.string,
|
||||||
|
vol.Optional("conversation_command"): cv.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
||||||
@ -772,6 +806,10 @@ class StartTimerIntentHandler(intent.IntentHandler):
|
|||||||
if "seconds" in slots:
|
if "seconds" in slots:
|
||||||
seconds = int(slots["seconds"]["value"])
|
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(
|
timer_manager.start_timer(
|
||||||
intent_obj.device_id,
|
intent_obj.device_id,
|
||||||
hours,
|
hours,
|
||||||
@ -779,6 +817,8 @@ class StartTimerIntentHandler(intent.IntentHandler):
|
|||||||
seconds,
|
seconds,
|
||||||
language=intent_obj.language,
|
language=intent_obj.language,
|
||||||
name=name,
|
name=name,
|
||||||
|
conversation_command=conversation_command,
|
||||||
|
conversation_agent_id=intent_obj.conversation_agent_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
return intent_obj.create_response()
|
return intent_obj.create_response()
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Wyoming Protocol",
|
"name": "Wyoming Protocol",
|
||||||
"codeowners": ["@balloob", "@synesthesiam"],
|
"codeowners": ["@balloob", "@synesthesiam"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["assist_pipeline", "intent"],
|
"dependencies": ["assist_pipeline", "intent", "conversation"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/wyoming",
|
"documentation": "https://www.home-assistant.io/integrations/wyoming",
|
||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
|
@ -104,6 +104,7 @@ async def async_handle(
|
|||||||
language: str | None = None,
|
language: str | None = None,
|
||||||
assistant: str | None = None,
|
assistant: str | None = None,
|
||||||
device_id: str | None = None,
|
device_id: str | None = None,
|
||||||
|
conversation_agent_id: str | None = None,
|
||||||
) -> IntentResponse:
|
) -> IntentResponse:
|
||||||
"""Handle an intent."""
|
"""Handle an intent."""
|
||||||
handler = hass.data.get(DATA_KEY, {}).get(intent_type)
|
handler = hass.data.get(DATA_KEY, {}).get(intent_type)
|
||||||
@ -127,6 +128,7 @@ async def async_handle(
|
|||||||
language=language,
|
language=language,
|
||||||
assistant=assistant,
|
assistant=assistant,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
conversation_agent_id=conversation_agent_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -1156,6 +1158,7 @@ class Intent:
|
|||||||
"category",
|
"category",
|
||||||
"assistant",
|
"assistant",
|
||||||
"device_id",
|
"device_id",
|
||||||
|
"conversation_agent_id",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -1170,6 +1173,7 @@ class Intent:
|
|||||||
category: IntentCategory | None = None,
|
category: IntentCategory | None = None,
|
||||||
assistant: str | None = None,
|
assistant: str | None = None,
|
||||||
device_id: str | None = None,
|
device_id: str | None = None,
|
||||||
|
conversation_agent_id: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize an intent."""
|
"""Initialize an intent."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
@ -1182,6 +1186,7 @@ class Intent:
|
|||||||
self.category = category
|
self.category = category
|
||||||
self.assistant = assistant
|
self.assistant = assistant
|
||||||
self.device_id = device_id
|
self.device_id = device_id
|
||||||
|
self.conversation_agent_id = conversation_agent_id
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def create_response(self) -> IntentResponse:
|
def create_response(self) -> IntentResponse:
|
||||||
|
@ -927,6 +927,7 @@ async def test_non_default_response(hass: HomeAssistant, init_components) -> Non
|
|||||||
conversation_id=None,
|
conversation_id=None,
|
||||||
device_id=None,
|
device_id=None,
|
||||||
language=hass.config.language,
|
language=hass.config.language,
|
||||||
|
agent_id=None,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert len(calls) == 1
|
assert len(calls) == 1
|
||||||
|
@ -555,6 +555,7 @@ async def test_trigger_with_device_id(hass: HomeAssistant) -> None:
|
|||||||
conversation_id=None,
|
conversation_id=None,
|
||||||
device_id="my_device",
|
device_id="my_device",
|
||||||
language=hass.config.language,
|
language=hass.config.language,
|
||||||
|
agent_id=None,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert result.response.speech["plain"]["speech"] == "my_device"
|
assert result.response.speech["plain"]["speech"] == "my_device"
|
||||||
|
@ -1421,6 +1421,48 @@ def test_round_time() -> None:
|
|||||||
assert _round_time(0, 0, 35) == (0, 0, 30)
|
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(
|
async def test_pause_unpause_timer_disambiguate(
|
||||||
hass: HomeAssistant, init_components
|
hass: HomeAssistant, init_components
|
||||||
) -> None:
|
) -> None:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user