mirror of https://github.com/home-assistant/core
1590 lines
49 KiB
Python
1590 lines
49 KiB
Python
"""Tests for intent timers."""
|
|
|
|
import asyncio
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from homeassistant.components.intent.timers import (
|
|
MultipleTimersMatchedError,
|
|
TimerEventType,
|
|
TimerInfo,
|
|
TimerManager,
|
|
TimerNotFoundError,
|
|
TimersNotSupportedError,
|
|
_round_time,
|
|
async_device_supports_timers,
|
|
async_register_timer_handler,
|
|
)
|
|
from homeassistant.const import ATTR_DEVICE_ID, ATTR_NAME
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.helpers import (
|
|
area_registry as ar,
|
|
device_registry as dr,
|
|
floor_registry as fr,
|
|
intent,
|
|
)
|
|
from homeassistant.setup import async_setup_component
|
|
|
|
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, "intent", {})
|
|
|
|
|
|
async def test_start_finish_timer(hass: HomeAssistant, init_components) -> None:
|
|
"""Test starting a timer and having it finish."""
|
|
device_id = "test_device"
|
|
timer_name = "test timer"
|
|
started_event = asyncio.Event()
|
|
finished_event = asyncio.Event()
|
|
|
|
timer_id: str | None = None
|
|
|
|
@callback
|
|
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
|
nonlocal timer_id
|
|
|
|
assert timer.name == timer_name
|
|
assert timer.device_id == device_id
|
|
assert timer.start_hours is None
|
|
assert timer.start_minutes is None
|
|
assert timer.start_seconds == 0
|
|
assert timer.seconds_left == 0
|
|
assert timer.created_seconds == 0
|
|
|
|
if event_type == TimerEventType.STARTED:
|
|
timer_id = timer.id
|
|
started_event.set()
|
|
elif event_type == TimerEventType.FINISHED:
|
|
assert timer.id == timer_id
|
|
finished_event.set()
|
|
|
|
async_register_timer_handler(hass, device_id, handle_timer)
|
|
|
|
# A device that has been registered to handle timers is required
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{
|
|
"name": {"value": timer_name},
|
|
"seconds": {"value": 0},
|
|
},
|
|
device_id=device_id,
|
|
)
|
|
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await asyncio.gather(started_event.wait(), finished_event.wait())
|
|
|
|
|
|
async def test_cancel_timer(hass: HomeAssistant, init_components) -> None:
|
|
"""Test cancelling a timer."""
|
|
device_id = "test_device"
|
|
timer_name: str | None = None
|
|
started_event = asyncio.Event()
|
|
cancelled_event = asyncio.Event()
|
|
|
|
timer_id: str | None = None
|
|
|
|
@callback
|
|
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
|
nonlocal timer_id
|
|
|
|
assert timer.device_id == device_id
|
|
assert timer.start_hours == 1
|
|
assert timer.start_minutes == 2
|
|
assert timer.start_seconds == 3
|
|
|
|
if timer_name is not None:
|
|
assert timer.name == timer_name
|
|
|
|
if event_type == TimerEventType.STARTED:
|
|
timer_id = timer.id
|
|
assert (
|
|
timer.seconds_left
|
|
== (60 * 60 * timer.start_hours)
|
|
+ (60 * timer.start_minutes)
|
|
+ timer.start_seconds
|
|
)
|
|
started_event.set()
|
|
elif event_type == TimerEventType.CANCELLED:
|
|
assert timer.id == timer_id
|
|
assert timer.seconds_left == 0
|
|
cancelled_event.set()
|
|
|
|
async_register_timer_handler(hass, device_id, handle_timer)
|
|
|
|
# Cancel by starting time
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{
|
|
"hours": {"value": 1},
|
|
"minutes": {"value": 2},
|
|
"seconds": {"value": 3},
|
|
},
|
|
device_id=device_id,
|
|
)
|
|
|
|
async with asyncio.timeout(1):
|
|
await started_event.wait()
|
|
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_CANCEL_TIMER,
|
|
{
|
|
"start_hours": {"value": 1},
|
|
"start_minutes": {"value": 2},
|
|
"start_seconds": {"value": 3},
|
|
},
|
|
device_id=device_id,
|
|
)
|
|
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await cancelled_event.wait()
|
|
|
|
# Cancel by name
|
|
timer_name = "test timer"
|
|
started_event.clear()
|
|
cancelled_event.clear()
|
|
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{
|
|
"name": {"value": timer_name},
|
|
"hours": {"value": 1},
|
|
"minutes": {"value": 2},
|
|
"seconds": {"value": 3},
|
|
},
|
|
device_id=device_id,
|
|
)
|
|
|
|
async with asyncio.timeout(1):
|
|
await started_event.wait()
|
|
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_CANCEL_TIMER,
|
|
{"name": {"value": timer_name}},
|
|
device_id=device_id,
|
|
)
|
|
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await cancelled_event.wait()
|
|
|
|
# Cancel without a device
|
|
timer_name = None
|
|
started_event.clear()
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{
|
|
"hours": {"value": 1},
|
|
"minutes": {"value": 2},
|
|
"seconds": {"value": 3},
|
|
},
|
|
device_id=device_id,
|
|
)
|
|
|
|
async with asyncio.timeout(1):
|
|
await started_event.wait()
|
|
|
|
result = await intent.async_handle(hass, "test", intent.INTENT_CANCEL_TIMER, {})
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
|
|
async def test_increase_timer(hass: HomeAssistant, init_components) -> None:
|
|
"""Test increasing the time of a running timer."""
|
|
device_id = "test_device"
|
|
started_event = asyncio.Event()
|
|
updated_event = asyncio.Event()
|
|
cancelled_event = asyncio.Event()
|
|
|
|
timer_name = "test timer"
|
|
timer_id: str | None = None
|
|
original_total_seconds = -1
|
|
seconds_added = 0
|
|
|
|
@callback
|
|
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
|
nonlocal timer_id, original_total_seconds
|
|
|
|
assert timer.device_id == device_id
|
|
assert timer.start_hours == 1
|
|
assert timer.start_minutes == 2
|
|
assert timer.start_seconds == 3
|
|
|
|
if timer_name is not None:
|
|
assert timer.name == timer_name
|
|
|
|
if event_type == TimerEventType.STARTED:
|
|
timer_id = timer.id
|
|
original_total_seconds = (
|
|
(60 * 60 * timer.start_hours)
|
|
+ (60 * timer.start_minutes)
|
|
+ timer.start_seconds
|
|
)
|
|
assert timer.created_seconds == original_total_seconds
|
|
started_event.set()
|
|
elif event_type == TimerEventType.UPDATED:
|
|
assert timer.id == timer_id
|
|
|
|
# Timer was increased
|
|
assert timer.seconds_left > original_total_seconds
|
|
assert timer.created_seconds == original_total_seconds + seconds_added
|
|
updated_event.set()
|
|
elif event_type == TimerEventType.CANCELLED:
|
|
assert timer.id == timer_id
|
|
cancelled_event.set()
|
|
|
|
async_register_timer_handler(hass, device_id, handle_timer)
|
|
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{
|
|
"name": {"value": timer_name},
|
|
"hours": {"value": 1},
|
|
"minutes": {"value": 2},
|
|
"seconds": {"value": 3},
|
|
},
|
|
device_id=device_id,
|
|
)
|
|
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await started_event.wait()
|
|
|
|
# Adding 0 seconds has no effect
|
|
seconds_added = 0
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_INCREASE_TIMER,
|
|
{
|
|
"start_hours": {"value": 1},
|
|
"start_minutes": {"value": 2},
|
|
"start_seconds": {"value": 3},
|
|
"hours": {"value": 0},
|
|
"minutes": {"value": 0},
|
|
"seconds": {"value": 0},
|
|
},
|
|
)
|
|
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
assert not updated_event.is_set()
|
|
|
|
# Add 30 seconds to the timer
|
|
seconds_added = (1 * 60 * 60) + (5 * 60) + 30
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_INCREASE_TIMER,
|
|
{
|
|
"start_hours": {"value": 1},
|
|
"start_minutes": {"value": 2},
|
|
"start_seconds": {"value": 3},
|
|
"hours": {"value": 1},
|
|
"minutes": {"value": 5},
|
|
"seconds": {"value": 30},
|
|
},
|
|
)
|
|
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await updated_event.wait()
|
|
|
|
# Cancel the timer
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_CANCEL_TIMER,
|
|
{"name": {"value": timer_name}},
|
|
)
|
|
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await cancelled_event.wait()
|
|
|
|
|
|
async def test_decrease_timer(hass: HomeAssistant, init_components) -> None:
|
|
"""Test decreasing the time of a running timer."""
|
|
device_id = "test_device"
|
|
started_event = asyncio.Event()
|
|
updated_event = asyncio.Event()
|
|
cancelled_event = asyncio.Event()
|
|
|
|
timer_name = "test timer"
|
|
timer_id: str | None = None
|
|
original_total_seconds = 0
|
|
|
|
@callback
|
|
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
|
nonlocal timer_id, original_total_seconds
|
|
|
|
assert timer.device_id == device_id
|
|
assert timer.start_hours == 1
|
|
assert timer.start_minutes == 2
|
|
assert timer.start_seconds == 3
|
|
|
|
if timer_name is not None:
|
|
assert timer.name == timer_name
|
|
|
|
if event_type == TimerEventType.STARTED:
|
|
timer_id = timer.id
|
|
original_total_seconds = (
|
|
(60 * 60 * timer.start_hours)
|
|
+ (60 * timer.start_minutes)
|
|
+ timer.start_seconds
|
|
)
|
|
started_event.set()
|
|
elif event_type == TimerEventType.UPDATED:
|
|
assert timer.id == timer_id
|
|
|
|
# Timer was decreased
|
|
assert timer.seconds_left <= (original_total_seconds - 30)
|
|
assert timer.created_seconds == original_total_seconds
|
|
|
|
updated_event.set()
|
|
elif event_type == TimerEventType.CANCELLED:
|
|
assert timer.id == timer_id
|
|
cancelled_event.set()
|
|
|
|
async_register_timer_handler(hass, device_id, handle_timer)
|
|
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{
|
|
"name": {"value": timer_name},
|
|
"hours": {"value": 1},
|
|
"minutes": {"value": 2},
|
|
"seconds": {"value": 3},
|
|
},
|
|
device_id=device_id,
|
|
)
|
|
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await started_event.wait()
|
|
|
|
# Remove 30 seconds from the timer
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_DECREASE_TIMER,
|
|
{
|
|
"start_hours": {"value": 1},
|
|
"start_minutes": {"value": 2},
|
|
"start_seconds": {"value": 3},
|
|
"seconds": {"value": 30},
|
|
},
|
|
)
|
|
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await started_event.wait()
|
|
|
|
# Cancel the timer
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_CANCEL_TIMER,
|
|
{"name": {"value": timer_name}},
|
|
)
|
|
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await cancelled_event.wait()
|
|
|
|
|
|
async def test_decrease_timer_below_zero(hass: HomeAssistant, init_components) -> None:
|
|
"""Test decreasing the time of a running timer below 0 seconds."""
|
|
started_event = asyncio.Event()
|
|
updated_event = asyncio.Event()
|
|
finished_event = asyncio.Event()
|
|
|
|
device_id = "test_device"
|
|
timer_id: str | None = None
|
|
original_total_seconds = 0
|
|
|
|
@callback
|
|
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
|
nonlocal timer_id, original_total_seconds
|
|
|
|
assert timer.device_id == device_id
|
|
assert timer.name is None
|
|
assert timer.start_hours == 1
|
|
assert timer.start_minutes == 2
|
|
assert timer.start_seconds == 3
|
|
|
|
if event_type == TimerEventType.STARTED:
|
|
timer_id = timer.id
|
|
original_total_seconds = (
|
|
(60 * 60 * timer.start_hours)
|
|
+ (60 * timer.start_minutes)
|
|
+ timer.start_seconds
|
|
)
|
|
started_event.set()
|
|
elif event_type == TimerEventType.UPDATED:
|
|
assert timer.id == timer_id
|
|
|
|
# Timer was decreased below zero
|
|
assert timer.seconds_left == 0
|
|
|
|
updated_event.set()
|
|
elif event_type == TimerEventType.FINISHED:
|
|
assert timer.id == timer_id
|
|
finished_event.set()
|
|
|
|
async_register_timer_handler(hass, device_id, handle_timer)
|
|
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{
|
|
"hours": {"value": 1},
|
|
"minutes": {"value": 2},
|
|
"seconds": {"value": 3},
|
|
},
|
|
device_id=device_id,
|
|
)
|
|
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await started_event.wait()
|
|
|
|
# Remove more time than was on the timer
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_DECREASE_TIMER,
|
|
{
|
|
"start_hours": {"value": 1},
|
|
"start_minutes": {"value": 2},
|
|
"start_seconds": {"value": 3},
|
|
"seconds": {"value": original_total_seconds + 1},
|
|
},
|
|
)
|
|
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await asyncio.gather(
|
|
started_event.wait(), updated_event.wait(), finished_event.wait()
|
|
)
|
|
|
|
|
|
async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None:
|
|
"""Test finding a timer with the wrong info."""
|
|
device_id = "test_device"
|
|
|
|
# No device id
|
|
with pytest.raises(TimersNotSupportedError):
|
|
await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{"minutes": {"value": 5}},
|
|
device_id=None,
|
|
)
|
|
|
|
# Unregistered device
|
|
with pytest.raises(TimersNotSupportedError):
|
|
await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{"minutes": {"value": 5}},
|
|
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
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{"name": {"value": "pizza"}, "minutes": {"value": 5}},
|
|
device_id=device_id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
# Right name
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_INCREASE_TIMER,
|
|
{"name": {"value": "PIZZA "}, "minutes": {"value": 1}},
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
# Wrong name
|
|
with pytest.raises(intent.IntentError):
|
|
await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_CANCEL_TIMER,
|
|
{"name": {"value": "does-not-exist"}},
|
|
)
|
|
|
|
# Right start time
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_INCREASE_TIMER,
|
|
{"start_minutes": {"value": 5}, "minutes": {"value": 1}},
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
# Wrong start time
|
|
with pytest.raises(intent.IntentError):
|
|
await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_CANCEL_TIMER,
|
|
{"start_minutes": {"value": 1}},
|
|
)
|
|
|
|
|
|
async def test_disambiguation(
|
|
hass: HomeAssistant,
|
|
init_components,
|
|
area_registry: ar.AreaRegistry,
|
|
device_registry: dr.DeviceRegistry,
|
|
floor_registry: fr.FloorRegistry,
|
|
) -> None:
|
|
"""Test finding a timer by disambiguating with area/floor."""
|
|
entry = MockConfigEntry()
|
|
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
|
|
floor_upstairs = floor_registry.async_create("upstairs")
|
|
area_study = area_registry.async_create("study")
|
|
area_study = area_registry.async_update(
|
|
area_study.id, floor_id=floor_upstairs.floor_id
|
|
)
|
|
device_alice_study = device_registry.async_get_or_create(
|
|
config_entry_id=entry.entry_id,
|
|
connections=set(),
|
|
identifiers={("test", "alice")},
|
|
)
|
|
device_registry.async_update_device(device_alice_study.id, area_id=area_study.id)
|
|
|
|
# Bob is downstairs in the kitchen
|
|
floor_downstairs = floor_registry.async_create("downstairs")
|
|
area_kitchen = area_registry.async_create("kitchen")
|
|
area_kitchen = area_registry.async_update(
|
|
area_kitchen.id, floor_id=floor_downstairs.floor_id
|
|
)
|
|
device_bob_kitchen_1 = device_registry.async_get_or_create(
|
|
config_entry_id=entry.entry_id,
|
|
connections=set(),
|
|
identifiers={("test", "bob")},
|
|
)
|
|
device_registry.async_update_device(
|
|
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
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{"minutes": {"value": 3}},
|
|
device_id=device_alice_study.id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
# Bob: set a 3 minute timer
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{"minutes": {"value": 3}},
|
|
device_id=device_bob_kitchen_1.id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
# Alice should hear her timer listed first
|
|
result = await intent.async_handle(
|
|
hass, "test", intent.INTENT_TIMER_STATUS, {}, device_id=device_alice_study.id
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
timers = result.speech_slots.get("timers", [])
|
|
assert len(timers) == 2
|
|
assert timers[0].get(ATTR_DEVICE_ID) == device_alice_study.id
|
|
assert timers[1].get(ATTR_DEVICE_ID) == device_bob_kitchen_1.id
|
|
|
|
# Bob should hear his timer listed first
|
|
result = await intent.async_handle(
|
|
hass, "test", intent.INTENT_TIMER_STATUS, {}, device_id=device_bob_kitchen_1.id
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
timers = result.speech_slots.get("timers", [])
|
|
assert len(timers) == 2
|
|
assert timers[0].get(ATTR_DEVICE_ID) == device_bob_kitchen_1.id
|
|
assert timers[1].get(ATTR_DEVICE_ID) == device_alice_study.id
|
|
|
|
# Alice: cancel my timer
|
|
cancelled_event.clear()
|
|
timer_info = None
|
|
result = await intent.async_handle(
|
|
hass, "test", intent.INTENT_CANCEL_TIMER, {}, device_id=device_alice_study.id
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await cancelled_event.wait()
|
|
|
|
# Verify this is the 3 minute timer from Alice
|
|
assert timer_info is not None
|
|
assert timer_info.device_id == device_alice_study.id
|
|
assert timer_info.start_minutes == 3
|
|
|
|
# Cancel Bob's timer
|
|
result = await intent.async_handle(
|
|
hass, "test", intent.INTENT_CANCEL_TIMER, {}, device_id=device_bob_kitchen_1.id
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
# Add two new devices in two new areas, one upstairs and one downstairs
|
|
area_bedroom = area_registry.async_create("bedroom")
|
|
area_bedroom = area_registry.async_update(
|
|
area_bedroom.id, floor_id=floor_upstairs.floor_id
|
|
)
|
|
device_alice_bedroom = device_registry.async_get_or_create(
|
|
config_entry_id=entry.entry_id,
|
|
connections=set(),
|
|
identifiers={("test", "alice-2")},
|
|
)
|
|
device_registry.async_update_device(
|
|
device_alice_bedroom.id, area_id=area_bedroom.id
|
|
)
|
|
|
|
area_living_room = area_registry.async_create("living_room")
|
|
area_living_room = area_registry.async_update(
|
|
area_living_room.id, floor_id=floor_downstairs.floor_id
|
|
)
|
|
device_bob_living_room = device_registry.async_get_or_create(
|
|
config_entry_id=entry.entry_id,
|
|
connections=set(),
|
|
identifiers={("test", "bob-2")},
|
|
)
|
|
device_registry.async_update_device(
|
|
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)
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{"minutes": {"value": 3}},
|
|
device_id=device_alice_study.id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
# Alice: set a 3 minute timer (bedroom)
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{"minutes": {"value": 3}},
|
|
device_id=device_alice_bedroom.id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
# Bob: set a 3 minute timer (kitchen)
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{"minutes": {"value": 3}},
|
|
device_id=device_bob_kitchen_1.id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
# Bob: set a 3 minute timer (living room)
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{"minutes": {"value": 3}},
|
|
device_id=device_bob_living_room.id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
# Alice should hear the timer in her area first, then on her floor, then
|
|
# elsewhere.
|
|
result = await intent.async_handle(
|
|
hass, "test", intent.INTENT_TIMER_STATUS, {}, device_id=device_alice_study.id
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
timers = result.speech_slots.get("timers", [])
|
|
assert len(timers) == 4
|
|
assert timers[0].get(ATTR_DEVICE_ID) == device_alice_study.id
|
|
assert timers[1].get(ATTR_DEVICE_ID) == device_alice_bedroom.id
|
|
assert timers[2].get(ATTR_DEVICE_ID) == device_bob_kitchen_1.id
|
|
assert timers[3].get(ATTR_DEVICE_ID) == device_bob_living_room.id
|
|
|
|
# Alice cancels the study timer from study
|
|
cancelled_event.clear()
|
|
timer_info = None
|
|
result = await intent.async_handle(
|
|
hass, "test", intent.INTENT_CANCEL_TIMER, {}, device_id=device_alice_study.id
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await cancelled_event.wait()
|
|
|
|
# Verify this is the 3 minute timer from Alice in the study
|
|
assert timer_info is not None
|
|
assert timer_info.device_id == device_alice_study.id
|
|
assert timer_info.start_minutes == 3
|
|
|
|
# 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):
|
|
await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_CANCEL_TIMER,
|
|
{},
|
|
device_id=device_garage.id,
|
|
)
|
|
|
|
# Alice cancels the bedroom timer from study (same floor)
|
|
cancelled_event.clear()
|
|
timer_info = None
|
|
result = await intent.async_handle(
|
|
hass, "test", intent.INTENT_CANCEL_TIMER, {}, device_id=device_alice_study.id
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await cancelled_event.wait()
|
|
|
|
# Verify this is the 3 minute timer from Alice in the bedroom
|
|
assert timer_info is not None
|
|
assert timer_info.device_id == device_alice_bedroom.id
|
|
assert timer_info.start_minutes == 3
|
|
|
|
# Add a second device in the kitchen
|
|
device_bob_kitchen_2 = device_registry.async_get_or_create(
|
|
config_entry_id=entry.entry_id,
|
|
connections=set(),
|
|
identifiers={("test", "bob-3")},
|
|
)
|
|
device_registry.async_update_device(
|
|
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
|
|
cancelled_event.clear()
|
|
timer_info = None
|
|
result = await intent.async_handle(
|
|
hass, "test", intent.INTENT_CANCEL_TIMER, {}, device_id=device_bob_kitchen_2.id
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await cancelled_event.wait()
|
|
|
|
assert timer_info is not None
|
|
assert timer_info.device_id == device_bob_kitchen_1.id
|
|
assert timer_info.start_minutes == 3
|
|
|
|
# Bob cancels the living room timer from the kitchen
|
|
cancelled_event.clear()
|
|
timer_info = None
|
|
result = await intent.async_handle(
|
|
hass, "test", intent.INTENT_CANCEL_TIMER, {}, device_id=device_bob_kitchen_2.id
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await cancelled_event.wait()
|
|
|
|
assert timer_info is not None
|
|
assert timer_info.device_id == device_bob_living_room.id
|
|
assert timer_info.start_minutes == 3
|
|
|
|
|
|
async def test_pause_unpause_timer(hass: HomeAssistant, init_components) -> None:
|
|
"""Test pausing and unpausing a running timer."""
|
|
device_id = "test_device"
|
|
|
|
started_event = asyncio.Event()
|
|
updated_event = asyncio.Event()
|
|
|
|
expected_active = True
|
|
|
|
@callback
|
|
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
|
if event_type == TimerEventType.STARTED:
|
|
started_event.set()
|
|
elif event_type == TimerEventType.UPDATED:
|
|
assert timer.is_active == expected_active
|
|
updated_event.set()
|
|
|
|
async_register_timer_handler(hass, device_id, handle_timer)
|
|
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{"minutes": {"value": 5}},
|
|
device_id=device_id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await started_event.wait()
|
|
|
|
# Pause the timer
|
|
expected_active = False
|
|
result = await intent.async_handle(hass, "test", intent.INTENT_PAUSE_TIMER, {})
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await updated_event.wait()
|
|
|
|
# Pausing again will fail because there are no running timers
|
|
with pytest.raises(TimerNotFoundError):
|
|
await intent.async_handle(hass, "test", intent.INTENT_PAUSE_TIMER, {})
|
|
|
|
# Unpause the timer
|
|
updated_event.clear()
|
|
expected_active = True
|
|
result = await intent.async_handle(hass, "test", intent.INTENT_UNPAUSE_TIMER, {})
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await updated_event.wait()
|
|
|
|
# Unpausing again will fail because there are no paused timers
|
|
with pytest.raises(TimerNotFoundError):
|
|
await intent.async_handle(hass, "test", intent.INTENT_UNPAUSE_TIMER, {})
|
|
|
|
|
|
async def test_timer_not_found(hass: HomeAssistant) -> None:
|
|
"""Test invalid timer ids raise TimerNotFoundError."""
|
|
timer_manager = TimerManager(hass)
|
|
|
|
with pytest.raises(TimerNotFoundError):
|
|
timer_manager.cancel_timer("does-not-exist")
|
|
|
|
with pytest.raises(TimerNotFoundError):
|
|
timer_manager.add_time("does-not-exist", 1)
|
|
|
|
with pytest.raises(TimerNotFoundError):
|
|
timer_manager.remove_time("does-not-exist", 1)
|
|
|
|
with pytest.raises(TimerNotFoundError):
|
|
timer_manager.pause_timer("does-not-exist")
|
|
|
|
with pytest.raises(TimerNotFoundError):
|
|
timer_manager.unpause_timer("does-not-exist")
|
|
|
|
|
|
async def test_timer_manager_pause_unpause(hass: HomeAssistant) -> None:
|
|
"""Test that pausing/unpausing again will not have an affect."""
|
|
timer_manager = TimerManager(hass)
|
|
|
|
# Start a timer
|
|
handle_timer = MagicMock()
|
|
|
|
device_id = "test_device"
|
|
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,
|
|
)
|
|
|
|
assert timer_id in timer_manager.timers
|
|
assert timer_manager.timers[timer_id].is_active
|
|
|
|
# Pause
|
|
handle_timer.reset_mock()
|
|
timer_manager.pause_timer(timer_id)
|
|
handle_timer.assert_called_once()
|
|
|
|
# Pausing again does not call handler
|
|
handle_timer.reset_mock()
|
|
timer_manager.pause_timer(timer_id)
|
|
handle_timer.assert_not_called()
|
|
|
|
# Unpause
|
|
handle_timer.reset_mock()
|
|
timer_manager.unpause_timer(timer_id)
|
|
handle_timer.assert_called_once()
|
|
|
|
# Unpausing again does not call handler
|
|
handle_timer.reset_mock()
|
|
timer_manager.unpause_timer(timer_id)
|
|
handle_timer.assert_not_called()
|
|
|
|
|
|
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 not crash
|
|
timer_manager.add_time(timer_id, 1)
|
|
|
|
timer_manager.remove_time(timer_id, 1)
|
|
|
|
timer_manager.pause_timer(timer_id)
|
|
|
|
timer_manager.unpause_timer(timer_id)
|
|
|
|
timer_manager.cancel_timer(timer_id)
|
|
|
|
|
|
async def test_timer_status_with_names(hass: HomeAssistant, init_components) -> None:
|
|
"""Test getting the status of named timers."""
|
|
device_id = "test_device"
|
|
|
|
started_event = asyncio.Event()
|
|
num_started = 0
|
|
|
|
@callback
|
|
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
|
nonlocal num_started
|
|
|
|
if event_type == TimerEventType.STARTED:
|
|
num_started += 1
|
|
if num_started == 4:
|
|
started_event.set()
|
|
|
|
async_register_timer_handler(hass, device_id, handle_timer)
|
|
|
|
# Start timers with names
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{"name": {"value": "pizza"}, "minutes": {"value": 10}},
|
|
device_id=device_id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{"name": {"value": "pizza"}, "minutes": {"value": 15}},
|
|
device_id=device_id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{"name": {"value": "cookies"}, "minutes": {"value": 20}},
|
|
device_id=device_id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{"name": {"value": "chicken"}, "hours": {"value": 2}, "seconds": {"value": 30}},
|
|
device_id=device_id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
# Wait for all timers to start
|
|
async with asyncio.timeout(1):
|
|
await started_event.wait()
|
|
|
|
# No constraints returns all timers
|
|
for handle_device_id in (device_id, None):
|
|
result = await intent.async_handle(
|
|
hass, "test", intent.INTENT_TIMER_STATUS, {}, device_id=handle_device_id
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
timers = result.speech_slots.get("timers", [])
|
|
assert len(timers) == 4
|
|
assert {t.get(ATTR_NAME) for t in timers} == {"pizza", "cookies", "chicken"}
|
|
|
|
# Get status of cookie timer
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_TIMER_STATUS,
|
|
{"name": {"value": "cookies"}},
|
|
device_id=device_id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
timers = result.speech_slots.get("timers", [])
|
|
assert len(timers) == 1
|
|
assert timers[0].get(ATTR_NAME) == "cookies"
|
|
assert timers[0].get("start_minutes") == 20
|
|
|
|
# Get status of pizza timers
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_TIMER_STATUS,
|
|
{"name": {"value": "pizza"}},
|
|
device_id=device_id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
timers = result.speech_slots.get("timers", [])
|
|
assert len(timers) == 2
|
|
assert timers[0].get(ATTR_NAME) == "pizza"
|
|
assert timers[1].get(ATTR_NAME) == "pizza"
|
|
assert {timers[0].get("start_minutes"), timers[1].get("start_minutes")} == {10, 15}
|
|
|
|
# Get status of one pizza timer
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_TIMER_STATUS,
|
|
{"name": {"value": "pizza"}, "start_minutes": {"value": 10}},
|
|
device_id=device_id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
timers = result.speech_slots.get("timers", [])
|
|
assert len(timers) == 1
|
|
assert timers[0].get(ATTR_NAME) == "pizza"
|
|
assert timers[0].get("start_minutes") == 10
|
|
|
|
# Get status of one chicken timer
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_TIMER_STATUS,
|
|
{
|
|
"name": {"value": "chicken"},
|
|
"start_hours": {"value": 2},
|
|
"start_seconds": {"value": 30},
|
|
},
|
|
device_id=device_id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
timers = result.speech_slots.get("timers", [])
|
|
assert len(timers) == 1
|
|
assert timers[0].get(ATTR_NAME) == "chicken"
|
|
assert timers[0].get("start_hours") == 2
|
|
assert timers[0].get("start_minutes") == 0
|
|
assert timers[0].get("start_seconds") == 30
|
|
|
|
# Wrong name results in an empty list
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_TIMER_STATUS,
|
|
{"name": {"value": "does-not-exist"}},
|
|
device_id=device_id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
timers = result.speech_slots.get("timers", [])
|
|
assert len(timers) == 0
|
|
|
|
# Wrong start time results in an empty list
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_TIMER_STATUS,
|
|
{
|
|
"start_hours": {"value": 100},
|
|
"start_minutes": {"value": 100},
|
|
"start_seconds": {"value": 100},
|
|
},
|
|
device_id=device_id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
timers = result.speech_slots.get("timers", [])
|
|
assert len(timers) == 0
|
|
|
|
|
|
async def test_area_filter(
|
|
hass: HomeAssistant,
|
|
init_components,
|
|
area_registry: ar.AreaRegistry,
|
|
device_registry: dr.DeviceRegistry,
|
|
) -> None:
|
|
"""Test targeting timers by area name."""
|
|
entry = MockConfigEntry()
|
|
entry.add_to_hass(hass)
|
|
|
|
area_kitchen = area_registry.async_create("kitchen")
|
|
device_kitchen = device_registry.async_get_or_create(
|
|
config_entry_id=entry.entry_id,
|
|
connections=set(),
|
|
identifiers={("test", "kitchen-device")},
|
|
)
|
|
device_registry.async_update_device(device_kitchen.id, area_id=area_kitchen.id)
|
|
|
|
area_living_room = area_registry.async_create("living room")
|
|
device_living_room = device_registry.async_get_or_create(
|
|
config_entry_id=entry.entry_id,
|
|
connections=set(),
|
|
identifiers={("test", "living_room-device")},
|
|
)
|
|
device_registry.async_update_device(
|
|
device_living_room.id, area_id=area_living_room.id
|
|
)
|
|
|
|
started_event = asyncio.Event()
|
|
num_timers = 3
|
|
num_started = 0
|
|
|
|
@callback
|
|
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
|
nonlocal num_started
|
|
|
|
if event_type == TimerEventType.STARTED:
|
|
num_started += 1
|
|
if num_started == num_timers:
|
|
started_event.set()
|
|
|
|
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
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{"name": {"value": "pizza"}, "minutes": {"value": 10}},
|
|
device_id=device_kitchen.id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{"name": {"value": "tv"}, "minutes": {"value": 10}},
|
|
device_id=device_living_room.id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{"name": {"value": "media"}, "minutes": {"value": 15}},
|
|
device_id=device_living_room.id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
# Wait for all timers to start
|
|
async with asyncio.timeout(1):
|
|
await started_event.wait()
|
|
|
|
# No constraints returns all timers
|
|
result = await intent.async_handle(
|
|
hass, "test", intent.INTENT_TIMER_STATUS, {}, device_id=device_kitchen.id
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
timers = result.speech_slots.get("timers", [])
|
|
assert len(timers) == num_timers
|
|
assert {t.get(ATTR_NAME) for t in timers} == {"pizza", "tv", "media"}
|
|
|
|
# Filter by area (target kitchen from living room)
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_TIMER_STATUS,
|
|
{"area": {"value": "kitchen"}},
|
|
device_id=device_living_room.id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
timers = result.speech_slots.get("timers", [])
|
|
assert len(timers) == 1
|
|
assert timers[0].get(ATTR_NAME) == "pizza"
|
|
|
|
# Filter by area (target living room from kitchen)
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_TIMER_STATUS,
|
|
{"area": {"value": "living room"}},
|
|
device_id=device_kitchen.id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
timers = result.speech_slots.get("timers", [])
|
|
assert len(timers) == 2
|
|
assert {t.get(ATTR_NAME) for t in timers} == {"tv", "media"}
|
|
|
|
# Filter by area + name
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_TIMER_STATUS,
|
|
{"area": {"value": "living room"}, "name": {"value": "tv"}},
|
|
device_id=device_kitchen.id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
timers = result.speech_slots.get("timers", [])
|
|
assert len(timers) == 1
|
|
assert timers[0].get(ATTR_NAME) == "tv"
|
|
|
|
# Filter by area + time
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_TIMER_STATUS,
|
|
{"area": {"value": "living room"}, "start_minutes": {"value": 15}},
|
|
device_id=device_kitchen.id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
timers = result.speech_slots.get("timers", [])
|
|
assert len(timers) == 1
|
|
assert timers[0].get(ATTR_NAME) == "media"
|
|
|
|
# Filter by area that doesn't exist
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_TIMER_STATUS,
|
|
{"area": {"value": "does-not-exist"}},
|
|
device_id=device_kitchen.id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
timers = result.speech_slots.get("timers", [])
|
|
assert len(timers) == 0
|
|
|
|
# Cancel by area + time
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_CANCEL_TIMER,
|
|
{"area": {"value": "living room"}, "start_minutes": {"value": 15}},
|
|
device_id=device_living_room.id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
# Cancel by area
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_CANCEL_TIMER,
|
|
{"area": {"value": "living room"}},
|
|
device_id=device_living_room.id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
# Get status with device missing
|
|
with patch(
|
|
"homeassistant.helpers.device_registry.DeviceRegistry.async_get",
|
|
return_value=None,
|
|
):
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_TIMER_STATUS,
|
|
device_id=device_kitchen.id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
timers = result.speech_slots.get("timers", [])
|
|
assert len(timers) == 1
|
|
|
|
# Get status with area missing
|
|
with patch(
|
|
"homeassistant.helpers.area_registry.AreaRegistry.async_get_area",
|
|
return_value=None,
|
|
):
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_TIMER_STATUS,
|
|
device_id=device_kitchen.id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
timers = result.speech_slots.get("timers", [])
|
|
assert len(timers) == 1
|
|
|
|
|
|
def test_round_time() -> None:
|
|
"""Test lower-precision time rounded."""
|
|
|
|
# hours
|
|
assert _round_time(1, 10, 30) == (1, 0, 0)
|
|
assert _round_time(1, 48, 30) == (2, 0, 0)
|
|
assert _round_time(2, 25, 30) == (2, 30, 0)
|
|
|
|
# minutes
|
|
assert _round_time(0, 1, 10) == (0, 1, 0)
|
|
assert _round_time(0, 1, 48) == (0, 2, 0)
|
|
assert _round_time(0, 2, 25) == (0, 2, 30)
|
|
|
|
# seconds
|
|
assert _round_time(0, 0, 6) == (0, 0, 6)
|
|
assert _round_time(0, 0, 15) == (0, 0, 10)
|
|
assert _round_time(0, 0, 58) == (0, 1, 0)
|
|
assert _round_time(0, 0, 25) == (0, 0, 20)
|
|
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"
|
|
|
|
mock_handle_timer = MagicMock()
|
|
async_register_timer_handler(hass, device_id, mock_handle_timer)
|
|
|
|
timer_manager = TimerManager(hass)
|
|
with pytest.raises(ValueError):
|
|
timer_manager.start_timer(
|
|
device_id=None,
|
|
hours=None,
|
|
minutes=5,
|
|
seconds=None,
|
|
language=hass.config.language,
|
|
)
|
|
|
|
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
|
|
|
|
# No timer events for delayed commands
|
|
mock_handle_timer.assert_not_called()
|
|
|
|
# Wait for process service call to finish
|
|
await hass.async_block_till_done()
|
|
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:
|
|
"""Test disamgibuating timers by their paused state."""
|
|
device_id = "test_device"
|
|
started_timer_ids: list[str] = []
|
|
paused_timer_ids: list[str] = []
|
|
unpaused_timer_ids: list[str] = []
|
|
|
|
started_event = asyncio.Event()
|
|
updated_event = asyncio.Event()
|
|
|
|
@callback
|
|
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
|
if event_type == TimerEventType.STARTED:
|
|
started_event.set()
|
|
started_timer_ids.append(timer.id)
|
|
elif event_type == TimerEventType.UPDATED:
|
|
updated_event.set()
|
|
if timer.is_active:
|
|
unpaused_timer_ids.append(timer.id)
|
|
else:
|
|
paused_timer_ids.append(timer.id)
|
|
|
|
async_register_timer_handler(hass, device_id, handle_timer)
|
|
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{"minutes": {"value": 5}},
|
|
device_id=device_id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await started_event.wait()
|
|
|
|
# Pause the timer
|
|
result = await intent.async_handle(
|
|
hass, "test", intent.INTENT_PAUSE_TIMER, {}, device_id=device_id
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await updated_event.wait()
|
|
|
|
# Start another timer
|
|
started_event.clear()
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_START_TIMER,
|
|
{"minutes": {"value": 10}},
|
|
device_id=device_id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await started_event.wait()
|
|
assert len(started_timer_ids) == 2
|
|
|
|
# We can pause the more recent timer without more information because the
|
|
# first one is paused.
|
|
updated_event.clear()
|
|
result = await intent.async_handle(
|
|
hass, "test", intent.INTENT_PAUSE_TIMER, {}, device_id=device_id
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await updated_event.wait()
|
|
assert len(paused_timer_ids) == 2
|
|
assert paused_timer_ids[1] == started_timer_ids[1]
|
|
|
|
# We have to explicitly unpause now
|
|
updated_event.clear()
|
|
result = await intent.async_handle(
|
|
hass,
|
|
"test",
|
|
intent.INTENT_UNPAUSE_TIMER,
|
|
{"start_minutes": {"value": 10}},
|
|
device_id=device_id,
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await updated_event.wait()
|
|
assert len(unpaused_timer_ids) == 1
|
|
assert unpaused_timer_ids[0] == started_timer_ids[1]
|
|
|
|
# We can resume the older timer without more information because the
|
|
# second one is running.
|
|
updated_event.clear()
|
|
result = await intent.async_handle(
|
|
hass, "test", intent.INTENT_UNPAUSE_TIMER, {}, device_id=device_id
|
|
)
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
async with asyncio.timeout(1):
|
|
await updated_event.wait()
|
|
assert len(unpaused_timer_ids) == 2
|
|
assert unpaused_timer_ids[1] == started_timer_ids[0]
|
|
|
|
|
|
async def test_async_device_supports_timers(hass: HomeAssistant) -> None:
|
|
"""Test async_device_supports_timers function."""
|
|
device_id = "test_device"
|
|
|
|
# Before intent initialization
|
|
assert not async_device_supports_timers(hass, device_id)
|
|
|
|
# After intent initialization
|
|
assert await async_setup_component(hass, "intent", {})
|
|
assert not async_device_supports_timers(hass, device_id)
|
|
|
|
@callback
|
|
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
|
|
pass
|
|
|
|
async_register_timer_handler(hass, device_id, handle_timer)
|
|
|
|
# After handler registration
|
|
assert async_device_supports_timers(hass, device_id)
|