From 742e1b6f07a27693adeaff844f055f99e52569af Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 27 Sep 2026 17:17:06 +0200 Subject: [PATCH] Cancel otp sensor update timer when the entity is removed (#183320) Co-authored-by: Claude --- homeassistant/components/otp/sensor.py | 13 ++++++++++++- tests/components/otp/test_init.py | 26 +++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/otp/sensor.py b/homeassistant/components/otp/sensor.py index 0e89e0f5d392..46084edef825 100644 --- a/homeassistant/components/otp/sensor.py +++ b/homeassistant/components/otp/sensor.py @@ -1,5 +1,6 @@ """Support for One-Time Password (OTP).""" +import asyncio import time from typing import override @@ -39,6 +40,7 @@ class TOTPSensor(SensorEntity): _attr_should_poll = False _attr_native_value: StateType = None _next_expiration: float | None = None + _update_timer: asyncio.TimerHandle | None = None _attr_has_entity_name = True _attr_name = None @@ -58,6 +60,13 @@ class TOTPSensor(SensorEntity): """Handle when an entity is about to be added to Home Assistant.""" self._call_loop() + @override + async def async_will_remove_from_hass(self) -> None: + """Cancel the update timer when the entity is removed.""" + if self._update_timer: + self._update_timer.cancel() + self._update_timer = None + @callback def _call_loop(self) -> None: self._attr_native_value = self._otp.now() @@ -66,4 +75,6 @@ class TOTPSensor(SensorEntity): # Update must occur at even TIME_STEP, e.g. 12:00:00, 12:00:30, # 12:01:00, etc. in order to have synced time (see RFC6238) self._next_expiration = TIME_STEP - (time.time() % TIME_STEP) - self.hass.loop.call_later(self._next_expiration, self._call_loop) + self._update_timer = self.hass.loop.call_later( + self._next_expiration, self._call_loop + ) diff --git a/tests/components/otp/test_init.py b/tests/components/otp/test_init.py index 0ce8f44523e4..4ec1a55ffde5 100644 --- a/tests/components/otp/test_init.py +++ b/tests/components/otp/test_init.py @@ -1,9 +1,13 @@ """Test the One-Time Password (OTP) init.""" +from datetime import timedelta +from unittest.mock import MagicMock + from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_entry_setup_unload( @@ -21,3 +25,23 @@ async def test_entry_setup_unload( await hass.async_block_till_done() assert otp_config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_update_timer_cancelled_on_unload( + hass: HomeAssistant, + otp_config_entry: MockConfigEntry, + mock_pyotp: MagicMock, +) -> None: + """Test the sensor stops updating after the entry is unloaded.""" + otp_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(otp_config_entry.entry_id) + await hass.async_block_till_done() + + await hass.config_entries.async_unload(otp_config_entry.entry_id) + await hass.async_block_till_done() + mock_pyotp.TOTP().now.reset_mock() + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=1)) + await hass.async_block_till_done() + + mock_pyotp.TOTP().now.assert_not_called()