8000 Constant polling for Husqvarna Automower by Thomas55555 · Pull Request #147957 · home-assistant/core · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Constant polling for Husqvarna Automower #147957

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions homeassistant/components/husqvarna_automower/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ async def _async_update_data(self) -> 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()
Expand All @@ -86,11 +86,27 @@ async def _async_update_data(self) -> 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,
Expand Down
73 changes: 71 additions & 2 deletions tests/components/husqvarna_automower/test_init.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -20,7 +22,7 @@
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

Expand Down Expand Up @@ -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,
Expand Down
0