From f2db39c38ac9599acd5f1fe270b58ebcdaac5286 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 14 Jul 2026 09:31:44 -0400 Subject: [PATCH 1/2] Fix thermostat rounding and Sinope polling --- tests/test_climate.py | 188 +++++++++++++++--- zha/application/platforms/climate/__init__.py | 85 +++++++- 2 files changed, 233 insertions(+), 40 deletions(-) diff --git a/tests/test_climate.py b/tests/test_climate.py index 3109c298c..6254931da 100644 --- a/tests/test_climate.py +++ b/tests/test_climate.py @@ -3,7 +3,7 @@ # pylint: disable=redefined-outer-name,too-many-lines import asyncio -from collections.abc import Iterator +from collections.abc import Iterable, Iterator import logging from typing import Any from unittest.mock import AsyncMock, MagicMock, call, patch @@ -43,10 +43,11 @@ from zha.application.platforms.climate import ( HVAC_MODE_2_SYSTEM, SEQ_OF_OPERATION, + SinopeTechnologiesThermostat as SinopeThermostatEntity, Thermostat as ThermostatEntity, ZehnderThermostat, ) -from zha.application.platforms.climate.const import FanState +from zha.application.platforms.climate.const import PRECISION_TENTHS, FanState from zha.application.platforms.number import NumberConfigurationEntity from zha.application.platforms.sensor import ( Sensor, @@ -242,7 +243,7 @@ async def device_climate_mock( "ep_attribute", "sinope_manufacturer_specific", ) -async def device_climate_sinope(zha_gateway: Gateway): +async def device_climate_sinope(zha_gateway: Gateway) -> Device: """Sinope thermostat.""" return await device_climate_mock( @@ -253,6 +254,33 @@ async def device_climate_sinope(zha_gateway: Gateway): ) +async def sinope_climate_entity( + zha_gateway: Gateway, +) -> tuple[Device, SinopeThermostatEntity]: + """Create a Sinope thermostat and return its climate entity.""" + device = await device_climate_sinope(zha_gateway) + entity = get_entity( + device, platform=Platform.CLIMATE, entity_type=SinopeThermostatEntity + ) + assert isinstance(entity, SinopeThermostatEntity) + return device, entity + + +def sinope_time_update_tasks( + entity: SinopeThermostatEntity, + tasks: Iterable[asyncio.Future[Any]] | None = None, +) -> list[asyncio.Task[Any]]: + """Return the Sinope updater tasks in an ownership collection.""" + if tasks is None: + tasks = entity._tracked_tasks + task_name = f"sinope_time_updater_{entity.unique_id}" + return [ + task + for task in tasks + if isinstance(task, asyncio.Task) and task.get_name() == task_name + ] + + def test_sequence_mappings(): """Test correct mapping between control sequence -> HVAC Mode -> Sysmode.""" @@ -372,14 +400,10 @@ async def test_sinope_time( ): """Test hvac action via running state.""" - dev_climate_sinope = await device_climate_sinope(zha_gateway) + dev_climate_sinope, entity = await sinope_climate_entity(zha_gateway) mfg_cluster = dev_climate_sinope.device.endpoints[1].sinope_manufacturer_specific assert mfg_cluster is not None - entity: ThermostatEntity = get_entity( - dev_climate_sinope, platform=Platform.CLIMATE, entity_type=ThermostatEntity - ) - entity._async_update_time = AsyncMock(wraps=entity._async_update_time) await asyncio.sleep(4600) @@ -432,6 +456,109 @@ async def test_sinope_time( entity._async_update_time.reset_mock() +async def test_sinope_time_update_task_lifecycle_is_idempotent( + zha_gateway: Gateway, +) -> None: + """Test a burst of toggles defers exactly one updater restart.""" + _, entity = await sinope_climate_entity(zha_gateway) + time_update_task = entity._time_update_task + assert time_update_task is not None + assert sinope_time_update_tasks(entity) == [time_update_task] + assert sinope_time_update_tasks( + entity, zha_gateway._untracked_background_tasks + ) == [time_update_task] + + entity.enable() + assert entity._time_update_task is time_update_task + assert sinope_time_update_tasks(entity) == [time_update_task] + + for _ in range(25): + entity.disable() + entity.enable() + + assert entity.enabled + assert entity._time_update_task is time_update_task + assert time_update_task.cancelling() == 1 + assert sinope_time_update_tasks(entity) == [time_update_task] + assert sinope_time_update_tasks( + entity, zha_gateway._untracked_background_tasks + ) == [time_update_task] + + await asyncio.gather(time_update_task, return_exceptions=True) + await asyncio.sleep(0) + + replacement_task = entity._time_update_task + assert replacement_task is not None + assert replacement_task is not time_update_task + + assert time_update_task.done() + assert sinope_time_update_tasks(entity) == [replacement_task] + assert sinope_time_update_tasks( + entity, zha_gateway._untracked_background_tasks + ) == [replacement_task] + + await entity.on_remove() + assert replacement_task.done() + assert entity._time_update_task is None + assert not sinope_time_update_tasks(entity) + assert not sinope_time_update_tasks(entity, zha_gateway._untracked_background_tasks) + + +async def test_sinope_removal_blocks_updater_restart_races( + zha_gateway: Gateway, +) -> None: + """Test removal prevents updater work from escaping cleanup.""" + _, entity = await sinope_climate_entity(zha_gateway) + initial_task = entity._time_update_task + assert initial_task is not None + entity.disable() + await asyncio.gather(initial_task, return_exceptions=True) + await asyncio.sleep(0) + + cancellation_started = asyncio.Event() + allow_completion = asyncio.Event() + + async def update_until_removed() -> None: + try: + await asyncio.Event().wait() + except asyncio.CancelledError: + cancellation_started.set() + while not allow_completion.is_set(): + try: + await allow_completion.wait() + except asyncio.CancelledError: + cancellation_started.set() + + with patch.object(entity, "_update_time", new=update_until_removed): + entity.enable() + time_update_task = entity._time_update_task + assert time_update_task is not None + + remove_task = asyncio.create_task(entity.on_remove()) + await cancellation_started.wait() + + assert not entity.enabled + assert sinope_time_update_tasks(entity) == [time_update_task] + entity.enable() + entity.start_polling() + assert entity._time_update_task is time_update_task + + concurrent_remove_task = asyncio.create_task(entity.on_remove()) + await asyncio.sleep(0) + assert not concurrent_remove_task.done() + + allow_completion.set() + await asyncio.gather(remove_task, concurrent_remove_task) + + entity.enable() + entity.start_polling() + assert not entity.enabled + assert time_update_task.done() + assert entity._time_update_task is None + assert not sinope_time_update_tasks(entity) + assert not sinope_time_update_tasks(entity, zha_gateway._untracked_background_tasks) + + async def test_climate_hvac_action_running_state_zen( zha_gateway: Gateway, ): @@ -1037,34 +1164,34 @@ async def test_set_temperature_heat_cool( assert entity.state["target_temperature_high"] == 25.0 assert thrm_cluster.write_attributes.await_count == 0 - await entity.async_set_temperature(target_temp_high=26, target_temp_low=19) + await entity.async_set_temperature(target_temp_high=20.4, target_temp_low=19.9) await zha_gateway.async_block_till_done() - assert entity.state["target_temperature_low"] == 19.0 - assert entity.state["target_temperature_high"] == 26.0 + assert entity.state["target_temperature_low"] == 19.9 + assert entity.state["target_temperature_high"] == 20.4 assert thrm_cluster.write_attributes.await_count == 2 assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { - "occupied_heating_setpoint": 1900 + "occupied_heating_setpoint": 1990 } assert thrm_cluster.write_attributes.call_args_list[1][0][0] == { - "occupied_cooling_setpoint": 2600 + "occupied_cooling_setpoint": 2040 } await entity.async_set_preset_mode("away") await zha_gateway.async_block_till_done() thrm_cluster.write_attributes.reset_mock() - await entity.async_set_temperature(target_temp_high=30, target_temp_low=15) + await entity.async_set_temperature(target_temp_high=-19.9, target_temp_low=-20.4) await zha_gateway.async_block_till_done() - assert entity.state["target_temperature_low"] == 15.0 - assert entity.state["target_temperature_high"] == 30.0 + assert entity.state["target_temperature_low"] == -20.4 + assert entity.state["target_temperature_high"] == -19.9 assert thrm_cluster.write_attributes.await_count == 2 assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { - "unoccupied_heating_setpoint": 1500 + "unoccupied_heating_setpoint": -2040 } assert thrm_cluster.write_attributes.call_args_list[1][0][0] == { - "unoccupied_cooling_setpoint": 3000 + "unoccupied_cooling_setpoint": -1990 } @@ -1090,6 +1217,7 @@ async def test_set_temperature_heat( entity: ThermostatEntity = get_entity( device_climate, platform=Platform.CLIMATE, entity_type=ThermostatEntity ) + assert entity._attr_precision == PRECISION_TENTHS assert entity.state["hvac_mode"] == "heat" @@ -1101,30 +1229,30 @@ async def test_set_temperature_heat( assert entity.state["target_temperature"] == 20.0 assert thrm_cluster.write_attributes.await_count == 0 - await entity.async_set_temperature(temperature=21) + await entity.async_set_temperature(temperature=19.9) await zha_gateway.async_block_till_done() assert entity.state["target_temperature_low"] is None assert entity.state["target_temperature_high"] is None - assert entity.state["target_temperature"] == 21.0 + assert entity.state["target_temperature"] == 19.9 assert thrm_cluster.write_attributes.await_count == 1 assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { - "occupied_heating_setpoint": 2100 + "occupied_heating_setpoint": 1990 } await entity.async_set_preset_mode("away") await zha_gateway.async_block_till_done() thrm_cluster.write_attributes.reset_mock() - await entity.async_set_temperature(temperature=22) + await entity.async_set_temperature(temperature=-19.9) await zha_gateway.async_block_till_done() assert entity.state["target_temperature_low"] is None assert entity.state["target_temperature_high"] is None - assert entity.state["target_temperature"] == 22.0 + assert entity.state["target_temperature"] == -19.9 assert thrm_cluster.write_attributes.await_count == 1 assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { - "unoccupied_heating_setpoint": 2200 + "unoccupied_heating_setpoint": -1990 } @@ -1161,30 +1289,30 @@ async def test_set_temperature_cool( assert entity.state["target_temperature"] == 25.0 assert thrm_cluster.write_attributes.await_count == 0 - await entity.async_set_temperature(temperature=21) + await entity.async_set_temperature(temperature=20.4) await zha_gateway.async_block_till_done() assert entity.state["target_temperature_low"] is None assert entity.state["target_temperature_high"] is None - assert entity.state["target_temperature"] == 21.0 + assert entity.state["target_temperature"] == 20.4 assert thrm_cluster.write_attributes.await_count == 1 assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { - "occupied_cooling_setpoint": 2100 + "occupied_cooling_setpoint": 2040 } await entity.async_set_preset_mode("away") await zha_gateway.async_block_till_done() thrm_cluster.write_attributes.reset_mock() - await entity.async_set_temperature(temperature=22) + await entity.async_set_temperature(temperature=-20.4) await zha_gateway.async_block_till_done() assert entity.state["target_temperature_low"] is None assert entity.state["target_temperature_high"] is None - assert entity.state["target_temperature"] == 22.0 + assert entity.state["target_temperature"] == -20.4 assert thrm_cluster.write_attributes.await_count == 1 assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { - "unoccupied_cooling_setpoint": 2200 + "unoccupied_cooling_setpoint": -2040 } diff --git a/zha/application/platforms/climate/__init__.py b/zha/application/platforms/climate/__init__.py index 6e5d9a27a..8a40b7b55 100644 --- a/zha/application/platforms/climate/__init__.py +++ b/zha/application/platforms/climate/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod +import asyncio from asyncio import Task from dataclasses import dataclass import datetime as dt @@ -881,23 +882,23 @@ async def async_set_temperature( if self.hvac_mode == HVACMode.HEAT_COOL: if target_temp_low is not None: await self._async_set_heating_setpoint( - temperature=int(target_temp_low * ZCL_TEMP), + temperature=round(target_temp_low * ZCL_TEMP), is_away=is_away, ) if target_temp_high is not None: await self._async_set_cooling_setpoint( - temperature=int(target_temp_high * ZCL_TEMP), + temperature=round(target_temp_high * ZCL_TEMP), is_away=is_away, ) elif temperature is not None: if self.hvac_mode == HVACMode.COOL: await self._async_set_cooling_setpoint( - temperature=int(temperature * ZCL_TEMP), + temperature=round(temperature * ZCL_TEMP), is_away=is_away, ) elif self.hvac_mode == HVACMode.HEAT: await self._async_set_heating_setpoint( - temperature=int(temperature * ZCL_TEMP), + temperature=round(temperature * ZCL_TEMP), is_away=is_away, ) else: @@ -945,7 +946,10 @@ def __init__( self._sinope_cluster = endpoint.zigpy_endpoint.in_clusters[ SINOPE_MANUFACTURER_CLUSTER ] - self._time_update_task: Task | None = None + self._time_update_task: Task[Any] | None = None + self._time_update_restart_pending = False + self._time_update_stopping = False + self._time_update_remove_lock = asyncio.Lock() def recompute_capabilities(self) -> None: """Recompute capabilities and feature flags.""" @@ -957,15 +961,56 @@ def on_add(self) -> None: super().on_add() self.start_polling() + def _track_time_update_task(self, task: Task[Any]) -> None: + """Track ownership of a time updater task.""" + if not any(tracked_task is task for tracked_task in self._tracked_tasks): + self._tracked_tasks.append(task) + + def _time_update_task_done(self, task: Task[Any]) -> None: + """Release ownership of a completed time updater task.""" + self._tracked_tasks[:] = [ + tracked_task + for tracked_task in self._tracked_tasks + if tracked_task is not task + ] + if self._time_update_task is not task: + return + + self._time_update_task = None + restart_pending = self._time_update_restart_pending + self._time_update_restart_pending = False + if not restart_pending or not self.enabled or self._time_update_stopping: + return + + self.start_polling() + def start_polling(self) -> None: """Start polling.""" + if not self.enabled or self._time_update_stopping: + self._time_update_restart_pending = False + return + + current_task = self._time_update_task + if current_task is not None: + if current_task.done(): + self._time_update_restart_pending = True + self._time_update_task_done(current_task) + return + + self._track_time_update_task(current_task) + if current_task.cancelling(): + self._time_update_restart_pending = True + return + + self._time_update_restart_pending = False self._time_update_task = self.device.gateway.async_create_background_task( self._update_time(), name=f"sinope_time_updater_{self.unique_id}", eager_start=True, untracked=True, ) - self._tracked_tasks.append(self._time_update_task) + self._track_time_update_task(self._time_update_task) + self._time_update_task.add_done_callback(self._time_update_task_done) self.debug( "started time updating interval of %s", getattr(self, "__polling_interval"), @@ -973,16 +1018,36 @@ def start_polling(self) -> None: def enable(self) -> None: """Enable the entity.""" + if self._time_update_stopping: + return super().enable() self.start_polling() def disable(self) -> None: """Disable the entity.""" super().disable() - if self._time_update_task: - self._tracked_tasks.remove(self._time_update_task) - self._time_update_task.cancel() - self._time_update_task = None + self._time_update_restart_pending = False + time_update_task = self._time_update_task + if time_update_task is None: + return + if time_update_task.done(): + self._time_update_task_done(time_update_task) + return + + self._track_time_update_task(time_update_task) + if not time_update_task.cancelling(): + time_update_task.cancel() + + async def on_remove(self) -> None: + """Stop time updates before removing the entity.""" + async with self._time_update_remove_lock: + if self._time_update_stopping: + return + + self._time_update_stopping = True + self._time_update_restart_pending = False + self.disable() + await super().on_remove() @periodic((2700, 4500)) async def _update_time(self) -> None: From 7a512058a9ec4886b96cd3541300192ec77baa87 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 14 Jul 2026 09:57:20 -0400 Subject: [PATCH 2/2] Simplify Sinope time updater lifecycle --- tests/test_climate.py | 115 ++++++------------ zha/application/platforms/climate/__init__.py | 71 +++-------- 2 files changed, 49 insertions(+), 137 deletions(-) diff --git a/tests/test_climate.py b/tests/test_climate.py index 6254931da..09e7059b5 100644 --- a/tests/test_climate.py +++ b/tests/test_climate.py @@ -3,7 +3,7 @@ # pylint: disable=redefined-outer-name,too-many-lines import asyncio -from collections.abc import Iterable, Iterator +from collections.abc import Iterator import logging from typing import Any from unittest.mock import AsyncMock, MagicMock, call, patch @@ -266,21 +266,6 @@ async def sinope_climate_entity( return device, entity -def sinope_time_update_tasks( - entity: SinopeThermostatEntity, - tasks: Iterable[asyncio.Future[Any]] | None = None, -) -> list[asyncio.Task[Any]]: - """Return the Sinope updater tasks in an ownership collection.""" - if tasks is None: - tasks = entity._tracked_tasks - task_name = f"sinope_time_updater_{entity.unique_id}" - return [ - task - for task in tasks - if isinstance(task, asyncio.Task) and task.get_name() == task_name - ] - - def test_sequence_mappings(): """Test correct mapping between control sequence -> HVAC Mode -> Sysmode.""" @@ -459,30 +444,28 @@ async def test_sinope_time( async def test_sinope_time_update_task_lifecycle_is_idempotent( zha_gateway: Gateway, ) -> None: - """Test a burst of toggles defers exactly one updater restart.""" + """Test polling is idempotent and restarts after cancellation completes.""" _, entity = await sinope_climate_entity(zha_gateway) time_update_task = entity._time_update_task assert time_update_task is not None - assert sinope_time_update_tasks(entity) == [time_update_task] - assert sinope_time_update_tasks( - entity, zha_gateway._untracked_background_tasks - ) == [time_update_task] + assert entity._tracked_tasks == [time_update_task] + assert time_update_task in zha_gateway._untracked_background_tasks - entity.enable() + for _ in range(3): + entity.enable() + entity.start_polling() assert entity._time_update_task is time_update_task - assert sinope_time_update_tasks(entity) == [time_update_task] + assert entity._tracked_tasks == [time_update_task] - for _ in range(25): - entity.disable() - entity.enable() + entity.disable() + entity.start_polling() + assert not entity.enabled + assert entity._time_update_task is time_update_task + assert time_update_task.cancelling() == 1 + entity.enable() assert entity.enabled assert entity._time_update_task is time_update_task - assert time_update_task.cancelling() == 1 - assert sinope_time_update_tasks(entity) == [time_update_task] - assert sinope_time_update_tasks( - entity, zha_gateway._untracked_background_tasks - ) == [time_update_task] await asyncio.gather(time_update_task, return_exceptions=True) await asyncio.sleep(0) @@ -490,73 +473,45 @@ async def test_sinope_time_update_task_lifecycle_is_idempotent( replacement_task = entity._time_update_task assert replacement_task is not None assert replacement_task is not time_update_task - assert time_update_task.done() - assert sinope_time_update_tasks(entity) == [replacement_task] - assert sinope_time_update_tasks( - entity, zha_gateway._untracked_background_tasks - ) == [replacement_task] + assert entity._tracked_tasks == [replacement_task] + assert replacement_task in zha_gateway._untracked_background_tasks + assert time_update_task not in zha_gateway._untracked_background_tasks - await entity.on_remove() + entity.disable() + await asyncio.gather(replacement_task, return_exceptions=True) + await asyncio.sleep(0) assert replacement_task.done() assert entity._time_update_task is None - assert not sinope_time_update_tasks(entity) - assert not sinope_time_update_tasks(entity, zha_gateway._untracked_background_tasks) + assert not entity._tracked_tasks + assert replacement_task not in zha_gateway._untracked_background_tasks -async def test_sinope_removal_blocks_updater_restart_races( +async def test_sinope_removal_prevents_updater_restart( zha_gateway: Gateway, ) -> None: - """Test removal prevents updater work from escaping cleanup.""" + """Test removal cancels the updater and prevents a pending restart.""" _, entity = await sinope_climate_entity(zha_gateway) - initial_task = entity._time_update_task - assert initial_task is not None - entity.disable() - await asyncio.gather(initial_task, return_exceptions=True) - await asyncio.sleep(0) - - cancellation_started = asyncio.Event() - allow_completion = asyncio.Event() - - async def update_until_removed() -> None: - try: - await asyncio.Event().wait() - except asyncio.CancelledError: - cancellation_started.set() - while not allow_completion.is_set(): - try: - await allow_completion.wait() - except asyncio.CancelledError: - cancellation_started.set() - - with patch.object(entity, "_update_time", new=update_until_removed): - entity.enable() - time_update_task = entity._time_update_task - assert time_update_task is not None - - remove_task = asyncio.create_task(entity.on_remove()) - await cancellation_started.wait() + time_update_task = entity._time_update_task + assert time_update_task is not None - assert not entity.enabled - assert sinope_time_update_tasks(entity) == [time_update_task] + entity.disable() entity.enable() - entity.start_polling() + assert entity.enabled assert entity._time_update_task is time_update_task - concurrent_remove_task = asyncio.create_task(entity.on_remove()) - await asyncio.sleep(0) - assert not concurrent_remove_task.done() - - allow_completion.set() - await asyncio.gather(remove_task, concurrent_remove_task) + await entity.on_remove() + assert not entity.enabled + assert time_update_task.done() + assert entity._time_update_task is None + assert not entity._tracked_tasks + assert time_update_task not in zha_gateway._untracked_background_tasks entity.enable() entity.start_polling() assert not entity.enabled - assert time_update_task.done() assert entity._time_update_task is None - assert not sinope_time_update_tasks(entity) - assert not sinope_time_update_tasks(entity, zha_gateway._untracked_background_tasks) + assert not entity._tracked_tasks async def test_climate_hvac_action_running_state_zen( diff --git a/zha/application/platforms/climate/__init__.py b/zha/application/platforms/climate/__init__.py index 8a40b7b55..354cc7207 100644 --- a/zha/application/platforms/climate/__init__.py +++ b/zha/application/platforms/climate/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from abc import ABC, abstractmethod -import asyncio from asyncio import Task from dataclasses import dataclass import datetime as dt @@ -947,9 +946,7 @@ def __init__( SINOPE_MANUFACTURER_CLUSTER ] self._time_update_task: Task[Any] | None = None - self._time_update_restart_pending = False - self._time_update_stopping = False - self._time_update_remove_lock = asyncio.Lock() + self._time_update_removed = False def recompute_capabilities(self) -> None: """Recompute capabilities and feature flags.""" @@ -961,55 +958,28 @@ def on_add(self) -> None: super().on_add() self.start_polling() - def _track_time_update_task(self, task: Task[Any]) -> None: - """Track ownership of a time updater task.""" - if not any(tracked_task is task for tracked_task in self._tracked_tasks): - self._tracked_tasks.append(task) - def _time_update_task_done(self, task: Task[Any]) -> None: - """Release ownership of a completed time updater task.""" - self._tracked_tasks[:] = [ - tracked_task - for tracked_task in self._tracked_tasks - if tracked_task is not task - ] - if self._time_update_task is not task: - return - + """Release the updater task and restart it if the entity was re-enabled.""" + self._tracked_tasks.remove(task) self._time_update_task = None - restart_pending = self._time_update_restart_pending - self._time_update_restart_pending = False - if not restart_pending or not self.enabled or self._time_update_stopping: - return - self.start_polling() def start_polling(self) -> None: """Start polling.""" - if not self.enabled or self._time_update_stopping: - self._time_update_restart_pending = False - return - - current_task = self._time_update_task - if current_task is not None: - if current_task.done(): - self._time_update_restart_pending = True - self._time_update_task_done(current_task) - return - - self._track_time_update_task(current_task) - if current_task.cancelling(): - self._time_update_restart_pending = True + if ( + self._time_update_removed + or not self.enabled + or self._time_update_task is not None + ): return - self._time_update_restart_pending = False self._time_update_task = self.device.gateway.async_create_background_task( self._update_time(), name=f"sinope_time_updater_{self.unique_id}", eager_start=True, untracked=True, ) - self._track_time_update_task(self._time_update_task) + self._tracked_tasks.append(self._time_update_task) self._time_update_task.add_done_callback(self._time_update_task_done) self.debug( "started time updating interval of %s", @@ -1018,7 +988,7 @@ def start_polling(self) -> None: def enable(self) -> None: """Enable the entity.""" - if self._time_update_stopping: + if self._time_update_removed: return super().enable() self.start_polling() @@ -1026,28 +996,15 @@ def enable(self) -> None: def disable(self) -> None: """Disable the entity.""" super().disable() - self._time_update_restart_pending = False time_update_task = self._time_update_task - if time_update_task is None: - return - if time_update_task.done(): - self._time_update_task_done(time_update_task) - return - - self._track_time_update_task(time_update_task) - if not time_update_task.cancelling(): + if time_update_task is not None and not time_update_task.cancelling(): time_update_task.cancel() async def on_remove(self) -> None: """Stop time updates before removing the entity.""" - async with self._time_update_remove_lock: - if self._time_update_stopping: - return - - self._time_update_stopping = True - self._time_update_restart_pending = False - self.disable() - await super().on_remove() + self._time_update_removed = True + super().disable() + await super().on_remove() @periodic((2700, 4500)) async def _update_time(self) -> None: