mirror of
https://github.com/home-assistant/core.git
synced 2025-08-30 09:51:37 +02:00
Make event entity dependend on websocket in Husqvarna Automower (#151203)
This commit is contained in:
@@ -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)
|
||||||
|
@@ -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()
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user