Refactor dynalite integration for multi-platform (#32335)

* refactoring for multi platform

* adopted test_bridge to refactoring

* refactoring tests for multi-platform
additional coverage in config and init

* comment for clarity

* more specific imports from lib

* library version bump

* removed async_update

* changed parameter order to start with hass

* removed pylint disable

* unsubscribe from signal dispatcher
inherit from Entity

* use device.unique_id

* changed hass_obj to hass

* added test for remove entity
bug fix from the test

* removed the polling try_connect. hate polling... it is now part of the async_setup()
significantly makes the code clearer and simplifies the tests

* removed leftover debug logs in the library

* changed tests to get the entry_id from hass

* changed place to assign hass.data only after success

* fixes for test_init

* removed assert

* removed device_info

* removed bridge internal from common

* modified test_bridge to work without the bridge directly

* removed bridge from test_existing_update

* changed update to not use bridge internals

* dyn_bridge fixture no longer used - removed
This commit is contained in:
Ziv 2020-03-01 16:44:24 -05:00 committed by GitHub
parent fc98faa425
commit e13d5bdc10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 348 additions and 280 deletions

View File

@ -120,16 +120,11 @@ async def async_setup_entry(hass, entry):
"""Set up a bridge from a config entry.""" """Set up a bridge from a config entry."""
LOGGER.debug("Setting up entry %s", entry.data) LOGGER.debug("Setting up entry %s", entry.data)
bridge = DynaliteBridge(hass, entry.data) bridge = DynaliteBridge(hass, entry.data)
hass.data[DOMAIN][entry.entry_id] = bridge
entry.add_update_listener(async_entry_changed) entry.add_update_listener(async_entry_changed)
if not await bridge.async_setup(): if not await bridge.async_setup():
LOGGER.error("Could not set up bridge for entry %s", entry.data) LOGGER.error("Could not set up bridge for entry %s", entry.data)
hass.data[DOMAIN].pop(entry.entry_id)
return False
if not await bridge.try_connection():
LOGGER.error("Could not connect with entry %s", entry)
hass.data[DOMAIN].pop(entry.entry_id)
raise ConfigEntryNotReady raise ConfigEntryNotReady
hass.data[DOMAIN][entry.entry_id] = bridge
hass.async_create_task( hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, "light") hass.config_entries.async_forward_entry_setup(entry, "light")
) )

View File

