From 017347520560c1e8b748443173f96bb24cb5bfc6 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 20 Jul 2026 14:03:18 -0500 Subject: [PATCH] Clean up --- .../components/esphome/timer_list.py | 9 ++++++--- homeassistant/components/timer_list/local.py | 3 +++ .../cloud/snapshots/test_http_api.ambr | 8 ++++++-- .../esphome/test_assist_satellite.py | 18 ++++++++++++++++++ tests/components/timer_list/test_init.py | 19 +++++++++++++++++++ 5 files changed, 52 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/timer_list.py b/homeassistant/components/esphome/timer_list.py index 82c538fc01ca..a9cb54dfb7dc 100644 --- a/homeassistant/components/esphome/timer_list.py +++ b/homeassistant/components/esphome/timer_list.py @@ -2,7 +2,7 @@ from functools import partial -from aioesphomeapi import VoiceAssistantTimerEventType +from aioesphomeapi import VoiceAssistantFeature, VoiceAssistantTimerEventType from homeassistant.components.timer_list import ( InMemoryTimerListEntity, @@ -46,11 +46,14 @@ async def async_setup_entry( entry: ESPHomeConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up the timer list for a voice-capable ESPHome device.""" + """Set up the timer list for a timer-capable ESPHome device.""" entry_data = entry.runtime_data device_info = entry_data.device_info assert device_info is not None - if not device_info.voice_assistant_feature_flags_compat(entry_data.api_version): + feature_flags = device_info.voice_assistant_feature_flags_compat( + entry_data.api_version + ) + if not (feature_flags & VoiceAssistantFeature.TIMERS): return mac = device_info.mac_address diff --git a/homeassistant/components/timer_list/local.py b/homeassistant/components/timer_list/local.py index 3cbb309ef72b..739407cc4cd3 100644 --- a/homeassistant/components/timer_list/local.py +++ b/homeassistant/components/timer_list/local.py @@ -101,6 +101,9 @@ class InMemoryTimerListEntity(TimerListEntity): async def async_cancel_timer(self, timer_id: str) -> None: """Cancel a timer, archiving it in the ``cancelled`` state.""" timer = self._get_timer(timer_id) + if timer.status in _FINISHED_STATUSES: + # Already archived (finished or cancelled); nothing to cancel. + return self._unschedule(timer_id) timer.status = TimerStatus.CANCELLED timer.finishes_at = None diff --git a/tests/components/cloud/snapshots/test_http_api.ambr b/tests/components/cloud/snapshots/test_http_api.ambr index 72b13befc4ce..905331d7ca82 100644 --- a/tests/components/cloud/snapshots/test_http_api.ambr +++ b/tests/components/cloud/snapshots/test_http_api.ambr @@ -21,7 +21,7 @@ ## Active Integrations - Built-in integrations: 23 + Built-in integrations: 25 Custom integrations: 1
Built-in integrations @@ -48,9 +48,11 @@ repairs | Repairs stt | Speech-to-text (STT) system_health | System Health + timer_list | Timer list tts | Text-to-speech (TTS) web_rtc | WebRTC webhook | Webhook + websocket_api | Home Assistant WebSocket API
@@ -157,7 +159,7 @@ ## Active Integrations - Built-in integrations: 23 + Built-in integrations: 25 Custom integrations: 0
Built-in integrations @@ -184,9 +186,11 @@ repairs | Repairs stt | Speech-to-text (STT) system_health | System Health + timer_list | Timer list tts | Text-to-speech (TTS) web_rtc | WebRTC webhook | Webhook + websocket_api | Home Assistant WebSocket API
diff --git a/tests/components/esphome/test_assist_satellite.py b/tests/components/esphome/test_assist_satellite.py index 7cb8d446324d..8a21f87f87e2 100644 --- a/tests/components/esphome/test_assist_satellite.py +++ b/tests/components/esphome/test_assist_satellite.py @@ -833,6 +833,24 @@ async def test_timer_events( ) +async def test_no_timer_list_without_timers_flag( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, +) -> None: + """Test a voice device without the TIMERS flag gets no timer list entity.""" + await mock_esphome_device( + mock_client=mock_client, + device_info={ + "voice_assistant_feature_flags": VoiceAssistantFeature.VOICE_ASSISTANT + }, + ) + await hass.async_block_till_done() + + assert hass.states.async_entity_ids("timer_list") == [] + + async def test_unknown_timer_event( hass: HomeAssistant, device_registry: dr.DeviceRegistry, diff --git a/tests/components/timer_list/test_init.py b/tests/components/timer_list/test_init.py index dd688a73c5cf..154e774ad70f 100644 --- a/tests/components/timer_list/test_init.py +++ b/tests/components/timer_list/test_init.py @@ -172,6 +172,25 @@ async def test_cancel_timer_archives_timer(hass: HomeAssistant) -> None: assert timers[0]["status"] == "cancelled" +@pytest.mark.usefixtures("test_entity") +async def test_cancel_already_archived_timer_is_noop( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: + """Test cancelling an already-archived timer does not rewrite it.""" + timer_id = await _start_timer(hass) + await _call(hass, "cancel_timer", timer_id=timer_id) + finished_at = (await _get_timers(hass))[0]["finished_at"] + + # Cancelling again must not refresh the timestamp or re-fire a change + freezer.tick(timedelta(seconds=5)) + await _call(hass, "cancel_timer", timer_id=timer_id) + + timers = await _get_timers(hass) + assert len(timers) == 1 + assert timers[0]["status"] == "cancelled" + assert timers[0]["finished_at"] == finished_at + + @pytest.mark.usefixtures("test_entity") async def test_archive_limit_evicts_oldest( hass: HomeAssistant, freezer: FrozenDateTimeFactory