Add BPUP (push updates) support to bond (#45550)
parent
2fc1c19a45
commit
6a62ebb6a4
|
@ -3,7 +3,7 @@ import asyncio
|
|||
from asyncio import TimeoutError as AsyncIOTimeoutError
|
||||
|
||||
from aiohttp import ClientError, ClientTimeout
|
||||
from bond_api import Bond
|
||||
from bond_api import Bond, BPUPSubscriptions, start_bpup
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST
|
||||
|
@ -12,7 +12,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
|||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity import SLOW_UPDATE_WARNING
|
||||
|
||||
from .const import BRIDGE_MAKE, DOMAIN
|
||||
from .const import BPUP_STOP, BPUP_SUBS, BRIDGE_MAKE, DOMAIN, HUB
|
||||
from .utils import BondHub
|
||||
|
||||
PLATFORMS = ["cover", "fan", "light", "switch"]
|
||||
|
@ -38,7 +38,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|||
except (ClientError, AsyncIOTimeoutError, OSError) as error:
|
||||
raise ConfigEntryNotReady from error
|
||||
|
||||
hass.data[DOMAIN][config_entry_id] = hub
|
||||
bpup_subs = BPUPSubscriptions()
|
||||
stop_bpup = await start_bpup(host, bpup_subs)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
HUB: hub,
|
||||
BPUP_SUBS: bpup_subs,
|
||||
BPUP_STOP: stop_bpup,
|
||||
}
|
||||
|
||||
if not entry.unique_id:
|
||||
hass.config_entries.async_update_entry(entry, unique_id=hub.bond_id)
|
||||
|
@ -74,6 +81,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
)
|
||||
)
|
||||
|
||||
data = hass.data[DOMAIN][entry.entry_id]
|
||||
if BPUP_STOP in data:
|
||||
data[BPUP_STOP]()
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
"""Handle a config flow for Bond."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
|
||||
|
||||
_discovered: dict = None
|
||||
|
||||
|
|
|
@ -5,3 +5,8 @@ BRIDGE_MAKE = "Olibra"
|
|||
DOMAIN = "bond"
|
||||
|
||||
CONF_BOND_ID: str = "bond_id"
|
||||
|
||||
|
||||
HUB = "hub"
|
||||
BPUP_SUBS = "bpup_subs"
|
||||
BPUP_STOP = "bpup_stop"
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
"""Support for Bond covers."""
|
||||
from typing import Any, Callable, List, Optional
|
||||
|
||||
from bond_api import Action, DeviceType
|
||||
from bond_api import Action, BPUPSubscriptions, DeviceType
|
||||
|
||||
from homeassistant.components.cover import DEVICE_CLASS_SHADE, CoverEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import BPUP_SUBS, DOMAIN, HUB
|
||||
from .entity import BondEntity
|
||||
from .utils import BondDevice, BondHub
|
||||
|
||||
|
@ -19,10 +19,12 @@ async def async_setup_entry(
|
|||
async_add_entities: Callable[[List[Entity], bool], None],
|
||||
) -> None:
|
||||
"""Set up Bond cover devices."""
|
||||
hub: BondHub = hass.data[DOMAIN][entry.entry_id]
|
||||
data = hass.data[DOMAIN][entry.entry_id]
|
||||
hub: BondHub = data[HUB]
|
||||
bpup_subs: BPUPSubscriptions = data[BPUP_SUBS]
|
||||
|
||||
covers = [
|
||||
BondCover(hub, device)
|
||||
BondCover(hub, device, bpup_subs)
|
||||
for device in hub.devices
|
||||
if device.type == DeviceType.MOTORIZED_SHADES
|
||||
]
|
||||
|
@ -33,9 +35,9 @@ async def async_setup_entry(
|
|||
class BondCover(BondEntity, CoverEntity):
|
||||
"""Representation of a Bond cover."""
|
||||
|
||||
def __init__(self, hub: BondHub, device: BondDevice):
|
||||
def __init__(self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions):
|
||||
"""Create HA entity representing Bond cover."""
|
||||
super().__init__(hub, device)
|
||||
super().__init__(hub, device, bpup_subs)
|
||||
|
||||
self._closed: Optional[bool] = None
|
||||
|
||||
|
|
|
@ -1,37 +1,51 @@
|
|||
"""An abstract class common to all Bond entities."""
|
||||
from abc import abstractmethod
|
||||
from asyncio import TimeoutError as AsyncIOTimeoutError
|
||||
from asyncio import Lock, TimeoutError as AsyncIOTimeoutError
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from aiohttp import ClientError
|
||||
from bond_api import BPUPSubscriptions
|
||||
|
||||
from homeassistant.const import ATTR_NAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
from .const import DOMAIN
|
||||
from .utils import BondDevice, BondHub
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_FALLBACK_SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
|
||||
class BondEntity(Entity):
|
||||
"""Generic Bond entity encapsulating common features of any Bond controlled device."""
|
||||
|
||||
def __init__(
|
||||
self, hub: BondHub, device: BondDevice, sub_device: Optional[str] = None
|
||||
self,
|
||||
hub: BondHub,
|
||||
device: BondDevice,
|
||||
bpup_subs: BPUPSubscriptions,
|
||||
sub_device: Optional[str] = None,
|
||||
):
|
||||
"""Initialize entity with API and device info."""
|
||||
self._hub = hub
|
||||
self._device = device
|
||||
self._device_id = device.device_id
|
||||
self._sub_device = sub_device
|
||||
self._available = True
|
||||
self._bpup_subs = bpup_subs
|
||||
self._update_lock = None
|
||||
self._initialized = False
|
||||
|
||||
@property
|
||||
def unique_id(self) -> Optional[str]:
|
||||
"""Get unique ID for the entity."""
|
||||
hub_id = self._hub.bond_id
|
||||
device_id = self._device.device_id
|
||||
device_id = self._device_id
|
||||
sub_device_id: str = f"_{self._sub_device}" if self._sub_device else ""
|
||||
return f"{hub_id}_{device_id}{sub_device_id}"
|
||||
|
||||
|
@ -40,13 +54,18 @@ class BondEntity(Entity):
|
|||
"""Get entity name."""
|
||||
return self._device.name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_info(self) -> Optional[Dict[str, Any]]:
|
||||
"""Get a an HA device representing this Bond controlled device."""
|
||||
device_info = {
|
||||
ATTR_NAME: self.name,
|
||||
"manufacturer": self._hub.make,
|
||||
"identifiers": {(DOMAIN, self._hub.bond_id, self._device.device_id)},
|
||||
"identifiers": {(DOMAIN, self._hub.bond_id, self._device_id)},
|
||||
"via_device": (DOMAIN, self._hub.bond_id),
|
||||
}
|
||||
if not self._hub.is_bridge:
|
||||
|
@ -75,8 +94,29 @@ class BondEntity(Entity):
|
|||
|
||||
async def async_update(self):
|
||||
"""Fetch assumed state of the cover from the hub using API."""
|
||||
await self._async_update_from_api()
|
||||
|
||||
async def _async_update_if_bpup_not_alive(self, *_):
|
||||
"""Fetch via the API if BPUP is not alive."""
|
||||
if self._bpup_subs.alive and self._initialized:
|
||||
return
|
||||
|
||||
if self._update_lock.locked():
|
||||
_LOGGER.warning(
|
||||
"Updating %s took longer than the scheduled update interval %s",
|
||||
self.entity_id,
|
||||
_FALLBACK_SCAN_INTERVAL,
|
||||
)
|
||||
return
|
||||
|
||||
async with self._update_lock:
|
||||
await self._async_update_from_api()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def _async_update_from_api(self):
|
||||
"""Fetch via the API."""
|
||||
try:
|
||||
state: dict = await self._hub.bond.device_state(self._device.device_id)
|
||||
state: dict = await self._hub.bond.device_state(self._device_id)
|
||||
except (ClientError, AsyncIOTimeoutError, OSError) as error:
|
||||
if self._available:
|
||||
_LOGGER.warning(
|
||||
|
@ -84,12 +124,42 @@ class BondEntity(Entity):
|
|||
)
|
||||
self._available = False
|
||||
else:
|
||||
_LOGGER.debug("Device state for %s is:\n%s", self.entity_id, state)
|
||||
if not self._available:
|
||||
_LOGGER.info("Entity %s has come back", self.entity_id)
|
||||
self._available = True
|
||||
self._apply_state(state)
|
||||
self._async_state_callback(state)
|
||||
|
||||
@abstractmethod
|
||||
def _apply_state(self, state: dict):
|
||||
raise NotImplementedError
|
||||
|
||||
@callback
|
||||
def _async_state_callback(self, state):
|
||||
"""Process a state change."""
|
||||
self._initialized = True
|
||||
if not self._available:
|
||||
_LOGGER.info("Entity %s has come back", self.entity_id)
|
||||
self._available = True
|
||||
_LOGGER.debug(
|
||||
"Device state for %s (%s) is:\n%s", self.name, self.entity_id, state
|
||||
)
|
||||
self._apply_state(state)
|
||||
|
||||
@callback
|
||||
def _async_bpup_callback(self, state):
|
||||
"""Process a state change from BPUP."""
|
||||
self._async_state_callback(state)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe to BPUP and start polling."""
|
||||
await super().async_added_to_hass()
|
||||
self._update_lock = Lock()
|
||||
self._bpup_subs.subscribe(self._device_id, self._async_bpup_callback)
|
||||
self.async_on_remove(
|
||||
async_track_time_interval(
|
||||
self.hass, self._async_update_if_bpup_not_alive, _FALLBACK_SCAN_INTERVAL
|
||||
)
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Unsubscribe from BPUP data on remove."""
|
||||
await super().async_will_remove_from_hass()
|
||||
self._bpup_subs.unsubscribe(self._device_id, self._async_bpup_callback)
|
||||
|
|
|
@ -3,7 +3,7 @@ import logging
|
|||
import math
|
||||
from typing import Any, Callable, List, Optional, Tuple
|
||||
|
||||
from bond_api import Action, DeviceType, Direction
|
||||
from bond_api import Action, BPUPSubscriptions, DeviceType, Direction
|
||||
|
||||
from homeassistant.components.fan import (
|
||||
DIRECTION_FORWARD,
|
||||
|
@ -20,7 +20,7 @@ from homeassistant.util.percentage import (
|
|||
ranged_value_to_percentage,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import BPUP_SUBS, DOMAIN, HUB
|
||||
from .entity import BondEntity
|
||||
from .utils import BondDevice, BondHub
|
||||
|
||||
|
@ -33,10 +33,14 @@ async def async_setup_entry(
|
|||
async_add_entities: Callable[[List[Entity], bool], None],
|
||||
) -> None:
|
||||
"""Set up Bond fan devices."""
|
||||
hub: BondHub = hass.data[DOMAIN][entry.entry_id]
|
||||
data = hass.data[DOMAIN][entry.entry_id]
|
||||
hub: BondHub = data[HUB]
|
||||
bpup_subs: BPUPSubscriptions = data[BPUP_SUBS]
|
||||
|
||||
fans = [
|
||||
BondFan(hub, device) for device in hub.devices if DeviceType.is_fan(device.type)
|
||||
BondFan(hub, device, bpup_subs)
|
||||
for device in hub.devices
|
||||
if DeviceType.is_fan(device.type)
|
||||
]
|
||||
|
||||
async_add_entities(fans, True)
|
||||
|
@ -45,9 +49,9 @@ async def async_setup_entry(
|
|||
class BondFan(BondEntity, FanEntity):
|
||||
"""Representation of a Bond fan."""
|
||||
|
||||
def __init__(self, hub: BondHub, device: BondDevice):
|
||||
def __init__(self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions):
|
||||
"""Create HA entity representing Bond fan."""
|
||||
super().__init__(hub, device)
|
||||
super().__init__(hub, device, bpup_subs)
|
||||
|
||||
self._power: Optional[bool] = None
|
||||
self._speed: Optional[int] = None
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import logging
|
||||
from typing import Any, Callable, List, Optional
|
||||
|
||||
from bond_api import Action, DeviceType
|
||||
from bond_api import Action, BPUPSubscriptions, DeviceType
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from . import BondHub
|
||||
from .const import DOMAIN
|
||||
from .const import BPUP_SUBS, DOMAIN, HUB
|
||||
from .entity import BondEntity
|
||||
from .utils import BondDevice
|
||||
|
||||
|
@ -27,28 +27,30 @@ async def async_setup_entry(
|
|||
async_add_entities: Callable[[List[Entity], bool], None],
|
||||
) -> None:
|
||||
"""Set up Bond light devices."""
|
||||
hub: BondHub = hass.data[DOMAIN][entry.entry_id]
|
||||
data = hass.data[DOMAIN][entry.entry_id]
|
||||
hub: BondHub = data[HUB]
|
||||
bpup_subs: BPUPSubscriptions = data[BPUP_SUBS]
|
||||
|
||||
fan_lights: List[Entity] = [
|
||||
BondLight(hub, device)
|
||||
BondLight(hub, device, bpup_subs)
|
||||
for device in hub.devices
|
||||
if DeviceType.is_fan(device.type) and device.supports_light()
|
||||
]
|
||||
|
||||
fireplaces: List[Entity] = [
|
||||
BondFireplace(hub, device)
|
||||
BondFireplace(hub, device, bpup_subs)
|
||||
for device in hub.devices
|
||||
if DeviceType.is_fireplace(device.type)
|
||||
]
|
||||
|
||||
fp_lights: List[Entity] = [
|
||||
BondLight(hub, device, "light")
|
||||
BondLight(hub, device, bpup_subs, "light")
|
||||
for device in hub.devices
|
||||
if DeviceType.is_fireplace(device.type) and device.supports_light()
|
||||
]
|
||||
|
||||
lights: List[Entity] = [
|
||||
BondLight(hub, device)
|
||||
BondLight(hub, device, bpup_subs)
|
||||
for device in hub.devices
|
||||
if DeviceType.is_light(device.type)
|
||||
]
|
||||
|
@ -60,10 +62,14 @@ class BondLight(BondEntity, LightEntity):
|
|||
"""Representation of a Bond light."""
|
||||
|
||||
def __init__(
|
||||
self, hub: BondHub, device: BondDevice, sub_device: Optional[str] = None
|
||||
self,
|
||||
hub: BondHub,
|
||||
device: BondDevice,
|
||||
bpup_subs: BPUPSubscriptions,
|
||||
sub_device: Optional[str] = None,
|
||||
):
|
||||
"""Create HA entity representing Bond fan."""
|
||||
super().__init__(hub, device, sub_device)
|
||||
super().__init__(hub, device, bpup_subs, sub_device)
|
||||
self._brightness: Optional[int] = None
|
||||
self._light: Optional[int] = None
|
||||
|
||||
|
@ -110,9 +116,9 @@ class BondLight(BondEntity, LightEntity):
|
|||
class BondFireplace(BondEntity, LightEntity):
|
||||
"""Representation of a Bond-controlled fireplace."""
|
||||
|
||||
def __init__(self, hub: BondHub, device: BondDevice):
|
||||
def __init__(self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions):
|
||||
"""Create HA entity representing Bond fireplace."""
|
||||
super().__init__(hub, device)
|
||||
super().__init__(hub, device, bpup_subs)
|
||||
|
||||
self._power: Optional[bool] = None
|
||||
# Bond flame level, 0-100
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Bond",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/bond",
|
||||
"requirements": ["bond-api==0.1.8"],
|
||||
"requirements": ["bond-api==0.1.9"],
|
||||
"zeroconf": ["_bond._tcp.local."],
|
||||
"codeowners": ["@prystupa"],
|
||||
"quality_scale": "platinum"
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
"""Support for Bond generic devices."""
|
||||
from typing import Any, Callable, List, Optional
|
||||
|
||||
from bond_api import Action, DeviceType
|
||||
from bond_api import Action, BPUPSubscriptions, DeviceType
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import BPUP_SUBS, DOMAIN, HUB
|
||||
from .entity import BondEntity
|
||||
from .utils import BondDevice, BondHub
|
||||
|
||||
|
@ -19,10 +19,12 @@ async def async_setup_entry(
|
|||
async_add_entities: Callable[[List[Entity], bool], None],
|
||||
) -> None:
|
||||
"""Set up Bond generic devices."""
|
||||
hub: BondHub = hass.data[DOMAIN][entry.entry_id]
|
||||
data = hass.data[DOMAIN][entry.entry_id]
|
||||
hub: BondHub = data[HUB]
|
||||
bpup_subs: BPUPSubscriptions = data[BPUP_SUBS]
|
||||
|
||||
switches = [
|
||||
BondSwitch(hub, device)
|
||||
BondSwitch(hub, device, bpup_subs)
|
||||
for device in hub.devices
|
||||
if DeviceType.is_generic(device.type)
|
||||
]
|
||||
|
@ -33,9 +35,9 @@ async def async_setup_entry(
|
|||
class BondSwitch(BondEntity, SwitchEntity):
|
||||
"""Representation of a Bond generic device."""
|
||||
|
||||
def __init__(self, hub: BondHub, device: BondDevice):
|
||||
def __init__(self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions):
|
||||
"""Create HA entity representing Bond generic device (switch)."""
|
||||
super().__init__(hub, device)
|
||||
super().__init__(hub, device, bpup_subs)
|
||||
|
||||
self._power: Optional[bool] = None
|
||||
|
||||
|
|
|
@ -367,7 +367,7 @@ blockchain==1.4.4
|
|||
# bme680==1.0.5
|
||||
|
||||
# homeassistant.components.bond
|
||||
bond-api==0.1.8
|
||||
bond-api==0.1.9
|
||||
|
||||
# homeassistant.components.amazon_polly
|
||||
# homeassistant.components.route53
|
||||
|
|
|
@ -201,7 +201,7 @@ blebox_uniapi==1.3.2
|
|||
blinkpy==0.16.4
|
||||
|
||||
# homeassistant.components.bond
|
||||
bond-api==0.1.8
|
||||
bond-api==0.1.9
|
||||
|
||||
# homeassistant.components.braviatv
|
||||
bravia-tv==1.0.8
|
||||
|
|
|
@ -3,7 +3,7 @@ from asyncio import TimeoutError as AsyncIOTimeoutError
|
|||
from contextlib import nullcontext
|
||||
from datetime import timedelta
|
||||
from typing import Any, Dict, Optional
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.components.bond.const import DOMAIN as BOND_DOMAIN
|
||||
|
@ -33,9 +33,11 @@ async def setup_bond_entity(
|
|||
"""Set up Bond entity."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch_bond_version(enabled=patch_version), patch_bond_device_ids(
|
||||
enabled=patch_device_ids
|
||||
), patch_setup_entry("cover", enabled=patch_platforms), patch_setup_entry(
|
||||
with patch_start_bpup(), patch_bond_version(
|
||||
enabled=patch_version
|
||||
), patch_bond_device_ids(enabled=patch_device_ids), patch_setup_entry(
|
||||
"cover", enabled=patch_platforms
|
||||
), patch_setup_entry(
|
||||
"fan", enabled=patch_platforms
|
||||
), patch_setup_entry(
|
||||
"light", enabled=patch_platforms
|
||||
|
@ -65,7 +67,7 @@ async def setup_platform(
|
|||
with patch("homeassistant.components.bond.PLATFORMS", [platform]):
|
||||
with patch_bond_version(return_value=bond_version), patch_bond_device_ids(
|
||||
return_value=[bond_device_id]
|
||||
), patch_bond_device(
|
||||
), patch_start_bpup(), patch_bond_device(
|
||||
return_value=discovered_device
|
||||
), patch_bond_device_properties(
|
||||
return_value=props
|
||||
|
@ -118,6 +120,14 @@ def patch_bond_device(return_value=None):
|
|||
)
|
||||
|
||||
|
||||
def patch_start_bpup():
|
||||
"""Patch start_bpup."""
|
||||
return patch(
|
||||
"homeassistant.components.bond.start_bpup",
|
||||
return_value=MagicMock(),
|
||||
)
|
||||
|
||||
|
||||
def patch_bond_action():
|
||||
"""Patch Bond API action endpoint."""
|
||||
return patch("homeassistant.components.bond.Bond.action")
|
||||
|
|
|
@ -20,6 +20,7 @@ from .common import (
|
|||
patch_bond_device_state,
|
||||
patch_bond_version,
|
||||
patch_setup_entry,
|
||||
patch_start_bpup,
|
||||
setup_bond_entity,
|
||||
)
|
||||
|
||||
|
@ -141,7 +142,7 @@ async def test_old_identifiers_are_removed(hass: HomeAssistant):
|
|||
"target": "test-model",
|
||||
"fw_ver": "test-version",
|
||||
}
|
||||
), patch_bond_device_ids(
|
||||
), patch_start_bpup(), patch_bond_device_ids(
|
||||
return_value=["bond-device-id", "device_id"]
|
||||
), patch_bond_device(
|
||||
return_value={
|
||||
|
|
Loading…
Reference in New Issue