@ -1,16 +1,11 @@
"""Code to handle a Dynalite bridge.""" """Code to handle a Dynalite bridge."""
import asyncio from dynalite_devices_lib.dynalite_devices import DynaliteDevices
from dynalite_devices_lib import DynaliteDevices
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import CONF_ALL, CONF_HOST, LOGGER from .const import CONF_ALL, CONF_HOST, ENTITY_PLATFORMS, LOGGER
CONNECT_TIMEOUT = 30
CONNECT_INTERVAL = 1
class DynaliteBridge: class DynaliteBridge:
@ -20,8 +15,8 @@ class DynaliteBridge:
"""Initialize the system based on host parameter.""" """Initialize the system based on host parameter."""
self.hass = hass self.hass = hass
self.area = {} self.area = {}
self.async_add_devices = None self.async_add_devices = {}
self.waiting_devices = [] self.waiting_devices = {}
self.host = config[CONF_HOST] self.host = config[CONF_HOST]
# Configure the dynalite devices # Configure the dynalite devices
self.dynalite_devices = DynaliteDevices( self.dynalite_devices = DynaliteDevices(
@ -38,7 +33,7 @@ class DynaliteBridge:
async def reload_config(self, config): async def reload_config(self, config):
"""Reconfigure a bridge when config changes.""" """Reconfigure a bridge when config changes."""
LOGGER.debug("Setting up bridge - host %s, config %s", self.host, config) LOGGER.debug("Reloading bridge - host %s, config %s", self.host, config)
self.dynalite_devices.configure(config) self.dynalite_devices.configure(config)
def update_signal(self, device=None): def update_signal(self, device=None):
@ -62,27 +57,22 @@ class DynaliteBridge:
else: else:
async_dispatcher_send(self.hass, self.update_signal(device)) 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 @callback
def register_add_devices(self, async_add_devices): def register_add_devices(self, platform, async_add_devices):
"""Add an async_add_entities for a category.""" """Add an async_add_entities for a category."""
self.async_add_devices = async_add_devices self.async_add_devices[platform] = async_add_devices
if self.waiting_devices: if platform in self.waiting_devices:
self.async_add_devices(self.waiting_devices) self.async_add_devices[platform](self.waiting_devices[platform])
def add_devices_when_registered(self, devices): def add_devices_when_registered(self, devices):
"""Add the devices to HA if the add devices callback was registered, otherwise queue until it is.""" """Add the devices to HA if the add devices callback was registered, otherwise queue until it is."""
if not devices: for platform in ENTITY_PLATFORMS:
return platform_devices = [
if self.async_add_devices: device for device in devices if device.category == platform
self.async_add_devices(devices) ]
if platform in self.async_add_devices:
self.async_add_devices[platform](platform_devices)
else: # handle it later when it is registered else: # handle it later when it is registered
self.waiting_devices.extend(devices) if platform not in self.waiting_devices:
self.waiting_devices[platform] = []
self.waiting_devices[platform].extend(platform_devices)

View File

@ -31,8 +31,6 @@ class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
bridge = DynaliteBridge(self.hass, import_info) bridge = DynaliteBridge(self.hass, import_info)
if not await bridge.async_setup(): if not await bridge.async_setup():
LOGGER.error("Unable to setup bridge - import info=%s", import_info) 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") return self.async_abort(reason="no_connection")
LOGGER.debug("Creating entry for the bridge - %s", import_info) LOGGER.debug("Creating entry for the bridge - %s", import_info)
return self.async_create_entry(title=host, data=import_info) return self.async_create_entry(title=host, data=import_info)

View File

@ -4,6 +4,8 @@ import logging
LOGGER = logging.getLogger(__package__) LOGGER = logging.getLogger(__package__)
DOMAIN = "dynalite" DOMAIN = "dynalite"
ENTITY_PLATFORMS = ["light"]
CONF_ACTIVE = "active" CONF_ACTIVE = "active"
CONF_ALL = "ALL" CONF_ALL = "ALL"
CONF_AREA = "area" CONF_AREA = "area"
@ -17,5 +19,6 @@ CONF_NAME = "name"
CONF_POLLTIMER = "polltimer" CONF_POLLTIMER = "polltimer"
CONF_PORT = "port" CONF_PORT = "port"
DEFAULT_NAME = "dynalite" DEFAULT_NAME = "dynalite"
DEFAULT_PORT = 12345 DEFAULT_PORT = 12345

View File

@ -0,0 +1,85 @@
"""Support for the Dynalite devices as entities."""
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from .const import DOMAIN, LOGGER
def async_setup_entry_base(
hass, config_entry, async_add_entities, platform, entity_from_device
):
"""Record the async_add_entities function to add them later when received from Dynalite."""
LOGGER.debug("Setting up %s entry = %s", platform, config_entry.data)
bridge = hass.data[DOMAIN][config_entry.entry_id]
@callback
def async_add_entities_platform(devices):
# assumes it is called with a single platform
added_entities = []
for device in devices:
if device.category == platform:
added_entities.append(entity_from_device(device, bridge))
if added_entities:
async_add_entities(added_entities)
bridge.register_add_devices(platform, async_add_entities_platform)
class DynaliteBase(Entity):
"""Base class for the Dynalite entities."""
def __init__(self, device, bridge):
"""Initialize the base class."""
self._device = device
self._bridge = bridge
self._unsub_dispatchers = []
@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 device_info(self):
"""Device info for this entity."""
return {
"identifiers": {(DOMAIN, self._device.unique_id)},
"name": self.name,
"manufacturer": "Dynalite",
}
async def async_added_to_hass(self):
"""Added to hass so need to register to dispatch."""
# register for device specific update
self._unsub_dispatchers.append(
async_dispatcher_connect(
self.hass,
self._bridge.update_signal(self._device),
self.async_schedule_update_ha_state,
)
)
# register for wide update
self._unsub_dispatchers.append(
async_dispatcher_connect(
self.hass,
self._bridge.update_signal(),
self.async_schedule_update_ha_state,
)
)
async def async_will_remove_from_hass(self):
"""Unregister signal dispatch listeners when being removed."""
for unsub in self._unsub_dispatchers:
unsub()
self._unsub_dispatchers = []

View File

@ -1,64 +1,25 @@
"""Support for Dynalite channels as lights.""" """Support for Dynalite channels as lights."""
from homeassistant.components.light import SUPPORT_BRIGHTNESS, Light from homeassistant.components.light import SUPPORT_BRIGHTNESS, Light
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import DOMAIN, LOGGER from .dynalitebase import DynaliteBase, async_setup_entry_base
async def async_setup_entry(hass, config_entry, async_add_entities): 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.""" """Record the async_add_entities function to add them later when received from Dynalite."""
LOGGER.debug("Setting up light entry = %s", config_entry.data)
bridge = hass.data[DOMAIN][config_entry.entry_id]
@callback @callback
def async_add_lights(devices): def light_from_device(device, bridge):
added_lights = [] return DynaliteLight(device, bridge)
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) async_setup_entry_base(
hass, config_entry, async_add_entities, "light", light_from_device
)
class DynaliteLight(Light): class DynaliteLight(DynaliteBase, Light):
"""Representation of a Dynalite Channel as a Home Assistant 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 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
async def async_update(self):
"""Update the entity."""
return
@property
def device_info(self):
"""Device info for this entity."""
return {
"identifiers": {(DOMAIN, self.unique_id)},
"name": self.name,
"manufacturer": "Dynalite",
}
@property @property
def brightness(self): def brightness(self):
"""Return the brightness of this light between 0..255.""" """Return the brightness of this light between 0..255."""
@ -81,16 +42,3 @@ class DynaliteLight(Light):
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""
return SUPPORT_BRIGHTNESS return SUPPORT_BRIGHTNESS
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
)

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/dynalite", "documentation": "https://www.home-assistant.io/integrations/dynalite",
"dependencies": [], "dependencies": [],
"codeowners": ["@ziv1234"], "codeowners": ["@ziv1234"],
"requirements": ["dynalite_devices==0.1.26"] "requirements": ["dynalite_devices==0.1.30"]
} }

