From 4467409e5c1dca41b0c9e17a3c381cbf43afcbbe Mon Sep 17 00:00:00 2001 From: Ziv <16467659+ziv1234@users.noreply.github.com> Date: Mon, 10 Feb 2020 23:16:04 +0200 Subject: [PATCH] Dynalite Integration (#27841) * Initial commit * ran hassfest and gen_requirements_all scripts * fixes per request from Paulus Schoutsen * ran gen_requirements_all * updated library version - removed some debug leftover * get_requirements again... * added documentation URL * ran isort * changed storage in hass.data[DOMAIN] to use entry_id instead of host * adopted unit tests to latest fix * Update const.py Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 1 + homeassistant/components/dynalite/__init__.py | 93 ++++++++++++ homeassistant/components/dynalite/bridge.py | 118 +++++++++++++++ .../components/dynalite/config_flow.py | 58 ++++++++ homeassistant/components/dynalite/const.py | 11 ++ homeassistant/components/dynalite/light.py | 84 +++++++++++ .../components/dynalite/manifest.json | 9 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/dynalite/__init__.py | 1 + tests/components/dynalite/test_bridge.py | 136 ++++++++++++++++++ tests/components/dynalite/test_config_flow.py | 36 +++++ tests/components/dynalite/test_init.py | 74 ++++++++++ tests/components/dynalite/test_light.py | 44 ++++++ 15 files changed, 672 insertions(+) create mode 100755 homeassistant/components/dynalite/__init__.py create mode 100755 homeassistant/components/dynalite/bridge.py create mode 100755 homeassistant/components/dynalite/config_flow.py create mode 100755 homeassistant/components/dynalite/const.py create mode 100755 homeassistant/components/dynalite/light.py create mode 100755 homeassistant/components/dynalite/manifest.json create mode 100755 tests/components/dynalite/__init__.py create mode 100755 tests/components/dynalite/test_bridge.py create mode 100755 tests/components/dynalite/test_config_flow.py create mode 100755 tests/components/dynalite/test_init.py create mode 100755 tests/components/dynalite/test_light.py diff --git a/CODEOWNERS b/CODEOWNERS index 46c3d416b5dcd0..8f44c3caebc327 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -83,6 +83,7 @@ homeassistant/components/discogs/* @thibmaek homeassistant/components/doorbird/* @oblogic7 homeassistant/components/dsmr_reader/* @depl0y homeassistant/components/dweet/* @fabaff +homeassistant/components/dynalite/* @ziv1234 homeassistant/components/dyson/* @etheralm homeassistant/components/ecobee/* @marthoc homeassistant/components/ecovacs/* @OverloadUT diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py new file mode 100755 index 00000000000000..cb6b52483b74bf --- /dev/null +++ b/homeassistant/components/dynalite/__init__.py @@ -0,0 +1,93 @@ +"""Support for the Dynalite networks.""" +from dynalite_devices_lib import BRIDGE_CONFIG_SCHEMA +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST +from homeassistant.helpers import config_validation as cv + +# Loading the config flow file will register the flow +from .bridge import DynaliteBridge +from .config_flow import configured_hosts +from .const import CONF_BRIDGES, DATA_CONFIGS, DOMAIN, LOGGER + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_BRIDGES): vol.All( + cv.ensure_list, [BRIDGE_CONFIG_SCHEMA] + ) + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the Dynalite platform.""" + + conf = config.get(DOMAIN) + LOGGER.debug("Setting up dynalite component config = %s", conf) + + if conf is None: + conf = {} + + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_CONFIGS] = {} + + configured = configured_hosts(hass) + + # User has configured bridges + if CONF_BRIDGES not in conf: + return True + + bridges = conf[CONF_BRIDGES] + + for bridge_conf in bridges: + host = bridge_conf[CONF_HOST] + LOGGER.debug("async_setup host=%s conf=%s", host, bridge_conf) + + # Store config in hass.data so the config entry can find it + hass.data[DOMAIN][DATA_CONFIGS][host] = bridge_conf + + if host in configured: + LOGGER.debug("async_setup host=%s already configured", host) + continue + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_HOST: bridge_conf[CONF_HOST]}, + ) + ) + + return True + + +async def async_setup_entry(hass, entry): + """Set up a bridge from a config entry.""" + LOGGER.debug("__init async_setup_entry %s", entry.data) + host = entry.data[CONF_HOST] + config = hass.data[DOMAIN][DATA_CONFIGS].get(host) + + if config is None: + LOGGER.error("__init async_setup_entry empty config for host %s", host) + return False + + bridge = DynaliteBridge(hass, entry) + + if not await bridge.async_setup(): + LOGGER.error("bridge.async_setup failed") + return False + hass.data[DOMAIN][entry.entry_id] = bridge + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + LOGGER.error("async_unload_entry %s", entry.data) + bridge = hass.data[DOMAIN].pop(entry.entry_id) + return await bridge.async_reset() diff --git a/homeassistant/components/dynalite/bridge.py b/homeassistant/components/dynalite/bridge.py new file mode 100755 index 00000000000000..1bf86001cc5570 --- /dev/null +++ b/homeassistant/components/dynalite/bridge.py @@ -0,0 +1,118 @@ +"""Code to handle a Dynalite bridge.""" + +from dynalite_devices_lib import DynaliteDevices +from dynalite_lib import CONF_ALL + +from homeassistant.const import CONF_HOST +from homeassistant.core import callback + +from .const import DATA_CONFIGS, DOMAIN, LOGGER +from .light import DynaliteLight + + +class BridgeError(Exception): + """Class to throw exceptions from DynaliteBridge.""" + + def __init__(self, message): + """Initialize the exception.""" + super().__init__() + self.message = message + + +class DynaliteBridge: + """Manages a single Dynalite bridge.""" + + def __init__(self, hass, config_entry): + """Initialize the system based on host parameter.""" + self.config_entry = config_entry + self.hass = hass + self.area = {} + self.async_add_entities = None + self.waiting_entities = [] + self.all_entities = {} + self.config = None + self.host = config_entry.data[CONF_HOST] + if self.host not in hass.data[DOMAIN][DATA_CONFIGS]: + LOGGER.info("invalid host - %s", self.host) + raise BridgeError(f"invalid host - {self.host}") + self.config = hass.data[DOMAIN][DATA_CONFIGS][self.host] + # Configure the dynalite devices + self.dynalite_devices = DynaliteDevices( + config=self.config, + newDeviceFunc=self.add_devices, + updateDeviceFunc=self.update_device, + ) + + async def async_setup(self, tries=0): + """Set up a Dynalite bridge.""" + # Configure the dynalite devices + await self.dynalite_devices.async_setup() + + self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, "light" + ) + ) + + return True + + @callback + def add_devices(self, devices): + """Call when devices should be added to home assistant.""" + added_entities = [] + + for device in devices: + if device.category == "light": + entity = DynaliteLight(device, self) + else: + LOGGER.debug("Illegal device category %s", device.category) + continue + added_entities.append(entity) + self.all_entities[entity.unique_id] = entity + + if added_entities: + self.add_entities_when_registered(added_entities) + + @callback + def update_device(self, device): + """Call when a device or all devices should be updated.""" + if device == CONF_ALL: + # This is used to signal connection or disconnection, so all devices may become available or not. + if self.dynalite_devices.available: + LOGGER.info("Connected to dynalite host") + else: + LOGGER.info("Disconnected from dynalite host") + for uid in self.all_entities: + self.all_entities[uid].try_schedule_ha() + else: + uid = device.unique_id + if uid in self.all_entities: + self.all_entities[uid].try_schedule_ha() + + @callback + def register_add_entities(self, async_add_entities): + """Add an async_add_entities for a category.""" + self.async_add_entities = async_add_entities + if self.waiting_entities: + self.async_add_entities(self.waiting_entities) + + def add_entities_when_registered(self, entities): + """Add the entities to HA if async_add_entities was registered, otherwise queue until it is.""" + if not entities: + return + if self.async_add_entities: + self.async_add_entities(entities) + else: # handle it later when it is registered + self.waiting_entities.extend(entities) + + async def async_reset(self): + """Reset this bridge to default state. + + Will cancel any scheduled setup retry and will unload + the config entry. + """ + result = await self.hass.config_entries.async_forward_entry_unload( + self.config_entry, "light" + ) + # None and True are OK + return result diff --git a/homeassistant/components/dynalite/config_flow.py b/homeassistant/components/dynalite/config_flow.py new file mode 100755 index 00000000000000..9aaaee0071792f --- /dev/null +++ b/homeassistant/components/dynalite/config_flow.py @@ -0,0 +1,58 @@ +"""Config flow to configure Dynalite hub.""" +import asyncio + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST +from homeassistant.core import callback + +from .const import DOMAIN, LOGGER + + +@callback +def configured_hosts(hass): + """Return a set of the configured hosts.""" + return set( + entry.data[CONF_HOST] for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Dynalite config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + + def __init__(self): + """Initialize the Dynalite flow.""" + self.host = None + + async def async_step_import(self, import_info): + """Import a new bridge as a config entry.""" + LOGGER.debug("async_step_import - %s", import_info) + host = self.context[CONF_HOST] = import_info[CONF_HOST] + return await self._entry_from_bridge(host) + + async def _entry_from_bridge(self, host): + """Return a config entry from an initialized bridge.""" + LOGGER.debug("entry_from_bridge - %s", host) + # Remove all other entries of hubs with same ID or host + + same_hub_entries = [ + entry.entry_id + for entry in self.hass.config_entries.async_entries(DOMAIN) + if entry.data[CONF_HOST] == host + ] + + LOGGER.debug("entry_from_bridge same_hub - %s", same_hub_entries) + + if same_hub_entries: + await asyncio.wait( + [ + self.hass.config_entries.async_remove(entry_id) + for entry_id in same_hub_entries + ] + ) + + return self.async_create_entry(title=host, data={CONF_HOST: host}) diff --git a/homeassistant/components/dynalite/const.py b/homeassistant/components/dynalite/const.py new file mode 100755 index 00000000000000..f433214913a26d --- /dev/null +++ b/homeassistant/components/dynalite/const.py @@ -0,0 +1,11 @@ +"""Constants for the Dynalite component.""" +import logging + +LOGGER = logging.getLogger(__package__) +DOMAIN = "dynalite" +DATA_CONFIGS = "dynalite_configs" + +CONF_BRIDGES = "bridges" + +DEFAULT_NAME = "dynalite" +DEFAULT_PORT = 12345 diff --git a/homeassistant/components/dynalite/light.py b/homeassistant/components/dynalite/light.py new file mode 100755 index 00000000000000..d3263941f9f22c --- /dev/null +++ b/homeassistant/components/dynalite/light.py @@ -0,0 +1,84 @@ +"""Support for Dynalite channels as lights.""" +from homeassistant.components.light import SUPPORT_BRIGHTNESS, Light +from homeassistant.core import callback + +from .const import DOMAIN, LOGGER + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Record the async_add_entities function to add them later when received from Dynalite.""" + LOGGER.debug("async_setup_entry light entry = %s", config_entry.data) + bridge = hass.data[DOMAIN][config_entry.entry_id] + bridge.register_add_entities(async_add_entities) + + +class DynaliteLight(Light): + """Representation of a Dynalite Channel as a Home Assistant Light.""" + + def __init__(self, device, bridge): + """Initialize the base class.""" + self._device = device + self._bridge = bridge + + @property + def device(self): + """Return the underlying device - mostly for testing.""" + return self._device + + @property + def name(self): + """Return the name of the entity.""" + return self._device.name + + @property + def unique_id(self): + """Return the unique ID of the entity.""" + return self._device.unique_id + + @property + def available(self): + """Return if entity is available.""" + return self._device.available + + @property + def hidden(self): + """Return true if this entity should be hidden from UI.""" + return self._device.hidden + + async def async_update(self): + """Update the entity.""" + return + + @property + def device_info(self): + """Device info for this entity.""" + return self._device.device_info + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return self._device.brightness + + @property + def is_on(self): + """Return true if device is on.""" + return self._device.is_on + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + await self._device.async_turn_on(**kwargs) + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + await self._device.async_turn_off(**kwargs) + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + + @callback + def try_schedule_ha(self): + """Schedule update HA state if configured.""" + if self.hass: + self.schedule_update_ha_state() diff --git a/homeassistant/components/dynalite/manifest.json b/homeassistant/components/dynalite/manifest.json new file mode 100755 index 00000000000000..4df580c16a21f7 --- /dev/null +++ b/homeassistant/components/dynalite/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "dynalite", + "name": "Philips Dynalite", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/dynalite", + "dependencies": [], + "codeowners": ["@ziv1234"], + "requirements": ["dynalite_devices==0.1.17"] +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index c77a8de9388c4d..8b6c0e77585b3d 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -20,6 +20,7 @@ "daikin", "deconz", "dialogflow", + "dynalite", "ecobee", "elgato", "emulated_roku", diff --git a/requirements_all.txt b/requirements_all.txt index acfb089c20327c..e22b3ccf0f4407 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -455,6 +455,9 @@ dsmr_parser==0.18 # homeassistant.components.dweet dweepy==0.3.0 +# homeassistant.components.dynalite +dynalite_devices==0.1.17 + # homeassistant.components.rainforest_eagle eagle200_reader==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af22deb6b6cd25..8aa12234896c0a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -167,6 +167,9 @@ distro==1.4.0 # homeassistant.components.dsmr dsmr_parser==0.18 +# homeassistant.components.dynalite +dynalite_devices==0.1.17 + # homeassistant.components.ee_brightbox eebrightbox==0.0.4 diff --git a/tests/components/dynalite/__init__.py b/tests/components/dynalite/__init__.py new file mode 100755 index 00000000000000..f97770cbac90ca --- /dev/null +++ b/tests/components/dynalite/__init__.py @@ -0,0 +1 @@ +"""Tests for the Dynalite component.""" diff --git a/tests/components/dynalite/test_bridge.py b/tests/components/dynalite/test_bridge.py new file mode 100755 index 00000000000000..c0aa2b3c14379b --- /dev/null +++ b/tests/components/dynalite/test_bridge.py @@ -0,0 +1,136 @@ +"""Test Dynalite bridge.""" +from unittest.mock import Mock, call, patch + +from dynalite_lib import CONF_ALL +import pytest + +from homeassistant.components.dynalite import DATA_CONFIGS, DOMAIN +from homeassistant.components.dynalite.bridge import BridgeError, DynaliteBridge + +from tests.common import mock_coro + + +async def test_bridge_setup(): + """Test a successful setup.""" + hass = Mock() + entry = Mock() + host = "1.2.3.4" + entry.data = {"host": host} + hass.data = {DOMAIN: {DATA_CONFIGS: {host: {}}}} + dyn_bridge = DynaliteBridge(hass, entry) + + with patch.object( + dyn_bridge.dynalite_devices, "async_setup", return_value=mock_coro(True) + ): + assert await dyn_bridge.async_setup() is True + + forward_entries = set( + c[1][1] for c in hass.config_entries.async_forward_entry_setup.mock_calls + ) + hass.config_entries.async_forward_entry_setup.assert_called_once() + assert forward_entries == set(["light"]) + + +async def test_invalid_host(): + """Test without host in hass.data.""" + hass = Mock() + entry = Mock() + host = "1.2.3.4" + entry.data = {"host": host} + hass.data = {DOMAIN: {DATA_CONFIGS: {}}} + + dyn_bridge = None + with pytest.raises(BridgeError): + dyn_bridge = DynaliteBridge(hass, entry) + assert dyn_bridge is None + + +async def test_add_devices_then_register(): + """Test that add_devices work.""" + hass = Mock() + entry = Mock() + host = "1.2.3.4" + entry.data = {"host": host} + hass.data = {DOMAIN: {DATA_CONFIGS: {host: {}}}} + dyn_bridge = DynaliteBridge(hass, entry) + + device1 = Mock() + device1.category = "light" + device2 = Mock() + device2.category = "switch" + dyn_bridge.add_devices([device1, device2]) + reg_func = Mock() + dyn_bridge.register_add_entities(reg_func) + reg_func.assert_called_once() + assert reg_func.mock_calls[0][1][0][0].device is device1 + + +async def test_register_then_add_devices(): + """Test that add_devices work after register_add_entities.""" + hass = Mock() + entry = Mock() + host = "1.2.3.4" + entry.data = {"host": host} + hass.data = {DOMAIN: {DATA_CONFIGS: {host: {}}}} + dyn_bridge = DynaliteBridge(hass, entry) + + device1 = Mock() + device1.category = "light" + device2 = Mock() + device2.category = "switch" + reg_func = Mock() + dyn_bridge.register_add_entities(reg_func) + dyn_bridge.add_devices([device1, device2]) + reg_func.assert_called_once() + assert reg_func.mock_calls[0][1][0][0].device is device1 + + +async def test_update_device(): + """Test the update_device callback.""" + hass = Mock() + entry = Mock() + host = "1.2.3.4" + entry.data = {"host": host} + hass.data = {DOMAIN: {DATA_CONFIGS: {host: {}}}} + dyn_bridge = DynaliteBridge(hass, entry) + with patch.object(dyn_bridge, "dynalite_devices") as devices_mock: + # Single device update + device1 = Mock() + device1.unique_id = "testing1" + device2 = Mock() + device2.unique_id = "testing2" + dyn_bridge.all_entities = { + device1.unique_id: device1, + device2.unique_id: device2, + } + dyn_bridge.update_device(device1) + device1.try_schedule_ha.assert_called_once() + device2.try_schedule_ha.assert_not_called() + # connected to network - all devices update + devices_mock.available = True + dyn_bridge.update_device(CONF_ALL) + assert device1.try_schedule_ha.call_count == 2 + device2.try_schedule_ha.assert_called_once() + # disconnected from network - all devices update + devices_mock.available = False + dyn_bridge.update_device(CONF_ALL) + assert device1.try_schedule_ha.call_count == 3 + assert device2.try_schedule_ha.call_count == 2 + + +async def test_async_reset(): + """Test async_reset.""" + hass = Mock() + hass.config_entries.async_forward_entry_unload = Mock( + return_value=mock_coro(Mock()) + ) + entry = Mock() + host = "1.2.3.4" + entry.data = {"host": host} + hass.data = {DOMAIN: {DATA_CONFIGS: {host: {}}}} + dyn_bridge = DynaliteBridge(hass, entry) + await dyn_bridge.async_reset() + hass.config_entries.async_forward_entry_unload.assert_called_once() + assert hass.config_entries.async_forward_entry_unload.mock_calls[0] == call( + entry, "light" + ) diff --git a/tests/components/dynalite/test_config_flow.py b/tests/components/dynalite/test_config_flow.py new file mode 100755 index 00000000000000..1cf82143f1b1d0 --- /dev/null +++ b/tests/components/dynalite/test_config_flow.py @@ -0,0 +1,36 @@ +"""Test Dynalite config flow.""" +from unittest.mock import Mock, call, patch + +from homeassistant.components.dynalite import config_flow + +from tests.common import mock_coro + + +async def test_step_import(): + """Test a successful setup.""" + flow_handler = config_flow.DynaliteFlowHandler() + with patch.object(flow_handler, "context", create=True): + with patch.object(flow_handler, "hass", create=True) as mock_hass: + with patch.object( + flow_handler, "async_create_entry", create=True + ) as mock_create: + host = "1.2.3.4" + entry1 = Mock() + entry1.data = {"host": host} + entry2 = Mock() + entry2.data = {"host": "5.5"} + mock_hass.config_entries.async_entries = Mock( + return_value=[entry1, entry2] + ) + mock_hass.config_entries.async_remove = Mock( + return_value=mock_coro(Mock()) + ) + await flow_handler.async_step_import({"host": "1.2.3.4"}) + mock_hass.config_entries.async_remove.assert_called_once() + assert mock_hass.config_entries.async_remove.mock_calls[0] == call( + entry1.entry_id + ) + mock_create.assert_called_once() + assert mock_create.mock_calls[0] == call( + title=host, data={"host": host} + ) diff --git a/tests/components/dynalite/test_init.py b/tests/components/dynalite/test_init.py new file mode 100755 index 00000000000000..beb96a5e78f83b --- /dev/null +++ b/tests/components/dynalite/test_init.py @@ -0,0 +1,74 @@ +"""Test Dynalite __init__.""" +from unittest.mock import Mock, call, patch + +from homeassistant.components.dynalite import DATA_CONFIGS, DOMAIN, LOGGER +from homeassistant.components.dynalite.__init__ import ( + async_setup, + async_setup_entry, + async_unload_entry, +) + +from tests.common import mock_coro + + +async def test_async_setup(): + """Test a successful setup.""" + new_host = "1.2.3.4" + old_host = "5.6.7.8" + hass = Mock() + hass.data = {} + config = {DOMAIN: {"bridges": [{"host": old_host}, {"host": new_host}]}} + mock_conf_host = Mock(return_value=[old_host]) + with patch( + "homeassistant.components.dynalite.__init__.configured_hosts", mock_conf_host + ): + await async_setup(hass, config) + mock_conf_host.assert_called_once() + assert mock_conf_host.mock_calls[0] == call(hass) + assert hass.data[DOMAIN][DATA_CONFIGS] == { + new_host: {"host": new_host}, + old_host: {"host": old_host}, + } + hass.async_create_task.assert_called_once() + + +async def test_async_setup_entry(): + """Test setup of an entry.""" + + def async_mock(mock): + """Return the return value of a mock from async.""" + + async def async_func(*args, **kwargs): + return mock() + + return async_func + + host = "1.2.3.4" + hass = Mock() + entry = Mock() + entry.data = {"host": host} + hass.data = {} + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_CONFIGS] = {host: {}} + mock_async_setup = Mock(return_value=True) + with patch( + "homeassistant.components.dynalite.__init__.DynaliteBridge.async_setup", + async_mock(mock_async_setup), + ): + assert await async_setup_entry(hass, entry) + mock_async_setup.assert_called_once() + + +async def test_async_unload_entry(): + """Test unloading of an entry.""" + hass = Mock() + mock_bridge = Mock() + mock_bridge.async_reset.return_value = mock_coro(True) + entry = Mock() + hass.data = {} + hass.data[DOMAIN] = {} + hass.data[DOMAIN][entry.entry_id] = mock_bridge + await async_unload_entry(hass, entry) + LOGGER.error("XXX calls=%s", mock_bridge.mock_calls) + mock_bridge.async_reset.assert_called_once() + assert mock_bridge.mock_calls[0] == call.async_reset() diff --git a/tests/components/dynalite/test_light.py b/tests/components/dynalite/test_light.py new file mode 100755 index 00000000000000..cfc9d42d0e4934 --- /dev/null +++ b/tests/components/dynalite/test_light.py @@ -0,0 +1,44 @@ +"""Test Dynalite light.""" +from unittest.mock import Mock, call, patch + +from homeassistant.components.dynalite import DOMAIN +from homeassistant.components.dynalite.light import DynaliteLight, async_setup_entry + +from tests.common import mock_coro + + +async def test_light_setup(): + """Test a successful setup.""" + hass = Mock() + entry = Mock() + async_add = Mock() + bridge = Mock() + hass.data = {DOMAIN: {entry.entry_id: bridge}} + await async_setup_entry(hass, entry, async_add) + bridge.register_add_entities.assert_called_once() + assert bridge.register_add_entities.mock_calls[0] == call(async_add) + + +async def test_light(): + """Test the light entity.""" + device = Mock() + device.async_turn_on = Mock(return_value=mock_coro(Mock())) + device.async_turn_off = Mock(return_value=mock_coro(Mock())) + bridge = Mock() + dyn_light = DynaliteLight(device, bridge) + assert dyn_light.name is device.name + assert dyn_light.unique_id is device.unique_id + assert dyn_light.available is device.available + assert dyn_light.hidden is device.hidden + await dyn_light.async_update() # does nothing + assert dyn_light.device_info is device.device_info + assert dyn_light.brightness is device.brightness + assert dyn_light.is_on is device.is_on + await dyn_light.async_turn_on(aaa="bbb") + assert device.async_turn_on.mock_calls[0] == call(aaa="bbb") + await dyn_light.async_turn_off(ccc="ddd") + assert device.async_turn_off.mock_calls[0] == call(ccc="ddd") + with patch.object(dyn_light, "hass"): + with patch.object(dyn_light, "schedule_update_ha_state") as update_ha: + dyn_light.try_schedule_ha() + update_ha.assert_called_once()