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.pong: datetime | None = None
self.websocket_alive: bool = False
self.websocket_callbacks: list[Callable[[bool], None]] = []
self._watchdog_task: asyncio.Task | None = None
@override
@@ -198,12 +199,17 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
)
async def _pong_watchdog(self) -> None:
"""Watchdog to check for pong messages."""
_LOGGER.debug("Watchdog started")
try:
while True:
_LOGGER.debug("Sending ping")
self.websocket_alive = await self.api.send_empty_message()
_LOGGER.debug("Ping result: %s", self.websocket_alive)
is_alive = await self.api.send_empty_message()
_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)
_LOGGER.debug("Websocket alive %s", self.websocket_alive)

View File

@@ -1,6 +1,7 @@
"""Creates the event entities for supported mowers."""
from collections.abc import Callable
import logging
from aioautomower.model import SingleMessageData
@@ -18,6 +19,7 @@ from .const import ERROR_KEYS
from .coordinator import AutomowerDataUpdateCoordinator
from .entity import AutomowerBaseEntity
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1
ATTR_SEVERITY = "severity"
@@ -80,6 +82,12 @@ class AutomowerMessageEventEntity(AutomowerBaseEntity, EventEntity):
"""Initialize Automower message event entity."""
super().__init__(mower_id, coordinator)
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
def _handle(self, msg: SingleMessageData) -> None:
@@ -102,7 +110,17 @@ class AutomowerMessageEventEntity(AutomowerBaseEntity, EventEntity):
"""Register callback when entity is added to hass."""
await super().async_added_to_hass()
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:
"""Unregister WebSocket callback when entity is removed."""
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."""
import asyncio
from collections.abc import Generator
from collections.abc import Callable, Generator
import time
from unittest.mock import AsyncMock, create_autospec, patch
@@ -16,7 +16,7 @@ from homeassistant.components.application_credentials import (
async_import_client_credential,
)
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.util import dt as dt_util
@@ -137,3 +137,21 @@ def mock_automower_client(
spec_set=True,
)
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,
freezer: FrozenDateTimeFactory,
values: dict[str, MowerAttributes],
automower_ws_ready: list[Callable[[], None]],
) -> None:
"""Test that a new message arriving over the websocket creates and updates the sensor."""
callbacks: list[Callable[[SingleMessageData], None]] = []
@@ -46,11 +47,17 @@ async def test_event(
mock_automower_client.register_single_message_callback.side_effect = (
fake_register_websocket_response
)
mock_automower_client.send_empty_message.return_value = True
# Set up integration
await setup_integration(hass, mock_config_entry)
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
assert mock_automower_client.register_single_message_callback.called
@@ -76,6 +83,7 @@ async def test_event(
for cb in callbacks:
cb(message)
await hass.async_block_till_done()
state = hass.states.get("event.test_mower_1_message")
assert state is not None
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.async_block_till_done()
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")
assert state is not None
assert state.attributes[ATTR_EVENT_TYPE] == "wheel_motor_overloaded_rear_left"
@@ -129,6 +143,7 @@ async def test_event(
for cb in callbacks:
cb(message)
await hass.async_block_till_done()
entry = entity_registry.async_get("event.test_mower_1_message")
assert entry is not None
assert state.attributes[ATTR_EVENT_TYPE] == "alarm_mower_lifted"
@@ -154,9 +169,9 @@ async def test_event_snapshot(
hass: HomeAssistant,
mock_automower_client: AsyncMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
automower_ws_ready: list[Callable[[], None]],
) -> None:
"""Test that a new message arriving over the websocket updates the sensor."""
with patch(
@@ -179,6 +194,11 @@ async def test_event_snapshot(
await setup_integration(hass, mock_config_entry)
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
assert mock_automower_client.register_single_message_callback.called