Constant polling for Husqvarna Automower (#147957)

This commit is contained in:
Thomas55555 2025-07-04 23:42:17 +02:00 committed by GitHub
parent dcad5bbe04
commit 528daad854
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 89 additions and 4 deletions

View File

@ -74,7 +74,7 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
"""Subscribe for websocket and poll data from the API."""
if not self.ws_connected:
await self.api.connect()
self.api.register_data_callback(self.callback)
self.api.register_data_callback(self.handle_websocket_updates)
self.ws_connected = True
try:
data = await self.api.get_status()
@ -86,11 +86,27 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
return data
@callback
def callback(self, ws_data: MowerDictionary) -> None:
def handle_websocket_updates(self, ws_data: MowerDictionary) -> None:
"""Process websocket callbacks and write them to the DataUpdateCoordinator."""
self.async_set_updated_data(ws_data)
self._async_add_remove_devices_and_entities(ws_data)
@callback
def async_set_updated_data(self, data: MowerDictionary) -> None:
"""Override DataUpdateCoordinator to preserve fixed polling interval.
The built-in implementation resets the polling timer on every websocket
update. Since websockets do not deliver all required data (e.g. statistics
or work area details), we enforce a constant REST polling cadence.
"""
self.data = data
self.last_update_success = True
self.logger.debug(
"Manually updated %s data",
self.name,
)
self.async_update_listeners()
async def client_listen(
self,
hass: HomeAssistant,

View File

@ -1,7 +1,9 @@
"""Tests for init module."""
from asyncio import Event
from datetime import datetime
from collections.abc import Callable
from copy import deepcopy
from datetime import datetime, timedelta
import http
import time
from unittest.mock import AsyncMock, patch
@ -20,7 +22,7 @@ from syrupy.assertion import SnapshotAssertion
from homeassistant.components.husqvarna_automower.const import DOMAIN, OAUTH2_TOKEN
from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.util import dt as dt_util
@ -221,6 +223,73 @@ async def test_device_info(
assert reg_device == snapshot
async def test_constant_polling(
hass: HomeAssistant,
mock_automower_client: AsyncMock,
mock_config_entry: MockConfigEntry,
values: dict[str, MowerAttributes],
freezer: FrozenDateTimeFactory,
) -> None:
"""Verify that receiving a WebSocket update does not interrupt the regular polling cycle.
The test simulates a WebSocket update that changes an entity's state, then advances time
to trigger a scheduled poll to confirm polled data also arrives.
"""
test_values = deepcopy(values)
callback_holder: dict[str, Callable] = {}
@callback
def fake_register_websocket_response(
cb: Callable[[dict[str, MowerAttributes]], None],
) -> None:
callback_holder["cb"] = cb
mock_automower_client.register_data_callback.side_effect = (
fake_register_websocket_response
)
await setup_integration(hass, mock_config_entry)
await hass.async_block_till_done()
assert mock_automower_client.register_data_callback.called
assert "cb" in callback_holder
state = hass.states.get("sensor.test_mower_1_battery")
assert state is not None
assert state.state == "100"
state = hass.states.get("sensor.test_mower_1_front_lawn_progress")
assert state is not None
assert state.state == "40"
test_values[TEST_MOWER_ID].battery.battery_percent = 77
freezer.tick(SCAN_INTERVAL - timedelta(seconds=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
callback_holder["cb"](test_values)
await hass.async_block_till_done()
state = hass.states.get("sensor.test_mower_1_battery")
assert state is not None
assert state.state == "77"
state = hass.states.get("sensor.test_mower_1_front_lawn_progress")
assert state is not None
assert state.state == "40"
test_values[TEST_MOWER_ID].work_areas[123456].progress = 50
mock_automower_client.get_status.return_value = test_values
freezer.tick(timedelta(seconds=4))
async_fire_time_changed(hass)
await hass.async_block_till_done()
mock_automower_client.get_status.assert_awaited()
state = hass.states.get("sensor.test_mower_1_battery")
assert state is not None
assert state.state == "77"
state = hass.states.get("sensor.test_mower_1_front_lawn_progress")
assert state is not None
assert state.state == "50"
async def test_coordinator_automatic_registry_cleanup(
hass: HomeAssistant,
mock_automower_client: AsyncMock,