Make event entity dependend on websocket in Husqvarna Automower (#151203)

This commit is contained in:
Thomas55555
2025-08-27 07:47:14 +02:00
committed by GitHub
parent d72cc45ca8
commit 0bb16befbd
4 changed files with 67 additions and 5 deletions

View File

@@ -62,6 +62,7 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
self.new_areas_callbacks: list[Callable[[str, set[int]], None]] = [] self.new_areas_callbacks: list[Callable[[str, set[int]], None]] = []
self.pong: datetime | None = None self.pong: datetime | None = None
self.websocket_alive: bool = False self.websocket_alive: bool = False
self.websocket_callbacks: list[Callable[[bool], None]] = []
self._watchdog_task: asyncio.Task | None = None self._watchdog_task: asyncio.Task | None = None
@override @override
@@ -198,12 +199,17 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
) )
async def _pong_watchdog(self) -> None: async def _pong_watchdog(self) -> None:
"""Watchdog to check for pong messages."""
_LOGGER.debug("Watchdog started") _LOGGER.debug("Watchdog started")
try: try:
while True: while True:
_LOGGER.debug("Sending ping") _LOGGER.debug("Sending ping")
self.websocket_alive = await self.api.send_empty_message() is_alive = await self.api.send_empty_message()
_LOGGER.debug("Ping result: %s", self.websocket_alive) _LOGGER.debug("Ping result: %s", is_alive)
if self.websocket_alive != is_alive:
self.websocket_alive = is_alive
for ws_callback in self.websocket_callbacks:
ws_callback(is_alive)
await asyncio.sleep(PING_INTERVAL) await asyncio.sleep(PING_INTERVAL)
_LOGGER.debug("Websocket alive %s", self.websocket_alive) _LOGGER.debug("Websocket alive %s", self.websocket_alive)

View File

@@ -1,6 +1,7 @@
"""Creates the event entities for supported mowers.""" """Creates the event entities for supported mowers."""
from collections.abc import Callable from collections.abc import Callable
import logging
from aioautomower.model import SingleMessageData from aioautomower.model import SingleMessageData
@@ -18,6 +19,7 @@ from .const import ERROR_KEYS
from .coordinator import AutomowerDataUpdateCoordinator from .coordinator import AutomowerDataUpdateCoordinator
from .entity import AutomowerBaseEntity from .entity import AutomowerBaseEntity
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
ATTR_SEVERITY = "severity" ATTR_SEVERITY = "severity"
@@ -80,6 +82,12 @@ class AutomowerMessageEventEntity(AutomowerBaseEntity, EventEntity):
"""Initialize Automower message event entity.""" """Initialize Automower message event entity."""
super().__init__(mower_id, coordinator) super().__init__(mower_id, coordinator)
self._attr_unique_id = f"{mower_id}_message" self._attr_unique_id = f"{mower_id}_message"
self.websocket_alive: bool = coordinator.websocket_alive
@property
def available(self) -> bool:
"""Return True if the entity is available."""
return self.websocket_alive and self.mower_id in self.coordinator.data
@callback @callback
def _handle(self, msg: SingleMessageData) -> None: def _handle(self, msg: SingleMessageData) -> None:
@@ -102,7 +110,17 @@ class AutomowerMessageEventEntity(AutomowerBaseEntity, EventEntity):
"""Register callback when entity is added to hass.""" """Register callback when entity is added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
self.coordinator.api.register_single_message_callback(self._handle) self.coordinator.api.register_single_message_callback(self._handle)
self.coordinator.websocket_callbacks.append(self._handle_websocket_update)
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Unregister WebSocket callback when entity is removed.""" """Unregister WebSocket callback when entity is removed."""
self.coordinator.api.unregister_single_message_callback(self._handle) self.coordinator.api.unregister_single_message_callback(self._handle)
self.coordinator.websocket_callbacks.remove(self._handle_websocket_update)
def _handle_websocket_update(self, is_alive: bool) -> None:
"""Handle websocket status changes."""
if self.websocket_alive == is_alive:
return
self.websocket_alive = is_alive
_LOGGER.debug("WebSocket status changed to %s, updating entity state", is_alive)
self.async_write_ha_state()

View File