View File

@ -463,7 +463,7 @@ dsmr_parser==0.18
dweepy==0.3.0 dweepy==0.3.0
# homeassistant.components.dynalite # homeassistant.components.dynalite
dynalite_devices==0.1.26 dynalite_devices==0.1.30
# homeassistant.components.rainforest_eagle # homeassistant.components.rainforest_eagle
eagle200_reader==0.2.1 eagle200_reader==0.2.1

View File

@ -171,7 +171,7 @@ distro==1.4.0
dsmr_parser==0.18 dsmr_parser==0.18
# homeassistant.components.dynalite # homeassistant.components.dynalite
dynalite_devices==0.1.26 dynalite_devices==0.1.30
# homeassistant.components.ee_brightbox # homeassistant.components.ee_brightbox
eebrightbox==0.0.4 eebrightbox==0.0.4

View File

@ -1,9 +1,64 @@
"""Common functions for the Dynalite tests.""" """Common functions for tests."""
from asynctest import CoroutineMock, Mock, call, patch
from homeassistant.components import dynalite from homeassistant.components import dynalite
from homeassistant.helpers import entity_registry
from tests.common import MockConfigEntry
ATTR_SERVICE = "service"
ATTR_METHOD = "method"
ATTR_ARGS = "args"
def get_bridge_from_hass(hass_obj): def create_mock_device(platform, spec):
"""Get the bridge from hass.data.""" """Create a dynalite mock device for a platform according to a spec."""
key = next(iter(hass_obj.data[dynalite.DOMAIN])) device = Mock(spec=spec)
return hass_obj.data[dynalite.DOMAIN][key] device.category = platform
device.unique_id = "UNIQUE"
device.name = "NAME"
device.device_class = "Device Class"
return device
async def get_entry_id_from_hass(hass):
"""Get the config entry id from hass."""
ent_reg = await entity_registry.async_get_registry(hass)
assert ent_reg
conf_entries = hass.config_entries.async_entries(dynalite.DOMAIN)
assert len(conf_entries) == 1
return conf_entries[0].entry_id
async def create_entity_from_device(hass, device):
"""Set up the component and platform and create a light based on the device provided."""
host = "1.2.3.4"
entry = MockConfigEntry(domain=dynalite.DOMAIN, data={dynalite.CONF_HOST: host})
entry.add_to_hass(hass)
with patch(
"homeassistant.components.dynalite.bridge.DynaliteDevices"
) as mock_dyn_dev:
mock_dyn_dev().async_setup = CoroutineMock(return_value=True)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
new_device_func = mock_dyn_dev.mock_calls[1][2]["newDeviceFunc"]
new_device_func([device])
await hass.async_block_till_done()
async def run_service_tests(hass, device, platform, services):
"""Run a series of service calls and check that the entity and device behave correctly."""
for cur_item in services:
service = cur_item[ATTR_SERVICE]
args = cur_item.get(ATTR_ARGS, {})
service_data = {"entity_id": f"{platform}.name", **args}
await hass.services.async_call(platform, service, service_data, blocking=True)
await hass.async_block_till_done()
for check_item in services:
check_method = getattr(device, check_item[ATTR_METHOD])
if check_item[ATTR_SERVICE] == service:
check_method.assert_called_once()
assert check_method.mock_calls == [call(**args)]
check_method.reset_mock()
else:
check_method.assert_not_called()

