Allow timer management from any device (#120440)
parent
8ce53d28e7
commit
d3ceaef098
|
@ -490,7 +490,7 @@ class FindTimerFilter(StrEnum):
|
|||
|
||||
def _find_timer(
|
||||
hass: HomeAssistant,
|
||||
device_id: str,
|
||||
device_id: str | None,
|
||||
slots: dict[str, Any],
|
||||
find_filter: FindTimerFilter | None = None,
|
||||
) -> TimerInfo:
|
||||
|
@ -577,7 +577,7 @@ def _find_timer(
|
|||
return matching_timers[0]
|
||||
|
||||
# Use device id
|
||||
if matching_timers:
|
||||
if matching_timers and device_id:
|
||||
matching_device_timers = [
|
||||
t for t in matching_timers if (t.device_id == device_id)
|
||||
]
|
||||
|
@ -626,7 +626,7 @@ def _find_timer(
|
|||
|
||||
|
||||
def _find_timers(
|
||||
hass: HomeAssistant, device_id: str, slots: dict[str, Any]
|
||||
hass: HomeAssistant, device_id: str | None, slots: dict[str, Any]
|
||||
) -> list[TimerInfo]:
|
||||
"""Match multiple timers with constraints or raise an error."""
|
||||
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
||||
|
@ -689,6 +689,10 @@ def _find_timers(
|
|||
# No matches
|
||||
return matching_timers
|
||||
|
||||
if not device_id:
|
||||
# Can't order using area/floor
|
||||
return matching_timers
|
||||
|
||||
# Use device id to order remaining timers
|
||||
device_registry = dr.async_get(hass)
|
||||
device = device_registry.async_get(device_id)
|
||||
|
@ -861,12 +865,6 @@ class CancelTimerIntentHandler(intent.IntentHandler):
|
|||
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
||||
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)
|
||||
|
||||
timer = _find_timer(hass, intent_obj.device_id, slots)
|
||||
timer_manager.cancel_timer(timer.id)
|
||||
return intent_obj.create_response()
|
||||
|
@ -890,12 +888,6 @@ class IncreaseTimerIntentHandler(intent.IntentHandler):
|
|||
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
||||
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)
|
||||
|
||||
total_seconds = _get_total_seconds(slots)
|
||||
timer = _find_timer(hass, intent_obj.device_id, slots)
|
||||
timer_manager.add_time(timer.id, total_seconds)
|
||||
|
@ -920,12 +912,6 @@ class DecreaseTimerIntentHandler(intent.IntentHandler):
|
|||
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
||||
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)
|
||||
|
||||
total_seconds = _get_total_seconds(slots)
|
||||
timer = _find_timer(hass, intent_obj.device_id, slots)
|
||||
timer_manager.remove_time(timer.id, total_seconds)
|
||||
|
@ -949,12 +935,6 @@ class PauseTimerIntentHandler(intent.IntentHandler):
|
|||
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
||||
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)
|
||||
|
||||
timer = _find_timer(
|
||||
hass, intent_obj.device_id, slots, find_filter=FindTimerFilter.ONLY_ACTIVE
|
||||
)
|
||||
|
@ -979,12 +959,6 @@ class UnpauseTimerIntentHandler(intent.IntentHandler):
|
|||
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
||||
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)
|
||||
|
||||
timer = _find_timer(
|
||||
hass, intent_obj.device_id, slots, find_filter=FindTimerFilter.ONLY_INACTIVE
|
||||
)
|
||||
|
@ -1006,15 +980,8 @@ class TimerStatusIntentHandler(intent.IntentHandler):
|
|||
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
||||
"""Handle the intent."""
|
||||
hass = intent_obj.hass
|
||||
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
||||
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)
|
||||
|
||||
statuses: list[dict[str, Any]] = []
|
||||
for timer in _find_timers(hass, intent_obj.device_id, slots):
|
||||
total_seconds = timer.seconds_left
|
||||
|
|
|
@ -355,7 +355,7 @@ class AssistAPI(API):
|
|||
if not llm_context.device_id or not async_device_supports_timers(
|
||||
self.hass, llm_context.device_id
|
||||
):
|
||||
prompt.append("This device does not support timers.")
|
||||
prompt.append("This device is not able to start timers.")
|
||||
|
||||
if exposed_entities:
|
||||
prompt.append(
|
||||
|
|
|
@ -860,13 +860,27 @@ async def test_error_feature_not_supported(hass: HomeAssistant) -> None:
|
|||
|
||||
|
||||
@pytest.mark.usefixtures("init_components")
|
||||
async def test_error_no_timer_support(hass: HomeAssistant) -> None:
|
||||
async def test_error_no_timer_support(
|
||||
hass: HomeAssistant,
|
||||
area_registry: ar.AreaRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test error message when a device does not support timers (no handler is registered)."""
|
||||
device_id = "test_device"
|
||||
area_kitchen = area_registry.async_create("kitchen")
|
||||
|
||||
entry = MockConfigEntry()
|
||||
entry.add_to_hass(hass)
|
||||
device_kitchen = device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
connections=set(),
|
||||
identifiers={("demo", "device-kitchen")},
|
||||
)
|
||||
device_registry.async_update_device(device_kitchen.id, area_id=area_kitchen.id)
|
||||
device_id = device_kitchen.id
|
||||
|
||||
# No timer handler is registered for the device
|
||||
result = await conversation.async_converse(
|
||||
hass, "pause timer", None, Context(), None, device_id=device_id
|
||||
hass, "set a 5 minute timer", None, Context(), None, device_id=device_id
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
|
|
|
@ -64,6 +64,7 @@ async def test_start_finish_timer(hass: HomeAssistant, init_components) -> None:
|
|||
|
||||
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",
|
||||
|
@ -185,6 +186,27 @@ async def test_cancel_timer(hass: HomeAssistant, init_components) -> None:
|
|||
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."""
|
||||
|
@ -260,7 +282,6 @@ async def test_increase_timer(hass: HomeAssistant, init_components) -> None:
|
|||
"minutes": {"value": 0},
|
||||
"seconds": {"value": 0},
|
||||
},
|
||||
device_id=device_id,
|
||||
)
|
||||
|
||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
|
@ -279,7 +300,6 @@ async def test_increase_timer(hass: HomeAssistant, init_components) -> None:
|
|||
"minutes": {"value": 5},
|
||||
"seconds": {"value": 30},
|
||||
},
|
||||
device_id=device_id,
|
||||
)
|
||||
|
||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
|
@ -293,7 +313,6 @@ async def test_increase_timer(hass: HomeAssistant, init_components) -> None:
|
|||
"test",
|
||||
intent.INTENT_CANCEL_TIMER,
|
||||
{"name": {"value": timer_name}},
|
||||
device_id=device_id,
|
||||
)
|
||||
|
||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
|
@ -375,7 +394,6 @@ async def test_decrease_timer(hass: HomeAssistant, init_components) -> None:
|
|||
"start_seconds": {"value": 3},
|
||||
"seconds": {"value": 30},
|
||||
},
|
||||
device_id=device_id,
|
||||
)
|
||||
|
||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
|
@ -389,7 +407,6 @@ async def test_decrease_timer(hass: HomeAssistant, init_components) -> None:
|
|||
"test",
|
||||
intent.INTENT_CANCEL_TIMER,
|
||||
{"name": {"value": timer_name}},
|
||||
device_id=device_id,
|
||||
)
|
||||
|
||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
|
@ -467,7 +484,6 @@ async def test_decrease_timer_below_zero(hass: HomeAssistant, init_components) -
|
|||
"start_seconds": {"value": 3},
|
||||
"seconds": {"value": original_total_seconds + 1},
|
||||
},
|
||||
device_id=device_id,
|
||||
)
|
||||
|
||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
|
@ -482,31 +498,13 @@ async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None:
|
|||
"""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,
|
||||
intent.INTENT_START_TIMER,
|
||||
{"minutes": {"value": 5}},
|
||||
device_id=None,
|
||||
)
|
||||
|
||||
|
@ -515,8 +513,8 @@ async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None:
|
|||
await intent.async_handle(
|
||||
hass,
|
||||
"test",
|
||||
intent_name,
|
||||
slots,
|
||||
intent.INTENT_START_TIMER,
|
||||
{"minutes": {"value": 5}},
|
||||
device_id=device_id,
|
||||
)
|
||||
|
||||
|
@ -543,7 +541,6 @@ async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None:
|
|||
"test",
|
||||
intent.INTENT_INCREASE_TIMER,
|
||||
{"name": {"value": "PIZZA "}, "minutes": {"value": 1}},
|
||||
device_id=device_id,
|
||||
)
|
||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
|
||||
|
@ -554,7 +551,6 @@ async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None:
|
|||
"test",
|
||||
intent.INTENT_CANCEL_TIMER,
|
||||
{"name": {"value": "does-not-exist"}},
|
||||
device_id=device_id,
|
||||
)
|
||||
|
||||
# Right start time
|
||||
|
@ -563,7 +559,6 @@ async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None:
|
|||
"test",
|
||||
intent.INTENT_INCREASE_TIMER,
|
||||
{"start_minutes": {"value": 5}, "minutes": {"value": 1}},
|
||||
device_id=device_id,
|
||||
)
|
||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
|
||||
|
@ -574,7 +569,6 @@ async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None:
|
|||
"test",
|
||||
intent.INTENT_CANCEL_TIMER,
|
||||
{"start_minutes": {"value": 1}},
|
||||
device_id=device_id,
|
||||
)
|
||||
|
||||
|
||||
|
@ -903,9 +897,7 @@ async def test_pause_unpause_timer(hass: HomeAssistant, init_components) -> None
|
|||
|
||||
# Pause the timer
|
||||
expected_active = False
|
||||
result = await intent.async_handle(
|
||||
hass, "test", intent.INTENT_PAUSE_TIMER, {}, device_id=device_id
|
||||
)
|
||||
result = await intent.async_handle(hass, "test", intent.INTENT_PAUSE_TIMER, {})
|
||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
|
||||
async with asyncio.timeout(1):
|
||||
|
@ -913,16 +905,12 @@ async def test_pause_unpause_timer(hass: HomeAssistant, init_components) -> None
|
|||
|
||||
# Pausing again will fail because there are no running timers
|
||||
with pytest.raises(TimerNotFoundError):
|
||||
await intent.async_handle(
|
||||
hass, "test", intent.INTENT_PAUSE_TIMER, {}, device_id=device_id
|
||||
)
|
||||
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, {}, device_id=device_id
|
||||
)
|
||||
result = await intent.async_handle(hass, "test", intent.INTENT_UNPAUSE_TIMER, {})
|
||||
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
|
||||
async with asyncio.timeout(1):
|
||||
|
@ -930,9 +918,7 @@ async def test_pause_unpause_timer(hass: HomeAssistant, init_components) -> None
|
|||
|
||||
# Unpausing again will fail because there are no paused timers
|
||||
with pytest.raises(TimerNotFoundError):
|
||||
await intent.async_handle(
|
||||
hass, "test", intent.INTENT_UNPAUSE_TIMER, {}, device_id=device_id
|
||||
)
|
||||
await intent.async_handle(hass, "test", intent.INTENT_UNPAUSE_TIMER, {})
|
||||
|
||||
|
||||
async def test_timer_not_found(hass: HomeAssistant) -> None:
|
||||
|
@ -1101,8 +1087,9 @@ async def test_timer_status_with_names(hass: HomeAssistant, init_components) ->
|
|||
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=device_id
|
||||
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", [])
|
||||
|
|
|
@ -578,7 +578,7 @@ async def test_assist_api_prompt(
|
|||
"(what comes before the dot in its entity id). "
|
||||
"When controlling an area, prefer passing just area name and domain."
|
||||
)
|
||||
no_timer_prompt = "This device does not support timers."
|
||||
no_timer_prompt = "This device is not able to start timers."
|
||||
|
||||
area_prompt = (
|
||||
"When a user asks to turn on all devices of a specific type, "
|
||||
|
|
Loading…
Reference in New Issue