Add support for bond up and down lights (#46233)

This commit is contained in:
J. Nick Koston 2021-02-20 08:03:40 -10:00 committed by GitHub
parent 65e8835f28
commit adf480025d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 294 additions and 25 deletions

View File

@ -52,6 +52,9 @@ class BondEntity(Entity):
@property @property
def name(self) -> Optional[str]: def name(self) -> Optional[str]:
"""Get entity name.""" """Get entity name."""
if self._sub_device:
sub_device_name = self._sub_device.replace("_", " ").title()
return f"{self._device.name} {sub_device_name}"
return self._device.name return self._device.name
@property @property

View File

@ -34,7 +34,21 @@ async def async_setup_entry(
fan_lights: List[Entity] = [ fan_lights: List[Entity] = [
BondLight(hub, device, bpup_subs) BondLight(hub, device, bpup_subs)
for device in hub.devices for device in hub.devices
if DeviceType.is_fan(device.type) and device.supports_light() if DeviceType.is_fan(device.type)
and device.supports_light()
and not (device.supports_up_light() and device.supports_down_light())
]
fan_up_lights: List[Entity] = [
BondUpLight(hub, device, bpup_subs, "up_light")
for device in hub.devices
if DeviceType.is_fan(device.type) and device.supports_up_light()
]
fan_down_lights: List[Entity] = [
BondDownLight(hub, device, bpup_subs, "down_light")
for device in hub.devices
if DeviceType.is_fan(device.type) and device.supports_down_light()
] ]
fireplaces: List[Entity] = [ fireplaces: List[Entity] = [
@ -55,10 +69,13 @@ async def async_setup_entry(
if DeviceType.is_light(device.type) if DeviceType.is_light(device.type)
] ]
async_add_entities(fan_lights + fireplaces + fp_lights + lights, True) async_add_entities(
fan_lights + fan_up_lights + fan_down_lights + fireplaces + fp_lights + lights,
True,
)
class BondLight(BondEntity, LightEntity): class BondBaseLight(BondEntity, LightEntity):
"""Representation of a Bond light.""" """Representation of a Bond light."""
def __init__( def __init__(
@ -68,10 +85,34 @@ class BondLight(BondEntity, LightEntity):
bpup_subs: BPUPSubscriptions, bpup_subs: BPUPSubscriptions,
sub_device: Optional[str] = None, sub_device: Optional[str] = None,
): ):
"""Create HA entity representing Bond fan.""" """Create HA entity representing Bond light."""
super().__init__(hub, device, bpup_subs, sub_device)
self._light: Optional[int] = None
@property
def is_on(self) -> bool:
"""Return if light is currently on."""
return self._light == 1
@property
def supported_features(self) -> Optional[int]:
"""Flag supported features."""
return 0
class BondLight(BondBaseLight, BondEntity, LightEntity):
"""Representation of a Bond light."""
def __init__(
self,
hub: BondHub,
device: BondDevice,
bpup_subs: BPUPSubscriptions,
sub_device: Optional[str] = None,
):
"""Create HA entity representing Bond light."""
super().__init__(hub, device, bpup_subs, sub_device) super().__init__(hub, device, bpup_subs, sub_device)
self._brightness: Optional[int] = None self._brightness: Optional[int] = None
self._light: Optional[int] = None
def _apply_state(self, state: dict): def _apply_state(self, state: dict):
self._light = state.get("light") self._light = state.get("light")
@ -84,11 +125,6 @@ class BondLight(BondEntity, LightEntity):
return SUPPORT_BRIGHTNESS return SUPPORT_BRIGHTNESS
return 0 return 0
@property
def is_on(self) -> bool:
"""Return if light is currently on."""
return self._light == 1
@property @property
def brightness(self) -> int: def brightness(self) -> int:
"""Return the brightness of this light between 1..255.""" """Return the brightness of this light between 1..255."""
@ -113,6 +149,44 @@ class BondLight(BondEntity, LightEntity):
await self._hub.bond.action(self._device.device_id, Action.turn_light_off()) await self._hub.bond.action(self._device.device_id, Action.turn_light_off())
class BondDownLight(BondBaseLight, BondEntity, LightEntity):
"""Representation of a Bond light."""
def _apply_state(self, state: dict):
self._light = state.get("down_light") and state.get("light")
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the light."""
await self._hub.bond.action(
self._device.device_id, Action(Action.TURN_DOWN_LIGHT_ON)
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the light."""
await self._hub.bond.action(
self._device.device_id, Action(Action.TURN_DOWN_LIGHT_OFF)
)
class BondUpLight(BondBaseLight, BondEntity, LightEntity):
"""Representation of a Bond light."""
def _apply_state(self, state: dict):
self._light = state.get("up_light") and state.get("light")
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the light."""
await self._hub.bond.action(
self._device.device_id, Action(Action.TURN_UP_LIGHT_ON)
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the light."""
await self._hub.bond.action(
self._device.device_id, Action(Action.TURN_UP_LIGHT_OFF)
)
class BondFireplace(BondEntity, LightEntity): class BondFireplace(BondEntity, LightEntity):
"""Representation of a Bond-controlled fireplace.""" """Representation of a Bond-controlled fireplace."""

View File

@ -1,7 +1,7 @@
"""Reusable utilities for the Bond component.""" """Reusable utilities for the Bond component."""
import asyncio import asyncio
import logging import logging
from typing import List, Optional from typing import List, Optional, Set
from aiohttp import ClientResponseError from aiohttp import ClientResponseError
from bond_api import Action, Bond from bond_api import Action, Bond
@ -58,31 +58,39 @@ class BondDevice:
"""Check if Trust State is turned on.""" """Check if Trust State is turned on."""
return self.props.get("trust_state", False) return self.props.get("trust_state", False)
def _has_any_action(self, actions: Set[str]):
"""Check to see if the device supports any of the actions."""
supported_actions: List[str] = self._attrs["actions"]
for action in supported_actions:
if action in actions:
return True
return False
def supports_speed(self) -> bool: def supports_speed(self) -> bool:
"""Return True if this device supports any of the speed related commands.""" """Return True if this device supports any of the speed related commands."""
actions: List[str] = self._attrs["actions"] return self._has_any_action({Action.SET_SPEED})
return bool([action for action in actions if action in [Action.SET_SPEED]])
def supports_direction(self) -> bool: def supports_direction(self) -> bool:
"""Return True if this device supports any of the direction related commands.""" """Return True if this device supports any of the direction related commands."""
actions: List[str] = self._attrs["actions"] return self._has_any_action({Action.SET_DIRECTION})
return bool([action for action in actions if action in [Action.SET_DIRECTION]])
def supports_light(self) -> bool: def supports_light(self) -> bool:
"""Return True if this device supports any of the light related commands.""" """Return True if this device supports any of the light related commands."""
actions: List[str] = self._attrs["actions"] return self._has_any_action({Action.TURN_LIGHT_ON, Action.TURN_LIGHT_OFF})
return bool(
[ def supports_up_light(self) -> bool:
action """Return true if the device has an up light."""
for action in actions return self._has_any_action({Action.TURN_UP_LIGHT_ON, Action.TURN_UP_LIGHT_OFF})
if action in [Action.TURN_LIGHT_ON, Action.TURN_LIGHT_OFF]
] def supports_down_light(self) -> bool:
"""Return true if the device has a down light."""
return self._has_any_action(
{Action.TURN_DOWN_LIGHT_ON, Action.TURN_DOWN_LIGHT_OFF}
) )
def supports_set_brightness(self) -> bool: def supports_set_brightness(self) -> bool:
"""Return True if this device supports setting a light brightness.""" """Return True if this device supports setting a light brightness."""
actions: List[str] = self._attrs["actions"] return self._has_any_action({Action.SET_BRIGHTNESS})
return bool([action for action in actions if action in [Action.SET_BRIGHTNESS]])
class BondHub: class BondHub:

View File

@ -56,6 +56,24 @@ def dimmable_ceiling_fan(name: str):
} }
def down_light_ceiling_fan(name: str):
"""Create a ceiling fan (that has built-in down light) with given name."""
return {
"name": name,
"type": DeviceType.CEILING_FAN,
"actions": [Action.TURN_DOWN_LIGHT_ON, Action.TURN_DOWN_LIGHT_OFF],
}
def up_light_ceiling_fan(name: str):
"""Create a ceiling fan (that has built-in down light) with given name."""
return {
"name": name,
"type": DeviceType.CEILING_FAN,
"actions": [Action.TURN_UP_LIGHT_ON, Action.TURN_UP_LIGHT_OFF],
}
def fireplace(name: str): def fireplace(name: str):
"""Create a fireplace with given name.""" """Create a fireplace with given name."""
return { return {
@ -94,6 +112,36 @@ async def test_fan_entity_registry(hass: core.HomeAssistant):
assert entity.unique_id == "test-hub-id_test-device-id" assert entity.unique_id == "test-hub-id_test-device-id"
async def test_fan_up_light_entity_registry(hass: core.HomeAssistant):
"""Tests that fan with up light devices are registered in the entity registry."""
await setup_platform(
hass,
LIGHT_DOMAIN,
up_light_ceiling_fan("fan-name"),
bond_version={"bondid": "test-hub-id"},
bond_device_id="test-device-id",
)
registry: EntityRegistry = await hass.helpers.entity_registry.async_get_registry()
entity = registry.entities["light.fan_name_up_light"]
assert entity.unique_id == "test-hub-id_test-device-id_up_light"
async def test_fan_down_light_entity_registry(hass: core.HomeAssistant):
"""Tests that fan with down light devices are registered in the entity registry."""
await setup_platform(
hass,
LIGHT_DOMAIN,
down_light_ceiling_fan("fan-name"),
bond_version={"bondid": "test-hub-id"},
bond_device_id="test-device-id",
)
registry: EntityRegistry = await hass.helpers.entity_registry.async_get_registry()
entity = registry.entities["light.fan_name_down_light"]
assert entity.unique_id == "test-hub-id_test-device-id_down_light"
async def test_fireplace_entity_registry(hass: core.HomeAssistant): async def test_fireplace_entity_registry(hass: core.HomeAssistant):
"""Tests that flame fireplace devices are registered in the entity registry.""" """Tests that flame fireplace devices are registered in the entity registry."""
await setup_platform( await setup_platform(
@ -122,7 +170,7 @@ async def test_fireplace_with_light_entity_registry(hass: core.HomeAssistant):
registry: EntityRegistry = await hass.helpers.entity_registry.async_get_registry() registry: EntityRegistry = await hass.helpers.entity_registry.async_get_registry()
entity_flame = registry.entities["light.fireplace_name"] entity_flame = registry.entities["light.fireplace_name"]
assert entity_flame.unique_id == "test-hub-id_test-device-id" assert entity_flame.unique_id == "test-hub-id_test-device-id"
entity_light = registry.entities["light.fireplace_name_2"] entity_light = registry.entities["light.fireplace_name_light"]
assert entity_light.unique_id == "test-hub-id_test-device-id_light" assert entity_light.unique_id == "test-hub-id_test-device-id_light"
@ -269,6 +317,98 @@ async def test_turn_on_light_with_brightness(hass: core.HomeAssistant):
) )
async def test_turn_on_up_light(hass: core.HomeAssistant):
"""Tests that turn on command, on an up light, delegates to API."""
await setup_platform(
hass,
LIGHT_DOMAIN,
up_light_ceiling_fan("name-1"),
bond_device_id="test-device-id",
)
with patch_bond_action() as mock_turn_on, patch_bond_device_state():
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "light.name_1_up_light"},
blocking=True,
)
await hass.async_block_till_done()
mock_turn_on.assert_called_once_with(
"test-device-id", Action(Action.TURN_UP_LIGHT_ON)
)
async def test_turn_off_up_light(hass: core.HomeAssistant):
"""Tests that turn off command, on an up light, delegates to API."""
await setup_platform(
hass,
LIGHT_DOMAIN,
up_light_ceiling_fan("name-1"),
bond_device_id="test-device-id",
)
with patch_bond_action() as mock_turn_off, patch_bond_device_state():
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "light.name_1_up_light"},
blocking=True,
)
await hass.async_block_till_done()
mock_turn_off.assert_called_once_with(
"test-device-id", Action(Action.TURN_UP_LIGHT_OFF)
)
async def test_turn_on_down_light(hass: core.HomeAssistant):
"""Tests that turn on command, on a down light, delegates to API."""
await setup_platform(
hass,
LIGHT_DOMAIN,
down_light_ceiling_fan("name-1"),
bond_device_id="test-device-id",
)
with patch_bond_action() as mock_turn_on, patch_bond_device_state():
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "light.name_1_down_light"},
blocking=True,
)
await hass.async_block_till_done()
mock_turn_on.assert_called_once_with(
"test-device-id", Action(Action.TURN_DOWN_LIGHT_ON)
)
async def test_turn_off_down_light(hass: core.HomeAssistant):
"""Tests that turn off command, on a down light, delegates to API."""
await setup_platform(
hass,
LIGHT_DOMAIN,
down_light_ceiling_fan("name-1"),
bond_device_id="test-device-id",
)
with patch_bond_action() as mock_turn_off, patch_bond_device_state():
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "light.name_1_down_light"},
blocking=True,
)
await hass.async_block_till_done()
mock_turn_off.assert_called_once_with(
"test-device-id", Action(Action.TURN_DOWN_LIGHT_OFF)
)
async def test_update_reports_light_is_on(hass: core.HomeAssistant): async def test_update_reports_light_is_on(hass: core.HomeAssistant):
"""Tests that update command sets correct state when Bond API reports the light is on.""" """Tests that update command sets correct state when Bond API reports the light is on."""
await setup_platform(hass, LIGHT_DOMAIN, ceiling_fan("name-1")) await setup_platform(hass, LIGHT_DOMAIN, ceiling_fan("name-1"))
@ -291,6 +431,50 @@ async def test_update_reports_light_is_off(hass: core.HomeAssistant):
assert hass.states.get("light.name_1").state == "off" assert hass.states.get("light.name_1").state == "off"
async def test_update_reports_up_light_is_on(hass: core.HomeAssistant):
"""Tests that update command sets correct state when Bond API reports the up light is on."""
await setup_platform(hass, LIGHT_DOMAIN, up_light_ceiling_fan("name-1"))
with patch_bond_device_state(return_value={"up_light": 1, "light": 1}):
async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))
await hass.async_block_till_done()
assert hass.states.get("light.name_1_up_light").state == "on"
async def test_update_reports_up_light_is_off(hass: core.HomeAssistant):
"""Tests that update command sets correct state when Bond API reports the up light is off."""
await setup_platform(hass, LIGHT_DOMAIN, up_light_ceiling_fan("name-1"))
with patch_bond_device_state(return_value={"up_light": 0, "light": 0}):
async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))
await hass.async_block_till_done()
assert hass.states.get("light.name_1_up_light").state == "off"
async def test_update_reports_down_light_is_on(hass: core.HomeAssistant):
"""Tests that update command sets correct state when Bond API reports the down light is on."""
await setup_platform(hass, LIGHT_DOMAIN, down_light_ceiling_fan("name-1"))
with patch_bond_device_state(return_value={"down_light": 1, "light": 1}):
async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))
await hass.async_block_till_done()
assert hass.states.get("light.name_1_down_light").state == "on"
async def test_update_reports_down_light_is_off(hass: core.HomeAssistant):
"""Tests that update command sets correct state when Bond API reports the down light is off."""
await setup_platform(hass, LIGHT_DOMAIN, down_light_ceiling_fan("name-1"))
with patch_bond_device_state(return_value={"down_light": 0, "light": 0}):
async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))
await hass.async_block_till_done()
assert hass.states.get("light.name_1_down_light").state == "off"
async def test_turn_on_fireplace_with_brightness(hass: core.HomeAssistant): async def test_turn_on_fireplace_with_brightness(hass: core.HomeAssistant):
"""Tests that turn on command delegates to set flame API.""" """Tests that turn on command delegates to set flame API."""
await setup_platform( await setup_platform(