mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 16:27:08 +00:00
Add area filter and rounded time to timers (#117527)
* Add area filter * Add rounded time to status * Fix test * Extend test * Increase test coverage
This commit is contained in:
parent
ba395fb9f3
commit
e168cb96e9
@ -71,6 +71,9 @@ class TimerInfo:
|
|||||||
area_id: str | None = None
|
area_id: str | None = None
|
||||||
"""Id of area that the device belongs to."""
|
"""Id of area that the device belongs to."""
|
||||||
|
|
||||||
|
area_name: str | None = None
|
||||||
|
"""Normalized name of the area that the device belongs to."""
|
||||||
|
|
||||||
floor_id: str | None = None
|
floor_id: str | None = None
|
||||||
"""Id of floor that the device's area belongs to."""
|
"""Id of floor that the device's area belongs to."""
|
||||||
|
|
||||||
@ -85,12 +88,9 @@ class TimerInfo:
|
|||||||
return max(0, self.seconds - seconds_running)
|
return max(0, self.seconds - seconds_running)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def name_normalized(self) -> str | None:
|
def name_normalized(self) -> str:
|
||||||
"""Return normalized timer name."""
|
"""Return normalized timer name."""
|
||||||
if self.name is None:
|
return _normalize_name(self.name or "")
|
||||||
return None
|
|
||||||
|
|
||||||
return self.name.strip().casefold()
|
|
||||||
|
|
||||||
def cancel(self) -> None:
|
def cancel(self) -> None:
|
||||||
"""Cancel the timer."""
|
"""Cancel the timer."""
|
||||||
@ -223,6 +223,7 @@ class TimerManager:
|
|||||||
if device.area_id and (
|
if device.area_id and (
|
||||||
area := area_registry.async_get_area(device.area_id)
|
area := area_registry.async_get_area(device.area_id)
|
||||||
):
|
):
|
||||||
|
timer.area_name = _normalize_name(area.name)
|
||||||
timer.floor_id = area.floor_id
|
timer.floor_id = area.floor_id
|
||||||
|
|
||||||
self.timers[timer_id] = timer
|
self.timers[timer_id] = timer
|
||||||
@ -422,13 +423,26 @@ def _find_timer(
|
|||||||
has_filter = True
|
has_filter = True
|
||||||
name = slots["name"]["value"]
|
name = slots["name"]["value"]
|
||||||
assert name is not None
|
assert name is not None
|
||||||
name_norm = name.strip().casefold()
|
name_norm = _normalize_name(name)
|
||||||
|
|
||||||
matching_timers = [t for t in matching_timers if t.name_normalized == name_norm]
|
matching_timers = [t for t in matching_timers if t.name_normalized == name_norm]
|
||||||
if len(matching_timers) == 1:
|
if len(matching_timers) == 1:
|
||||||
# Only 1 match
|
# Only 1 match
|
||||||
return matching_timers[0]
|
return matching_timers[0]
|
||||||
|
|
||||||
|
# Search by area name
|
||||||
|
area_name: str | None = None
|
||||||
|
if "area" in slots:
|
||||||
|
has_filter = True
|
||||||
|
area_name = slots["area"]["value"]
|
||||||
|
assert area_name is not None
|
||||||
|
area_name_norm = _normalize_name(area_name)
|
||||||
|
|
||||||
|
matching_timers = [t for t in matching_timers if t.area_name == area_name_norm]
|
||||||
|
if len(matching_timers) == 1:
|
||||||
|
# Only 1 match
|
||||||
|
return matching_timers[0]
|
||||||
|
|
||||||
# Use starting time to disambiguate
|
# Use starting time to disambiguate
|
||||||
start_hours: int | None = None
|
start_hours: int | None = None
|
||||||
if "start_hours" in slots:
|
if "start_hours" in slots:
|
||||||
@ -501,8 +515,9 @@ def _find_timer(
|
|||||||
raise MultipleTimersMatchedError
|
raise MultipleTimersMatchedError
|
||||||
|
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Timer not found: name=%s, hours=%s, minutes=%s, seconds=%s, device_id=%s",
|
"Timer not found: name=%s, area=%s, hours=%s, minutes=%s, seconds=%s, device_id=%s",
|
||||||
name,
|
name,
|
||||||
|
area_name,
|
||||||
start_hours,
|
start_hours,
|
||||||
start_minutes,
|
start_minutes,
|
||||||
start_seconds,
|
start_seconds,
|
||||||
@ -524,13 +539,25 @@ def _find_timers(
|
|||||||
if "name" in slots:
|
if "name" in slots:
|
||||||
name = slots["name"]["value"]
|
name = slots["name"]["value"]
|
||||||
assert name is not None
|
assert name is not None
|
||||||
name_norm = name.strip().casefold()
|
name_norm = _normalize_name(name)
|
||||||
|
|
||||||
matching_timers = [t for t in matching_timers if t.name_normalized == name_norm]
|
matching_timers = [t for t in matching_timers if t.name_normalized == name_norm]
|
||||||
if not matching_timers:
|
if not matching_timers:
|
||||||
# No matches
|
# No matches
|
||||||
return matching_timers
|
return matching_timers
|
||||||
|
|
||||||
|
# Filter by area name
|
||||||
|
area_name: str | None = None
|
||||||
|
if "area" in slots:
|
||||||
|
area_name = slots["area"]["value"]
|
||||||
|
assert area_name is not None
|
||||||
|
area_name_norm = _normalize_name(area_name)
|
||||||
|
|
||||||
|
matching_timers = [t for t in matching_timers if t.area_name == area_name_norm]
|
||||||
|
if not matching_timers:
|
||||||
|
# No matches
|
||||||
|
return matching_timers
|
||||||
|
|
||||||
# Use starting time to filter, if present
|
# Use starting time to filter, if present
|
||||||
start_hours: int | None = None
|
start_hours: int | None = None
|
||||||
if "start_hours" in slots:
|
if "start_hours" in slots:
|
||||||
@ -590,6 +617,11 @@ def _find_timers(
|
|||||||
return matching_timers
|
return matching_timers
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_name(name: str) -> str:
|
||||||
|
"""Normalize name for comparison."""
|
||||||
|
return name.strip().casefold()
|
||||||
|
|
||||||
|
|
||||||
def _get_total_seconds(slots: dict[str, Any]) -> int:
|
def _get_total_seconds(slots: dict[str, Any]) -> int:
|
||||||
"""Return the total number of seconds from hours/minutes/seconds slots."""
|
"""Return the total number of seconds from hours/minutes/seconds slots."""
|
||||||
total_seconds = 0
|
total_seconds = 0
|
||||||
@ -605,6 +637,55 @@ def _get_total_seconds(slots: dict[str, Any]) -> int:
|
|||||||
return total_seconds
|
return total_seconds
|
||||||
|
|
||||||
|
|
||||||
|
def _round_time(hours: int, minutes: int, seconds: int) -> tuple[int, int, int]:
|
||||||
|
"""Round time to a lower precision for feedback."""
|
||||||
|
if hours > 0:
|
||||||
|
# No seconds, round up above 45 minutes and down below 15
|
||||||
|
rounded_hours = hours
|
||||||
|
rounded_seconds = 0
|
||||||
|
if minutes > 45:
|
||||||
|
# 01:50:30 -> 02:00:00
|
||||||
|
rounded_hours += 1
|
||||||
|
rounded_minutes = 0
|
||||||
|
elif minutes < 15:
|
||||||
|
# 01:10:30 -> 01:00:00
|
||||||
|
rounded_minutes = 0
|
||||||
|
else:
|
||||||
|
# 01:25:30 -> 01:30:00
|
||||||
|
rounded_minutes = 30
|
||||||
|
elif minutes > 0:
|
||||||
|
# Round up above 45 seconds, down below 15
|
||||||
|
rounded_hours = 0
|
||||||
|
rounded_minutes = minutes
|
||||||
|
if seconds > 45:
|
||||||
|
# 00:01:50 -> 00:02:00
|
||||||
|
rounded_minutes += 1
|
||||||
|
rounded_seconds = 0
|
||||||
|
elif seconds < 15:
|
||||||
|
# 00:01:10 -> 00:01:00
|
||||||
|
rounded_seconds = 0
|
||||||
|
else:
|
||||||
|
# 00:01:25 -> 00:01:30
|
||||||
|
rounded_seconds = 30
|
||||||
|
else:
|
||||||
|
# Round up above 50 seconds, exact below 10, and down to nearest 10
|
||||||
|
# otherwise.
|
||||||
|
rounded_hours = 0
|
||||||
|
rounded_minutes = 0
|
||||||
|
if seconds > 50:
|
||||||
|
# 00:00:55 -> 00:01:00
|
||||||
|
rounded_minutes = 1
|
||||||
|
rounded_seconds = 0
|
||||||
|
elif seconds < 10:
|
||||||
|
# 00:00:09 -> 00:00:09
|
||||||
|
rounded_seconds = seconds
|
||||||
|
else:
|
||||||
|
# 00:01:25 -> 00:01:20
|
||||||
|
rounded_seconds = seconds - (seconds % 10)
|
||||||
|
|
||||||
|
return rounded_hours, rounded_minutes, rounded_seconds
|
||||||
|
|
||||||
|
|
||||||
class StartTimerIntentHandler(intent.IntentHandler):
|
class StartTimerIntentHandler(intent.IntentHandler):
|
||||||
"""Intent handler for starting a new timer."""
|
"""Intent handler for starting a new timer."""
|
||||||
|
|
||||||
@ -655,6 +736,7 @@ class CancelTimerIntentHandler(intent.IntentHandler):
|
|||||||
slot_schema = {
|
slot_schema = {
|
||||||
vol.Any("start_hours", "start_minutes", "start_seconds"): cv.positive_int,
|
vol.Any("start_hours", "start_minutes", "start_seconds"): cv.positive_int,
|
||||||
vol.Optional("name"): cv.string,
|
vol.Optional("name"): cv.string,
|
||||||
|
vol.Optional("area"): cv.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
||||||
@ -677,6 +759,7 @@ class IncreaseTimerIntentHandler(intent.IntentHandler):
|
|||||||
vol.Any("hours", "minutes", "seconds"): cv.positive_int,
|
vol.Any("hours", "minutes", "seconds"): cv.positive_int,
|
||||||
vol.Any("start_hours", "start_minutes", "start_seconds"): cv.positive_int,
|
vol.Any("start_hours", "start_minutes", "start_seconds"): cv.positive_int,
|
||||||
vol.Optional("name"): cv.string,
|
vol.Optional("name"): cv.string,
|
||||||
|
vol.Optional("area"): cv.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
||||||
@ -700,6 +783,7 @@ class DecreaseTimerIntentHandler(intent.IntentHandler):
|
|||||||
vol.Required(vol.Any("hours", "minutes", "seconds")): cv.positive_int,
|
vol.Required(vol.Any("hours", "minutes", "seconds")): cv.positive_int,
|
||||||
vol.Any("start_hours", "start_minutes", "start_seconds"): cv.positive_int,
|
vol.Any("start_hours", "start_minutes", "start_seconds"): cv.positive_int,
|
||||||
vol.Optional("name"): cv.string,
|
vol.Optional("name"): cv.string,
|
||||||
|
vol.Optional("area"): cv.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
||||||
@ -722,6 +806,7 @@ class PauseTimerIntentHandler(intent.IntentHandler):
|
|||||||
slot_schema = {
|
slot_schema = {
|
||||||
vol.Any("start_hours", "start_minutes", "start_seconds"): cv.positive_int,
|
vol.Any("start_hours", "start_minutes", "start_seconds"): cv.positive_int,
|
||||||
vol.Optional("name"): cv.string,
|
vol.Optional("name"): cv.string,
|
||||||
|
vol.Optional("area"): cv.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
||||||
@ -743,6 +828,7 @@ class UnpauseTimerIntentHandler(intent.IntentHandler):
|
|||||||
slot_schema = {
|
slot_schema = {
|
||||||
vol.Any("start_hours", "start_minutes", "start_seconds"): cv.positive_int,
|
vol.Any("start_hours", "start_minutes", "start_seconds"): cv.positive_int,
|
||||||
vol.Optional("name"): cv.string,
|
vol.Optional("name"): cv.string,
|
||||||
|
vol.Optional("area"): cv.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
||||||
@ -764,6 +850,7 @@ class TimerStatusIntentHandler(intent.IntentHandler):
|
|||||||
slot_schema = {
|
slot_schema = {
|
||||||
vol.Any("start_hours", "start_minutes", "start_seconds"): cv.positive_int,
|
vol.Any("start_hours", "start_minutes", "start_seconds"): cv.positive_int,
|
||||||
vol.Optional("name"): cv.string,
|
vol.Optional("name"): cv.string,
|
||||||
|
vol.Optional("area"): cv.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
||||||
@ -778,6 +865,11 @@ class TimerStatusIntentHandler(intent.IntentHandler):
|
|||||||
minutes, seconds = divmod(total_seconds, 60)
|
minutes, seconds = divmod(total_seconds, 60)
|
||||||
hours, minutes = divmod(minutes, 60)
|
hours, minutes = divmod(minutes, 60)
|
||||||
|
|
||||||
|
# Get lower-precision time for feedback
|
||||||
|
rounded_hours, rounded_minutes, rounded_seconds = _round_time(
|
||||||
|
hours, minutes, seconds
|
||||||
|
)
|
||||||
|
|
||||||
statuses.append(
|
statuses.append(
|
||||||
{
|
{
|
||||||
ATTR_ID: timer.id,
|
ATTR_ID: timer.id,
|
||||||
@ -791,6 +883,9 @@ class TimerStatusIntentHandler(intent.IntentHandler):
|
|||||||
"hours_left": hours,
|
"hours_left": hours,
|
||||||
"minutes_left": minutes,
|
"minutes_left": minutes,
|
||||||
"seconds_left": seconds,
|
"seconds_left": seconds,
|
||||||
|
"rounded_hours_left": rounded_hours,
|
||||||
|
"rounded_minutes_left": rounded_minutes,
|
||||||
|
"rounded_seconds_left": rounded_seconds,
|
||||||
"total_seconds_left": total_seconds,
|
"total_seconds_left": total_seconds,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Tests for intent timers."""
|
"""Tests for intent timers."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -10,6 +11,7 @@ from homeassistant.components.intent.timers import (
|
|||||||
TimerInfo,
|
TimerInfo,
|
||||||
TimerManager,
|
TimerManager,
|
||||||
TimerNotFoundError,
|
TimerNotFoundError,
|
||||||
|
_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
|
||||||
@ -238,6 +240,25 @@ async def test_increase_timer(hass: HomeAssistant, init_components) -> None:
|
|||||||
async with asyncio.timeout(1):
|
async with asyncio.timeout(1):
|
||||||
await started_event.wait()
|
await started_event.wait()
|
||||||
|
|
||||||
|
# Adding 0 seconds has no effect
|
||||||
|
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},
|
||||||
|
},
|
||||||
|
device_id=device_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
|
assert not updated_event.is_set()
|
||||||
|
|
||||||
# Add 30 seconds to the timer
|
# Add 30 seconds to the timer
|
||||||
result = await intent.async_handle(
|
result = await intent.async_handle(
|
||||||
hass,
|
hass,
|
||||||
@ -979,3 +1000,213 @@ async def test_timer_status_with_names(hass: HomeAssistant, init_components) ->
|
|||||||
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) == 0
|
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
|
||||||
|
|
||||||
|
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, 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, {})
|
||||||
|
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 (kitchen)
|
||||||
|
result = await intent.async_handle(
|
||||||
|
hass,
|
||||||
|
"test",
|
||||||
|
intent.INTENT_TIMER_STATUS,
|
||||||
|
{"area": {"value": "kitchen"}},
|
||||||
|
)
|
||||||
|
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 (living room)
|
||||||
|
result = await intent.async_handle(
|
||||||
|
hass,
|
||||||
|
"test",
|
||||||
|
intent.INTENT_TIMER_STATUS,
|
||||||
|
{"area": {"value": "living room"}},
|
||||||
|
)
|
||||||
|
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"}},
|
||||||
|
)
|
||||||
|
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}},
|
||||||
|
)
|
||||||
|
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"}},
|
||||||
|
)
|
||||||
|
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}},
|
||||||
|
)
|
||||||
|
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"}},
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user