@@ -1,7 +1,7 @@
"""Test helpers for Husqvarna Automower.""" """Test helpers for Husqvarna Automower."""
import asyncio import asyncio
from collections.abc import Generator from collections.abc import Callable, Generator
import time import time
from unittest.mock import AsyncMock, create_autospec, patch from unittest.mock import AsyncMock, create_autospec, patch
@@ -16,7 +16,7 @@ from homeassistant.components.application_credentials import (
async_import_client_credential, async_import_client_credential,
) )
from homeassistant.components.husqvarna_automower.const import DOMAIN from homeassistant.components.husqvarna_automower.const import DOMAIN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@@ -137,3 +137,21 @@ def mock_automower_client(
spec_set=True, spec_set=True,
) )
yield mock_instance yield mock_instance
@pytest.fixture
def automower_ws_ready(mock_automower_client: AsyncMock) -> list[Callable[[], None]]:
"""Fixture to capture ws_ready_callbacks."""
ws_ready_callbacks: list[Callable[[], None]] = []
@callback
def fake_register_ws_ready_callback(cb: Callable[[], None]) -> None:
ws_ready_callbacks.append(cb)
mock_automower_client.register_ws_ready_callback.side_effect = (
fake_register_ws_ready_callback
)
mock_automower_client.send_empty_message.return_value = True
return ws_ready_callbacks

View File

@@ -33,6 +33,7 @@ async def test_event(
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,
values: dict[str, MowerAttributes], values: dict[str, MowerAttributes],
automower_ws_ready: list[Callable[[], None]],
) -> None: ) -> None:
"""Test that a new message arriving over the websocket creates and updates the sensor.""" """Test that a new message arriving over the websocket creates and updates the sensor."""
callbacks: list[Callable[[SingleMessageData], None]] = [] callbacks: list[Callable[[SingleMessageData], None]] = []
@@ -46,11 +47,17 @@ async def test_event(
mock_automower_client.register_single_message_callback.side_effect = ( mock_automower_client.register_single_message_callback.side_effect = (
fake_register_websocket_response fake_register_websocket_response
) )
mock_automower_client.send_empty_message.return_value = True
# Set up integration # Set up integration
await setup_integration(hass, mock_config_entry) await setup_integration(hass, mock_config_entry)
await hass.async_block_till_done() await hass.async_block_till_done()
# Start the watchdog and let it run once to set websocket_alive=True
for cb in automower_ws_ready:
cb()
await hass.async_block_till_done()
# Ensure callback was registered for the test mower # Ensure callback was registered for the test mower
assert mock_automower_client.register_single_message_callback.called assert mock_automower_client.register_single_message_callback.called
@@ -76,6 +83,7 @@ async def test_event(
for cb in callbacks: for cb in callbacks:
cb(message) cb(message)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("event.test_mower_1_message") state = hass.states.get("event.test_mower_1_message")
assert state is not None assert state is not None
assert state.attributes[ATTR_EVENT_TYPE] == "wheel_motor_overloaded_rear_left" assert state.attributes[ATTR_EVENT_TYPE] == "wheel_motor_overloaded_rear_left"
@@ -84,6 +92,12 @@ async def test_event(
await hass.config_entries.async_reload(mock_config_entry.entry_id) await hass.config_entries.async_reload(mock_config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED assert mock_config_entry.state is ConfigEntryState.LOADED
# Start the new watchdog and let it run
for cb in automower_ws_ready:
cb()
await hass.async_block_till_done()
state = hass.states.get("event.test_mower_1_message") state = hass.states.get("event.test_mower_1_message")
assert state is not None assert state is not None
assert state.attributes[ATTR_EVENT_TYPE] == "wheel_motor_overloaded_rear_left" assert state.attributes[ATTR_EVENT_TYPE] == "wheel_motor_overloaded_rear_left"
@@ -129,6 +143,7 @@ async def test_event(
for cb in callbacks: for cb in callbacks:
cb(message) cb(message)
await hass.async_block_till_done() await hass.async_block_till_done()
entry = entity_registry.async_get("event.test_mower_1_message") entry = entity_registry.async_get("event.test_mower_1_message")
assert entry is not None assert entry is not None
assert state.attributes[ATTR_EVENT_TYPE] == "alarm_mower_lifted" assert state.attributes[ATTR_EVENT_TYPE] == "alarm_mower_lifted"
@@ -154,9 +169,9 @@ async def test_event_snapshot(
hass: HomeAssistant, hass: HomeAssistant,
mock_automower_client: AsyncMock, mock_automower_client: AsyncMock,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
automower_ws_ready: list[Callable[[], None]],
) -> None: ) -> None:
"""Test that a new message arriving over the websocket updates the sensor.""" """Test that a new message arriving over the websocket updates the sensor."""
with patch( with patch(
@@ -179,6 +194,11 @@ async def test_event_snapshot(
await setup_integration(hass, mock_config_entry) await setup_integration(hass, mock_config_entry)
await hass.async_block_till_done() await hass.async_block_till_done()
# Start the watchdog and let it run once
for cb in automower_ws_ready:
cb()
await hass.async_block_till_done()
# Ensure callback was registered for the test mower # Ensure callback was registered for the test mower
assert mock_automower_client.register_single_message_callback.called assert mock_automower_client.register_single_message_callback.called