diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 64a17c661c6a..8f6ac37f359a 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -44,6 +44,7 @@ from aioesphomeapi import ( UpdateInfo, UserService, ValveInfo, + VoiceAssistantFeature, WaterHeaterInfo, build_device_unique_id, ) @@ -355,9 +356,15 @@ class RuntimeEntryData: # and we don't want to load the update platform since it needs # a complete device_info. needed_platforms.add(Platform.UPDATE) - if self.device_info.voice_assistant_feature_flags_compat(self.api_version): + feature_flags = self.device_info.voice_assistant_feature_flags_compat( + self.api_version + ) + if feature_flags: needed_platforms.add(Platform.BINARY_SENSOR) needed_platforms.add(Platform.SELECT) + if feature_flags & VoiceAssistantFeature.TIMERS: + # Matches what timer_list.async_setup_entry creates, so the + # platform is not marked loaded before it can make an entity. needed_platforms.add(Platform.TIMER_LIST) # Make a dict of the EntityInfo by type and send diff --git a/homeassistant/components/intent/llm.py b/homeassistant/components/intent/llm.py index 0825890df23f..a29427f669e4 100644 --- a/homeassistant/components/intent/llm.py +++ b/homeassistant/components/intent/llm.py @@ -17,21 +17,23 @@ from homeassistant.helpers import ( from homeassistant.helpers.llm import LLM_API_ASSIST, IntentTool, LLMContext, Tool from .const import DOMAIN -from .timers import async_device_supports_timers +from .timers import async_device_supports_timer_intent # Generic intents exposed as LLM tools regardless of a timer-capable device. LLM_INTENTS = ( intent.INTENT_TURN_ON, intent.INTENT_TURN_OFF, - intent.INTENT_CANCEL_ALL_TIMERS, intent.INTENT_SET_POSITION, intent.INTENT_STOP_MOVING, ) -# Timer intents, only exposed for a device that supports timers. +# Timer intents, only exposed for a device that supports timers. All of them +# resolve the requesting device's own timer list, so without one they can only +# fail. TIMER_INTENTS = ( intent.INTENT_START_TIMER, intent.INTENT_CANCEL_TIMER, + intent.INTENT_CANCEL_ALL_TIMERS, intent.INTENT_INCREASE_TIMER, intent.INTENT_DECREASE_TIMER, intent.INTENT_PAUSE_TIMER, @@ -55,13 +57,14 @@ def async_get_tools( if api_id != LLM_API_ASSIST: return None - supports_timers = ( - llm_context.device_id is not None - and async_device_supports_timers(hass, llm_context.device_id) - ) wanted = set(LLM_INTENTS) - if supports_timers: - wanted.update(TIMER_INTENTS) + if (device_id := llm_context.device_id) is not None: + # Offer only the timer tools the device's own list can actually serve. + wanted.update( + intent_type + for intent_type in TIMER_INTENTS + if async_device_supports_timer_intent(hass, device_id, intent_type) + ) exposed_domains = { state.domain @@ -116,6 +119,6 @@ def async_get_tools( ) prompt_parts = [DEVICE_CONTROL_TOOL_USAGE_PROMPT, area_prompt] - if not supports_timers: + if intent.INTENT_START_TIMER not in wanted: prompt_parts.append("This device is not able to start timers.") return LLMTools(tools=tools, prompt="\n".join(prompt_parts)) diff --git a/homeassistant/components/intent/timers.py b/homeassistant/components/intent/timers.py index 09982cb2ba51..0be61dd84ab0 100644 --- a/homeassistant/components/intent/timers.py +++ b/homeassistant/components/intent/timers.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant.components.timer_list import ( TimerItem, TimerListEntity, + TimerListEntityFeature, TimerStatus, async_get_timer_list_entity, ) @@ -55,21 +56,61 @@ class TimersNotSupportedError(intent.IntentHandleError): ) +# The entity feature each timer intent needs. INTENT_TIMER_STATUS is absent +# because it only reads the `timers` property, which every list provides. +_INTENT_FEATURES: dict[str, TimerListEntityFeature] = { + intent.INTENT_START_TIMER: TimerListEntityFeature.CREATE_TIMER, + intent.INTENT_CANCEL_TIMER: TimerListEntityFeature.CANCEL_TIMER, + intent.INTENT_CANCEL_ALL_TIMERS: TimerListEntityFeature.CANCEL_TIMER, + intent.INTENT_INCREASE_TIMER: TimerListEntityFeature.ADD_TIME, + intent.INTENT_DECREASE_TIMER: TimerListEntityFeature.ADD_TIME, + intent.INTENT_PAUSE_TIMER: TimerListEntityFeature.PAUSE_TIMER, + intent.INTENT_UNPAUSE_TIMER: TimerListEntityFeature.PAUSE_TIMER, +} + + +@callback +def _supports_intent(entity: TimerListEntity, intent_type: str) -> bool: + """Return True if a timer list advertises what an intent needs.""" + if (feature := _INTENT_FEATURES.get(intent_type)) is None: + return True + return bool((entity.supported_features or 0) & feature) + + @callback def async_device_supports_timers(hass: HomeAssistant, device_id: str) -> bool: """Return True if a device has a timer_list entity to manage timers.""" return async_get_timer_list_entity(hass, device_id) is not None +@callback +def async_device_supports_timer_intent( + hass: HomeAssistant, device_id: str, intent_type: str +) -> bool: + """Return True if a device's timer list can serve a given timer intent.""" + if (entity := async_get_timer_list_entity(hass, device_id)) is None: + return False + return _supports_intent(entity, intent_type) + + # ----------------------------------------------------------------------------- @callback -def _get_timer_entity(hass: HomeAssistant, device_id: str | None) -> TimerListEntity: - """Return the requesting device's timer_list entity or raise if it has none.""" +def _get_timer_entity( + hass: HomeAssistant, device_id: str | None, intent_type: str +) -> TimerListEntity: + """Return the requesting device's timer_list entity. + + Raises if the device has no timer list, or if its list does not advertise + the feature the intent needs, so an unsupported action reports as + unsupported rather than reaching a base method that raises + ``NotImplementedError``. + """ if ( device_id is None or (entity := async_get_timer_list_entity(hass, device_id)) is None + or not _supports_intent(entity, intent_type) ): raise TimersNotSupportedError(device_id) return entity @@ -324,7 +365,7 @@ class StartTimerIntentHandler(intent.IntentHandler): async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: """Handle the intent.""" hass = intent_obj.hass - entity = _get_timer_entity(hass, intent_obj.device_id) + entity = _get_timer_entity(hass, intent_obj.device_id, self.intent_type) slots = self.async_validate_slots(intent_obj.slots) name: str | None = None @@ -361,7 +402,7 @@ class CancelTimerIntentHandler(intent.IntentHandler): async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: """Handle the intent.""" hass = intent_obj.hass - entity = _get_timer_entity(hass, intent_obj.device_id) + entity = _get_timer_entity(hass, intent_obj.device_id, self.intent_type) slots = self.async_validate_slots(intent_obj.slots) timer = _find_timer(entity, slots) @@ -379,7 +420,7 @@ class CancelAllTimersIntentHandler(intent.IntentHandler): async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: """Handle the intent.""" hass = intent_obj.hass - entity = _get_timer_entity(hass, intent_obj.device_id) + entity = _get_timer_entity(hass, intent_obj.device_id, self.intent_type) slots = self.async_validate_slots(intent_obj.slots) canceled = 0 @@ -408,7 +449,7 @@ class IncreaseTimerIntentHandler(intent.IntentHandler): async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: """Handle the intent.""" hass = intent_obj.hass - entity = _get_timer_entity(hass, intent_obj.device_id) + entity = _get_timer_entity(hass, intent_obj.device_id, self.intent_type) slots = self.async_validate_slots(intent_obj.slots) total_seconds = _get_total_seconds(slots) @@ -435,7 +476,7 @@ class DecreaseTimerIntentHandler(intent.IntentHandler): async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: """Handle the intent.""" hass = intent_obj.hass - entity = _get_timer_entity(hass, intent_obj.device_id) + entity = _get_timer_entity(hass, intent_obj.device_id, self.intent_type) slots = self.async_validate_slots(intent_obj.slots) total_seconds = _get_total_seconds(slots) @@ -461,7 +502,7 @@ class PauseTimerIntentHandler(intent.IntentHandler): async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: """Handle the intent.""" hass = intent_obj.hass - entity = _get_timer_entity(hass, intent_obj.device_id) + entity = _get_timer_entity(hass, intent_obj.device_id, self.intent_type) slots = self.async_validate_slots(intent_obj.slots) timer = _find_timer(entity, slots, find_filter=FindTimerFilter.ONLY_ACTIVE) @@ -483,7 +524,7 @@ class UnpauseTimerIntentHandler(intent.IntentHandler): async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: """Handle the intent.""" hass = intent_obj.hass - entity = _get_timer_entity(hass, intent_obj.device_id) + entity = _get_timer_entity(hass, intent_obj.device_id, self.intent_type) slots = self.async_validate_slots(intent_obj.slots) timer = _find_timer(entity, slots, find_filter=FindTimerFilter.ONLY_INACTIVE) @@ -505,7 +546,7 @@ class TimerStatusIntentHandler(intent.IntentHandler): async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: """Handle the intent.""" hass = intent_obj.hass - entity = _get_timer_entity(hass, intent_obj.device_id) + entity = _get_timer_entity(hass, intent_obj.device_id, self.intent_type) slots = self.async_validate_slots(intent_obj.slots) now = dt_util.utcnow() diff --git a/homeassistant/components/timer_list/__init__.py b/homeassistant/components/timer_list/__init__.py index 9a0f33515b1c..9ba9f976c32f 100644 --- a/homeassistant/components/timer_list/__init__.py +++ b/homeassistant/components/timer_list/__init__.py @@ -302,8 +302,9 @@ class TimerListEntity(Entity): def timers(self) -> list[TimerItem]: """Return the timers in the list. - Includes archived (``finished`` and ``cancelled``) timers, which are - what voice commands like "how long is left on my timer?" report on. + Includes archived (``finished`` and ``cancelled``) timers, which the + websocket API and ``get_timers`` report. Voice commands only ever + consider active and paused timers. """ raise NotImplementedError @@ -368,7 +369,9 @@ class TimerListEntity(Entity): Emits ``TIME_CHANGED`` carrying ``duration`` as the event's signed ``delta``. ``created_duration`` must not change, and ``total_duration`` must be raised so it stays at least the new remaining time. Subtracting - more than is left finishes the timer instead, emitting ``FINISHED``. + more than is left finishes the timer instead, emitting ``FINISHED``, + whether it was active or paused. A zero ``duration`` changes nothing + and emits no event. """ raise NotImplementedError diff --git a/homeassistant/components/timer_list/local.py b/homeassistant/components/timer_list/local.py index 8d66402f1638..bd3d2081791b 100644 --- a/homeassistant/components/timer_list/local.py +++ b/homeassistant/components/timer_list/local.py @@ -109,7 +109,6 @@ class InMemoryTimerListEntity(TimerListEntity): if timer.status in _FINISHED_STATUSES: # Already archived (finished or cancelled); nothing to cancel. return - self._unschedule(timer_id) self._archive(timer, TimerStatus.CANCELLED) @override @@ -119,27 +118,30 @@ class InMemoryTimerListEntity(TimerListEntity): if timer.status in _FINISHED_STATUSES: # Already archived (finished or cancelled); nothing to finish. return - self._unschedule(timer_id) self._archive(timer, TimerStatus.FINISHED) @override async def async_add_time(self, timer_id: str, duration: timedelta) -> None: """Add (or, with a negative duration, subtract) time on a timer.""" timer = self._get_timer(timer_id) + if not duration: + return now = dt_util.utcnow() if timer.status == TimerStatus.ACTIVE and timer.finishes_at is not None: finishes_at = timer.finishes_at + duration if finishes_at <= now: # Subtracted past the end; finish now rather than schedule the past. - self._unschedule(timer_id) self._archive(timer, TimerStatus.FINISHED) return timer.finishes_at = finishes_at self._schedule(timer) elif timer.status == TimerStatus.PAUSED and timer.paused_remaining is not None: - timer.paused_remaining = max( - timedelta(0), timer.paused_remaining + duration - ) + paused_remaining = timer.paused_remaining + duration + if paused_remaining <= timedelta(0): + # Same as the active case: nothing left to resume to. + self._archive(timer, TimerStatus.FINISHED) + return + timer.paused_remaining = paused_remaining else: return # Rounded to whole seconds: timers are second-granularity, and the @@ -187,6 +189,7 @@ class InMemoryTimerListEntity(TimerListEntity): @callback def _archive(self, timer: TimerItem, status: TimerStatus) -> None: """Move a timer to a terminal status and notify subscribers.""" + self._unschedule(timer.timer_id) timer.status = status timer.finishes_at = None timer.paused_remaining = None diff --git a/homeassistant/components/timer_list/trigger.py b/homeassistant/components/timer_list/trigger.py index 75d1ca1d1a40..d9d9d8c08e11 100644 --- a/homeassistant/components/timer_list/trigger.py +++ b/homeassistant/components/timer_list/trigger.py @@ -10,6 +10,7 @@ from homeassistant.const import ATTR_ENTITY_ID, CONF_OPTIONS, CONF_TARGET from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback, split_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.target import TargetEntityChangeTracker, TargetSelection from homeassistant.helpers.trigger import ( Trigger, @@ -62,13 +63,26 @@ class TimerEventListener(TargetEntityChangeTracker): self._unsubscribe_listeners = [] component = self._hass.data[DATA_COMPONENT] + pending: set[str] = set() for entity_id in tracked_entities: if (entity := component.get_entity(entity_id)) is None: + pending.add(entity_id) continue self._unsubscribe_listeners.append( entity.async_subscribe_updates(partial(self._listener, entity_id)) ) + if pending: + # Entity registry creation fires before EntityPlatform hands the + # entity to the component, so a targeted list added after this + # trigger is not resolvable yet. Its first state write is, so retry + # then instead of dropping it for the lifetime of the automation. + self._unsubscribe_listeners.append( + async_track_state_change_event( + self._hass, pending, self._handle_target_update + ) + ) + @override @callback def _unsubscribe(self) -> None: diff --git a/tests/components/intent/test_llm.py b/tests/components/intent/test_llm.py index e53fc6e7f9f6..502b162423f6 100644 --- a/tests/components/intent/test_llm.py +++ b/tests/components/intent/test_llm.py @@ -56,7 +56,11 @@ async def test_generic_intents_exposed(hass: HomeAssistant) -> None: async def test_timer_intents_require_timer_device(hass: HomeAssistant) -> None: """Test timer intents are not exposed without a timer-capable device.""" - assert "intent__HassStartTimer" not in await _tool_names(hass) + names = await _tool_names(hass) + assert "intent__HassStartTimer" not in names + # Cancel-all resolves the requesting device's list too, so it cannot work + # without one either. + assert "intent__HassCancelAllTimers" not in names async def _add_timer_device(hass: HomeAssistant) -> str: @@ -90,6 +94,7 @@ async def test_timer_intents_offered_for_timer_device(hass: HomeAssistant) -> No names = {tool.name for tool in result.tools} assert "intent__HassStartTimer" in names assert "intent__HassTimerStatus" in names + assert "intent__HassCancelAllTimers" in names async def test_set_position_requires_exposed_cover(hass: HomeAssistant) -> None: diff --git a/tests/components/intent/test_timers.py b/tests/components/intent/test_timers.py index b8950f066f09..8b96c8239a50 100644 --- a/tests/components/intent/test_timers.py +++ b/tests/components/intent/test_timers.py @@ -3,6 +3,7 @@ import asyncio from collections.abc import Callable from datetime import timedelta +from typing import Any from unittest.mock import MagicMock import pytest @@ -12,6 +13,7 @@ from homeassistant.components.intent.timers import ( TimerNotFoundError, TimersNotSupportedError, _round_time, + async_device_supports_timer_intent, async_device_supports_timers, ) from homeassistant.components.local_timer_list import LocalTimerListEntity @@ -20,6 +22,7 @@ from homeassistant.components.timer_list import ( DOMAIN as TIMER_LIST_DOMAIN, TimerItem, TimerListEntity, + TimerListEntityFeature, TimerListEvent, TimerListEventType, TimerStatus, @@ -1106,6 +1109,58 @@ async def test_async_device_supports_timers(hass: HomeAssistant) -> None: assert async_device_supports_timers(hass, device_id) +@pytest.mark.parametrize( + ("intent_type", "slots"), + [ + pytest.param( + intent.INTENT_START_TIMER, {"minutes": {"value": 5}}, id="start_timer" + ), + pytest.param(intent.INTENT_CANCEL_TIMER, {}, id="cancel_timer"), + pytest.param(intent.INTENT_CANCEL_ALL_TIMERS, {}, id="cancel_all_timers"), + pytest.param(intent.INTENT_PAUSE_TIMER, {}, id="pause_timer"), + pytest.param(intent.INTENT_UNPAUSE_TIMER, {}, id="unpause_timer"), + pytest.param( + intent.INTENT_INCREASE_TIMER, {"minutes": {"value": 1}}, id="increase_timer" + ), + pytest.param( + intent.INTENT_DECREASE_TIMER, {"minutes": {"value": 1}}, id="decrease_timer" + ), + ], +) +async def test_intents_require_the_entity_feature( + hass: HomeAssistant, init_components, intent_type: str, slots: dict[str, Any] +) -> None: + """Test intents report unsupported when the list lacks the needed feature.""" + device_id = _make_timer_device_id(hass) + await _register_timer_device(hass, device_id) + entity = _get_timer_entity(hass, device_id) + entity._attr_supported_features = TimerListEntityFeature(0) + + assert not async_device_supports_timer_intent(hass, device_id, intent_type) + + with pytest.raises(TimersNotSupportedError): + await intent.async_handle(hass, "test", intent_type, slots, device_id=device_id) + + +async def test_timer_status_needs_no_feature( + hass: HomeAssistant, init_components +) -> None: + """Test reading timers works on a list that advertises nothing.""" + device_id = _make_timer_device_id(hass) + await _register_timer_device(hass, device_id) + entity = _get_timer_entity(hass, device_id) + entity._attr_supported_features = TimerListEntityFeature(0) + + assert async_device_supports_timer_intent( + hass, device_id, intent.INTENT_TIMER_STATUS + ) + + result = await intent.async_handle( + hass, "test", intent.INTENT_TIMER_STATUS, {}, device_id=device_id + ) + assert result.speech_slots["timers"] == [] + + async def test_cancel_all_timers(hass: HomeAssistant, init_components) -> None: """Test cancelling all timers.""" device_id = _make_timer_device_id(hass) diff --git a/tests/components/timer_list/test_init.py b/tests/components/timer_list/test_init.py index ff88e9906c75..8c38af08ac80 100644 --- a/tests/components/timer_list/test_init.py +++ b/tests/components/timer_list/test_init.py @@ -168,14 +168,59 @@ async def test_add_and_subtract_time( @pytest.mark.usefixtures("test_entity") -async def test_subtract_time_finishes_timer(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "setup_services", + [ + pytest.param([], id="active"), + pytest.param(["pause_timer"], id="paused"), + ], +) +async def test_subtract_time_finishes_timer( + hass: HomeAssistant, setup_services: list[str] +) -> None: """Test subtracting more time than remaining finishes the timer immediately.""" timer_id = await _create_timer(hass, duration=60) + for service in setup_services: + await _call(hass, service, timer_id=timer_id) await _call(hass, "subtract_time", timer_id=timer_id, duration={"seconds": 120}) assert hass.states.get(TEST_ENTITY_ID).state == "0" - assert (await _get_timers(hass))[0]["status"] == "finished" + timers = await _get_timers(hass) + assert timers[0]["status"] == "finished" + assert timers[0]["ended_at"] is not None + + +@pytest.mark.usefixtures("test_entity") +@pytest.mark.parametrize( + "service", + [ + pytest.param("add_time", id="add_time"), + pytest.param("subtract_time", id="subtract_time"), + ], +) +async def test_zero_duration_is_noop( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator, service: str +) -> None: + """Test a zero duration changes nothing and emits no event.""" + timer_id = await _create_timer(hass, duration=60) + before = (await _get_timers(hass))[0] + + client = await hass_ws_client(hass) + await client.send_json_auto_id( + {"type": "timer_list/item/subscribe", "entity_id": TEST_ENTITY_ID} + ) + assert (await client.receive_json())["success"] + assert (await client.receive_json())["event"]["type"] == "timers" + + await _call(hass, service, timer_id=timer_id, duration={"seconds": 0}) + + assert (await _get_timers(hass))[0]["finishes_at"] == before["finishes_at"] + # A change event would arrive before the reply to this round trip. + await client.send_json_auto_id( + {"type": "timer_list/item/list", "entity_id": TEST_ENTITY_ID} + ) + assert (await client.receive_json())["success"] @pytest.mark.usefixtures("test_entity") @@ -192,19 +237,19 @@ async def test_cancel_timer_archives_timer(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("test_entity") @pytest.mark.parametrize( - "paused", + "setup_services", [ - pytest.param(False, id="active"), - pytest.param(True, id="paused"), + pytest.param([], id="active"), + pytest.param(["pause_timer"], id="paused"), ], ) async def test_finish_timer_archives_as_finished( - hass: HomeAssistant, paused: bool + hass: HomeAssistant, setup_services: list[str] ) -> None: """Test finishing a timer early archives it as finished.""" timer_id = await _create_timer(hass, duration=3600) - if paused: - await _call(hass, "pause_timer", timer_id=timer_id) + for service in setup_services: + await _call(hass, service, timer_id=timer_id) await _call(hass, "finish_timer", timer_id=timer_id) diff --git a/tests/components/timer_list/test_trigger.py b/tests/components/timer_list/test_trigger.py index 99ab8838f776..8bfe3b20d884 100644 --- a/tests/components/timer_list/test_trigger.py +++ b/tests/components/timer_list/test_trigger.py @@ -40,7 +40,10 @@ async def setup_entity(hass: HomeAssistant) -> None: async def _setup_automation( - hass: HomeAssistant, trigger_type: str, extra_data: dict[str, str] | None = None + hass: HomeAssistant, + trigger_type: str, + extra_data: dict[str, str] | None = None, + entity_id: str = TEST_ENTITY_ID, ) -> None: """Set up an automation for the given timer list trigger.""" assert await async_setup_component( @@ -51,7 +54,7 @@ async def _setup_automation( "triggers": [ { CONF_PLATFORM: f"{DOMAIN}.{trigger_type}", - CONF_TARGET: {CONF_ENTITY_ID: TEST_ENTITY_ID}, + CONF_TARGET: {CONF_ENTITY_ID: entity_id}, } ], "action": { @@ -211,6 +214,29 @@ async def test_timer_time_changed_trigger_reports_delta( assert service_calls[0].data["delta"] == expected_delta +async def test_trigger_picks_up_entity_added_after_setup( + hass: HomeAssistant, service_calls: list[ServiceCall] +) -> None: + """Test a timer list created after the automation still fires the trigger.""" + late_entity_id = "timer_list.late" + await _setup_automation(hass, "timer_created", entity_id=late_entity_id) + + entity = MockTimerListEntity(name="Late") + entity.entity_id = late_entity_id + await create_mock_platform(hass, [entity]) + + await hass.services.async_call( + DOMAIN, + "create_timer", + {"duration": {"seconds": 60}}, + target={ATTR_ENTITY_ID: late_entity_id}, + blocking=True, + ) + + assert len(service_calls) == 1 + assert service_calls[0].data["entity_id"] == late_entity_id + + async def test_trigger_options_supported(hass: HomeAssistant) -> None: """Test the timer list triggers do not advertise behavior or duration.""" for trigger_type in (