mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
Constant polling for Husqvarna Automower (#147957)
This commit is contained in:
parent
dcad5bbe04
commit
528daad854
@ -74,7 +74,7 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
|
|||||||
"""Subscribe for websocket and poll data from the API."""
|
"""Subscribe for websocket and poll data from the API."""
|
||||||
if not self.ws_connected:
|
if not self.ws_connected:
|
||||||
await self.api.connect()
|
await self.api.connect()
|
||||||
self.api.register_data_callback(self.callback)
|
self.api.register_data_callback(self.handle_websocket_updates)
|
||||||
self.ws_connected = True
|
self.ws_connected = True
|
||||||
try:
|
try:
|
||||||
data = await self.api.get_status()
|
data = await self.api.get_status()
|
||||||
@ -86,11 +86,27 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
@callback
|
@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."""
|
"""Process websocket callbacks and write them to the DataUpdateCoordinator."""
|
||||||
self.async_set_updated_data(ws_data)
|
self.async_set_updated_data(ws_data)
|
||||||
self._async_add_remove_devices_and_entities(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(
|
async def client_listen(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
"""Tests for init module."""
|
"""Tests for init module."""
|
||||||
|
|
||||||
from asyncio import Event
|
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 http
|
||||||
import time
|
import time
|
||||||
from unittest.mock import AsyncMock, patch
|
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.const import DOMAIN, OAUTH2_TOKEN
|
||||||
from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL
|
from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
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.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
@ -221,6 +223,73 @@ async def test_device_info(
|
|||||||
assert reg_device == snapshot
|
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(
|
async def test_coordinator_automatic_registry_cleanup(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_automower_client: AsyncMock,
|
mock_automower_client: AsyncMock,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user