View File

@ -1,81 +1,85 @@
"""Test Dynalite bridge.""" """Test Dynalite bridge."""
from unittest.mock import Mock, call
from asynctest import patch from asynctest import CoroutineMock, Mock, patch
from dynalite_devices_lib import CONF_ALL from dynalite_devices_lib.const import CONF_ALL
import pytest
from homeassistant.components import dynalite from homeassistant.components import dynalite
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from tests.common import MockConfigEntry
@pytest.fixture async def test_update_device(hass):
def dyn_bridge(): """Test that update works."""
"""Define a basic mock bridge."""
hass = Mock()
host = "1.2.3.4" host = "1.2.3.4"
bridge = dynalite.DynaliteBridge(hass, {dynalite.CONF_HOST: host}) entry = MockConfigEntry(domain=dynalite.DOMAIN, data={dynalite.CONF_HOST: host})
return bridge entry.add_to_hass(hass)
async def test_update_device(dyn_bridge):
"""Test a successful setup."""
async_dispatch = Mock()
with patch( with patch(
"homeassistant.components.dynalite.bridge.async_dispatcher_send", async_dispatch "homeassistant.components.dynalite.bridge.DynaliteDevices"
): ) as mock_dyn_dev:
dyn_bridge.update_device(CONF_ALL) mock_dyn_dev().async_setup = CoroutineMock(return_value=True)
async_dispatch.assert_called_once() assert await hass.config_entries.async_setup(entry.entry_id)
assert async_dispatch.mock_calls[0] == call( # Not waiting so it add the devices before registration
dyn_bridge.hass, f"dynalite-update-{dyn_bridge.host}" update_device_func = mock_dyn_dev.mock_calls[1][2]["updateDeviceFunc"]
) device = Mock()
async_dispatch.reset_mock()
device = Mock
device.unique_id = "abcdef" device.unique_id = "abcdef"
dyn_bridge.update_device(device) wide_func = Mock()
async_dispatch.assert_called_once() async_dispatcher_connect(hass, f"dynalite-update-{host}", wide_func)
assert async_dispatch.mock_calls[0] == call( specific_func = Mock()
dyn_bridge.hass, f"dynalite-update-{dyn_bridge.host}-{device.unique_id}" async_dispatcher_connect(
hass, f"dynalite-update-{host}-{device.unique_id}", specific_func
) )
update_device_func(CONF_ALL)
await hass.async_block_till_done()
wide_func.assert_called_once()
specific_func.assert_not_called()
update_device_func(device)
await hass.async_block_till_done()
wide_func.assert_called_once()
specific_func.assert_called_once()
async def test_add_devices_then_register(dyn_bridge): async def test_add_devices_then_register(hass):
"""Test that add_devices work.""" """Test that add_devices work."""
# First test empty host = "1.2.3.4"
dyn_bridge.add_devices_when_registered([]) entry = MockConfigEntry(domain=dynalite.DOMAIN, data={dynalite.CONF_HOST: host})
assert not dyn_bridge.waiting_devices entry.add_to_hass(hass)
with patch(
"homeassistant.components.dynalite.bridge.DynaliteDevices"
) as mock_dyn_dev:
mock_dyn_dev().async_setup = CoroutineMock(return_value=True)
assert await hass.config_entries.async_setup(entry.entry_id)
# Not waiting so it add the devices before registration
new_device_func = mock_dyn_dev.mock_calls[1][2]["newDeviceFunc"]
# Now with devices # Now with devices
device1 = Mock() device1 = Mock()
device1.category = "light" device1.category = "light"
device1.name = "NAME"
device2 = Mock() device2 = Mock()
device2.category = "switch" device2.category = "switch"
dyn_bridge.add_devices_when_registered([device1, device2]) new_device_func([device1, device2])
reg_func = Mock() await hass.async_block_till_done()
dyn_bridge.register_add_devices(reg_func) assert hass.states.get("light.name")
reg_func.assert_called_once()
assert reg_func.mock_calls[0][1][0][0] is device1
async def test_register_then_add_devices(dyn_bridge): async def test_register_then_add_devices(hass):
"""Test that add_devices work after register_add_entities.""" """Test that add_devices work after register_add_entities."""
host = "1.2.3.4"
entry = MockConfigEntry(domain=dynalite.DOMAIN, data={dynalite.CONF_HOST: host})
entry.add_to_hass(hass)
with patch(
"homeassistant.components.dynalite.bridge.DynaliteDevices"
) as mock_dyn_dev:
mock_dyn_dev().async_setup = CoroutineMock(return_value=True)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
new_device_func = mock_dyn_dev.mock_calls[1][2]["newDeviceFunc"]
# Now with devices
device1 = Mock() device1 = Mock()
device1.category = "light" device1.category = "light"
device1.name = "NAME"
device2 = Mock() device2 = Mock()
device2.category = "switch" device2.category = "switch"
reg_func = Mock() new_device_func([device1, device2])
dyn_bridge.register_add_devices(reg_func) await hass.async_block_till_done()
dyn_bridge.add_devices_when_registered([device1, device2]) assert hass.states.get("light.name")
reg_func.assert_called_once()
assert reg_func.mock_calls[0][1][0][0] is device1
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()

