mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 16:27:08 +00:00
Require registered device id for all timer intents (#117946)
* Require device id when registering timer handlers * Require device id for timer intents * Raise errors for unregistered device ids * Add callback * Add types for callback to __all__ * Clean up * More clean up
This commit is contained in:
parent
77e385db52
commit
5be15c94bc
@ -45,6 +45,8 @@ from .timers import (
|
|||||||
IncreaseTimerIntentHandler,
|
IncreaseTimerIntentHandler,
|
||||||
PauseTimerIntentHandler,
|
PauseTimerIntentHandler,
|
||||||
StartTimerIntentHandler,
|
StartTimerIntentHandler,
|
||||||
|
TimerEventType,
|
||||||
|
TimerInfo,
|
||||||
TimerManager,
|
TimerManager,
|
||||||
TimerStatusIntentHandler,
|
TimerStatusIntentHandler,
|
||||||
UnpauseTimerIntentHandler,
|
UnpauseTimerIntentHandler,
|
||||||
@ -57,6 +59,8 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"async_register_timer_handler",
|
"async_register_timer_handler",
|
||||||
|
"TimerInfo",
|
||||||
|
"TimerEventType",
|
||||||
"DOMAIN",
|
"DOMAIN",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
TIMER_NOT_FOUND_RESPONSE = "timer_not_found"
|
TIMER_NOT_FOUND_RESPONSE = "timer_not_found"
|
||||||
MULTIPLE_TIMERS_MATCHED_RESPONSE = "multiple_timers_matched"
|
MULTIPLE_TIMERS_MATCHED_RESPONSE = "multiple_timers_matched"
|
||||||
|
NO_TIMER_SUPPORT_RESPONSE = "no_timer_support"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -44,7 +45,7 @@ class TimerInfo:
|
|||||||
seconds: int
|
seconds: int
|
||||||
"""Total number of seconds the timer should run for."""
|
"""Total number of seconds the timer should run for."""
|
||||||
|
|
||||||
device_id: str | None
|
device_id: str
|
||||||
"""Id of the device where the timer was set."""
|
"""Id of the device where the timer was set."""
|
||||||
|
|
||||||
start_hours: int | None
|
start_hours: int | None
|
||||||
@ -159,6 +160,17 @@ class MultipleTimersMatchedError(intent.IntentHandleError):
|
|||||||
super().__init__("Multiple timers matched", MULTIPLE_TIMERS_MATCHED_RESPONSE)
|
super().__init__("Multiple timers matched", MULTIPLE_TIMERS_MATCHED_RESPONSE)
|
||||||
|
|
||||||
|
|
||||||
|
class TimersNotSupportedError(intent.IntentHandleError):
|
||||||
|
"""Error when a timer intent is used from a device that isn't registered to handle timer events."""
|
||||||
|
|
||||||
|
def __init__(self, device_id: str | None = None) -> None:
|
||||||
|
"""Initialize error."""
|
||||||
|
super().__init__(
|
||||||
|
f"Device does not support timers: device_id={device_id}",
|
||||||
|
NO_TIMER_SUPPORT_RESPONSE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TimerManager:
|
class TimerManager:
|
||||||
"""Manager for intent timers."""
|
"""Manager for intent timers."""
|
||||||
|
|
||||||
@ -170,26 +182,36 @@ class TimerManager:
|
|||||||
self.timers: dict[str, TimerInfo] = {}
|
self.timers: dict[str, TimerInfo] = {}
|
||||||
self.timer_tasks: dict[str, asyncio.Task] = {}
|
self.timer_tasks: dict[str, asyncio.Task] = {}
|
||||||
|
|
||||||
self.handlers: list[TimerHandler] = []
|
# device_id -> handler
|
||||||
|
self.handlers: dict[str, TimerHandler] = {}
|
||||||
|
|
||||||
def register_handler(self, handler: TimerHandler) -> Callable[[], None]:
|
def register_handler(
|
||||||
|
self, device_id: str, handler: TimerHandler
|
||||||
|
) -> Callable[[], None]:
|
||||||
"""Register a timer handler.
|
"""Register a timer handler.
|
||||||
|
|
||||||
Returns a callable to unregister.
|
Returns a callable to unregister.
|
||||||
"""
|
"""
|
||||||
self.handlers.append(handler)
|
self.handlers[device_id] = handler
|
||||||
return lambda: self.handlers.remove(handler)
|
|
||||||
|
def unregister() -> None:
|
||||||
|
self.handlers.pop(device_id)
|
||||||
|
|
||||||
|
return unregister
|
||||||
|
|
||||||
def start_timer(
|
def start_timer(
|
||||||
self,
|
self,
|
||||||
|
device_id: str,
|
||||||
hours: int | None,
|
hours: int | None,
|
||||||
minutes: int | None,
|
minutes: int | None,
|
||||||
seconds: int | None,
|
seconds: int | None,
|
||||||
language: str,
|
language: str,
|
||||||
device_id: str | None,
|
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Start a timer."""
|
"""Start a timer."""
|
||||||
|
if not self.is_timer_device(device_id):
|
||||||
|
raise TimersNotSupportedError(device_id)
|
||||||
|
|
||||||
total_seconds = 0
|
total_seconds = 0
|
||||||
if hours is not None:
|
if hours is not None:
|
||||||
total_seconds += 60 * 60 * hours
|
total_seconds += 60 * 60 * hours
|
||||||
@ -232,9 +254,7 @@ class TimerManager:
|
|||||||
name=f"Timer {timer_id}",
|
name=f"Timer {timer_id}",
|
||||||
)
|
)
|
||||||
|
|
||||||
for handler in self.handlers:
|
self.handlers[timer.device_id](TimerEventType.STARTED, timer)
|
||||||
handler(TimerEventType.STARTED, timer)
|
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Timer started: id=%s, name=%s, hours=%s, minutes=%s, seconds=%s, device_id=%s",
|
"Timer started: id=%s, name=%s, hours=%s, minutes=%s, seconds=%s, device_id=%s",
|
||||||
timer_id,
|
timer_id,
|
||||||
@ -266,15 +286,16 @@ class TimerManager:
|
|||||||
if timer is None:
|
if timer is None:
|
||||||
raise TimerNotFoundError
|
raise TimerNotFoundError
|
||||||
|
|
||||||
|
if not self.is_timer_device(timer.device_id):
|
||||||
|
raise TimersNotSupportedError(timer.device_id)
|
||||||
|
|
||||||
if timer.is_active:
|
if timer.is_active:
|
||||||
task = self.timer_tasks.pop(timer_id)
|
task = self.timer_tasks.pop(timer_id)
|
||||||
task.cancel()
|
task.cancel()
|
||||||
|
|
||||||
timer.cancel()
|
timer.cancel()
|
||||||
|
|
||||||
for handler in self.handlers:
|
self.handlers[timer.device_id](TimerEventType.CANCELLED, timer)
|
||||||
handler(TimerEventType.CANCELLED, timer)
|
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Timer cancelled: id=%s, name=%s, seconds_left=%s, device_id=%s",
|
"Timer cancelled: id=%s, name=%s, seconds_left=%s, device_id=%s",
|
||||||
timer_id,
|
timer_id,
|
||||||
@ -289,6 +310,9 @@ class TimerManager:
|
|||||||
if timer is None:
|
if timer is None:
|
||||||
raise TimerNotFoundError
|
raise TimerNotFoundError
|
||||||
|
|
||||||
|
if not self.is_timer_device(timer.device_id):
|
||||||
|
raise TimersNotSupportedError(timer.device_id)
|
||||||
|
|
||||||
if seconds == 0:
|
if seconds == 0:
|
||||||
# Don't bother cancelling and recreating the timer task
|
# Don't bother cancelling and recreating the timer task
|
||||||
return
|
return
|
||||||
@ -302,8 +326,7 @@ class TimerManager:
|
|||||||
name=f"Timer {timer_id}",
|
name=f"Timer {timer_id}",
|
||||||
)
|
)
|
||||||
|
|
||||||
for handler in self.handlers:
|
self.handlers[timer.device_id](TimerEventType.UPDATED, timer)
|
||||||
handler(TimerEventType.UPDATED, timer)
|
|
||||||
|
|
||||||
if seconds > 0:
|
if seconds > 0:
|
||||||
log_verb = "increased"
|
log_verb = "increased"
|
||||||
@ -332,6 +355,9 @@ class TimerManager:
|
|||||||
if timer is None:
|
if timer is None:
|
||||||
raise TimerNotFoundError
|
raise TimerNotFoundError
|
||||||
|
|
||||||
|
if not self.is_timer_device(timer.device_id):
|
||||||
|
raise TimersNotSupportedError(timer.device_id)
|
||||||
|
|
||||||
if not timer.is_active:
|
if not timer.is_active:
|
||||||
# Already paused
|
# Already paused
|
||||||
return
|
return
|
||||||
@ -340,9 +366,7 @@ class TimerManager:
|
|||||||
task = self.timer_tasks.pop(timer_id)
|
task = self.timer_tasks.pop(timer_id)
|
||||||
task.cancel()
|
task.cancel()
|
||||||
|
|
||||||
for handler in self.handlers:
|
self.handlers[timer.device_id](TimerEventType.UPDATED, timer)
|
||||||
handler(TimerEventType.UPDATED, timer)
|
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Timer paused: id=%s, name=%s, seconds_left=%s, device_id=%s",
|
"Timer paused: id=%s, name=%s, seconds_left=%s, device_id=%s",
|
||||||
timer_id,
|
timer_id,
|
||||||
@ -357,6 +381,9 @@ class TimerManager:
|
|||||||
if timer is None:
|
if timer is None:
|
||||||
raise TimerNotFoundError
|
raise TimerNotFoundError
|
||||||
|
|
||||||
|
if not self.is_timer_device(timer.device_id):
|
||||||
|
raise TimersNotSupportedError(timer.device_id)
|
||||||
|
|
||||||
if timer.is_active:
|
if timer.is_active:
|
||||||
# Already unpaused
|
# Already unpaused
|
||||||
return
|
return
|
||||||
@ -367,9 +394,7 @@ class TimerManager:
|
|||||||
name=f"Timer {timer.id}",
|
name=f"Timer {timer.id}",
|
||||||
)
|
)
|
||||||
|
|
||||||
for handler in self.handlers:
|
self.handlers[timer.device_id](TimerEventType.UPDATED, timer)
|
||||||
handler(TimerEventType.UPDATED, timer)
|
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Timer unpaused: id=%s, name=%s, seconds_left=%s, device_id=%s",
|
"Timer unpaused: id=%s, name=%s, seconds_left=%s, device_id=%s",
|
||||||
timer_id,
|
timer_id,
|
||||||
@ -383,9 +408,8 @@ class TimerManager:
|
|||||||
timer = self.timers.pop(timer_id)
|
timer = self.timers.pop(timer_id)
|
||||||
|
|
||||||
timer.finish()
|
timer.finish()
|
||||||
for handler in self.handlers:
|
|
||||||
handler(TimerEventType.FINISHED, timer)
|
|
||||||
|
|
||||||
|
self.handlers[timer.device_id](TimerEventType.FINISHED, timer)
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Timer finished: id=%s, name=%s, device_id=%s",
|
"Timer finished: id=%s, name=%s, device_id=%s",
|
||||||
timer_id,
|
timer_id,
|
||||||
@ -393,24 +417,28 @@ class TimerManager:
|
|||||||
timer.device_id,
|
timer.device_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_register_timer_handler(
|
def async_register_timer_handler(
|
||||||
hass: HomeAssistant, handler: TimerHandler
|
hass: HomeAssistant, device_id: str, handler: TimerHandler
|
||||||
) -> Callable[[], None]:
|
) -> Callable[[], None]:
|
||||||
"""Register a handler for timer events.
|
"""Register a handler for timer events.
|
||||||
|
|
||||||
Returns a callable to unregister.
|
Returns a callable to unregister.
|
||||||
"""
|
"""
|
||||||
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
||||||
return timer_manager.register_handler(handler)
|
return timer_manager.register_handler(device_id, handler)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _find_timer(
|
def _find_timer(
|
||||||
hass: HomeAssistant, slots: dict[str, Any], device_id: str | None
|
hass: HomeAssistant, device_id: str, slots: dict[str, Any]
|
||||||
) -> TimerInfo:
|
) -> TimerInfo:
|
||||||
"""Match a single timer with constraints or raise an error."""
|
"""Match a single timer with constraints or raise an error."""
|
||||||
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
||||||
@ -479,7 +507,7 @@ def _find_timer(
|
|||||||
return matching_timers[0]
|
return matching_timers[0]
|
||||||
|
|
||||||
# Use device id
|
# Use device id
|
||||||
if matching_timers and device_id:
|
if matching_timers:
|
||||||
matching_device_timers = [
|
matching_device_timers = [
|
||||||
t for t in matching_timers if (t.device_id == device_id)
|
t for t in matching_timers if (t.device_id == device_id)
|
||||||
]
|
]
|
||||||
@ -528,7 +556,7 @@ def _find_timer(
|
|||||||
|
|
||||||
|
|
||||||
def _find_timers(
|
def _find_timers(
|
||||||
hass: HomeAssistant, slots: dict[str, Any], device_id: str | None
|
hass: HomeAssistant, device_id: str, slots: dict[str, Any]
|
||||||
) -> list[TimerInfo]:
|
) -> list[TimerInfo]:
|
||||||
"""Match multiple timers with constraints or raise an error."""
|
"""Match multiple timers with constraints or raise an error."""
|
||||||
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
||||||
@ -587,10 +615,6 @@ def _find_timers(
|
|||||||
# No matches
|
# No matches
|
||||||
return matching_timers
|
return matching_timers
|
||||||
|
|
||||||
if not device_id:
|
|
||||||
# Can't re-order based on area/floor
|
|
||||||
return matching_timers
|
|
||||||
|
|
||||||
# Use device id to order remaining timers
|
# Use device id to order remaining timers
|
||||||
device_registry = dr.async_get(hass)
|
device_registry = dr.async_get(hass)
|
||||||
device = device_registry.async_get(device_id)
|
device = device_registry.async_get(device_id)
|
||||||
@ -702,6 +726,12 @@ class StartTimerIntentHandler(intent.IntentHandler):
|
|||||||
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
||||||
slots = self.async_validate_slots(intent_obj.slots)
|
slots = self.async_validate_slots(intent_obj.slots)
|
||||||
|
|
||||||
|
if not (
|
||||||
|
intent_obj.device_id and timer_manager.is_timer_device(intent_obj.device_id)
|
||||||
|
):
|
||||||
|
# Fail early
|
||||||
|
raise TimersNotSupportedError(intent_obj.device_id)
|
||||||
|
|
||||||
name: str | None = None
|
name: str | None = None
|
||||||
if "name" in slots:
|
if "name" in slots:
|
||||||
name = slots["name"]["value"]
|
name = slots["name"]["value"]
|
||||||
@ -719,11 +749,11 @@ class StartTimerIntentHandler(intent.IntentHandler):
|
|||||||
seconds = int(slots["seconds"]["value"])
|
seconds = int(slots["seconds"]["value"])
|
||||||
|
|
||||||
timer_manager.start_timer(
|
timer_manager.start_timer(
|
||||||
|
intent_obj.device_id,
|
||||||
hours,
|
hours,
|
||||||
minutes,
|
minutes,
|
||||||
seconds,
|
seconds,
|
||||||
language=intent_obj.language,
|
language=intent_obj.language,
|
||||||
device_id=intent_obj.device_id,
|
|
||||||
name=name,
|
name=name,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -747,9 +777,16 @@ class CancelTimerIntentHandler(intent.IntentHandler):
|
|||||||
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
||||||
slots = self.async_validate_slots(intent_obj.slots)
|
slots = self.async_validate_slots(intent_obj.slots)
|
||||||
|
|
||||||
timer = _find_timer(hass, slots, intent_obj.device_id)
|
if not (
|
||||||
timer_manager.cancel_timer(timer.id)
|
intent_obj.device_id and timer_manager.is_timer_device(intent_obj.device_id)
|
||||||
|
):
|
||||||
|
# Fail early
|
||||||
|
raise TimersNotSupportedError(intent_obj.device_id)
|
||||||
|
|
||||||
|
assert intent_obj.device_id is not None
|
||||||
|
|
||||||
|
timer = _find_timer(hass, intent_obj.device_id, slots)
|
||||||
|
timer_manager.cancel_timer(timer.id)
|
||||||
return intent_obj.create_response()
|
return intent_obj.create_response()
|
||||||
|
|
||||||
|
|
||||||
@ -771,10 +808,17 @@ class IncreaseTimerIntentHandler(intent.IntentHandler):
|
|||||||
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
||||||
slots = self.async_validate_slots(intent_obj.slots)
|
slots = self.async_validate_slots(intent_obj.slots)
|
||||||
|
|
||||||
total_seconds = _get_total_seconds(slots)
|
if not (
|
||||||
timer = _find_timer(hass, slots, intent_obj.device_id)
|
intent_obj.device_id and timer_manager.is_timer_device(intent_obj.device_id)
|
||||||
timer_manager.add_time(timer.id, total_seconds)
|
):
|
||||||
|
# Fail early
|
||||||
|
raise TimersNotSupportedError(intent_obj.device_id)
|
||||||
|
|
||||||
|
assert intent_obj.device_id is not None
|
||||||
|
|
||||||
|
total_seconds = _get_total_seconds(slots)
|
||||||
|
timer = _find_timer(hass, intent_obj.device_id, slots)
|
||||||
|
timer_manager.add_time(timer.id, total_seconds)
|
||||||
return intent_obj.create_response()
|
return intent_obj.create_response()
|
||||||
|
|
||||||
|
|
||||||
@ -796,10 +840,17 @@ class DecreaseTimerIntentHandler(intent.IntentHandler):
|
|||||||
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
||||||
slots = self.async_validate_slots(intent_obj.slots)
|
slots = self.async_validate_slots(intent_obj.slots)
|
||||||
|
|
||||||
total_seconds = _get_total_seconds(slots)
|
if not (
|
||||||
timer = _find_timer(hass, slots, intent_obj.device_id)
|
intent_obj.device_id and timer_manager.is_timer_device(intent_obj.device_id)
|
||||||
timer_manager.remove_time(timer.id, total_seconds)
|
):
|
||||||
|
# Fail early
|
||||||
|
raise TimersNotSupportedError(intent_obj.device_id)
|
||||||
|
|
||||||
|
assert intent_obj.device_id is not None
|
||||||
|
|
||||||
|
total_seconds = _get_total_seconds(slots)
|
||||||
|
timer = _find_timer(hass, intent_obj.device_id, slots)
|
||||||
|
timer_manager.remove_time(timer.id, total_seconds)
|
||||||
return intent_obj.create_response()
|
return intent_obj.create_response()
|
||||||
|
|
||||||
|
|
||||||
@ -820,9 +871,16 @@ class PauseTimerIntentHandler(intent.IntentHandler):
|
|||||||
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
||||||
slots = self.async_validate_slots(intent_obj.slots)
|
slots = self.async_validate_slots(intent_obj.slots)
|
||||||
|
|
||||||
timer = _find_timer(hass, slots, intent_obj.device_id)
|
if not (
|
||||||
timer_manager.pause_timer(timer.id)
|
intent_obj.device_id and timer_manager.is_timer_device(intent_obj.device_id)
|
||||||
|
):
|
||||||
|
# Fail early
|
||||||
|
raise TimersNotSupportedError(intent_obj.device_id)
|
||||||
|
|
||||||
|
assert intent_obj.device_id is not None
|
||||||
|
|
||||||
|
timer = _find_timer(hass, intent_obj.device_id, slots)
|
||||||
|
timer_manager.pause_timer(timer.id)
|
||||||
return intent_obj.create_response()
|
return intent_obj.create_response()
|
||||||
|
|
||||||
|
|
||||||
@ -843,9 +901,16 @@ class UnpauseTimerIntentHandler(intent.IntentHandler):
|
|||||||
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
||||||
slots = self.async_validate_slots(intent_obj.slots)
|
slots = self.async_validate_slots(intent_obj.slots)
|
||||||
|
|
||||||
timer = _find_timer(hass, slots, intent_obj.device_id)
|
if not (
|
||||||
timer_manager.unpause_timer(timer.id)
|
intent_obj.device_id and timer_manager.is_timer_device(intent_obj.device_id)
|
||||||
|
):
|
||||||
|
# Fail early
|
||||||
|
raise TimersNotSupportedError(intent_obj.device_id)
|
||||||
|
|
||||||
|
assert intent_obj.device_id is not None
|
||||||
|
|
||||||
|
timer = _find_timer(hass, intent_obj.device_id, slots)
|
||||||
|
timer_manager.unpause_timer(timer.id)
|
||||||
return intent_obj.create_response()
|
return intent_obj.create_response()
|
||||||
|
|
||||||
|
|
||||||
@ -863,10 +928,19 @@ class TimerStatusIntentHandler(intent.IntentHandler):
|
|||||||
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
||||||
"""Handle the intent."""
|
"""Handle the intent."""
|
||||||
hass = intent_obj.hass
|
hass = intent_obj.hass
|
||||||
|
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
||||||
slots = self.async_validate_slots(intent_obj.slots)
|
slots = self.async_validate_slots(intent_obj.slots)
|
||||||
|
|
||||||
|
if not (
|
||||||
|
intent_obj.device_id and timer_manager.is_timer_device(intent_obj.device_id)
|
||||||
|
):
|
||||||
|
# Fail early
|
||||||
|
raise TimersNotSupportedError(intent_obj.device_id)
|
||||||
|
|
||||||
|
assert intent_obj.device_id is not None
|
||||||
|
|
||||||
statuses: list[dict[str, Any]] = []
|
statuses: list[dict[str, Any]] = []
|
||||||
for timer in _find_timers(hass, slots, intent_obj.device_id):
|
for timer in _find_timers(hass, intent_obj.device_id, slots):
|
||||||
total_seconds = timer.seconds_left
|
total_seconds = timer.seconds_left
|
||||||
|
|
||||||
minutes, seconds = divmod(total_seconds, 60)
|
minutes, seconds = divmod(total_seconds, 60)
|
||||||
|
@ -11,11 +11,12 @@ from homeassistant.components.intent.timers import (
|
|||||||
TimerInfo,
|
TimerInfo,
|
||||||
TimerManager,
|
TimerManager,
|
||||||
TimerNotFoundError,
|
TimerNotFoundError,
|
||||||
|
TimersNotSupportedError,
|
||||||
_round_time,
|
_round_time,
|
||||||
async_register_timer_handler,
|
async_register_timer_handler,
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_DEVICE_ID, ATTR_NAME
|
from homeassistant.const import ATTR_DEVICE_ID, ATTR_NAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import 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,
|
||||||
@ -42,6 +43,7 @@ async def test_start_finish_timer(hass: HomeAssistant, init_components) -> None:
|
|||||||
|
|
||||||
timer_id: str | None = None
|
timer_id: str | None = None
|
||||||
|
|
||||||
|
@callback
|
||||||
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
||||||
nonlocal timer_id
|
nonlocal timer_id
|
||||||
|
|
||||||
@ -59,7 +61,7 @@ async def test_start_finish_timer(hass: HomeAssistant, init_components) -> None:
|
|||||||
assert timer.id == timer_id
|
assert timer.id == timer_id
|
||||||
finished_event.set()
|
finished_event.set()
|
||||||
|
|
||||||
async_register_timer_handler(hass, handle_timer)
|
async_register_timer_handler(hass, device_id, handle_timer)
|
||||||
|
|
||||||
result = await intent.async_handle(
|
result = await intent.async_handle(
|
||||||
hass,
|
hass,
|
||||||
@ -87,6 +89,7 @@ async def test_cancel_timer(hass: HomeAssistant, init_components) -> None:
|
|||||||
|
|
||||||
timer_id: str | None = None
|
timer_id: str | None = None
|
||||||
|
|
||||||
|
@callback
|
||||||
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
||||||
nonlocal timer_id
|
nonlocal timer_id
|
||||||
|
|
||||||
@ -112,7 +115,7 @@ async def test_cancel_timer(hass: HomeAssistant, init_components) -> None:
|
|||||||
assert timer.seconds_left == 0
|
assert timer.seconds_left == 0
|
||||||
cancelled_event.set()
|
cancelled_event.set()
|
||||||
|
|
||||||
async_register_timer_handler(hass, handle_timer)
|
async_register_timer_handler(hass, device_id, handle_timer)
|
||||||
|
|
||||||
# Cancel by starting time
|
# Cancel by starting time
|
||||||
result = await intent.async_handle(
|
result = await intent.async_handle(
|
||||||
@ -139,6 +142,7 @@ async def test_cancel_timer(hass: HomeAssistant, init_components) -> None:
|
|||||||
"start_minutes": {"value": 2},
|
"start_minutes": {"value": 2},
|
||||||
"start_seconds": {"value": 3},
|
"start_seconds": {"value": 3},
|
||||||
},
|
},
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
@ -172,6 +176,7 @@ async def test_cancel_timer(hass: HomeAssistant, init_components) -> None:
|
|||||||
"test",
|
"test",
|
||||||
intent.INTENT_CANCEL_TIMER,
|
intent.INTENT_CANCEL_TIMER,
|
||||||
{"name": {"value": timer_name}},
|
{"name": {"value": timer_name}},
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
@ -191,6 +196,7 @@ async def test_increase_timer(hass: HomeAssistant, init_components) -> None:
|
|||||||
timer_id: str | None = None
|
timer_id: str | None = None
|
||||||
original_total_seconds = -1
|
original_total_seconds = -1
|
||||||
|
|
||||||
|
@callback
|
||||||
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
||||||
nonlocal timer_id, original_total_seconds
|
nonlocal timer_id, original_total_seconds
|
||||||
|
|
||||||
@ -220,7 +226,7 @@ async def test_increase_timer(hass: HomeAssistant, init_components) -> None:
|
|||||||
assert timer.id == timer_id
|
assert timer.id == timer_id
|
||||||
cancelled_event.set()
|
cancelled_event.set()
|
||||||
|
|
||||||
async_register_timer_handler(hass, handle_timer)
|
async_register_timer_handler(hass, device_id, handle_timer)
|
||||||
|
|
||||||
result = await intent.async_handle(
|
result = await intent.async_handle(
|
||||||
hass,
|
hass,
|
||||||
@ -286,6 +292,7 @@ async def test_increase_timer(hass: HomeAssistant, init_components) -> None:
|
|||||||
"test",
|
"test",
|
||||||
intent.INTENT_CANCEL_TIMER,
|
intent.INTENT_CANCEL_TIMER,
|
||||||
{"name": {"value": timer_name}},
|
{"name": {"value": timer_name}},
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
@ -305,6 +312,7 @@ async def test_decrease_timer(hass: HomeAssistant, init_components) -> None:
|
|||||||
timer_id: str | None = None
|
timer_id: str | None = None
|
||||||
original_total_seconds = 0
|
original_total_seconds = 0
|
||||||
|
|
||||||
|
@callback
|
||||||
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
||||||
nonlocal timer_id, original_total_seconds
|
nonlocal timer_id, original_total_seconds
|
||||||
|
|
||||||
@ -335,7 +343,7 @@ async def test_decrease_timer(hass: HomeAssistant, init_components) -> None:
|
|||||||
assert timer.id == timer_id
|
assert timer.id == timer_id
|
||||||
cancelled_event.set()
|
cancelled_event.set()
|
||||||
|
|
||||||
async_register_timer_handler(hass, handle_timer)
|
async_register_timer_handler(hass, device_id, handle_timer)
|
||||||
|
|
||||||
result = await intent.async_handle(
|
result = await intent.async_handle(
|
||||||
hass,
|
hass,
|
||||||
@ -380,6 +388,7 @@ async def test_decrease_timer(hass: HomeAssistant, init_components) -> None:
|
|||||||
"test",
|
"test",
|
||||||
intent.INTENT_CANCEL_TIMER,
|
intent.INTENT_CANCEL_TIMER,
|
||||||
{"name": {"value": timer_name}},
|
{"name": {"value": timer_name}},
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
@ -394,13 +403,15 @@ async def test_decrease_timer_below_zero(hass: HomeAssistant, init_components) -
|
|||||||
updated_event = asyncio.Event()
|
updated_event = asyncio.Event()
|
||||||
finished_event = asyncio.Event()
|
finished_event = asyncio.Event()
|
||||||
|
|
||||||
|
device_id = "test_device"
|
||||||
timer_id: str | None = None
|
timer_id: str | None = None
|
||||||
original_total_seconds = 0
|
original_total_seconds = 0
|
||||||
|
|
||||||
|
@callback
|
||||||
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
||||||
nonlocal timer_id, original_total_seconds
|
nonlocal timer_id, original_total_seconds
|
||||||
|
|
||||||
assert timer.device_id is None
|
assert timer.device_id == device_id
|
||||||
assert timer.name is None
|
assert timer.name is None
|
||||||
assert timer.start_hours == 1
|
assert timer.start_hours == 1
|
||||||
assert timer.start_minutes == 2
|
assert timer.start_minutes == 2
|
||||||
@ -425,7 +436,7 @@ async def test_decrease_timer_below_zero(hass: HomeAssistant, init_components) -
|
|||||||
assert timer.id == timer_id
|
assert timer.id == timer_id
|
||||||
finished_event.set()
|
finished_event.set()
|
||||||
|
|
||||||
async_register_timer_handler(hass, handle_timer)
|
async_register_timer_handler(hass, device_id, handle_timer)
|
||||||
|
|
||||||
result = await intent.async_handle(
|
result = await intent.async_handle(
|
||||||
hass,
|
hass,
|
||||||
@ -436,6 +447,7 @@ async def test_decrease_timer_below_zero(hass: HomeAssistant, init_components) -
|
|||||||
"minutes": {"value": 2},
|
"minutes": {"value": 2},
|
||||||
"seconds": {"value": 3},
|
"seconds": {"value": 3},
|
||||||
},
|
},
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
@ -454,6 +466,7 @@ async def test_decrease_timer_below_zero(hass: HomeAssistant, init_components) -
|
|||||||
"start_seconds": {"value": 3},
|
"start_seconds": {"value": 3},
|
||||||
"seconds": {"value": original_total_seconds + 1},
|
"seconds": {"value": original_total_seconds + 1},
|
||||||
},
|
},
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
@ -466,12 +479,60 @@ async def test_decrease_timer_below_zero(hass: HomeAssistant, init_components) -
|
|||||||
|
|
||||||
async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None:
|
async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None:
|
||||||
"""Test finding a timer with the wrong info."""
|
"""Test finding a timer with the wrong info."""
|
||||||
|
device_id = "test_device"
|
||||||
|
|
||||||
|
for intent_name in (
|
||||||
|
intent.INTENT_START_TIMER,
|
||||||
|
intent.INTENT_CANCEL_TIMER,
|
||||||
|
intent.INTENT_PAUSE_TIMER,
|
||||||
|
intent.INTENT_UNPAUSE_TIMER,
|
||||||
|
intent.INTENT_INCREASE_TIMER,
|
||||||
|
intent.INTENT_DECREASE_TIMER,
|
||||||
|
intent.INTENT_TIMER_STATUS,
|
||||||
|
):
|
||||||
|
if intent_name in (
|
||||||
|
intent.INTENT_START_TIMER,
|
||||||
|
intent.INTENT_INCREASE_TIMER,
|
||||||
|
intent.INTENT_DECREASE_TIMER,
|
||||||
|
):
|
||||||
|
slots = {"minutes": {"value": 5}}
|
||||||
|
else:
|
||||||
|
slots = {}
|
||||||
|
|
||||||
|
# No device id
|
||||||
|
with pytest.raises(TimersNotSupportedError):
|
||||||
|
await intent.async_handle(
|
||||||
|
hass,
|
||||||
|
"test",
|
||||||
|
intent_name,
|
||||||
|
slots,
|
||||||
|
device_id=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Unregistered device
|
||||||
|
with pytest.raises(TimersNotSupportedError):
|
||||||
|
await intent.async_handle(
|
||||||
|
hass,
|
||||||
|
"test",
|
||||||
|
intent_name,
|
||||||
|
slots,
|
||||||
|
device_id=device_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Must register a handler before we can do anything with timers
|
||||||
|
@callback
|
||||||
|
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async_register_timer_handler(hass, device_id, handle_timer)
|
||||||
|
|
||||||
# Start a 5 minute timer for pizza
|
# Start a 5 minute timer for pizza
|
||||||
result = await intent.async_handle(
|
result = await intent.async_handle(
|
||||||
hass,
|
hass,
|
||||||
"test",
|
"test",
|
||||||
intent.INTENT_START_TIMER,
|
intent.INTENT_START_TIMER,
|
||||||
{"name": {"value": "pizza"}, "minutes": {"value": 5}},
|
{"name": {"value": "pizza"}, "minutes": {"value": 5}},
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
|
|
||||||
@ -481,6 +542,7 @@ async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None:
|
|||||||
"test",
|
"test",
|
||||||
intent.INTENT_INCREASE_TIMER,
|
intent.INTENT_INCREASE_TIMER,
|
||||||
{"name": {"value": "PIZZA "}, "minutes": {"value": 1}},
|
{"name": {"value": "PIZZA "}, "minutes": {"value": 1}},
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
|
|
||||||
@ -491,6 +553,7 @@ async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None:
|
|||||||
"test",
|
"test",
|
||||||
intent.INTENT_CANCEL_TIMER,
|
intent.INTENT_CANCEL_TIMER,
|
||||||
{"name": {"value": "does-not-exist"}},
|
{"name": {"value": "does-not-exist"}},
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Right start time
|
# Right start time
|
||||||
@ -499,6 +562,7 @@ async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None:
|
|||||||
"test",
|
"test",
|
||||||
intent.INTENT_INCREASE_TIMER,
|
intent.INTENT_INCREASE_TIMER,
|
||||||
{"start_minutes": {"value": 5}, "minutes": {"value": 1}},
|
{"start_minutes": {"value": 5}, "minutes": {"value": 1}},
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
|
|
||||||
@ -509,6 +573,7 @@ async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None:
|
|||||||
"test",
|
"test",
|
||||||
intent.INTENT_CANCEL_TIMER,
|
intent.INTENT_CANCEL_TIMER,
|
||||||
{"start_minutes": {"value": 1}},
|
{"start_minutes": {"value": 1}},
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -523,6 +588,17 @@ async def test_disambiguation(
|
|||||||
entry = MockConfigEntry()
|
entry = MockConfigEntry()
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
cancelled_event = asyncio.Event()
|
||||||
|
timer_info: TimerInfo | None = None
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
||||||
|
nonlocal timer_info
|
||||||
|
|
||||||
|
if event_type == TimerEventType.CANCELLED:
|
||||||
|
timer_info = timer
|
||||||
|
cancelled_event.set()
|
||||||
|
|
||||||
# Alice is upstairs in the study
|
# Alice is upstairs in the study
|
||||||
floor_upstairs = floor_registry.async_create("upstairs")
|
floor_upstairs = floor_registry.async_create("upstairs")
|
||||||
area_study = area_registry.async_create("study")
|
area_study = area_registry.async_create("study")
|
||||||
@ -551,6 +627,9 @@ async def test_disambiguation(
|
|||||||
device_bob_kitchen_1.id, area_id=area_kitchen.id
|
device_bob_kitchen_1.id, area_id=area_kitchen.id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async_register_timer_handler(hass, device_alice_study.id, handle_timer)
|
||||||
|
async_register_timer_handler(hass, device_bob_kitchen_1.id, handle_timer)
|
||||||
|
|
||||||
# Alice: set a 3 minute timer
|
# Alice: set a 3 minute timer
|
||||||
result = await intent.async_handle(
|
result = await intent.async_handle(
|
||||||
hass,
|
hass,
|
||||||
@ -591,20 +670,9 @@ async def test_disambiguation(
|
|||||||
assert timers[0].get(ATTR_DEVICE_ID) == device_bob_kitchen_1.id
|
assert timers[0].get(ATTR_DEVICE_ID) == device_bob_kitchen_1.id
|
||||||
assert timers[1].get(ATTR_DEVICE_ID) == device_alice_study.id
|
assert timers[1].get(ATTR_DEVICE_ID) == device_alice_study.id
|
||||||
|
|
||||||
# Listen for timer cancellation
|
|
||||||
cancelled_event = asyncio.Event()
|
|
||||||
timer_info: TimerInfo | None = None
|
|
||||||
|
|
||||||
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
|
||||||
nonlocal timer_info
|
|
||||||
|
|
||||||
if event_type == TimerEventType.CANCELLED:
|
|
||||||
timer_info = timer
|
|
||||||
cancelled_event.set()
|
|
||||||
|
|
||||||
async_register_timer_handler(hass, handle_timer)
|
|
||||||
|
|
||||||
# Alice: cancel my timer
|
# Alice: cancel my timer
|
||||||
|
cancelled_event.clear()
|
||||||
|
timer_info = None
|
||||||
result = await intent.async_handle(
|
result = await intent.async_handle(
|
||||||
hass, "test", intent.INTENT_CANCEL_TIMER, {}, device_id=device_alice_study.id
|
hass, "test", intent.INTENT_CANCEL_TIMER, {}, device_id=device_alice_study.id
|
||||||
)
|
)
|
||||||
@ -651,6 +719,9 @@ async def test_disambiguation(
|
|||||||
device_bob_living_room.id, area_id=area_living_room.id
|
device_bob_living_room.id, area_id=area_living_room.id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async_register_timer_handler(hass, device_alice_bedroom.id, handle_timer)
|
||||||
|
async_register_timer_handler(hass, device_bob_living_room.id, handle_timer)
|
||||||
|
|
||||||
# Alice: set a 3 minute timer (study)
|
# Alice: set a 3 minute timer (study)
|
||||||
result = await intent.async_handle(
|
result = await intent.async_handle(
|
||||||
hass,
|
hass,
|
||||||
@ -720,13 +791,23 @@ async def test_disambiguation(
|
|||||||
assert timer_info.device_id == device_alice_study.id
|
assert timer_info.device_id == device_alice_study.id
|
||||||
assert timer_info.start_minutes == 3
|
assert timer_info.start_minutes == 3
|
||||||
|
|
||||||
# Trying to cancel the remaining two timers without area/floor info fails
|
# Trying to cancel the remaining two timers from a disconnected area fails
|
||||||
|
area_garage = area_registry.async_create("garage")
|
||||||
|
device_garage = device_registry.async_get_or_create(
|
||||||
|
config_entry_id=entry.entry_id,
|
||||||
|
connections=set(),
|
||||||
|
identifiers={("test", "garage")},
|
||||||
|
)
|
||||||
|
device_registry.async_update_device(device_garage.id, area_id=area_garage.id)
|
||||||
|
async_register_timer_handler(hass, device_garage.id, handle_timer)
|
||||||
|
|
||||||
with pytest.raises(MultipleTimersMatchedError):
|
with pytest.raises(MultipleTimersMatchedError):
|
||||||
await intent.async_handle(
|
await intent.async_handle(
|
||||||
hass,
|
hass,
|
||||||
"test",
|
"test",
|
||||||
intent.INTENT_CANCEL_TIMER,
|
intent.INTENT_CANCEL_TIMER,
|
||||||
{},
|
{},
|
||||||
|
device_id=device_garage.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Alice cancels the bedroom timer from study (same floor)
|
# Alice cancels the bedroom timer from study (same floor)
|
||||||
@ -755,6 +836,8 @@ async def test_disambiguation(
|
|||||||
device_bob_kitchen_2.id, area_id=area_kitchen.id
|
device_bob_kitchen_2.id, area_id=area_kitchen.id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async_register_timer_handler(hass, device_bob_kitchen_2.id, handle_timer)
|
||||||
|
|
||||||
# Bob cancels the kitchen timer from a different device
|
# Bob cancels the kitchen timer from a different device
|
||||||
cancelled_event.clear()
|
cancelled_event.clear()
|
||||||
timer_info = None
|
timer_info = None
|
||||||
@ -788,11 +871,14 @@ async def test_disambiguation(
|
|||||||
|
|
||||||
async def test_pause_unpause_timer(hass: HomeAssistant, init_components) -> None:
|
async def test_pause_unpause_timer(hass: HomeAssistant, init_components) -> None:
|
||||||
"""Test pausing and unpausing a running timer."""
|
"""Test pausing and unpausing a running timer."""
|
||||||
|
device_id = "test_device"
|
||||||
|
|
||||||
started_event = asyncio.Event()
|
started_event = asyncio.Event()
|
||||||
updated_event = asyncio.Event()
|
updated_event = asyncio.Event()
|
||||||
|
|
||||||
expected_active = True
|
expected_active = True
|
||||||
|
|
||||||
|
@callback
|
||||||
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
||||||
if event_type == TimerEventType.STARTED:
|
if event_type == TimerEventType.STARTED:
|
||||||
started_event.set()
|
started_event.set()
|
||||||
@ -800,10 +886,14 @@ async def test_pause_unpause_timer(hass: HomeAssistant, init_components) -> None
|
|||||||
assert timer.is_active == expected_active
|
assert timer.is_active == expected_active
|
||||||
updated_event.set()
|
updated_event.set()
|
||||||
|
|
||||||
async_register_timer_handler(hass, handle_timer)
|
async_register_timer_handler(hass, device_id, handle_timer)
|
||||||
|
|
||||||
result = await intent.async_handle(
|
result = await intent.async_handle(
|
||||||
hass, "test", intent.INTENT_START_TIMER, {"minutes": {"value": 5}}
|
hass,
|
||||||
|
"test",
|
||||||
|
intent.INTENT_START_TIMER,
|
||||||
|
{"minutes": {"value": 5}},
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
|
|
||||||
@ -812,7 +902,9 @@ async def test_pause_unpause_timer(hass: HomeAssistant, init_components) -> None
|
|||||||
|
|
||||||
# Pause the timer
|
# Pause the timer
|
||||||
expected_active = False
|
expected_active = False
|
||||||
result = await intent.async_handle(hass, "test", intent.INTENT_PAUSE_TIMER, {})
|
result = await intent.async_handle(
|
||||||
|
hass, "test", intent.INTENT_PAUSE_TIMER, {}, device_id=device_id
|
||||||
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
|
|
||||||
async with asyncio.timeout(1):
|
async with asyncio.timeout(1):
|
||||||
@ -820,14 +912,18 @@ async def test_pause_unpause_timer(hass: HomeAssistant, init_components) -> None
|
|||||||
|
|
||||||
# Pausing again will not fire the event
|
# Pausing again will not fire the event
|
||||||
updated_event.clear()
|
updated_event.clear()
|
||||||
result = await intent.async_handle(hass, "test", intent.INTENT_PAUSE_TIMER, {})
|
result = await intent.async_handle(
|
||||||
|
hass, "test", intent.INTENT_PAUSE_TIMER, {}, device_id=device_id
|
||||||
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
assert not updated_event.is_set()
|
assert not updated_event.is_set()
|
||||||
|
|
||||||
# Unpause the timer
|
# Unpause the timer
|
||||||
updated_event.clear()
|
updated_event.clear()
|
||||||
expected_active = True
|
expected_active = True
|
||||||
result = await intent.async_handle(hass, "test", intent.INTENT_UNPAUSE_TIMER, {})
|
result = await intent.async_handle(
|
||||||
|
hass, "test", intent.INTENT_UNPAUSE_TIMER, {}, device_id=device_id
|
||||||
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
|
|
||||||
async with asyncio.timeout(1):
|
async with asyncio.timeout(1):
|
||||||
@ -835,7 +931,9 @@ async def test_pause_unpause_timer(hass: HomeAssistant, init_components) -> None
|
|||||||
|
|
||||||
# Unpausing again will not fire the event
|
# Unpausing again will not fire the event
|
||||||
updated_event.clear()
|
updated_event.clear()
|
||||||
result = await intent.async_handle(hass, "test", intent.INTENT_UNPAUSE_TIMER, {})
|
result = await intent.async_handle(
|
||||||
|
hass, "test", intent.INTENT_UNPAUSE_TIMER, {}, device_id=device_id
|
||||||
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
assert not updated_event.is_set()
|
assert not updated_event.is_set()
|
||||||
|
|
||||||
@ -860,11 +958,63 @@ async def test_timer_not_found(hass: HomeAssistant) -> None:
|
|||||||
timer_manager.unpause_timer("does-not-exist")
|
timer_manager.unpause_timer("does-not-exist")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_timers_not_supported(hass: HomeAssistant) -> None:
|
||||||
|
"""Test unregistered device ids raise TimersNotSupportedError."""
|
||||||
|
timer_manager = TimerManager(hass)
|
||||||
|
|
||||||
|
with pytest.raises(TimersNotSupportedError):
|
||||||
|
timer_manager.start_timer(
|
||||||
|
"does-not-exist",
|
||||||
|
hours=None,
|
||||||
|
minutes=5,
|
||||||
|
seconds=None,
|
||||||
|
language=hass.config.language,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start a timer
|
||||||
|
@callback
|
||||||
|
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
device_id = "test_device"
|
||||||
|
unregister = timer_manager.register_handler(device_id, handle_timer)
|
||||||
|
|
||||||
|
timer_id = timer_manager.start_timer(
|
||||||
|
device_id,
|
||||||
|
hours=None,
|
||||||
|
minutes=5,
|
||||||
|
seconds=None,
|
||||||
|
language=hass.config.language,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Unregister handler so device no longer "supports" timers
|
||||||
|
unregister()
|
||||||
|
|
||||||
|
# All operations on the timer should fail now
|
||||||
|
with pytest.raises(TimersNotSupportedError):
|
||||||
|
timer_manager.add_time(timer_id, 1)
|
||||||
|
|
||||||
|
with pytest.raises(TimersNotSupportedError):
|
||||||
|
timer_manager.remove_time(timer_id, 1)
|
||||||
|
|
||||||
|
with pytest.raises(TimersNotSupportedError):
|
||||||
|
timer_manager.pause_timer(timer_id)
|
||||||
|
|
||||||
|
with pytest.raises(TimersNotSupportedError):
|
||||||
|
timer_manager.unpause_timer(timer_id)
|
||||||
|
|
||||||
|
with pytest.raises(TimersNotSupportedError):
|
||||||
|
timer_manager.cancel_timer(timer_id)
|
||||||
|
|
||||||
|
|
||||||
async def test_timer_status_with_names(hass: HomeAssistant, init_components) -> None:
|
async def test_timer_status_with_names(hass: HomeAssistant, init_components) -> None:
|
||||||
"""Test getting the status of named timers."""
|
"""Test getting the status of named timers."""
|
||||||
|
device_id = "test_device"
|
||||||
|
|
||||||
started_event = asyncio.Event()
|
started_event = asyncio.Event()
|
||||||
num_started = 0
|
num_started = 0
|
||||||
|
|
||||||
|
@callback
|
||||||
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
||||||
nonlocal num_started
|
nonlocal num_started
|
||||||
|
|
||||||
@ -873,7 +1023,7 @@ async def test_timer_status_with_names(hass: HomeAssistant, init_components) ->
|
|||||||
if num_started == 4:
|
if num_started == 4:
|
||||||
started_event.set()
|
started_event.set()
|
||||||
|
|
||||||
async_register_timer_handler(hass, handle_timer)
|
async_register_timer_handler(hass, device_id, handle_timer)
|
||||||
|
|
||||||
# Start timers with names
|
# Start timers with names
|
||||||
result = await intent.async_handle(
|
result = await intent.async_handle(
|
||||||
@ -881,6 +1031,7 @@ async def test_timer_status_with_names(hass: HomeAssistant, init_components) ->
|
|||||||
"test",
|
"test",
|
||||||
intent.INTENT_START_TIMER,
|
intent.INTENT_START_TIMER,
|
||||||
{"name": {"value": "pizza"}, "minutes": {"value": 10}},
|
{"name": {"value": "pizza"}, "minutes": {"value": 10}},
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
|
|
||||||
@ -889,6 +1040,7 @@ async def test_timer_status_with_names(hass: HomeAssistant, init_components) ->
|
|||||||
"test",
|
"test",
|
||||||
intent.INTENT_START_TIMER,
|
intent.INTENT_START_TIMER,
|
||||||
{"name": {"value": "pizza"}, "minutes": {"value": 15}},
|
{"name": {"value": "pizza"}, "minutes": {"value": 15}},
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
|
|
||||||
@ -897,6 +1049,7 @@ async def test_timer_status_with_names(hass: HomeAssistant, init_components) ->
|
|||||||
"test",
|
"test",
|
||||||
intent.INTENT_START_TIMER,
|
intent.INTENT_START_TIMER,
|
||||||
{"name": {"value": "cookies"}, "minutes": {"value": 20}},
|
{"name": {"value": "cookies"}, "minutes": {"value": 20}},
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
|
|
||||||
@ -905,6 +1058,7 @@ async def test_timer_status_with_names(hass: HomeAssistant, init_components) ->
|
|||||||
"test",
|
"test",
|
||||||
intent.INTENT_START_TIMER,
|
intent.INTENT_START_TIMER,
|
||||||
{"name": {"value": "chicken"}, "hours": {"value": 2}, "seconds": {"value": 30}},
|
{"name": {"value": "chicken"}, "hours": {"value": 2}, "seconds": {"value": 30}},
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
|
|
||||||
@ -913,7 +1067,9 @@ async def test_timer_status_with_names(hass: HomeAssistant, init_components) ->
|
|||||||
await started_event.wait()
|
await started_event.wait()
|
||||||
|
|
||||||
# No constraints returns all timers
|
# No constraints returns all timers
|
||||||
result = await intent.async_handle(hass, "test", intent.INTENT_TIMER_STATUS, {})
|
result = await intent.async_handle(
|
||||||
|
hass, "test", intent.INTENT_TIMER_STATUS, {}, device_id=device_id
|
||||||
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
timers = result.speech_slots.get("timers", [])
|
timers = result.speech_slots.get("timers", [])
|
||||||
assert len(timers) == 4
|
assert len(timers) == 4
|
||||||
@ -925,6 +1081,7 @@ async def test_timer_status_with_names(hass: HomeAssistant, init_components) ->
|
|||||||
"test",
|
"test",
|
||||||
intent.INTENT_TIMER_STATUS,
|
intent.INTENT_TIMER_STATUS,
|
||||||
{"name": {"value": "cookies"}},
|
{"name": {"value": "cookies"}},
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
timers = result.speech_slots.get("timers", [])
|
timers = result.speech_slots.get("timers", [])
|
||||||
@ -938,6 +1095,7 @@ async def test_timer_status_with_names(hass: HomeAssistant, init_components) ->
|
|||||||
"test",
|
"test",
|
||||||
intent.INTENT_TIMER_STATUS,
|
intent.INTENT_TIMER_STATUS,
|
||||||
{"name": {"value": "pizza"}},
|
{"name": {"value": "pizza"}},
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
timers = result.speech_slots.get("timers", [])
|
timers = result.speech_slots.get("timers", [])
|
||||||
@ -952,6 +1110,7 @@ async def test_timer_status_with_names(hass: HomeAssistant, init_components) ->
|
|||||||
"test",
|
"test",
|
||||||
intent.INTENT_TIMER_STATUS,
|
intent.INTENT_TIMER_STATUS,
|
||||||
{"name": {"value": "pizza"}, "start_minutes": {"value": 10}},
|
{"name": {"value": "pizza"}, "start_minutes": {"value": 10}},
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
timers = result.speech_slots.get("timers", [])
|
timers = result.speech_slots.get("timers", [])
|
||||||
@ -969,6 +1128,7 @@ async def test_timer_status_with_names(hass: HomeAssistant, init_components) ->
|
|||||||
"start_hours": {"value": 2},
|
"start_hours": {"value": 2},
|
||||||
"start_seconds": {"value": 30},
|
"start_seconds": {"value": 30},
|
||||||
},
|
},
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
timers = result.speech_slots.get("timers", [])
|
timers = result.speech_slots.get("timers", [])
|
||||||
@ -980,7 +1140,11 @@ async def test_timer_status_with_names(hass: HomeAssistant, init_components) ->
|
|||||||
|
|
||||||
# Wrong name results in an empty list
|
# Wrong name results in an empty list
|
||||||
result = await intent.async_handle(
|
result = await intent.async_handle(
|
||||||
hass, "test", intent.INTENT_TIMER_STATUS, {"name": {"value": "does-not-exist"}}
|
hass,
|
||||||
|
"test",
|
||||||
|
intent.INTENT_TIMER_STATUS,
|
||||||
|
{"name": {"value": "does-not-exist"}},
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
timers = result.speech_slots.get("timers", [])
|
timers = result.speech_slots.get("timers", [])
|
||||||
@ -996,6 +1160,7 @@ async def test_timer_status_with_names(hass: HomeAssistant, init_components) ->
|
|||||||
"start_minutes": {"value": 100},
|
"start_minutes": {"value": 100},
|
||||||
"start_seconds": {"value": 100},
|
"start_seconds": {"value": 100},
|
||||||
},
|
},
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
timers = result.speech_slots.get("timers", [])
|
timers = result.speech_slots.get("timers", [])
|
||||||
@ -1034,6 +1199,7 @@ async def test_area_filter(
|
|||||||
num_timers = 3
|
num_timers = 3
|
||||||
num_started = 0
|
num_started = 0
|
||||||
|
|
||||||
|
@callback
|
||||||
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
||||||
nonlocal num_started
|
nonlocal num_started
|
||||||
|
|
||||||
@ -1042,7 +1208,8 @@ async def test_area_filter(
|
|||||||
if num_started == num_timers:
|
if num_started == num_timers:
|
||||||
started_event.set()
|
started_event.set()
|
||||||
|
|
||||||
async_register_timer_handler(hass, handle_timer)
|
async_register_timer_handler(hass, device_kitchen.id, handle_timer)
|
||||||
|
async_register_timer_handler(hass, device_living_room.id, handle_timer)
|
||||||
|
|
||||||
# Start timers in different areas
|
# Start timers in different areas
|
||||||
result = await intent.async_handle(
|
result = await intent.async_handle(
|
||||||
@ -1077,30 +1244,34 @@ async def test_area_filter(
|
|||||||
await started_event.wait()
|
await started_event.wait()
|
||||||
|
|
||||||
# No constraints returns all timers
|
# No constraints returns all timers
|
||||||
result = await intent.async_handle(hass, "test", intent.INTENT_TIMER_STATUS, {})
|
result = await intent.async_handle(
|
||||||
|
hass, "test", intent.INTENT_TIMER_STATUS, {}, device_id=device_kitchen.id
|
||||||
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
timers = result.speech_slots.get("timers", [])
|
timers = result.speech_slots.get("timers", [])
|
||||||
assert len(timers) == num_timers
|
assert len(timers) == num_timers
|
||||||
assert {t.get(ATTR_NAME) for t in timers} == {"pizza", "tv", "media"}
|
assert {t.get(ATTR_NAME) for t in timers} == {"pizza", "tv", "media"}
|
||||||
|
|
||||||
# Filter by area (kitchen)
|
# Filter by area (target kitchen from living room)
|
||||||
result = await intent.async_handle(
|
result = await intent.async_handle(
|
||||||
hass,
|
hass,
|
||||||
"test",
|
"test",
|
||||||
intent.INTENT_TIMER_STATUS,
|
intent.INTENT_TIMER_STATUS,
|
||||||
{"area": {"value": "kitchen"}},
|
{"area": {"value": "kitchen"}},
|
||||||
|
device_id=device_living_room.id,
|
||||||
)
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
timers = result.speech_slots.get("timers", [])
|
timers = result.speech_slots.get("timers", [])
|
||||||
assert len(timers) == 1
|
assert len(timers) == 1
|
||||||
assert timers[0].get(ATTR_NAME) == "pizza"
|
assert timers[0].get(ATTR_NAME) == "pizza"
|
||||||
|
|
||||||
# Filter by area (living room)
|
# Filter by area (target living room from kitchen)
|
||||||
result = await intent.async_handle(
|
result = await intent.async_handle(
|
||||||
hass,
|
hass,
|
||||||
"test",
|
"test",
|
||||||
intent.INTENT_TIMER_STATUS,
|
intent.INTENT_TIMER_STATUS,
|
||||||
{"area": {"value": "living room"}},
|
{"area": {"value": "living room"}},
|
||||||
|
device_id=device_kitchen.id,
|
||||||
)
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
timers = result.speech_slots.get("timers", [])
|
timers = result.speech_slots.get("timers", [])
|
||||||
@ -1113,6 +1284,7 @@ async def test_area_filter(
|
|||||||
"test",
|
"test",
|
||||||
intent.INTENT_TIMER_STATUS,
|
intent.INTENT_TIMER_STATUS,
|
||||||
{"area": {"value": "living room"}, "name": {"value": "tv"}},
|
{"area": {"value": "living room"}, "name": {"value": "tv"}},
|
||||||
|
device_id=device_kitchen.id,
|
||||||
)
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
timers = result.speech_slots.get("timers", [])
|
timers = result.speech_slots.get("timers", [])
|
||||||
@ -1125,6 +1297,7 @@ async def test_area_filter(
|
|||||||
"test",
|
"test",
|
||||||
intent.INTENT_TIMER_STATUS,
|
intent.INTENT_TIMER_STATUS,
|
||||||
{"area": {"value": "living room"}, "start_minutes": {"value": 15}},
|
{"area": {"value": "living room"}, "start_minutes": {"value": 15}},
|
||||||
|
device_id=device_kitchen.id,
|
||||||
)
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
timers = result.speech_slots.get("timers", [])
|
timers = result.speech_slots.get("timers", [])
|
||||||
@ -1137,6 +1310,7 @@ async def test_area_filter(
|
|||||||
"test",
|
"test",
|
||||||
intent.INTENT_TIMER_STATUS,
|
intent.INTENT_TIMER_STATUS,
|
||||||
{"area": {"value": "does-not-exist"}},
|
{"area": {"value": "does-not-exist"}},
|
||||||
|
device_id=device_kitchen.id,
|
||||||
)
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
timers = result.speech_slots.get("timers", [])
|
timers = result.speech_slots.get("timers", [])
|
||||||
@ -1148,6 +1322,7 @@ async def test_area_filter(
|
|||||||
"test",
|
"test",
|
||||||
intent.INTENT_CANCEL_TIMER,
|
intent.INTENT_CANCEL_TIMER,
|
||||||
{"area": {"value": "living room"}, "start_minutes": {"value": 15}},
|
{"area": {"value": "living room"}, "start_minutes": {"value": 15}},
|
||||||
|
device_id=device_living_room.id,
|
||||||
)
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
|
|
||||||
@ -1157,6 +1332,7 @@ async def test_area_filter(
|
|||||||
"test",
|
"test",
|
||||||
intent.INTENT_CANCEL_TIMER,
|
intent.INTENT_CANCEL_TIMER,
|
||||||
{"area": {"value": "living room"}},
|
{"area": {"value": "living room"}},
|
||||||
|
device_id=device_living_room.id,
|
||||||
)
|
)
|
||||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user