From f32f73a7d19e54a3eff6b094239a4d42e8d42e8b Mon Sep 17 00:00:00 2001 From: Eugene Prystupa Date: Sun, 12 Jul 2020 19:45:47 -0400 Subject: [PATCH] Add basic support for lights in bond integration (#37802) --- homeassistant/components/bond/__init__.py | 2 +- homeassistant/components/bond/light.py | 61 +++++++++++++++ homeassistant/components/bond/utils.py | 14 ++++ tests/components/bond/test_init.py | 5 +- tests/components/bond/test_light.py | 93 +++++++++++++++++++++++ 5 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/bond/light.py create mode 100644 tests/components/bond/test_light.py diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index aa069bea17e..39db50bf9c1 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -11,7 +11,7 @@ from homeassistant.helpers import device_registry as dr from .const import DOMAIN from .utils import BondHub -PLATFORMS = ["cover", "fan"] +PLATFORMS = ["cover", "fan", "light"] async def async_setup(hass: HomeAssistant, config: dict): diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py new file mode 100644 index 00000000000..4ee6e77dd7b --- /dev/null +++ b/homeassistant/components/bond/light.py @@ -0,0 +1,61 @@ +"""Support for Bond lights.""" +from typing import Any, Callable, List, Optional + +from bond import DeviceTypes + +from homeassistant.components.light import LightEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity + +from . import BondHub +from .const import DOMAIN +from .entity import BondEntity +from .utils import BondDevice + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up Bond light devices.""" + hub: BondHub = hass.data[DOMAIN][entry.entry_id] + + devices = await hass.async_add_executor_job(hub.get_bond_devices) + + lights = [ + BondLight(hub, device) + for device in devices + if device.type == DeviceTypes.CEILING_FAN and device.supports_light() + ] + + async_add_entities(lights, True) + + +class BondLight(BondEntity, LightEntity): + """Representation of a Bond light.""" + + def __init__(self, hub: BondHub, device: BondDevice): + """Create HA entity representing Bond fan.""" + super().__init__(hub, device) + + self._light: Optional[int] = None + + @property + def is_on(self) -> bool: + """Return if light is currently on.""" + return self._light == 1 + + def update(self): + """Fetch assumed state of the light from the hub using API.""" + state: dict = self._hub.bond.getDeviceState(self._device.device_id) + self._light = state.get("light") + + def turn_on(self, **kwargs: Any) -> None: + """Turn on the light.""" + self._hub.bond.turnLightOn(self._device.device_id) + + def turn_off(self, **kwargs: Any) -> None: + """Turn off the light.""" + self._hub.bond.turnLightOff(self._device.device_id) diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index c019b2cc160..a8636d638cb 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -42,6 +42,20 @@ class BondDevice: > 0 ) + def supports_light(self) -> bool: + """Return True if this device supports any of the light related commands.""" + actions: List[str] = self._attrs["actions"] + return ( + len( + [ + action + for action in actions + if action in [Actions.TURN_LIGHT_ON, Actions.TOGGLE_LIGHT] + ] + ) + > 0 + ) + class BondHub: """Hub device representing Bond Bridge.""" diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index aac250cb85b..bec002b38a8 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -29,7 +29,9 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAss "homeassistant.components.bond.cover.async_setup_entry" ) as mock_cover_async_setup_entry, patch( "homeassistant.components.bond.fan.async_setup_entry" - ) as mock_fan_async_setup_entry: + ) as mock_fan_async_setup_entry, patch( + "homeassistant.components.bond.light.async_setup_entry" + ) as mock_light_async_setup_entry: result = await setup_bond_entity( hass, config_entry, @@ -58,6 +60,7 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAss # verify supported domains are setup assert len(mock_cover_async_setup_entry.mock_calls) == 1 assert len(mock_fan_async_setup_entry.mock_calls) == 1 + assert len(mock_light_async_setup_entry.mock_calls) == 1 async def test_unload_config_entry(hass: HomeAssistant): diff --git a/tests/components/bond/test_light.py b/tests/components/bond/test_light.py new file mode 100644 index 00000000000..4e0ea6155fb --- /dev/null +++ b/tests/components/bond/test_light.py @@ -0,0 +1,93 @@ +"""Tests for the Bond light device.""" +from datetime import timedelta +import logging + +from bond import Actions, DeviceTypes + +from homeassistant import core +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.helpers.entity_registry import EntityRegistry +from homeassistant.util import utcnow + +from .common import setup_platform + +from tests.async_mock import patch +from tests.common import async_fire_time_changed + +_LOGGER = logging.getLogger(__name__) + + +def ceiling_fan(name: str): + """Create a ceiling fan (that has built-in light) with given name.""" + return { + "name": name, + "type": DeviceTypes.CEILING_FAN, + "actions": [Actions.TOGGLE_LIGHT], + } + + +async def test_entity_registry(hass: core.HomeAssistant): + """Tests that the devices are registered in the entity registry.""" + await setup_platform(hass, LIGHT_DOMAIN, ceiling_fan("name-1")) + + registry: EntityRegistry = await hass.helpers.entity_registry.async_get_registry() + assert [key for key in registry.entities.keys()] == ["light.name_1"] + + +async def test_turn_on_light(hass: core.HomeAssistant): + """Tests that turn on command delegates to API.""" + await setup_platform(hass, LIGHT_DOMAIN, ceiling_fan("name-1")) + + with patch("homeassistant.components.bond.Bond.turnLightOn") as mock_turn_light_on: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.name_1"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_turn_light_on.assert_called_once() + + +async def test_turn_off_light(hass: core.HomeAssistant): + """Tests that turn off command delegates to API.""" + await setup_platform(hass, LIGHT_DOMAIN, ceiling_fan("name-1")) + + with patch( + "homeassistant.components.bond.Bond.turnLightOff" + ) as mock_turn_light_off: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.name_1"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_turn_light_off.assert_called_once() + + +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")) + + with patch( + "homeassistant.components.bond.Bond.getDeviceState", return_value={"light": 1} + ): + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + assert hass.states.get("light.name_1").state == "on" + + +async def test_update_reports_light_is_off(hass: core.HomeAssistant): + """Tests that update command sets correct state when Bond API reports the light is off.""" + await setup_platform(hass, LIGHT_DOMAIN, ceiling_fan("name-1")) + + with patch( + "homeassistant.components.bond.Bond.getDeviceState", return_value={"light": 0} + ): + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + assert hass.states.get("light.name_1").state == "off"