From adf480025d0ee380e5a514d90dfc16fec7467920 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Feb 2021 08:03:40 -1000 Subject: [PATCH] Add support for bond up and down lights (#46233) --- homeassistant/components/bond/entity.py | 3 + homeassistant/components/bond/light.py | 94 ++++++++++-- homeassistant/components/bond/utils.py | 36 +++-- tests/components/bond/test_light.py | 186 +++++++++++++++++++++++- 4 files changed, 294 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index f6165eb7890..5b2e27b94cc 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -52,6 +52,9 @@ class BondEntity(Entity): @property def name(self) -> Optional[str]: """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 @property diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py index 8d0dfe85246..194a009a857 100644 --- a/homeassistant/components/bond/light.py +++ b/homeassistant/components/bond/light.py @@ -34,7 +34,21 @@ async def async_setup_entry( fan_lights: List[Entity] = [ BondLight(hub, device, bpup_subs) 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] = [ @@ -55,10 +69,13 @@ async def async_setup_entry( 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.""" def __init__( @@ -68,10 +85,34 @@ class BondLight(BondEntity, LightEntity): bpup_subs: BPUPSubscriptions, 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) self._brightness: Optional[int] = None - self._light: Optional[int] = None def _apply_state(self, state: dict): self._light = state.get("light") @@ -84,11 +125,6 @@ class BondLight(BondEntity, LightEntity): return SUPPORT_BRIGHTNESS return 0 - @property - def is_on(self) -> bool: - """Return if light is currently on.""" - return self._light == 1 - @property def brightness(self) -> int: """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()) +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): """Representation of a Bond-controlled fireplace.""" diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index 6d3aacf5e42..55ef81778f0 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -1,7 +1,7 @@ """Reusable utilities for the Bond component.""" import asyncio import logging -from typing import List, Optional +from typing import List, Optional, Set from aiohttp import ClientResponseError from bond_api import Action, Bond @@ -58,31 +58,39 @@ class BondDevice: """Check if Trust State is turned on.""" 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: """Return True if this device supports any of the speed related commands.""" - actions: List[str] = self._attrs["actions"] - return bool([action for action in actions if action in [Action.SET_SPEED]]) + return self._has_any_action({Action.SET_SPEED}) def supports_direction(self) -> bool: """Return True if this device supports any of the direction related commands.""" - actions: List[str] = self._attrs["actions"] - return bool([action for action in actions if action in [Action.SET_DIRECTION]]) + return self._has_any_action({Action.SET_DIRECTION}) def supports_light(self) -> bool: """Return True if this device supports any of the light related commands.""" - actions: List[str] = self._attrs["actions"] - return bool( - [ - action - for action in actions - if action in [Action.TURN_LIGHT_ON, Action.TURN_LIGHT_OFF] - ] + return self._has_any_action({Action.TURN_LIGHT_ON, Action.TURN_LIGHT_OFF}) + + def supports_up_light(self) -> bool: + """Return true if the device has an up light.""" + return self._has_any_action({Action.TURN_UP_LIGHT_ON, Action.TURN_UP_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: """Return True if this device supports setting a light brightness.""" - actions: List[str] = self._attrs["actions"] - return bool([action for action in actions if action in [Action.SET_BRIGHTNESS]]) + return self._has_any_action({Action.SET_BRIGHTNESS}) class BondHub: diff --git a/tests/components/bond/test_light.py b/tests/components/bond/test_light.py index e4cd4e4e3e9..59d051fbe86 100644 --- a/tests/components/bond/test_light.py +++ b/tests/components/bond/test_light.py @@ -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): """Create a fireplace with given name.""" return { @@ -94,6 +112,36 @@ async def test_fan_entity_registry(hass: core.HomeAssistant): 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): """Tests that flame fireplace devices are registered in the entity registry.""" 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() entity_flame = registry.entities["light.fireplace_name"] 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" @@ -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): """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")) @@ -291,6 +431,50 @@ async def test_update_reports_light_is_off(hass: core.HomeAssistant): 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): """Tests that turn on command delegates to set flame API.""" await setup_platform(