diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py index cb6b52483b7..f4fc65b8261 100755 --- a/homeassistant/components/dynalite/__init__.py +++ b/homeassistant/components/dynalite/__init__.py @@ -1,24 +1,75 @@ """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.exceptions import ConfigEntryNotReady 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 +from .const import ( + CONF_ACTIVE, + CONF_AREA, + CONF_AUTO_DISCOVER, + CONF_BRIDGES, + CONF_CHANNEL, + CONF_DEFAULT, + CONF_FADE, + CONF_NAME, + CONF_POLLTIMER, + CONF_PORT, + DEFAULT_NAME, + DEFAULT_PORT, + DOMAIN, + LOGGER, +) + + +def num_string(value): + """Test if value is a string of digits, aka an integer.""" + new_value = str(value) + if new_value.isdigit(): + return new_value + raise vol.Invalid("Not a string with numbers") + + +CHANNEL_DATA_SCHEMA = vol.Schema( + {vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_FADE): vol.Coerce(float)} +) + +CHANNEL_SCHEMA = vol.Schema({num_string: CHANNEL_DATA_SCHEMA}) + +AREA_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_FADE): vol.Coerce(float), + vol.Optional(CONF_CHANNEL): CHANNEL_SCHEMA, + }, +) + +AREA_SCHEMA = vol.Schema({num_string: vol.Any(AREA_DATA_SCHEMA, None)}) + +PLATFORM_DEFAULTS_SCHEMA = vol.Schema({vol.Optional(CONF_FADE): vol.Coerce(float)}) + + +BRIDGE_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, + vol.Optional(CONF_AUTO_DISCOVER, default=False): vol.Coerce(bool), + vol.Optional(CONF_POLLTIMER, default=1.0): vol.Coerce(float), + vol.Optional(CONF_AREA): AREA_SCHEMA, + vol.Optional(CONF_DEFAULT): PLATFORM_DEFAULTS_SCHEMA, + vol.Optional(CONF_ACTIVE, default=False): vol.Coerce(bool), + } +) CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( - { - vol.Optional(CONF_BRIDGES): vol.All( - cv.ensure_list, [BRIDGE_CONFIG_SCHEMA] - ) - } + {vol.Optional(CONF_BRIDGES): vol.All(cv.ensure_list, [BRIDGE_SCHEMA])} ) }, extra=vol.ALLOW_EXTRA, @@ -35,9 +86,6 @@ async def async_setup(hass, config): conf = {} hass.data[DOMAIN] = {} - hass.data[DOMAIN][DATA_CONFIGS] = {} - - configured = configured_hosts(hass) # User has configured bridges if CONF_BRIDGES not in conf: @@ -47,20 +95,13 @@ async def async_setup(hass, config): 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 + LOGGER.debug("Starting config entry flow host=%s conf=%s", host, bridge_conf) hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_HOST: bridge_conf[CONF_HOST]}, + data=bridge_conf, ) ) @@ -69,25 +110,29 @@ async def async_setup(hass, config): 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) + LOGGER.debug("Setting up entry %s", entry.data) - if config is None: - LOGGER.error("__init async_setup_entry empty config for host %s", host) - return False - - bridge = DynaliteBridge(hass, entry) + bridge = DynaliteBridge(hass, entry.data) if not await bridge.async_setup(): - LOGGER.error("bridge.async_setup failed") + LOGGER.error("Could not set up bridge for entry %s", entry.data) return False + + if not await bridge.try_connection(): + LOGGER.errot("Could not connect with entry %s", entry) + raise ConfigEntryNotReady + hass.data[DOMAIN][entry.entry_id] = bridge + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "light") + ) 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() + LOGGER.debug("Unloading entry %s", entry.data) + hass.data[DOMAIN].pop(entry.entry_id) + result = await hass.config_entries.async_forward_entry_unload(entry, "light") + return result diff --git a/homeassistant/components/dynalite/bridge.py b/homeassistant/components/dynalite/bridge.py index 1bf86001cc5..cbe08fdadb5 100755 --- a/homeassistant/components/dynalite/bridge.py +++ b/homeassistant/components/dynalite/bridge.py @@ -1,118 +1,82 @@ """Code to handle a Dynalite bridge.""" +import asyncio + from dynalite_devices_lib import DynaliteDevices -from dynalite_lib import CONF_ALL -from homeassistant.const import CONF_HOST from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send -from .const import DATA_CONFIGS, DOMAIN, LOGGER -from .light import DynaliteLight +from .const import CONF_ALL, CONF_HOST, LOGGER - -class BridgeError(Exception): - """Class to throw exceptions from DynaliteBridge.""" - - def __init__(self, message): - """Initialize the exception.""" - super().__init__() - self.message = message +CONNECT_TIMEOUT = 30 +CONNECT_INTERVAL = 1 class DynaliteBridge: """Manages a single Dynalite bridge.""" - def __init__(self, hass, config_entry): + def __init__(self, hass, config): """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] + self.async_add_devices = None + self.waiting_devices = [] + self.host = config[CONF_HOST] # Configure the dynalite devices self.dynalite_devices = DynaliteDevices( - config=self.config, - newDeviceFunc=self.add_devices, + config=config, + newDeviceFunc=self.add_devices_when_registered, updateDeviceFunc=self.update_device, ) - async def async_setup(self, tries=0): + async def async_setup(self): """Set up a Dynalite bridge.""" # Configure the dynalite devices - await self.dynalite_devices.async_setup() + return 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) + def update_signal(self, device=None): + """Create signal to use to trigger entity update.""" + if device: + signal = f"dynalite-update-{self.host}-{device.unique_id}" + else: + signal = f"dynalite-update-{self.host}" + return signal @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() + log_string = ( + "Connected" if self.dynalite_devices.available else "Disconnected" + ) + LOGGER.info("%s to dynalite host", log_string) + async_dispatcher_send(self.hass, self.update_signal()) else: - uid = device.unique_id - if uid in self.all_entities: - self.all_entities[uid].try_schedule_ha() + async_dispatcher_send(self.hass, self.update_signal(device)) + + async def try_connection(self): + """Try to connect to dynalite with timeout.""" + # Currently by polling. Future - will need to change the library to be proactive + for _ in range(0, CONNECT_TIMEOUT): + if self.dynalite_devices.available: + return True + await asyncio.sleep(CONNECT_INTERVAL) + return False @callback - def register_add_entities(self, async_add_entities): + def register_add_devices(self, async_add_devices): """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) + self.async_add_devices = async_add_devices + if self.waiting_devices: + self.async_add_devices(self.waiting_devices) - 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: + def add_devices_when_registered(self, devices): + """Add the devices to HA if the add devices callback was registered, otherwise queue until it is.""" + if not devices: return - if self.async_add_entities: - self.async_add_entities(entities) + if self.async_add_devices: + self.async_add_devices(devices) 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 + self.waiting_devices.extend(devices) diff --git a/homeassistant/components/dynalite/config_flow.py b/homeassistant/components/dynalite/config_flow.py index 9aaaee00717..aac42172181 100755 --- a/homeassistant/components/dynalite/config_flow.py +++ b/homeassistant/components/dynalite/config_flow.py @@ -1,19 +1,9 @@ """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) - ) +from .bridge import DynaliteBridge +from .const import DOMAIN, LOGGER # pylint: disable=unused-import class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -30,29 +20,16 @@ class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 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}) + LOGGER.debug("Starting async_step_import - %s", import_info) + host = import_info[CONF_HOST] + await self.async_set_unique_id(host) + self._abort_if_unique_id_configured(import_info) + # New entry + bridge = DynaliteBridge(self.hass, import_info) + if not await bridge.async_setup(): + LOGGER.error("Unable to setup bridge - import info=%s", import_info) + return self.async_abort(reason="bridge_setup_failed") + if not await bridge.try_connection(): + return self.async_abort(reason="no_connection") + LOGGER.debug("Creating entry for the bridge - %s", import_info) + return self.async_create_entry(title=host, data=import_info) diff --git a/homeassistant/components/dynalite/const.py b/homeassistant/components/dynalite/const.py index f433214913a..f7795554465 100755 --- a/homeassistant/components/dynalite/const.py +++ b/homeassistant/components/dynalite/const.py @@ -3,9 +3,19 @@ import logging LOGGER = logging.getLogger(__package__) DOMAIN = "dynalite" -DATA_CONFIGS = "dynalite_configs" +CONF_ACTIVE = "active" +CONF_ALL = "ALL" +CONF_AREA = "area" +CONF_AUTO_DISCOVER = "autodiscover" CONF_BRIDGES = "bridges" +CONF_CHANNEL = "channel" +CONF_DEFAULT = "default" +CONF_FADE = "fade" +CONF_HOST = "host" +CONF_NAME = "name" +CONF_POLLTIMER = "polltimer" +CONF_PORT = "port" DEFAULT_NAME = "dynalite" DEFAULT_PORT = 12345 diff --git a/homeassistant/components/dynalite/light.py b/homeassistant/components/dynalite/light.py index d3263941f9f..652a6178705 100755 --- a/homeassistant/components/dynalite/light.py +++ b/homeassistant/components/dynalite/light.py @@ -1,15 +1,26 @@ """Support for Dynalite channels as lights.""" from homeassistant.components.light import SUPPORT_BRIGHTNESS, Light from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect 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) + LOGGER.debug("Setting up light entry = %s", config_entry.data) bridge = hass.data[DOMAIN][config_entry.entry_id] - bridge.register_add_entities(async_add_entities) + + @callback + def async_add_lights(devices): + added_lights = [] + for device in devices: + if device.category == "light": + added_lights.append(DynaliteLight(device, bridge)) + if added_lights: + async_add_entities(added_lights) + + bridge.register_add_devices(async_add_lights) class DynaliteLight(Light): @@ -20,11 +31,6 @@ class DynaliteLight(Light): 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.""" @@ -40,11 +46,6 @@ class DynaliteLight(Light): """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 @@ -52,7 +53,11 @@ class DynaliteLight(Light): @property def device_info(self): """Device info for this entity.""" - return self._device.device_info + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Dynalite", + } @property def brightness(self): @@ -77,8 +82,15 @@ class DynaliteLight(Light): """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() + async def async_added_to_hass(self): + """Added to hass so need to register to dispatch.""" + # register for device specific update + async_dispatcher_connect( + self.hass, + self._bridge.update_signal(self._device), + self.async_schedule_update_ha_state, + ) + # register for wide update + async_dispatcher_connect( + self.hass, self._bridge.update_signal(), self.async_schedule_update_ha_state + ) diff --git a/homeassistant/components/dynalite/manifest.json b/homeassistant/components/dynalite/manifest.json index 4df580c16a2..95667733d38 100755 --- a/homeassistant/components/dynalite/manifest.json +++ b/homeassistant/components/dynalite/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/dynalite", "dependencies": [], "codeowners": ["@ziv1234"], - "requirements": ["dynalite_devices==0.1.17"] + "requirements": ["dynalite_devices==0.1.22"] } diff --git a/requirements_all.txt b/requirements_all.txt index 03aa1089d35..d87baa2644b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -456,7 +456,7 @@ dsmr_parser==0.18 dweepy==0.3.0 # homeassistant.components.dynalite -dynalite_devices==0.1.17 +dynalite_devices==0.1.22 # homeassistant.components.rainforest_eagle eagle200_reader==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f82944b575..2e9ad49678e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -168,7 +168,7 @@ distro==1.4.0 dsmr_parser==0.18 # homeassistant.components.dynalite -dynalite_devices==0.1.17 +dynalite_devices==0.1.22 # homeassistant.components.ee_brightbox eebrightbox==0.0.4 diff --git a/tests/components/dynalite/test_bridge.py b/tests/components/dynalite/test_bridge.py index c0aa2b3c143..133e03d9f3d 100755 --- a/tests/components/dynalite/test_bridge.py +++ b/tests/components/dynalite/test_bridge.py @@ -1,136 +1,81 @@ """Test Dynalite bridge.""" -from unittest.mock import Mock, call, patch +from unittest.mock import Mock, call +from asynctest import 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 +from homeassistant.components import dynalite -async def test_bridge_setup(): +@pytest.fixture +def dyn_bridge(): + """Define a basic mock bridge.""" + hass = Mock() + host = "1.2.3.4" + bridge = dynalite.DynaliteBridge(hass, {dynalite.CONF_HOST: host}) + return bridge + + +async def test_update_device(dyn_bridge): """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) + async_dispatch = Mock() - with patch.object( - dyn_bridge.dynalite_devices, "async_setup", return_value=mock_coro(True) + with patch( + "homeassistant.components.dynalite.bridge.async_dispatcher_send", async_dispatch ): - 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"]) + dyn_bridge.update_device(CONF_ALL) + async_dispatch.assert_called_once() + assert async_dispatch.mock_calls[0] == call( + dyn_bridge.hass, f"dynalite-update-{dyn_bridge.host}" + ) + async_dispatch.reset_mock() + device = Mock + device.unique_id = "abcdef" + dyn_bridge.update_device(device) + async_dispatch.assert_called_once() + assert async_dispatch.mock_calls[0] == call( + dyn_bridge.hass, f"dynalite-update-{dyn_bridge.host}-{device.unique_id}" + ) -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(): +async def test_add_devices_then_register(dyn_bridge): """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) - + # First test empty + dyn_bridge.add_devices_when_registered([]) + assert not dyn_bridge.waiting_devices + # Now with devices device1 = Mock() device1.category = "light" device2 = Mock() device2.category = "switch" - dyn_bridge.add_devices([device1, device2]) + dyn_bridge.add_devices_when_registered([device1, device2]) reg_func = Mock() - dyn_bridge.register_add_entities(reg_func) + dyn_bridge.register_add_devices(reg_func) reg_func.assert_called_once() - assert reg_func.mock_calls[0][1][0][0].device is device1 + assert reg_func.mock_calls[0][1][0][0] is device1 -async def test_register_then_add_devices(): +async def test_register_then_add_devices(dyn_bridge): """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]) + dyn_bridge.register_add_devices(reg_func) + dyn_bridge.add_devices_when_registered([device1, device2]) reg_func.assert_called_once() - assert reg_func.mock_calls[0][1][0][0].device is device1 + assert reg_func.mock_calls[0][1][0][0] 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" - ) +async def test_try_connection(dyn_bridge): + """Test that try connection works.""" + # successful + with patch.object(dyn_bridge.dynalite_devices, "connected", True): + assert await dyn_bridge.try_connection() + # unsuccessful + with patch.object(dyn_bridge.dynalite_devices, "connected", False), patch( + "homeassistant.components.dynalite.bridge.CONNECT_INTERVAL", 0 + ): + assert not await dyn_bridge.try_connection() diff --git a/tests/components/dynalite/test_config_flow.py b/tests/components/dynalite/test_config_flow.py index 1cf82143f1b..1f8be61f646 100755 --- a/tests/components/dynalite/test_config_flow.py +++ b/tests/components/dynalite/test_config_flow.py @@ -1,36 +1,90 @@ """Test Dynalite config flow.""" -from unittest.mock import Mock, call, patch +from asynctest import patch -from homeassistant.components.dynalite import config_flow +from homeassistant import config_entries +from homeassistant.components import dynalite -from tests.common import mock_coro +from tests.common import MockConfigEntry -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} - ) +async def run_flow(hass, setup, connection): + """Run a flow with or without errors and return result.""" + host = "1.2.3.4" + with patch( + "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup", + return_value=setup, + ), patch( + "homeassistant.components.dynalite.bridge.DynaliteDevices.available", connection + ), patch( + "homeassistant.components.dynalite.bridge.CONNECT_INTERVAL", 0 + ): + result = await hass.config_entries.flow.async_init( + dynalite.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={dynalite.CONF_HOST: host}, + ) + return result + + +async def test_flow_works(hass): + """Test a successful config flow.""" + result = await run_flow(hass, True, True) + assert result["type"] == "create_entry" + + +async def test_flow_setup_fails(hass): + """Test a flow where async_setup fails.""" + result = await run_flow(hass, False, True) + assert result["type"] == "abort" + assert result["reason"] == "bridge_setup_failed" + + +async def test_flow_no_connection(hass): + """Test a flow where connection times out.""" + result = await run_flow(hass, True, False) + assert result["type"] == "abort" + assert result["reason"] == "no_connection" + + +async def test_existing(hass): + """Test when the entry exists with the same config.""" + host = "1.2.3.4" + MockConfigEntry( + domain=dynalite.DOMAIN, unique_id=host, data={dynalite.CONF_HOST: host} + ).add_to_hass(hass) + with patch( + "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup", + return_value=True, + ), patch( + "homeassistant.components.dynalite.bridge.DynaliteDevices.available", True + ): + result = await hass.config_entries.flow.async_init( + dynalite.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={dynalite.CONF_HOST: host}, + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_existing_update(hass): + """Test when the entry exists with the same config.""" + host = "1.2.3.4" + mock_entry = MockConfigEntry( + domain=dynalite.DOMAIN, unique_id=host, data={dynalite.CONF_HOST: host} + ) + mock_entry.add_to_hass(hass) + with patch( + "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup", + return_value=True, + ), patch( + "homeassistant.components.dynalite.bridge.DynaliteDevices.available", True + ): + result = await hass.config_entries.flow.async_init( + dynalite.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={dynalite.CONF_HOST: host, "aaa": "bbb"}, + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert mock_entry.data.get("aaa") == "bbb" diff --git a/tests/components/dynalite/test_init.py b/tests/components/dynalite/test_init.py index beb96a5e78f..d8ef0d7d259 100755 --- a/tests/components/dynalite/test_init.py +++ b/tests/components/dynalite/test_init.py @@ -1,74 +1,62 @@ """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 asynctest import patch -from tests.common import mock_coro +from homeassistant.components import dynalite +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry -async def test_async_setup(): +async def test_empty_config(hass): + """Test with an empty config.""" + assert await async_setup_component(hass, dynalite.DOMAIN, {}) is True + assert len(hass.config_entries.flow.async_progress()) == 0 + assert hass.data[dynalite.DOMAIN] == {} + + +async def test_async_setup(hass): """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() + "dynalite_devices_lib.DynaliteDevices.async_setup", return_value=True + ), patch("dynalite_devices_lib.DynaliteDevices.available", True): + assert await async_setup_component( + hass, + dynalite.DOMAIN, + {dynalite.DOMAIN: {dynalite.CONF_BRIDGES: [{dynalite.CONF_HOST: host}]}}, + ) + + assert len(hass.data[dynalite.DOMAIN]) == 1 -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() +async def test_async_setup_failed(hass): + """Test a setup when DynaliteBridge.async_setup fails.""" + host = "1.2.3.4" + with patch("dynalite_devices_lib.DynaliteDevices.async_setup", return_value=False): + assert await async_setup_component( + hass, + dynalite.DOMAIN, + {dynalite.DOMAIN: {dynalite.CONF_BRIDGES: [{dynalite.CONF_HOST: host}]}}, + ) + assert hass.data[dynalite.DOMAIN] == {} + + +async def test_unload_entry(hass): + """Test being able to unload an entry.""" + host = "1.2.3.4" + entry = MockConfigEntry(domain=dynalite.DOMAIN, data={"host": host}) + entry.add_to_hass(hass) + + with patch( + "dynalite_devices_lib.DynaliteDevices.async_setup", return_value=True + ), patch("dynalite_devices_lib.DynaliteDevices.available", True): + assert await async_setup_component( + hass, + dynalite.DOMAIN, + {dynalite.DOMAIN: {dynalite.CONF_BRIDGES: [{dynalite.CONF_HOST: host}]}}, + ) + assert hass.data[dynalite.DOMAIN].get(entry.entry_id) + + assert await hass.config_entries.async_unload(entry.entry_id) + assert not hass.data[dynalite.DOMAIN].get(entry.entry_id) diff --git a/tests/components/dynalite/test_light.py b/tests/components/dynalite/test_light.py index cfc9d42d0e4..9934bac8720 100755 --- a/tests/components/dynalite/test_light.py +++ b/tests/components/dynalite/test_light.py @@ -1,44 +1,78 @@ """Test Dynalite light.""" -from unittest.mock import Mock, call, patch +from unittest.mock import Mock -from homeassistant.components.dynalite import DOMAIN -from homeassistant.components.dynalite.light import DynaliteLight, async_setup_entry +from asynctest import CoroutineMock, patch +import pytest -from tests.common import mock_coro +from homeassistant.components import dynalite +from homeassistant.components.light import SUPPORT_BRIGHTNESS +from homeassistant.setup import async_setup_component -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.""" +@pytest.fixture +def mock_device(): + """Mock a Dynalite device.""" 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() + device.category = "light" + device.unique_id = "UNIQUE" + device.name = "NAME" + device.device_info = { + "identifiers": {(dynalite.DOMAIN, device.unique_id)}, + "name": device.name, + "manufacturer": "Dynalite", + } + return device + + +async def create_light_from_device(hass, device): + """Set up the component and platform and create a light based on the device provided.""" + host = "1.2.3.4" + with patch( + "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup", + return_value=True, + ), patch( + "homeassistant.components.dynalite.bridge.DynaliteDevices.available", True + ): + assert await async_setup_component( + hass, + dynalite.DOMAIN, + {dynalite.DOMAIN: {dynalite.CONF_BRIDGES: [{dynalite.CONF_HOST: host}]}}, + ) + await hass.async_block_till_done() + # Find the bridge + bridge = None + assert len(hass.data[dynalite.DOMAIN]) == 1 + key = next(iter(hass.data[dynalite.DOMAIN])) + bridge = hass.data[dynalite.DOMAIN][key] + bridge.dynalite_devices.newDeviceFunc([device]) + await hass.async_block_till_done() + + +async def test_light_setup(hass, mock_device): + """Test a successful setup.""" + await create_light_from_device(hass, mock_device) + entity_state = hass.states.get("light.name") + assert entity_state.attributes["brightness"] == mock_device.brightness + assert entity_state.attributes["supported_features"] == SUPPORT_BRIGHTNESS + + +async def test_turn_on(hass, mock_device): + """Test turning a light on.""" + mock_device.async_turn_on = CoroutineMock(return_value=True) + await create_light_from_device(hass, mock_device) + await hass.services.async_call( + "light", "turn_on", {"entity_id": "light.name"}, blocking=True + ) + await hass.async_block_till_done() + mock_device.async_turn_on.assert_awaited_once() + + +async def test_turn_off(hass, mock_device): + """Test turning a light off.""" + mock_device.async_turn_off = CoroutineMock(return_value=True) + await create_light_from_device(hass, mock_device) + await hass.services.async_call( + "light", "turn_off", {"entity_id": "light.name"}, blocking=True + ) + await hass.async_block_till_done() + mock_device.async_turn_off.assert_awaited_once()