From e0afcbc02be01eda96afc6212fc498f717eb6571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Fri, 3 Oct 2025 17:39:14 +0100 Subject: [PATCH] Debounce updates in Idasen Desk (#153503) --- .../components/idasen_desk/coordinator.py | 28 +++++++++++++++++-- tests/components/idasen_desk/__init__.py | 2 ++ tests/components/idasen_desk/test_cover.py | 17 +++++++---- tests/components/idasen_desk/test_sensor.py | 22 +++++++++++++-- 4 files changed, 59 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/idasen_desk/coordinator.py b/homeassistant/components/idasen_desk/coordinator.py index 5da3d57cf9a6..f7b7edd2cc1a 100644 --- a/homeassistant/components/idasen_desk/coordinator.py +++ b/homeassistant/components/idasen_desk/coordinator.py @@ -8,13 +8,16 @@ from idasen_ha import Desk from homeassistant.components import bluetooth from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator _LOGGER = logging.getLogger(__name__) type IdasenDeskConfigEntry = ConfigEntry[IdasenDeskCoordinator] +UPDATE_DEBOUNCE_TIME = 0.2 + class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]): """Class to manage updates for the Idasen Desk.""" @@ -33,9 +36,22 @@ class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]): hass, _LOGGER, config_entry=config_entry, name=config_entry.title ) self.address = address - self._expected_connected = False + self.desk = Desk(self._async_handle_update) - self.desk = Desk(self.async_set_updated_data) + self._expected_connected = False + self._height: int | None = None + + @callback + def async_update_data() -> None: + self.async_set_updated_data(self._height) + + self._debouncer = Debouncer( + hass=self.hass, + logger=_LOGGER, + cooldown=UPDATE_DEBOUNCE_TIME, + immediate=True, + function=async_update_data, + ) async def async_connect(self) -> bool: """Connect to desk.""" @@ -60,3 +76,9 @@ class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]): """Ensure that the desk is connected if that is the expected state.""" if self._expected_connected: await self.async_connect() + + @callback + def _async_handle_update(self, height: int | None) -> None: + """Handle an update from the desk.""" + self._height = height + self._debouncer.async_schedule_call() diff --git a/tests/components/idasen_desk/__init__.py b/tests/components/idasen_desk/__init__.py index b0d7cc5ac05b..42e00157b8f2 100644 --- a/tests/components/idasen_desk/__init__.py +++ b/tests/components/idasen_desk/__init__.py @@ -38,6 +38,8 @@ NOT_IDASEN_DISCOVERY_INFO = BluetoothServiceInfoBleak( tx_power=-127, ) +UPDATE_DEBOUNCE_TIME = 0.2 + async def init_integration(hass: HomeAssistant) -> MockConfigEntry: """Set up the IKEA Idasen Desk integration in Home Assistant.""" diff --git a/tests/components/idasen_desk/test_cover.py b/tests/components/idasen_desk/test_cover.py index 83312c04e725..84861ab68736 100644 --- a/tests/components/idasen_desk/test_cover.py +++ b/tests/components/idasen_desk/test_cover.py @@ -4,6 +4,7 @@ from typing import Any from unittest.mock import AsyncMock, MagicMock from bleak.exc import BleakError +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.cover import ( @@ -22,12 +23,13 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from . import init_integration +from . import UPDATE_DEBOUNCE_TIME, init_integration + +from tests.common import async_fire_time_changed async def test_cover_available( - hass: HomeAssistant, - mock_desk_api: MagicMock, + hass: HomeAssistant, mock_desk_api: MagicMock, freezer: FrozenDateTimeFactory ) -> None: """Test cover available property.""" entity_id = "cover.test" @@ -42,6 +44,9 @@ async def test_cover_available( mock_desk_api.is_connected = False mock_desk_api.trigger_update_callback(None) + freezer.tick(UPDATE_DEBOUNCE_TIME) + async_fire_time_changed(hass) + state = hass.states.get(entity_id) assert state assert state.state == STATE_UNAVAILABLE @@ -64,6 +69,7 @@ async def test_cover_services( service_data: dict[str, Any], expected_state: str, expected_position: int, + freezer: FrozenDateTimeFactory, ) -> None: """Test cover services.""" entity_id = "cover.test" @@ -78,7 +84,9 @@ async def test_cover_services( {"entity_id": entity_id, **service_data}, blocking=True, ) - await hass.async_block_till_done() + freezer.tick(UPDATE_DEBOUNCE_TIME) + async_fire_time_changed(hass) + state = hass.states.get(entity_id) assert state assert state.state == expected_state @@ -113,4 +121,3 @@ async def test_cover_services_exception( {"entity_id": entity_id, **service_data}, blocking=True, ) - await hass.async_block_till_done() diff --git a/tests/components/idasen_desk/test_sensor.py b/tests/components/idasen_desk/test_sensor.py index 614bce523e6d..dc8d6f4adf8d 100644 --- a/tests/components/idasen_desk/test_sensor.py +++ b/tests/components/idasen_desk/test_sensor.py @@ -2,18 +2,23 @@ from unittest.mock import MagicMock +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from . import init_integration +from . import UPDATE_DEBOUNCE_TIME, init_integration + +from tests.common import async_fire_time_changed EXPECTED_INITIAL_HEIGHT = "1" @pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_height_sensor(hass: HomeAssistant, mock_desk_api: MagicMock) -> None: +async def test_height_sensor( + hass: HomeAssistant, mock_desk_api: MagicMock, freezer: FrozenDateTimeFactory +) -> None: """Test height sensor.""" await init_integration(hass) @@ -24,6 +29,15 @@ async def test_height_sensor(hass: HomeAssistant, mock_desk_api: MagicMock) -> N mock_desk_api.height = 1.2 mock_desk_api.trigger_update_callback(None) + await hass.async_block_till_done() + + # State should still be the same due to the debouncer + state = hass.states.get(entity_id) + assert state + assert state.state == EXPECTED_INITIAL_HEIGHT + + freezer.tick(UPDATE_DEBOUNCE_TIME) + async_fire_time_changed(hass) state = hass.states.get(entity_id) assert state @@ -34,6 +48,7 @@ async def test_height_sensor(hass: HomeAssistant, mock_desk_api: MagicMock) -> N async def test_sensor_available( hass: HomeAssistant, mock_desk_api: MagicMock, + freezer: FrozenDateTimeFactory, ) -> None: """Test sensor available property.""" await init_integration(hass) @@ -46,6 +61,9 @@ async def test_sensor_available( mock_desk_api.is_connected = False mock_desk_api.trigger_update_callback(None) + freezer.tick(UPDATE_DEBOUNCE_TIME) + async_fire_time_changed(hass) + state = hass.states.get(entity_id) assert state assert state.state == STATE_UNAVAILABLE