mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Implement heartbeat in the Broadlink integration (#43878)
* Implement heartbeat in the Broadlink integration * Rename INTERVAL to HEARTBEAT_INTERVAL * Test that we log an error message when the heartbeat fails
This commit is contained in:
parent
a021fe301c
commit
7350942e9e
@ -1,8 +1,11 @@
|
||||
"""The Broadlink integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from .const import DOMAIN
|
||||
from .device import BroadlinkDevice
|
||||
from .heartbeat import BroadlinkHeartbeat
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -11,6 +14,7 @@ class BroadlinkData:
|
||||
|
||||
devices: dict = field(default_factory=dict)
|
||||
platforms: dict = field(default_factory=dict)
|
||||
heartbeat: BroadlinkHeartbeat | None = None
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
@ -21,11 +25,25 @@ async def async_setup(hass, config):
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Set up a Broadlink device from a config entry."""
|
||||
data = hass.data[DOMAIN]
|
||||
|
||||
if data.heartbeat is None:
|
||||
data.heartbeat = BroadlinkHeartbeat(hass)
|
||||
hass.async_create_task(data.heartbeat.async_setup())
|
||||
|
||||
device = BroadlinkDevice(hass, entry)
|
||||
return await device.async_setup()
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
"""Unload a config entry."""
|
||||
device = hass.data[DOMAIN].devices.pop(entry.entry_id)
|
||||
return await device.async_unload()
|
||||
data = hass.data[DOMAIN]
|
||||
|
||||
device = data.devices.pop(entry.entry_id)
|
||||
result = await device.async_unload()
|
||||
|
||||
if not data.devices:
|
||||
await data.heartbeat.async_unload()
|
||||
data.heartbeat = None
|
||||
|
||||
return result
|
||||
|
55
homeassistant/components/broadlink/heartbeat.py
Normal file
55
homeassistant/components/broadlink/heartbeat.py
Normal file
@ -0,0 +1,55 @@
|
||||
"""Heartbeats for Broadlink devices."""
|
||||
import datetime as dt
|
||||
import logging
|
||||
|
||||
import broadlink as blk
|
||||
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers import event
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BroadlinkHeartbeat:
|
||||
"""Manages heartbeats in the Broadlink integration.
|
||||
|
||||
Some devices reboot when they cannot reach the cloud. This mechanism
|
||||
feeds their watchdog timers so they can be used offline.
|
||||
"""
|
||||
|
||||
HEARTBEAT_INTERVAL = dt.timedelta(minutes=2)
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialize the heartbeat."""
|
||||
self._hass = hass
|
||||
self._unsubscribe = None
|
||||
|
||||
async def async_setup(self):
|
||||
"""Set up the heartbeat."""
|
||||
if self._unsubscribe is None:
|
||||
await self.async_heartbeat(dt.datetime.now())
|
||||
self._unsubscribe = event.async_track_time_interval(
|
||||
self._hass, self.async_heartbeat, self.HEARTBEAT_INTERVAL
|
||||
)
|
||||
|
||||
async def async_unload(self):
|
||||
"""Unload the heartbeat."""
|
||||
if self._unsubscribe is not None:
|
||||
self._unsubscribe()
|
||||
self._unsubscribe = None
|
||||
|
||||
async def async_heartbeat(self, now):
|
||||
"""Send packets to feed watchdog timers."""
|
||||
hass = self._hass
|
||||
config_entries = hass.config_entries.async_entries(DOMAIN)
|
||||
|
||||
for entry in config_entries:
|
||||
host = entry.data[CONF_HOST]
|
||||
try:
|
||||
await hass.async_add_executor_job(blk.ping, host)
|
||||
except OSError as err:
|
||||
_LOGGER.debug("Failed to send heartbeat to %s: %s", host, err)
|
||||
else:
|
||||
_LOGGER.debug("Heartbeat sent to %s", host)
|
108
tests/components/broadlink/test_heartbeat.py
Normal file
108
tests/components/broadlink/test_heartbeat.py
Normal file
@ -0,0 +1,108 @@
|
||||
"""Tests for Broadlink heartbeats."""
|
||||
from unittest.mock import call, patch
|
||||
|
||||
from homeassistant.components.broadlink.heartbeat import BroadlinkHeartbeat
|
||||
from homeassistant.util import dt
|
||||
|
||||
from . import get_device
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
DEVICE_PING = "homeassistant.components.broadlink.heartbeat.blk.ping"
|
||||
|
||||
|
||||
async def test_heartbeat_trigger_startup(hass):
|
||||
"""Test that the heartbeat is initialized with the first config entry."""
|
||||
device = get_device("Office")
|
||||
|
||||
with patch(DEVICE_PING) as mock_ping:
|
||||
await device.setup_entry(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_ping.call_count == 1
|
||||
assert mock_ping.call_args == call(device.host)
|
||||
|
||||
|
||||
async def test_heartbeat_ignore_oserror(hass, caplog):
|
||||
"""Test that an OSError is ignored."""
|
||||
device = get_device("Office")
|
||||
|
||||
with patch(DEVICE_PING, side_effect=OSError()):
|
||||
await device.setup_entry(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert "Failed to send heartbeat to" in caplog.text
|
||||
|
||||
|
||||
async def test_heartbeat_trigger_right_time(hass):
|
||||
"""Test that the heartbeat is triggered at the right time."""
|
||||
device = get_device("Office")
|
||||
|
||||
await device.setup_entry(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(DEVICE_PING) as mock_ping:
|
||||
async_fire_time_changed(
|
||||
hass, dt.utcnow() + BroadlinkHeartbeat.HEARTBEAT_INTERVAL
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_ping.call_count == 1
|
||||
assert mock_ping.call_args == call(device.host)
|
||||
|
||||
|
||||
async def test_heartbeat_do_not_trigger_before_time(hass):
|
||||
"""Test that the heartbeat is not triggered before the time."""
|
||||
device = get_device("Office")
|
||||
|
||||
await device.setup_entry(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(DEVICE_PING) as mock_ping:
|
||||
async_fire_time_changed(
|
||||
hass,
|
||||
dt.utcnow() + BroadlinkHeartbeat.HEARTBEAT_INTERVAL // 2,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_ping.call_count == 0
|
||||
|
||||
|
||||
async def test_heartbeat_unload(hass):
|
||||
"""Test that the heartbeat is deactivated when the last config entry is removed."""
|
||||
device = get_device("Office")
|
||||
|
||||
_, mock_entry = await device.setup_entry(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.config_entries.async_remove(mock_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(DEVICE_PING) as mock_ping:
|
||||
async_fire_time_changed(
|
||||
hass, dt.utcnow() + BroadlinkHeartbeat.HEARTBEAT_INTERVAL
|
||||
)
|
||||
|
||||
assert mock_ping.call_count == 0
|
||||
|
||||
|
||||
async def test_heartbeat_do_not_unload(hass):
|
||||
"""Test that the heartbeat is not deactivated until the last config entry is removed."""
|
||||
device_a = get_device("Office")
|
||||
device_b = get_device("Bedroom")
|
||||
|
||||
_, mock_entry_a = await device_a.setup_entry(hass)
|
||||
await device_b.setup_entry(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.config_entries.async_remove(mock_entry_a.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(DEVICE_PING) as mock_ping:
|
||||
async_fire_time_changed(
|
||||
hass, dt.utcnow() + BroadlinkHeartbeat.HEARTBEAT_INTERVAL
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_ping.call_count == 1
|
||||
assert mock_ping.call_args == call(device_b.host)
|
Loading…
x
Reference in New Issue
Block a user