View File

@ -1,53 +1,50 @@
"""Test Dynalite config flow.""" """Test Dynalite config flow."""
from asynctest import patch
from asynctest import CoroutineMock, patch
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import dynalite from homeassistant.components import dynalite
from .common import get_bridge_from_hass
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
async def run_flow(hass, setup, connection): async def run_flow(hass, connection):
"""Run a flow with or without errors and return result.""" """Run a flow with or without errors and return result."""
host = "1.2.3.4" host = "1.2.3.4"
with patch( with patch(
"homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup", "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup",
return_value=setup, side_effect=connection,
), patch(
"homeassistant.components.dynalite.bridge.DynaliteDevices.available", connection
), patch(
"homeassistant.components.dynalite.bridge.CONNECT_INTERVAL", 0
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
dynalite.DOMAIN, dynalite.DOMAIN,
context={"source": config_entries.SOURCE_IMPORT}, context={"source": config_entries.SOURCE_IMPORT},
data={dynalite.CONF_HOST: host}, data={dynalite.CONF_HOST: host},
) )
await hass.async_block_till_done()
return result return result
async def test_flow_works(hass): async def test_flow_works(hass):
"""Test a successful config flow.""" """Test a successful config flow."""
result = await run_flow(hass, True, True) result = await run_flow(hass, [True, True])
assert result["type"] == "create_entry" assert result["type"] == "create_entry"
assert result["result"].state == "loaded"
async def test_flow_setup_fails(hass): async def test_flow_setup_fails(hass):
"""Test a flow where async_setup fails.""" """Test a flow where async_setup fails."""
result = await run_flow(hass, False, True) result = await run_flow(hass, [False])
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["type"] == "abort"
assert result["reason"] == "no_connection" assert result["reason"] == "no_connection"
async def test_flow_setup_fails_in_setup_entry(hass):
"""Test a flow where the initial check works but inside setup_entry, the bridge setup fails."""
result = await run_flow(hass, [True, False])
assert result["type"] == "create_entry"
assert result["result"].state == "setup_retry"
async def test_existing(hass): async def test_existing(hass):
"""Test when the entry exists with the same config.""" """Test when the entry exists with the same config."""
host = "1.2.3.4" host = "1.2.3.4"
@ -57,8 +54,6 @@ async def test_existing(hass):
with patch( with patch(
"homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup", "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup",
return_value=True, return_value=True,
), patch(
"homeassistant.components.dynalite.bridge.DynaliteDevices.available", True
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
dynalite.DOMAIN, dynalite.DOMAIN,
@ -70,31 +65,30 @@ async def test_existing(hass):
async def test_existing_update(hass): async def test_existing_update(hass):
"""Test when the entry exists with the same config.""" """Test when the entry exists with a different config."""
host = "1.2.3.4" host = "1.2.3.4"
port1 = 7777 port1 = 7777
port2 = 8888 port2 = 8888
with patch( entry = MockConfigEntry(
"homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup", domain=dynalite.DOMAIN,
return_value=True,
), patch(
"homeassistant.components.dynalite.bridge.DynaliteDevices.available", True
):
assert await hass.config_entries.flow.async_init(
dynalite.DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={dynalite.CONF_HOST: host, dynalite.CONF_PORT: port1}, data={dynalite.CONF_HOST: host, dynalite.CONF_PORT: port1},
) )
entry.add_to_hass(hass)
with patch(
"homeassistant.components.dynalite.bridge.DynaliteDevices"
) as mock_dyn_dev:
mock_dyn_dev().async_setup = CoroutineMock(return_value=True)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
old_bridge = get_bridge_from_hass(hass) mock_dyn_dev().configure.assert_called_once()
assert old_bridge.dynalite_devices.port == port1 assert mock_dyn_dev().configure.mock_calls[0][1][0][dynalite.CONF_PORT] == port1
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
dynalite.DOMAIN, dynalite.DOMAIN,
context={"source": config_entries.SOURCE_IMPORT}, context={"source": config_entries.SOURCE_IMPORT},
data={dynalite.CONF_HOST: host, dynalite.CONF_PORT: port2}, data={dynalite.CONF_HOST: host, dynalite.CONF_PORT: port2},
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_dyn_dev().configure.call_count == 2
assert mock_dyn_dev().configure.mock_calls[1][1][0][dynalite.CONF_PORT] == port2
assert result["type"] == "abort" assert result["type"] == "abort"
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"
bridge = get_bridge_from_hass(hass)
assert bridge.dynalite_devices.port == port2

View File

@ -1,6 +1,7 @@
"""Test Dynalite __init__.""" """Test Dynalite __init__."""
from asynctest import patch
from asynctest import call, patch
from homeassistant.components import dynalite from homeassistant.components import dynalite
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -12,51 +13,75 @@ async def test_empty_config(hass):
"""Test with an empty config.""" """Test with an empty config."""
assert await async_setup_component(hass, dynalite.DOMAIN, {}) is True assert await async_setup_component(hass, dynalite.DOMAIN, {}) is True
assert len(hass.config_entries.flow.async_progress()) == 0 assert len(hass.config_entries.flow.async_progress()) == 0
assert hass.data[dynalite.DOMAIN] == {} assert len(hass.config_entries.async_entries(dynalite.DOMAIN)) == 0
async def test_async_setup(hass): async def test_async_setup(hass):
"""Test a successful setup.""" """Test a successful setup."""
host = "1.2.3.4" host = "1.2.3.4"
with patch( with patch(
"dynalite_devices_lib.DynaliteDevices.async_setup", return_value=True "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup",
), patch("dynalite_devices_lib.DynaliteDevices.available", True): return_value=True,
):
assert await async_setup_component( assert await async_setup_component(
hass, hass,
dynalite.DOMAIN, dynalite.DOMAIN,
{dynalite.DOMAIN: {dynalite.CONF_BRIDGES: [{dynalite.CONF_HOST: host}]}}, {
dynalite.DOMAIN: {
dynalite.CONF_BRIDGES: [
{
dynalite.CONF_HOST: host,
dynalite.CONF_AREA: {"1": {dynalite.CONF_NAME: "Name"}},
}
]
}
},
) )
await hass.async_block_till_done()
assert len(hass.data[dynalite.DOMAIN]) == 1 assert len(hass.config_entries.async_entries(dynalite.DOMAIN)) == 1
async def test_async_setup_failed(hass): async def test_async_setup_bad_config2(hass):
"""Test a setup when DynaliteBridge.async_setup fails.""" """Test a successful with bad config on numbers."""
host = "1.2.3.4" host = "1.2.3.4"
with patch("dynalite_devices_lib.DynaliteDevices.async_setup", return_value=False): with patch(
assert await async_setup_component( "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup",
return_value=True,
):
assert not await async_setup_component(
hass, hass,
dynalite.DOMAIN, dynalite.DOMAIN,
{dynalite.DOMAIN: {dynalite.CONF_BRIDGES: [{dynalite.CONF_HOST: host}]}}, {
dynalite.DOMAIN: {
dynalite.CONF_BRIDGES: [
{
dynalite.CONF_HOST: host,
dynalite.CONF_AREA: {"WRONG": {dynalite.CONF_NAME: "Name"}},
}
]
}
},
) )
assert hass.data[dynalite.DOMAIN] == {} await hass.async_block_till_done()
assert len(hass.config_entries.async_entries(dynalite.DOMAIN)) == 0
async def test_unload_entry(hass): async def test_unload_entry(hass):
"""Test being able to unload an entry.""" """Test being able to unload an entry."""
host = "1.2.3.4" host = "1.2.3.4"
entry = MockConfigEntry(domain=dynalite.DOMAIN, data={"host": host}) entry = MockConfigEntry(domain=dynalite.DOMAIN, data={dynalite.CONF_HOST: host})
entry.add_to_hass(hass) entry.add_to_hass(hass)
with patch( with patch(
"dynalite_devices_lib.DynaliteDevices.async_setup", return_value=True "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup",
), patch("dynalite_devices_lib.DynaliteDevices.available", True): return_value=True,
assert await async_setup_component( ):
hass, assert await hass.config_entries.async_setup(entry.entry_id)
dynalite.DOMAIN, await hass.async_block_till_done()
{dynalite.DOMAIN: {dynalite.CONF_BRIDGES: [{dynalite.CONF_HOST: host}]}}, assert len(hass.config_entries.async_entries(dynalite.DOMAIN)) == 1
) with patch.object(
assert hass.data[dynalite.DOMAIN].get(entry.entry_id) hass.config_entries, "async_forward_entry_unload", return_value=True
) as mock_unload:
assert await hass.config_entries.async_unload(entry.entry_id) assert await hass.config_entries.async_unload(entry.entry_id)
assert not hass.data[dynalite.DOMAIN].get(entry.entry_id) await hass.async_block_till_done()
mock_unload.assert_called_once()
assert mock_unload.mock_calls == [call(entry, "light")]

View File

@ -1,78 +1,49 @@
"""Test Dynalite light.""" """Test Dynalite light."""
from unittest.mock import Mock
from asynctest import CoroutineMock, patch from dynalite_devices_lib.light import DynaliteChannelLightDevice
import pytest import pytest
from homeassistant.components import dynalite
from homeassistant.components.light import SUPPORT_BRIGHTNESS from homeassistant.components.light import SUPPORT_BRIGHTNESS
from homeassistant.setup import async_setup_component
from .common import (
ATTR_METHOD,
ATTR_SERVICE,
create_entity_from_device,
create_mock_device,
get_entry_id_from_hass,
run_service_tests,
)
@pytest.fixture @pytest.fixture
def mock_device(): def mock_device():
"""Mock a Dynalite device.""" """Mock a Dynalite device."""
device = Mock() return create_mock_device("light", DynaliteChannelLightDevice)
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): async def test_light_setup(hass, mock_device):
"""Test a successful setup.""" """Test a successful setup."""
await create_light_from_device(hass, mock_device) await create_entity_from_device(hass, mock_device)
entity_state = hass.states.get("light.name") entity_state = hass.states.get("light.name")
assert entity_state.attributes["friendly_name"] == mock_device.name
assert entity_state.attributes["brightness"] == mock_device.brightness assert entity_state.attributes["brightness"] == mock_device.brightness
assert entity_state.attributes["supported_features"] == SUPPORT_BRIGHTNESS assert entity_state.attributes["supported_features"] == SUPPORT_BRIGHTNESS
await run_service_tests(
hass,
async def test_turn_on(hass, mock_device): mock_device,
"""Test turning a light on.""" "light",
mock_device.async_turn_on = CoroutineMock(return_value=True) [
await create_light_from_device(hass, mock_device) {ATTR_SERVICE: "turn_on", ATTR_METHOD: "async_turn_on"},
await hass.services.async_call( {ATTR_SERVICE: "turn_off", ATTR_METHOD: "async_turn_off"},
"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): async def test_remove_entity(hass, mock_device):
"""Test turning a light off.""" """Test when an entity is removed from HA."""
mock_device.async_turn_off = CoroutineMock(return_value=True) await create_entity_from_device(hass, mock_device)
await create_light_from_device(hass, mock_device) assert hass.states.get("light.name")
await hass.services.async_call( entry_id = await get_entry_id_from_hass(hass)
"light", "turn_off", {"entity_id": "light.name"}, blocking=True assert await hass.config_entries.async_unload(entry_id)
)
await hass.async_block_till_done() await hass.async_block_till_done()
mock_device.async_turn_off.assert_awaited_once() assert not hass.states.get("light.name")