mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +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."""
|
"""The Broadlink integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .device import BroadlinkDevice
|
from .device import BroadlinkDevice
|
||||||
|
from .heartbeat import BroadlinkHeartbeat
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -11,6 +14,7 @@ class BroadlinkData:
|
|||||||
|
|
||||||
devices: dict = field(default_factory=dict)
|
devices: dict = field(default_factory=dict)
|
||||||
platforms: dict = field(default_factory=dict)
|
platforms: dict = field(default_factory=dict)
|
||||||
|
heartbeat: BroadlinkHeartbeat | None = None
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
@ -21,11 +25,25 @@ async def async_setup(hass, config):
|
|||||||
|
|
||||||
async def async_setup_entry(hass, entry):
|
async def async_setup_entry(hass, entry):
|
||||||
"""Set up a Broadlink device from a config 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)
|
device = BroadlinkDevice(hass, entry)
|
||||||
return await device.async_setup()
|
return await device.async_setup()
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass, entry):
|
async def async_unload_entry(hass, entry):
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
device = hass.data[DOMAIN].devices.pop(entry.entry_id)
|
data = hass.data[DOMAIN]
|
||||||
return await device.async_unload()
|
|
||||||
|
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