deCONZ - retry if setup fails (#17772)

* Make component retry if setup fails
* Improve overall test coverage
This commit is contained in:
Robert Svensson 2018-10-31 22:38:04 +01:00 committed by GitHub
parent 145677ed75
commit a9140dc8f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 823 additions and 475 deletions

View File

@ -76,9 +76,6 @@ omit =
homeassistant/components/daikin.py homeassistant/components/daikin.py
homeassistant/components/*/daikin.py homeassistant/components/*/daikin.py
homeassistant/components/deconz/*
homeassistant/components/*/deconz.py
homeassistant/components/digital_ocean.py homeassistant/components/digital_ocean.py
homeassistant/components/*/digital_ocean.py homeassistant/components/*/digital_ocean.py

View File

@ -7,7 +7,7 @@ https://home-assistant.io/components/binary_sensor.deconz/
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.deconz.const import ( from homeassistant.components.deconz.const import (
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ,
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN) DECONZ_DOMAIN)
from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
@ -36,10 +36,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities.append(DeconzBinarySensor(sensor)) entities.append(DeconzBinarySensor(sensor))
async_add_entities(entities, True) async_add_entities(entities, True)
hass.data[DATA_DECONZ_UNSUB].append( hass.data[DATA_DECONZ].listeners.append(
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
async_add_sensor(hass.data[DATA_DECONZ].sensors.values()) async_add_sensor(hass.data[DATA_DECONZ].api.sensors.values())
class DeconzBinarySensor(BinarySensorDevice): class DeconzBinarySensor(BinarySensorDevice):
@ -52,7 +52,8 @@ class DeconzBinarySensor(BinarySensorDevice):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Subscribe sensors events.""" """Subscribe sensors events."""
self._sensor.register_async_callback(self.async_update_callback) self._sensor.register_async_callback(self.async_update_callback)
self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._sensor.deconz_id self.hass.data[DATA_DECONZ].deconz_ids[self.entity_id] = \
self._sensor.deconz_id
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Disconnect sensor object when removed.""" """Disconnect sensor object when removed."""
@ -127,7 +128,7 @@ class DeconzBinarySensor(BinarySensorDevice):
self._sensor.uniqueid.count(':') != 7): self._sensor.uniqueid.count(':') != 7):
return None return None
serial = self._sensor.uniqueid.split('-', 1)[0] serial = self._sensor.uniqueid.split('-', 1)[0]
bridgeid = self.hass.data[DATA_DECONZ].config.bridgeid bridgeid = self.hass.data[DATA_DECONZ].api.config.bridgeid
return { return {
'connections': {(CONNECTION_ZIGBEE, serial)}, 'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)}, 'identifiers': {(DECONZ_DOMAIN, serial)},

View File

@ -5,8 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.deconz/ https://home-assistant.io/components/cover.deconz/
""" """
from homeassistant.components.deconz.const import ( from homeassistant.components.deconz.const import (
COVER_TYPES, DAMPERS, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, COVER_TYPES, DAMPERS, DOMAIN as DATA_DECONZ, DECONZ_DOMAIN, WINDOW_COVERS)
DATA_DECONZ_UNSUB, DECONZ_DOMAIN, WINDOW_COVERS)
from homeassistant.components.cover import ( from homeassistant.components.cover import (
ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_STOP, ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_STOP,
SUPPORT_SET_POSITION) SUPPORT_SET_POSITION)
@ -42,10 +41,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities.append(DeconzCover(light)) entities.append(DeconzCover(light))
async_add_entities(entities, True) async_add_entities(entities, True)
hass.data[DATA_DECONZ_UNSUB].append( hass.data[DATA_DECONZ].listeners.append(
async_dispatcher_connect(hass, 'deconz_new_light', async_add_cover)) async_dispatcher_connect(hass, 'deconz_new_light', async_add_cover))
async_add_cover(hass.data[DATA_DECONZ].lights.values()) async_add_cover(hass.data[DATA_DECONZ].api.lights.values())
class DeconzCover(CoverDevice): class DeconzCover(CoverDevice):
@ -62,7 +61,8 @@ class DeconzCover(CoverDevice):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Subscribe to covers events.""" """Subscribe to covers events."""
self._cover.register_async_callback(self.async_update_callback) self._cover.register_async_callback(self.async_update_callback)
self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._cover.deconz_id self.hass.data[DATA_DECONZ].deconz_ids[self.entity_id] = \
self._cover.deconz_id
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Disconnect cover object when removed.""" """Disconnect cover object when removed."""
@ -103,7 +103,6 @@ class DeconzCover(CoverDevice):
return 'damper' return 'damper'
if self._cover.type in WINDOW_COVERS: if self._cover.type in WINDOW_COVERS:
return 'window' return 'window'
return None
@property @property
def supported_features(self): def supported_features(self):
@ -151,7 +150,7 @@ class DeconzCover(CoverDevice):
self._cover.uniqueid.count(':') != 7): self._cover.uniqueid.count(':') != 7):
return None return None
serial = self._cover.uniqueid.split('-', 1)[0] serial = self._cover.uniqueid.split('-', 1)[0]
bridgeid = self.hass.data[DATA_DECONZ].config.bridgeid bridgeid = self.hass.data[DATA_DECONZ].api.config.bridgeid
return { return {
'connections': {(CONNECTION_ZIGBEE, serial)}, 'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)}, 'identifiers': {(DECONZ_DOMAIN, serial)},

View File

@ -8,21 +8,15 @@ import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, CONF_EVENT, CONF_HOST, CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
CONF_ID, CONF_PORT, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import config_validation as cv
from homeassistant.core import EventOrigin, callback
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send)
from homeassistant.util import slugify
from homeassistant.util.json import load_json from homeassistant.util.json import load_json
# Loading the config flow file will register the flow # Loading the config flow file will register the flow
from .config_flow import configured_hosts from .config_flow import configured_hosts
from .const import ( from .const import CONFIG_FILE, DOMAIN, _LOGGER
CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT, from .gateway import DeconzGateway
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER)
REQUIREMENTS = ['pydeconz==47'] REQUIREMENTS = ['pydeconz==47']
@ -80,61 +74,26 @@ async def async_setup_entry(hass, config_entry):
Load config, group, light and sensor data for server information. Load config, group, light and sensor data for server information.
Start websocket for push notification of state changes from deCONZ. Start websocket for push notification of state changes from deCONZ.
""" """
from pydeconz import DeconzSession
if DOMAIN in hass.data: if DOMAIN in hass.data:
_LOGGER.error( _LOGGER.error(
"Config entry failed since one deCONZ instance already exists") "Config entry failed since one deCONZ instance already exists")
return False return False
@callback gateway = DeconzGateway(hass, config_entry)
def async_add_device_callback(device_type, device):
"""Handle event of new device creation in deCONZ."""
if not isinstance(device, list):
device = [device]
async_dispatcher_send(
hass, 'deconz_new_{}'.format(device_type), device)
session = aiohttp_client.async_get_clientsession(hass) hass.data[DOMAIN] = gateway
deconz = DeconzSession(hass.loop, session, **config_entry.data,
async_add_device=async_add_device_callback)
result = await deconz.async_load_parameters()
if result is False: if not await gateway.async_setup():
return False return False
hass.data[DOMAIN] = deconz
hass.data[DATA_DECONZ_ID] = {}
hass.data[DATA_DECONZ_EVENT] = []
hass.data[DATA_DECONZ_UNSUB] = []
for component in SUPPORTED_PLATFORMS:
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
config_entry, component))
@callback
def async_add_remote(sensors):
"""Set up remote from deCONZ."""
from pydeconz.sensor import SWITCH as DECONZ_REMOTE
allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True)
for sensor in sensors:
if sensor.type in DECONZ_REMOTE and \
not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
hass.data[DATA_DECONZ_EVENT].append(DeconzEvent(hass, sensor))
hass.data[DATA_DECONZ_UNSUB].append(
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_remote))
async_add_remote(deconz.sensors.values())
deconz.start()
device_registry = await \ device_registry = await \
hass.helpers.device_registry.async_get_registry() hass.helpers.device_registry.async_get_registry()
device_registry.async_get_or_create( device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id, config_entry_id=config_entry.entry_id,
connections={(CONNECTION_NETWORK_MAC, deconz.config.mac)}, connections={(CONNECTION_NETWORK_MAC, gateway.api.config.mac)},
identifiers={(DOMAIN, deconz.config.bridgeid)}, identifiers={(DOMAIN, gateway.api.config.bridgeid)},
manufacturer='Dresden Elektronik', model=deconz.config.modelid, manufacturer='Dresden Elektronik', model=gateway.api.config.modelid,
name=deconz.config.name, sw_version=deconz.config.swversion) name=gateway.api.config.name, sw_version=gateway.api.config.swversion)
async def async_configure(call): async def async_configure(call):
"""Set attribute of device in deCONZ. """Set attribute of device in deCONZ.
@ -155,121 +114,66 @@ async def async_setup_entry(hass, config_entry):
field = call.data.get(SERVICE_FIELD, '') field = call.data.get(SERVICE_FIELD, '')
entity_id = call.data.get(SERVICE_ENTITY) entity_id = call.data.get(SERVICE_ENTITY)
data = call.data.get(SERVICE_DATA) data = call.data.get(SERVICE_DATA)
deconz = hass.data[DOMAIN] gateway = hass.data[DOMAIN]
if entity_id: if entity_id:
try: try:
field = hass.data[DATA_DECONZ_ID][entity_id] + field field = gateway.deconz_ids[entity_id] + field
except KeyError: except KeyError:
_LOGGER.error('Could not find the entity %s', entity_id) _LOGGER.error('Could not find the entity %s', entity_id)
return return
await deconz.async_put_state(field, data) await gateway.api.async_put_state(field, data)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_DECONZ, async_configure, schema=SERVICE_SCHEMA) DOMAIN, SERVICE_DECONZ, async_configure, schema=SERVICE_SCHEMA)
async def async_refresh_devices(call): async def async_refresh_devices(call):
"""Refresh available devices from deCONZ.""" """Refresh available devices from deCONZ."""
deconz = hass.data[DOMAIN] gateway = hass.data[DOMAIN]
groups = list(deconz.groups.keys()) groups = set(gateway.api.groups.keys())
lights = list(deconz.lights.keys()) lights = set(gateway.api.lights.keys())
scenes = list(deconz.scenes.keys()) scenes = set(gateway.api.scenes.keys())
sensors = list(deconz.sensors.keys()) sensors = set(gateway.api.sensors.keys())
if not await deconz.async_load_parameters(): if not await gateway.api.async_load_parameters():
return return
async_add_device_callback( gateway.async_add_device_callback(
'group', [group 'group', [group
for group_id, group in deconz.groups.items() for group_id, group in gateway.api.groups.items()
if group_id not in groups] if group_id not in groups]
) )
async_add_device_callback( gateway.async_add_device_callback(
'light', [light 'light', [light
for light_id, light in deconz.lights.items() for light_id, light in gateway.api.lights.items()
if light_id not in lights] if light_id not in lights]
) )
async_add_device_callback( gateway.async_add_device_callback(
'scene', [scene 'scene', [scene
for scene_id, scene in deconz.scenes.items() for scene_id, scene in gateway.api.scenes.items()
if scene_id not in scenes] if scene_id not in scenes]
) )
async_add_device_callback( gateway.async_add_device_callback(
'sensor', [sensor 'sensor', [sensor
for sensor_id, sensor in deconz.sensors.items() for sensor_id, sensor in gateway.api.sensors.items()
if sensor_id not in sensors] if sensor_id not in sensors]
) )
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_DEVICE_REFRESH, async_refresh_devices) DOMAIN, SERVICE_DEVICE_REFRESH, async_refresh_devices)
@callback hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gateway.shutdown)
def deconz_shutdown(event):
"""
Wrap the call to deconz.close.
Used as an argument to EventBus.async_listen_once - EventBus calls
this method with the event as the first argument, which should not
be passed on to deconz.close.
"""
deconz.close()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz_shutdown)
return True return True
async def async_unload_entry(hass, config_entry): async def async_unload_entry(hass, config_entry):
"""Unload deCONZ config entry.""" """Unload deCONZ config entry."""
deconz = hass.data.pop(DOMAIN) gateway = hass.data.pop(DOMAIN)
hass.services.async_remove(DOMAIN, SERVICE_DECONZ) hass.services.async_remove(DOMAIN, SERVICE_DECONZ)
deconz.close() hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH)
return await gateway.async_reset()
for component in SUPPORTED_PLATFORMS:
await hass.config_entries.async_forward_entry_unload(
config_entry, component)
dispatchers = hass.data[DATA_DECONZ_UNSUB]
for unsub_dispatcher in dispatchers:
unsub_dispatcher()
hass.data[DATA_DECONZ_UNSUB] = []
for event in hass.data[DATA_DECONZ_EVENT]:
event.async_will_remove_from_hass()
hass.data[DATA_DECONZ_EVENT].remove(event)
hass.data[DATA_DECONZ_ID] = []
return True
class DeconzEvent:
"""When you want signals instead of entities.
Stateless sensors such as remotes are expected to generate an event
instead of a sensor entity in hass.
"""
def __init__(self, hass, device):
"""Register callback that will be used for signals."""
self._hass = hass
self._device = device
self._device.register_async_callback(self.async_update_callback)
self._event = 'deconz_{}'.format(CONF_EVENT)
self._id = slugify(self._device.name)
@callback
def async_will_remove_from_hass(self) -> None:
"""Disconnect event object when removed."""
self._device.remove_callback(self.async_update_callback)
self._device = None
@callback
def async_update_callback(self, reason):
"""Fire the event if reason is that state is updated."""
if reason['state']:
data = {CONF_ID: self._id, CONF_EVENT: self._device.state}
self._hass.bus.async_fire(self._event, data, EventOrigin.remote)

View File

@ -35,10 +35,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
self.deconz_config = {} self.deconz_config = {}
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
return await self.async_step_init(user_input)
async def async_step_init(self, user_input=None):
"""Handle a deCONZ config flow start. """Handle a deCONZ config flow start.
Only allows one instance to be set up. Only allows one instance to be set up.
@ -67,7 +63,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
for bridge in self.bridges: for bridge in self.bridges:
hosts.append(bridge[CONF_HOST]) hosts.append(bridge[CONF_HOST])
return self.async_show_form( return self.async_show_form(
step_id='init', step_id='user',
data_schema=vol.Schema({ data_schema=vol.Schema({
vol.Required(CONF_HOST): vol.In(hosts) vol.Required(CONF_HOST): vol.In(hosts)
}) })

View File

@ -13,6 +13,9 @@ DECONZ_DOMAIN = 'deconz'
CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor' CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor'
CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups' CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups'
SUPPORTED_PLATFORMS = ['binary_sensor', 'cover',
'light', 'scene', 'sensor', 'switch']
ATTR_DARK = 'dark' ATTR_DARK = 'dark'
ATTR_ON = 'on' ATTR_ON = 'on'

View File

@ -0,0 +1,165 @@
"""Representation of a deCONZ gateway."""
from homeassistant import config_entries
from homeassistant.const import CONF_EVENT, CONF_ID
from homeassistant.core import EventOrigin, callback
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send)
from homeassistant.util import slugify
from .const import (
_LOGGER, CONF_ALLOW_CLIP_SENSOR, SUPPORTED_PLATFORMS)
class DeconzGateway:
"""Manages a single deCONZ gateway."""
def __init__(self, hass, config_entry):
"""Initialize the system."""
self.hass = hass
self.config_entry = config_entry
self.api = None
self._cancel_retry_setup = None
self.deconz_ids = {}
self.events = []
self.listeners = []
async def async_setup(self, tries=0):
"""Set up a deCONZ gateway."""
hass = self.hass
self.api = await get_gateway(
hass, self.config_entry.data, self.async_add_device_callback
)
if self.api is False:
retry_delay = 2 ** (tries + 1)
_LOGGER.error(
"Error connecting to deCONZ gateway. Retrying in %d seconds",
retry_delay)
async def retry_setup(_now):
"""Retry setup."""
if await self.async_setup(tries + 1):
# This feels hacky, we should find a better way to do this
self.config_entry.state = config_entries.ENTRY_STATE_LOADED
self._cancel_retry_setup = hass.helpers.event.async_call_later(
retry_delay, retry_setup)
return False
for component in SUPPORTED_PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(
self.config_entry, component))
self.listeners.append(
async_dispatcher_connect(
hass, 'deconz_new_sensor', self.async_add_remote))
self.async_add_remote(self.api.sensors.values())
self.api.start()
return True
@callback
def async_add_device_callback(self, device_type, device):
"""Handle event of new device creation in deCONZ."""
if not isinstance(device, list):
device = [device]
async_dispatcher_send(
self.hass, 'deconz_new_{}'.format(device_type), device)
@callback
def async_add_remote(self, sensors):
"""Set up remote from deCONZ."""
from pydeconz.sensor import SWITCH as DECONZ_REMOTE
allow_clip_sensor = self.config_entry.data.get(
CONF_ALLOW_CLIP_SENSOR, True)
for sensor in sensors:
if sensor.type in DECONZ_REMOTE and \
not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
self.events.append(DeconzEvent(self.hass, sensor))
@callback
def shutdown(self, event):
"""Wrap the call to deconz.close.
Used as an argument to EventBus.async_listen_once.
"""
self.api.close()
async def async_reset(self):
"""Reset this gateway to default state.
Will cancel any scheduled setup retry and will unload
the config entry.
"""
# If we have a retry scheduled, we were never setup.
if self._cancel_retry_setup is not None:
self._cancel_retry_setup()
self._cancel_retry_setup = None
return True
self.api.close()
for component in SUPPORTED_PLATFORMS:
await self.hass.config_entries.async_forward_entry_unload(
self.config_entry, component)
for unsub_dispatcher in self.listeners:
unsub_dispatcher()
self.listeners = []
for event in self.events:
event.async_will_remove_from_hass()
self.events.remove(event)
self.deconz_ids = {}
return True
async def get_gateway(hass, config, async_add_device_callback):
"""Create a gateway object and verify configuration."""
from pydeconz import DeconzSession
session = aiohttp_client.async_get_clientsession(hass)
deconz = DeconzSession(hass.loop, session, **config,
async_add_device=async_add_device_callback)
result = await deconz.async_load_parameters()
if result:
return deconz
return result
class DeconzEvent:
"""When you want signals instead of entities.
Stateless sensors such as remotes are expected to generate an event
instead of a sensor entity in hass.
"""
def __init__(self, hass, device):
"""Register callback that will be used for signals."""
self._hass = hass
self._device = device
self._device.register_async_callback(self.async_update_callback)
self._event = 'deconz_{}'.format(CONF_EVENT)
self._id = slugify(self._device.name)
@callback
def async_will_remove_from_hass(self) -> None:
"""Disconnect event object when removed."""
self._device.remove_callback(self.async_update_callback)
self._device = None
@callback
def async_update_callback(self, reason):
"""Fire the event if reason is that state is updated."""
if reason['state']:
data = {CONF_ID: self._id, CONF_EVENT: self._device.state}
self._hass.bus.async_fire(self._event, data, EventOrigin.remote)

View File

@ -5,8 +5,7 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/light.deconz/ https://home-assistant.io/components/light.deconz/
""" """
from homeassistant.components.deconz.const import ( from homeassistant.components.deconz.const import (
CONF_ALLOW_DECONZ_GROUPS, DOMAIN as DATA_DECONZ, CONF_ALLOW_DECONZ_GROUPS, DOMAIN as DATA_DECONZ, DECONZ_DOMAIN,
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN,
COVER_TYPES, SWITCH_TYPES) COVER_TYPES, SWITCH_TYPES)
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR,
@ -38,7 +37,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities.append(DeconzLight(light)) entities.append(DeconzLight(light))
async_add_entities(entities, True) async_add_entities(entities, True)
hass.data[DATA_DECONZ_UNSUB].append( hass.data[DATA_DECONZ].listeners.append(
async_dispatcher_connect(hass, 'deconz_new_light', async_add_light)) async_dispatcher_connect(hass, 'deconz_new_light', async_add_light))
@callback @callback
@ -51,11 +50,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities.append(DeconzLight(group)) entities.append(DeconzLight(group))
async_add_entities(entities, True) async_add_entities(entities, True)
hass.data[DATA_DECONZ_UNSUB].append( hass.data[DATA_DECONZ].listeners.append(
async_dispatcher_connect(hass, 'deconz_new_group', async_add_group)) async_dispatcher_connect(hass, 'deconz_new_group', async_add_group))
async_add_light(hass.data[DATA_DECONZ].lights.values()) async_add_light(hass.data[DATA_DECONZ].api.lights.values())
async_add_group(hass.data[DATA_DECONZ].groups.values()) async_add_group(hass.data[DATA_DECONZ].api.groups.values())
class DeconzLight(Light): class DeconzLight(Light):
@ -81,7 +80,8 @@ class DeconzLight(Light):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Subscribe to lights events.""" """Subscribe to lights events."""
self._light.register_async_callback(self.async_update_callback) self._light.register_async_callback(self.async_update_callback)
self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._light.deconz_id self.hass.data[DATA_DECONZ].deconz_ids[self.entity_id] = \
self._light.deconz_id
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Disconnect light object when removed.""" """Disconnect light object when removed."""
@ -214,7 +214,7 @@ class DeconzLight(Light):
self._light.uniqueid.count(':') != 7): self._light.uniqueid.count(':') != 7):
return None return None
serial = self._light.uniqueid.split('-', 1)[0] serial = self._light.uniqueid.split('-', 1)[0]
bridgeid = self.hass.data[DATA_DECONZ].config.bridgeid bridgeid = self.hass.data[DATA_DECONZ].api.config.bridgeid
return { return {
'connections': {(CONNECTION_ZIGBEE, serial)}, 'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)}, 'identifiers': {(DECONZ_DOMAIN, serial)},

View File

@ -4,8 +4,7 @@ Support for deCONZ scenes.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/scene.deconz/ https://home-assistant.io/components/scene.deconz/
""" """
from homeassistant.components.deconz import ( from homeassistant.components.deconz import DOMAIN as DATA_DECONZ
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB)
from homeassistant.components.scene import Scene from homeassistant.components.scene import Scene
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -28,10 +27,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
for scene in scenes: for scene in scenes:
entities.append(DeconzScene(scene)) entities.append(DeconzScene(scene))
async_add_entities(entities) async_add_entities(entities)
hass.data[DATA_DECONZ_UNSUB].append( hass.data[DATA_DECONZ].listeners.append(
async_dispatcher_connect(hass, 'deconz_new_scene', async_add_scene)) async_dispatcher_connect(hass, 'deconz_new_scene', async_add_scene))
async_add_scene(hass.data[DATA_DECONZ].scenes.values()) async_add_scene(hass.data[DATA_DECONZ].api.scenes.values())
class DeconzScene(Scene): class DeconzScene(Scene):
@ -43,7 +42,8 @@ class DeconzScene(Scene):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Subscribe to sensors events.""" """Subscribe to sensors events."""
self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._scene.deconz_id self.hass.data[DATA_DECONZ].deconz_ids[self.entity_id] = \
self._scene.deconz_id
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Disconnect scene object when removed.""" """Disconnect scene object when removed."""

View File

@ -6,7 +6,7 @@ https://home-assistant.io/components/sensor.deconz/
""" """
from homeassistant.components.deconz.const import ( from homeassistant.components.deconz.const import (
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ,
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN) DECONZ_DOMAIN)
from homeassistant.const import ( from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY) ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY)
from homeassistant.core import callback from homeassistant.core import callback
@ -46,10 +46,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities.append(DeconzSensor(sensor)) entities.append(DeconzSensor(sensor))
async_add_entities(entities, True) async_add_entities(entities, True)
hass.data[DATA_DECONZ_UNSUB].append( hass.data[DATA_DECONZ].listeners.append(
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
async_add_sensor(hass.data[DATA_DECONZ].sensors.values()) async_add_sensor(hass.data[DATA_DECONZ].api.sensors.values())
class DeconzSensor(Entity): class DeconzSensor(Entity):
@ -62,7 +62,8 @@ class DeconzSensor(Entity):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Subscribe to sensors events.""" """Subscribe to sensors events."""
self._sensor.register_async_callback(self.async_update_callback) self._sensor.register_async_callback(self.async_update_callback)
self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._sensor.deconz_id self.hass.data[DATA_DECONZ].deconz_ids[self.entity_id] = \
self._sensor.deconz_id
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Disconnect sensor object when removed.""" """Disconnect sensor object when removed."""
@ -147,7 +148,7 @@ class DeconzSensor(Entity):
self._sensor.uniqueid.count(':') != 7): self._sensor.uniqueid.count(':') != 7):
return None return None
serial = self._sensor.uniqueid.split('-', 1)[0] serial = self._sensor.uniqueid.split('-', 1)[0]
bridgeid = self.hass.data[DATA_DECONZ].config.bridgeid bridgeid = self.hass.data[DATA_DECONZ].api.config.bridgeid
return { return {
'connections': {(CONNECTION_ZIGBEE, serial)}, 'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)}, 'identifiers': {(DECONZ_DOMAIN, serial)},
@ -171,7 +172,8 @@ class DeconzBattery(Entity):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Subscribe to sensors events.""" """Subscribe to sensors events."""
self._sensor.register_async_callback(self.async_update_callback) self._sensor.register_async_callback(self.async_update_callback)
self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._sensor.deconz_id self.hass.data[DATA_DECONZ].deconz_ids[self.entity_id] = \
self._sensor.deconz_id
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Disconnect sensor object when removed.""" """Disconnect sensor object when removed."""
@ -181,7 +183,7 @@ class DeconzBattery(Entity):
@callback @callback
def async_update_callback(self, reason): def async_update_callback(self, reason):
"""Update the battery's state, if needed.""" """Update the battery's state, if needed."""
if 'battery' in reason['attr']: if 'reachable' in reason['attr'] or 'battery' in reason['attr']:
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
@property @property
@ -229,7 +231,7 @@ class DeconzBattery(Entity):
self._sensor.uniqueid.count(':') != 7): self._sensor.uniqueid.count(':') != 7):
return None return None
serial = self._sensor.uniqueid.split('-', 1)[0] serial = self._sensor.uniqueid.split('-', 1)[0]
bridgeid = self.hass.data[DATA_DECONZ].config.bridgeid bridgeid = self.hass.data[DATA_DECONZ].api.config.bridgeid
return { return {
'connections': {(CONNECTION_ZIGBEE, serial)}, 'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)}, 'identifiers': {(DECONZ_DOMAIN, serial)},

View File

@ -5,8 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.deconz/ https://home-assistant.io/components/switch.deconz/
""" """
from homeassistant.components.deconz.const import ( from homeassistant.components.deconz.const import (
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN as DATA_DECONZ, DECONZ_DOMAIN, POWER_PLUGS, SIRENS)
DECONZ_DOMAIN, POWER_PLUGS, SIRENS)
from homeassistant.components.switch import SwitchDevice from homeassistant.components.switch import SwitchDevice
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
@ -37,10 +36,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities.append(DeconzSiren(light)) entities.append(DeconzSiren(light))
async_add_entities(entities, True) async_add_entities(entities, True)
hass.data[DATA_DECONZ_UNSUB].append( hass.data[DATA_DECONZ].listeners.append(
async_dispatcher_connect(hass, 'deconz_new_light', async_add_switch)) async_dispatcher_connect(hass, 'deconz_new_light', async_add_switch))
async_add_switch(hass.data[DATA_DECONZ].lights.values()) async_add_switch(hass.data[DATA_DECONZ].api.lights.values())
class DeconzSwitch(SwitchDevice): class DeconzSwitch(SwitchDevice):
@ -53,7 +52,8 @@ class DeconzSwitch(SwitchDevice):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Subscribe to switches events.""" """Subscribe to switches events."""
self._switch.register_async_callback(self.async_update_callback) self._switch.register_async_callback(self.async_update_callback)
self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._switch.deconz_id self.hass.data[DATA_DECONZ].deconz_ids[self.entity_id] = \
self._switch.deconz_id
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Disconnect switch object when removed.""" """Disconnect switch object when removed."""
@ -92,7 +92,7 @@ class DeconzSwitch(SwitchDevice):
self._switch.uniqueid.count(':') != 7): self._switch.uniqueid.count(':') != 7):
return None return None
serial = self._switch.uniqueid.split('-', 1)[0] serial = self._switch.uniqueid.split('-', 1)[0]
bridgeid = self.hass.data[DATA_DECONZ].config.bridgeid bridgeid = self.hass.data[DATA_DECONZ].api.config.bridgeid
return { return {
'connections': {(CONNECTION_ZIGBEE, serial)}, 'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)}, 'identifiers': {(DECONZ_DOMAIN, serial)},

View File

@ -4,6 +4,9 @@ from unittest.mock import Mock, patch
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import deconz from homeassistant.components import deconz
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.setup import async_setup_component
import homeassistant.components.binary_sensor as binary_sensor
from tests.common import mock_coro from tests.common import mock_coro
@ -14,7 +17,8 @@ SENSOR = {
"name": "Sensor 1 name", "name": "Sensor 1 name",
"type": "ZHAPresence", "type": "ZHAPresence",
"state": {"presence": False}, "state": {"presence": False},
"config": {} "config": {},
"uniqueid": "00:00:00:00:00:00:00:00-00"
}, },
"2": { "2": {
"id": "Sensor 2 id", "id": "Sensor 2 id",
@ -26,70 +30,105 @@ SENSOR = {
} }
async def setup_bridge(hass, data, allow_clip_sensor=True): ENTRY_CONFIG = {
deconz.const.CONF_ALLOW_CLIP_SENSOR: True,
deconz.const.CONF_ALLOW_DECONZ_GROUPS: True,
deconz.config_flow.CONF_API_KEY: "ABCDEF",
deconz.config_flow.CONF_BRIDGEID: "0123456789",
deconz.config_flow.CONF_HOST: "1.2.3.4",
deconz.config_flow.CONF_PORT: 80
}
async def setup_gateway(hass, data, allow_clip_sensor=True):
"""Load the deCONZ binary sensor platform.""" """Load the deCONZ binary sensor platform."""
from pydeconz import DeconzSession from pydeconz import DeconzSession
loop = Mock() loop = Mock()
session = Mock() session = Mock()
entry = Mock()
entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} ENTRY_CONFIG[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor
bridge = DeconzSession(loop, session, **entry.data)
bridge.config = Mock() config_entry = config_entries.ConfigEntry(
1, deconz.DOMAIN, 'Mock Title', ENTRY_CONFIG, 'test',
config_entries.CONN_CLASS_LOCAL_PUSH)
gateway = deconz.DeconzGateway(hass, config_entry)
gateway.api = DeconzSession(loop, session, **config_entry.data)
gateway.api.config = Mock()
hass.data[deconz.DOMAIN] = gateway
with patch('pydeconz.DeconzSession.async_get_state', with patch('pydeconz.DeconzSession.async_get_state',
return_value=mock_coro(data)): return_value=mock_coro(data)):
await bridge.async_load_parameters() await gateway.api.async_load_parameters()
hass.data[deconz.DOMAIN] = bridge
hass.data[deconz.DATA_DECONZ_UNSUB] = []
hass.data[deconz.DATA_DECONZ_ID] = {}
config_entry = config_entries.ConfigEntry(
1, deconz.DOMAIN, 'Mock Title',
{'host': 'mock-host', 'allow_clip_sensor': allow_clip_sensor}, 'test',
config_entries.CONN_CLASS_LOCAL_PUSH)
await hass.config_entries.async_forward_entry_setup( await hass.config_entries.async_forward_entry_setup(
config_entry, 'binary_sensor') config_entry, 'binary_sensor')
# To flush out the service call to update the group # To flush out the service call to update the group
await hass.async_block_till_done() await hass.async_block_till_done()
async def test_platform_manually_configured(hass):
"""Test that we do not discover anything or try to set up a gateway."""
assert await async_setup_component(hass, binary_sensor.DOMAIN, {
'binary_sensor': {
'platform': deconz.DOMAIN
}
}) is True
assert deconz.DOMAIN not in hass.data
async def test_no_binary_sensors(hass): async def test_no_binary_sensors(hass):
"""Test that no sensors in deconz results in no sensor entities.""" """Test that no sensors in deconz results in no sensor entities."""
data = {} data = {}
await setup_bridge(hass, data) await setup_gateway(hass, data)
assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0
assert len(hass.states.async_all()) == 0 assert len(hass.states.async_all()) == 0
async def test_binary_sensors(hass): async def test_binary_sensors(hass):
"""Test successful creation of binary sensor entities.""" """Test successful creation of binary sensor entities."""
data = {"sensors": SENSOR} data = {"sensors": SENSOR}
await setup_bridge(hass, data) await setup_gateway(hass, data)
assert "binary_sensor.sensor_1_name" in hass.data[deconz.DATA_DECONZ_ID] assert "binary_sensor.sensor_1_name" in \
hass.data[deconz.DOMAIN].deconz_ids
assert "binary_sensor.sensor_2_name" not in \ assert "binary_sensor.sensor_2_name" not in \
hass.data[deconz.DATA_DECONZ_ID] hass.data[deconz.DOMAIN].deconz_ids
assert len(hass.states.async_all()) == 1 assert len(hass.states.async_all()) == 1
hass.data[deconz.DOMAIN].api.sensors['1'].async_update(
{'state': {'on': False}})
async def test_add_new_sensor(hass): async def test_add_new_sensor(hass):
"""Test successful creation of sensor entities.""" """Test successful creation of sensor entities."""
data = {} data = {}
await setup_bridge(hass, data) await setup_gateway(hass, data)
sensor = Mock() sensor = Mock()
sensor.name = 'name' sensor.name = 'name'
sensor.type = 'ZHAPresence' sensor.type = 'ZHAPresence'
sensor.register_async_callback = Mock() sensor.register_async_callback = Mock()
async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) async_dispatcher_send(hass, 'deconz_new_sensor', [sensor])
await hass.async_block_till_done() await hass.async_block_till_done()
assert "binary_sensor.name" in hass.data[deconz.DATA_DECONZ_ID] assert "binary_sensor.name" in hass.data[deconz.DOMAIN].deconz_ids
async def test_do_not_allow_clip_sensor(hass): async def test_do_not_allow_clip_sensor(hass):
"""Test that clip sensors can be ignored.""" """Test that clip sensors can be ignored."""
data = {} data = {}
await setup_bridge(hass, data, allow_clip_sensor=False) await setup_gateway(hass, data, allow_clip_sensor=False)
sensor = Mock() sensor = Mock()
sensor.name = 'name' sensor.name = 'name'
sensor.type = 'CLIPPresence' sensor.type = 'CLIPPresence'
sensor.register_async_callback = Mock() sensor.register_async_callback = Mock()
async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) async_dispatcher_send(hass, 'deconz_new_sensor', [sensor])
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0
async def test_unload_switch(hass):
"""Test that it works to unload switch entities."""
data = {"sensors": SENSOR}
await setup_gateway(hass, data)
await hass.data[deconz.DOMAIN].async_reset()
assert len(hass.states.async_all()) == 0

View File

@ -5,6 +5,9 @@ from homeassistant import config_entries
from homeassistant.components import deconz from homeassistant.components import deconz
from homeassistant.components.deconz.const import COVER_TYPES from homeassistant.components.deconz.const import COVER_TYPES
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.setup import async_setup_component
import homeassistant.components.cover as cover
from tests.common import mock_coro from tests.common import mock_coro
@ -13,14 +16,15 @@ SUPPORTED_COVERS = {
"id": "Cover 1 id", "id": "Cover 1 id",
"name": "Cover 1 name", "name": "Cover 1 name",
"type": "Level controllable output", "type": "Level controllable output",
"state": {}, "state": {"bri": 255, "reachable": True},
"modelid": "Not zigbee spec" "modelid": "Not zigbee spec",
"uniqueid": "00:00:00:00:00:00:00:00-00"
}, },
"2": { "2": {
"id": "Cover 2 id", "id": "Cover 2 id",
"name": "Cover 2 name", "name": "Cover 2 name",
"type": "Window covering device", "type": "Window covering device",
"state": {}, "state": {"bri": 255, "reachable": True},
"modelid": "lumi.curtain" "modelid": "lumi.curtain"
} }
} }
@ -35,58 +39,109 @@ UNSUPPORTED_COVER = {
} }
async def setup_bridge(hass, data): ENTRY_CONFIG = {
deconz.const.CONF_ALLOW_CLIP_SENSOR: True,
deconz.const.CONF_ALLOW_DECONZ_GROUPS: True,
deconz.config_flow.CONF_API_KEY: "ABCDEF",
deconz.config_flow.CONF_BRIDGEID: "0123456789",
deconz.config_flow.CONF_HOST: "1.2.3.4",
deconz.config_flow.CONF_PORT: 80
}
async def setup_gateway(hass, data):
"""Load the deCONZ cover platform.""" """Load the deCONZ cover platform."""
from pydeconz import DeconzSession from pydeconz import DeconzSession
loop = Mock() loop = Mock()
session = Mock() session = Mock()
entry = Mock()
entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} config_entry = config_entries.ConfigEntry(
bridge = DeconzSession(loop, session, **entry.data) 1, deconz.DOMAIN, 'Mock Title', ENTRY_CONFIG, 'test',
config_entries.CONN_CLASS_LOCAL_PUSH)
gateway = deconz.DeconzGateway(hass, config_entry)
gateway.api = DeconzSession(loop, session, **config_entry.data)
gateway.api.config = Mock()
hass.data[deconz.DOMAIN] = gateway
with patch('pydeconz.DeconzSession.async_get_state', with patch('pydeconz.DeconzSession.async_get_state',
return_value=mock_coro(data)): return_value=mock_coro(data)):
await bridge.async_load_parameters() await gateway.api.async_load_parameters()
hass.data[deconz.DOMAIN] = bridge
hass.data[deconz.DATA_DECONZ_UNSUB] = []
hass.data[deconz.DATA_DECONZ_ID] = {}
config_entry = config_entries.ConfigEntry(
1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test',
config_entries.CONN_CLASS_LOCAL_PUSH)
await hass.config_entries.async_forward_entry_setup(config_entry, 'cover') await hass.config_entries.async_forward_entry_setup(config_entry, 'cover')
# To flush out the service call to update the group # To flush out the service call to update the group
await hass.async_block_till_done() await hass.async_block_till_done()
async def test_no_switches(hass): async def test_platform_manually_configured(hass):
"""Test that we do not discover anything or try to set up a gateway."""
assert await async_setup_component(hass, cover.DOMAIN, {
'cover': {
'platform': deconz.DOMAIN
}
}) is True
assert deconz.DOMAIN not in hass.data
async def test_no_covers(hass):
"""Test that no cover entities are created.""" """Test that no cover entities are created."""
data = {} await setup_gateway(hass, {})
await setup_bridge(hass, data) assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0
assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0
assert len(hass.states.async_all()) == 0 assert len(hass.states.async_all()) == 0
async def test_cover(hass): async def test_cover(hass):
"""Test that all supported cover entities are created.""" """Test that all supported cover entities are created."""
await setup_bridge(hass, {"lights": SUPPORTED_COVERS}) with patch('pydeconz.DeconzSession.async_put_state',
assert "cover.cover_1_name" in hass.data[deconz.DATA_DECONZ_ID] return_value=mock_coro(True)):
await setup_gateway(hass, {"lights": SUPPORTED_COVERS})
assert "cover.cover_1_name" in hass.data[deconz.DOMAIN].deconz_ids
assert len(SUPPORTED_COVERS) == len(COVER_TYPES) assert len(SUPPORTED_COVERS) == len(COVER_TYPES)
assert len(hass.states.async_all()) == 3 assert len(hass.states.async_all()) == 3
cover_1 = hass.states.get('cover.cover_1_name')
assert cover_1 is not None
assert cover_1.state == 'closed'
hass.data[deconz.DOMAIN].api.lights['1'].async_update({})
await hass.services.async_call('cover', 'open_cover', {
'entity_id': 'cover.cover_1_name'
}, blocking=True)
await hass.services.async_call('cover', 'close_cover', {
'entity_id': 'cover.cover_1_name'
}, blocking=True)
await hass.services.async_call('cover', 'stop_cover', {
'entity_id': 'cover.cover_1_name'
}, blocking=True)
await hass.services.async_call('cover', 'close_cover', {
'entity_id': 'cover.cover_2_name'
}, blocking=True)
async def test_add_new_cover(hass): async def test_add_new_cover(hass):
"""Test successful creation of cover entity.""" """Test successful creation of cover entity."""
data = {} data = {}
await setup_bridge(hass, data) await setup_gateway(hass, data)
cover = Mock() cover = Mock()
cover.name = 'name' cover.name = 'name'
cover.type = "Level controllable output" cover.type = "Level controllable output"
cover.register_async_callback = Mock() cover.register_async_callback = Mock()
async_dispatcher_send(hass, 'deconz_new_light', [cover]) async_dispatcher_send(hass, 'deconz_new_light', [cover])
await hass.async_block_till_done() await hass.async_block_till_done()
assert "cover.name" in hass.data[deconz.DATA_DECONZ_ID] assert "cover.name" in hass.data[deconz.DOMAIN].deconz_ids
async def test_unsupported_cover(hass): async def test_unsupported_cover(hass):
"""Test that unsupported covers are not created.""" """Test that unsupported covers are not created."""
await setup_bridge(hass, {"lights": UNSUPPORTED_COVER}) await setup_gateway(hass, {"lights": UNSUPPORTED_COVER})
assert len(hass.states.async_all()) == 0 assert len(hass.states.async_all()) == 0
async def test_unload_cover(hass):
"""Test that it works to unload switch entities."""
await setup_gateway(hass, {"lights": SUPPORTED_COVERS})
await hass.data[deconz.DOMAIN].async_reset()
assert len(hass.states.async_all()) == 1

View File

@ -20,7 +20,7 @@ async def test_flow_works(hass, aioclient_mock):
flow = config_flow.DeconzFlowHandler() flow = config_flow.DeconzFlowHandler()
flow.hass = hass flow.hass = hass
await flow.async_step_init() await flow.async_step_user()
await flow.async_step_link(user_input={}) await flow.async_step_link(user_input={})
result = await flow.async_step_options( result = await flow.async_step_options(
user_input={'allow_clip_sensor': True, 'allow_deconz_groups': True}) user_input={'allow_clip_sensor': True, 'allow_deconz_groups': True})
@ -45,7 +45,7 @@ async def test_flow_already_registered_bridge(hass):
flow = config_flow.DeconzFlowHandler() flow = config_flow.DeconzFlowHandler()
flow.hass = hass flow.hass = hass
result = await flow.async_step_init() result = await flow.async_step_user()
assert result['type'] == 'abort' assert result['type'] == 'abort'
@ -55,7 +55,7 @@ async def test_flow_no_discovered_bridges(hass, aioclient_mock):
flow = config_flow.DeconzFlowHandler() flow = config_flow.DeconzFlowHandler()
flow.hass = hass flow.hass = hass
result = await flow.async_step_init() result = await flow.async_step_user()
assert result['type'] == 'abort' assert result['type'] == 'abort'
@ -67,7 +67,7 @@ async def test_flow_one_bridge_discovered(hass, aioclient_mock):
flow = config_flow.DeconzFlowHandler() flow = config_flow.DeconzFlowHandler()
flow.hass = hass flow.hass = hass
result = await flow.async_step_init() result = await flow.async_step_user()
assert result['type'] == 'form' assert result['type'] == 'form'
assert result['step_id'] == 'link' assert result['step_id'] == 'link'
@ -81,9 +81,9 @@ async def test_flow_two_bridges_discovered(hass, aioclient_mock):
flow = config_flow.DeconzFlowHandler() flow = config_flow.DeconzFlowHandler()
flow.hass = hass flow.hass = hass
result = await flow.async_step_init() result = await flow.async_step_user()
assert result['type'] == 'form' assert result['type'] == 'form'
assert result['step_id'] == 'init' assert result['step_id'] == 'user'
with pytest.raises(vol.Invalid): with pytest.raises(vol.Invalid):
assert result['data_schema']({'host': '0.0.0.0'}) assert result['data_schema']({'host': '0.0.0.0'})
@ -92,6 +92,21 @@ async def test_flow_two_bridges_discovered(hass, aioclient_mock):
result['data_schema']({'host': '5.6.7.8'}) result['data_schema']({'host': '5.6.7.8'})
async def test_flow_two_bridges_selection(hass, aioclient_mock):
"""Test config flow selection of one of two bridges."""
flow = config_flow.DeconzFlowHandler()
flow.hass = hass
flow.bridges = [
{'bridgeid': 'id1', 'host': '1.2.3.4', 'port': 80},
{'bridgeid': 'id2', 'host': '5.6.7.8', 'port': 80}
]
result = await flow.async_step_user(user_input={'host': '1.2.3.4'})
assert result['type'] == 'form'
assert result['step_id'] == 'link'
assert flow.deconz_config['host'] == '1.2.3.4'
async def test_link_no_api_key(hass, aioclient_mock): async def test_link_no_api_key(hass, aioclient_mock):
"""Test config flow should abort if no API key was possible to retrieve.""" """Test config flow should abort if no API key was possible to retrieve."""
aioclient_mock.post('http://1.2.3.4:80/api', json=[]) aioclient_mock.post('http://1.2.3.4:80/api', json=[])

View File

@ -1,11 +1,11 @@
"""Test deCONZ component setup process.""" """Test deCONZ component setup process."""
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from homeassistant.components import deconz
from homeassistant.components.deconz import DATA_DECONZ_ID
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import mock_coro from homeassistant.components import deconz
from tests.common import mock_coro, MockConfigEntry
CONFIG = { CONFIG = {
"config": { "config": {
@ -99,173 +99,113 @@ async def test_setup_entry_no_available_bridge(hass):
async def test_setup_entry_successful(hass): async def test_setup_entry_successful(hass):
"""Test setup entry is successful.""" """Test setup entry is successful."""
entry = Mock() entry = MockConfigEntry(domain=deconz.DOMAIN, data={
entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} 'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'
with patch.object(hass, 'async_create_task') as mock_add_job, \ })
patch.object(hass, 'config_entries') as mock_config_entries, \ entry.add_to_hass(hass)
patch('pydeconz.DeconzSession.async_get_state', mock_registry = Mock()
return_value=mock_coro(CONFIG)), \ with patch.object(deconz, 'DeconzGateway') as mock_gateway, \
patch('pydeconz.DeconzSession.start', return_value=True), \
patch('homeassistant.helpers.device_registry.async_get_registry', patch('homeassistant.helpers.device_registry.async_get_registry',
return_value=mock_coro(Mock())): return_value=mock_coro(mock_registry)):
mock_gateway.return_value.async_setup.return_value = mock_coro(True)
assert await deconz.async_setup_entry(hass, entry) is True assert await deconz.async_setup_entry(hass, entry) is True
assert hass.data[deconz.DOMAIN] assert hass.data[deconz.DOMAIN]
assert hass.data[deconz.DATA_DECONZ_ID] == {}
assert len(hass.data[deconz.DATA_DECONZ_UNSUB]) == 1
assert len(mock_add_job.mock_calls) == \
len(deconz.SUPPORTED_PLATFORMS)
assert len(mock_config_entries.async_forward_entry_setup.mock_calls) == \
len(deconz.SUPPORTED_PLATFORMS)
assert mock_config_entries.async_forward_entry_setup.mock_calls[0][1] == \
(entry, 'binary_sensor')
assert mock_config_entries.async_forward_entry_setup.mock_calls[1][1] == \
(entry, 'cover')
assert mock_config_entries.async_forward_entry_setup.mock_calls[2][1] == \
(entry, 'light')
assert mock_config_entries.async_forward_entry_setup.mock_calls[3][1] == \
(entry, 'scene')
assert mock_config_entries.async_forward_entry_setup.mock_calls[4][1] == \
(entry, 'sensor')
assert mock_config_entries.async_forward_entry_setup.mock_calls[5][1] == \
(entry, 'switch')
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."""
entry = Mock() entry = MockConfigEntry(domain=deconz.DOMAIN, data={
entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} 'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'
entry.async_unload.return_value = mock_coro(True) })
deconzmock = Mock() entry.add_to_hass(hass)
deconzmock.async_load_parameters.return_value = mock_coro(True) mock_registry = Mock()
deconzmock.sensors = {} with patch.object(deconz, 'DeconzGateway') as mock_gateway, \
with patch('pydeconz.DeconzSession', return_value=deconzmock): patch('homeassistant.helpers.device_registry.async_get_registry',
return_value=mock_coro(mock_registry)):
mock_gateway.return_value.async_setup.return_value = mock_coro(True)
assert await deconz.async_setup_entry(hass, entry) is True assert await deconz.async_setup_entry(hass, entry) is True
assert deconz.DATA_DECONZ_EVENT in hass.data mock_gateway.return_value.async_reset.return_value = mock_coro(True)
hass.data[deconz.DATA_DECONZ_EVENT].append(Mock())
hass.data[deconz.DATA_DECONZ_ID] = {'id': 'deconzid'}
assert await deconz.async_unload_entry(hass, entry) assert await deconz.async_unload_entry(hass, entry)
assert deconz.DOMAIN not in hass.data assert deconz.DOMAIN not in hass.data
assert len(hass.data[deconz.DATA_DECONZ_UNSUB]) == 0
assert len(hass.data[deconz.DATA_DECONZ_EVENT]) == 0
assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0
async def test_add_new_device(hass):
"""Test adding a new device generates a signal for platforms."""
entry = Mock()
entry.data = {'host': '1.2.3.4', 'port': 80,
'api_key': '1234567890ABCDEF', 'allow_clip_sensor': False}
new_event = {
"t": "event",
"e": "added",
"r": "sensors",
"id": "1",
"sensor": {
"config": {
"on": "True",
"reachable": "True"
},
"name": "event",
"state": {},
"type": "ZHASwitch"
}
}
with patch.object(deconz, 'async_dispatcher_send') as mock_dispatch_send, \
patch('pydeconz.DeconzSession.async_get_state',
return_value=mock_coro(CONFIG)), \
patch('pydeconz.DeconzSession.start', return_value=True):
assert await deconz.async_setup_entry(hass, entry) is True
hass.data[deconz.DOMAIN].async_event_handler(new_event)
await hass.async_block_till_done()
assert len(mock_dispatch_send.mock_calls) == 1
assert len(mock_dispatch_send.mock_calls[0]) == 3
async def test_add_new_remote(hass):
"""Test new added device creates a new remote."""
entry = Mock()
entry.data = {'host': '1.2.3.4', 'port': 80,
'api_key': '1234567890ABCDEF', 'allow_clip_sensor': False}
remote = Mock()
remote.name = 'name'
remote.type = 'ZHASwitch'
remote.register_async_callback = Mock()
with patch('pydeconz.DeconzSession.async_get_state',
return_value=mock_coro(CONFIG)), \
patch('pydeconz.DeconzSession.start', return_value=True):
assert await deconz.async_setup_entry(hass, entry) is True
async_dispatcher_send(hass, 'deconz_new_sensor', [remote])
await hass.async_block_till_done()
assert len(hass.data[deconz.DATA_DECONZ_EVENT]) == 1
async def test_do_not_allow_clip_sensor(hass):
"""Test that clip sensors can be ignored."""
entry = Mock()
entry.data = {'host': '1.2.3.4', 'port': 80,
'api_key': '1234567890ABCDEF', 'allow_clip_sensor': False}
remote = Mock()
remote.name = 'name'
remote.type = 'CLIPSwitch'
remote.register_async_callback = Mock()
with patch('pydeconz.DeconzSession.async_get_state',
return_value=mock_coro(CONFIG)), \
patch('pydeconz.DeconzSession.start', return_value=True):
assert await deconz.async_setup_entry(hass, entry) is True
async_dispatcher_send(hass, 'deconz_new_sensor', [remote])
await hass.async_block_till_done()
assert len(hass.data[deconz.DATA_DECONZ_EVENT]) == 0
async def test_service_configure(hass): async def test_service_configure(hass):
"""Test that service invokes pydeconz with the correct path and data.""" """Test that service invokes pydeconz with the correct path and data."""
entry = Mock() entry = MockConfigEntry(domain=deconz.DOMAIN, data={
entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} 'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'
with patch('pydeconz.DeconzSession.async_get_state', })
return_value=mock_coro(CONFIG)), \ entry.add_to_hass(hass)
patch('pydeconz.DeconzSession.start', return_value=True), \ mock_registry = Mock()
with patch.object(deconz, 'DeconzGateway') as mock_gateway, \
patch('homeassistant.helpers.device_registry.async_get_registry', patch('homeassistant.helpers.device_registry.async_get_registry',
return_value=mock_coro(Mock())): return_value=mock_coro(mock_registry)):
mock_gateway.return_value.async_setup.return_value = mock_coro(True)
assert await deconz.async_setup_entry(hass, entry) is True assert await deconz.async_setup_entry(hass, entry) is True
hass.data[DATA_DECONZ_ID] = { hass.data[deconz.DOMAIN].deconz_ids = {
'light.test': '/light/1' 'light.test': '/light/1'
} }
data = {'on': True, 'attr1': 10, 'attr2': 20} data = {'on': True, 'attr1': 10, 'attr2': 20}
# only field # only field
with patch('pydeconz.DeconzSession.async_put_state') as async_put_state: with patch('pydeconz.DeconzSession.async_put_state',
return_value=mock_coro(True)):
await hass.services.async_call('deconz', 'configure', service_data={ await hass.services.async_call('deconz', 'configure', service_data={
'field': '/light/42', 'data': data 'field': '/light/42', 'data': data
}) })
await hass.async_block_till_done() await hass.async_block_till_done()
async_put_state.assert_called_with('/light/42', data)
# only entity # only entity
with patch('pydeconz.DeconzSession.async_put_state') as async_put_state: with patch('pydeconz.DeconzSession.async_put_state',
return_value=mock_coro(True)):
await hass.services.async_call('deconz', 'configure', service_data={ await hass.services.async_call('deconz', 'configure', service_data={
'entity': 'light.test', 'data': data 'entity': 'light.test', 'data': data
}) })
await hass.async_block_till_done() await hass.async_block_till_done()
async_put_state.assert_called_with('/light/1', data)
# entity + field # entity + field
with patch('pydeconz.DeconzSession.async_put_state') as async_put_state: with patch('pydeconz.DeconzSession.async_put_state',
return_value=mock_coro(True)):
await hass.services.async_call('deconz', 'configure', service_data={ await hass.services.async_call('deconz', 'configure', service_data={
'entity': 'light.test', 'field': '/state', 'data': data}) 'entity': 'light.test', 'field': '/state', 'data': data})
await hass.async_block_till_done() await hass.async_block_till_done()
async_put_state.assert_called_with('/light/1/state', data)
# non-existing entity (or not from deCONZ) # non-existing entity (or not from deCONZ)
with patch('pydeconz.DeconzSession.async_put_state') as async_put_state: with patch('pydeconz.DeconzSession.async_put_state',
return_value=mock_coro(True)):
await hass.services.async_call('deconz', 'configure', service_data={ await hass.services.async_call('deconz', 'configure', service_data={
'entity': 'light.nonexisting', 'field': '/state', 'data': data}) 'entity': 'light.nonexisting', 'field': '/state', 'data': data})
await hass.async_block_till_done() await hass.async_block_till_done()
async_put_state.assert_not_called()
# field does not start with / # field does not start with /
with patch('pydeconz.DeconzSession.async_put_state') as async_put_state: with patch('pydeconz.DeconzSession.async_put_state',
return_value=mock_coro(True)):
await hass.services.async_call('deconz', 'configure', service_data={ await hass.services.async_call('deconz', 'configure', service_data={
'entity': 'light.test', 'field': 'state', 'data': data}) 'entity': 'light.test', 'field': 'state', 'data': data})
await hass.async_block_till_done() await hass.async_block_till_done()
async_put_state.assert_not_called()
async def test_service_refresh_devices(hass):
"""Test that service can refresh devices."""
entry = MockConfigEntry(domain=deconz.DOMAIN, data={
'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'
})
entry.add_to_hass(hass)
mock_registry = Mock()
with patch.object(deconz, 'DeconzGateway') as mock_gateway, \
patch('homeassistant.helpers.device_registry.async_get_registry',
return_value=mock_coro(mock_registry)):
mock_gateway.return_value.async_setup.return_value = mock_coro(True)
assert await deconz.async_setup_entry(hass, entry) is True
with patch.object(hass.data[deconz.DOMAIN].api, 'async_load_parameters',
return_value=mock_coro(True)):
await hass.services.async_call(
'deconz', 'device_refresh', service_data={})
await hass.async_block_till_done()
with patch.object(hass.data[deconz.DOMAIN].api, 'async_load_parameters',
return_value=mock_coro(False)):
await hass.services.async_call(
'deconz', 'device_refresh', service_data={})
await hass.async_block_till_done()

View File

@ -4,6 +4,9 @@ from unittest.mock import Mock, patch
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import deconz from homeassistant.components import deconz
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.setup import async_setup_component
import homeassistant.components.light as light
from tests.common import mock_coro from tests.common import mock_coro
@ -12,7 +15,18 @@ LIGHT = {
"1": { "1": {
"id": "Light 1 id", "id": "Light 1 id",
"name": "Light 1 name", "name": "Light 1 name",
"state": {} "state": {
"on": True, "bri": 255, "colormode": "xy", "xy": (500, 500),
"reachable": True
},
"uniqueid": "00:00:00:00:00:00:00:00-00"
},
"2": {
"id": "Light 2 id",
"name": "Light 2 name",
"state": {
"on": True, "colormode": "ct", "ct": 2500, "reachable": True
}
} }
} }
@ -20,6 +34,7 @@ GROUP = {
"1": { "1": {
"id": "Group 1 id", "id": "Group 1 id",
"name": "Group 1 name", "name": "Group 1 name",
"type": "LightGroup",
"state": {}, "state": {},
"action": {}, "action": {},
"scenes": [], "scenes": [],
@ -47,85 +62,152 @@ SWITCH = {
} }
async def setup_bridge(hass, data, allow_deconz_groups=True): ENTRY_CONFIG = {
deconz.const.CONF_ALLOW_CLIP_SENSOR: True,
deconz.const.CONF_ALLOW_DECONZ_GROUPS: True,
deconz.config_flow.CONF_API_KEY: "ABCDEF",
deconz.config_flow.CONF_BRIDGEID: "0123456789",
deconz.config_flow.CONF_HOST: "1.2.3.4",
deconz.config_flow.CONF_PORT: 80
}
async def setup_gateway(hass, data, allow_deconz_groups=True):
"""Load the deCONZ light platform.""" """Load the deCONZ light platform."""
from pydeconz import DeconzSession from pydeconz import DeconzSession
loop = Mock() loop = Mock()
session = Mock() session = Mock()
entry = Mock()
entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} ENTRY_CONFIG[deconz.const.CONF_ALLOW_DECONZ_GROUPS] = allow_deconz_groups
bridge = DeconzSession(loop, session, **entry.data)
bridge.config = Mock() config_entry = config_entries.ConfigEntry(
1, deconz.DOMAIN, 'Mock Title', ENTRY_CONFIG, 'test',
config_entries.CONN_CLASS_LOCAL_PUSH)
gateway = deconz.DeconzGateway(hass, config_entry)
gateway.api = DeconzSession(loop, session, **config_entry.data)
gateway.api.config = Mock()
hass.data[deconz.DOMAIN] = gateway
with patch('pydeconz.DeconzSession.async_get_state', with patch('pydeconz.DeconzSession.async_get_state',
return_value=mock_coro(data)): return_value=mock_coro(data)):
await bridge.async_load_parameters() await gateway.api.async_load_parameters()
hass.data[deconz.DOMAIN] = bridge
hass.data[deconz.DATA_DECONZ_UNSUB] = []
hass.data[deconz.DATA_DECONZ_ID] = {}
config_entry = config_entries.ConfigEntry(
1, deconz.DOMAIN, 'Mock Title',
{'host': 'mock-host', 'allow_deconz_groups': allow_deconz_groups},
'test', config_entries.CONN_CLASS_LOCAL_PUSH)
await hass.config_entries.async_forward_entry_setup(config_entry, 'light') await hass.config_entries.async_forward_entry_setup(config_entry, 'light')
# To flush out the service call to update the group # To flush out the service call to update the group
await hass.async_block_till_done() await hass.async_block_till_done()
async def test_platform_manually_configured(hass):
"""Test that we do not discover anything or try to set up a gateway."""
assert await async_setup_component(hass, light.DOMAIN, {
'light': {
'platform': deconz.DOMAIN
}
}) is True
assert deconz.DOMAIN not in hass.data
async def test_no_lights_or_groups(hass): async def test_no_lights_or_groups(hass):
"""Test that no lights or groups entities are created.""" """Test that no lights or groups entities are created."""
data = {} await setup_gateway(hass, {})
await setup_bridge(hass, data) assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0
assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0
assert len(hass.states.async_all()) == 0 assert len(hass.states.async_all()) == 0
async def test_lights_and_groups(hass): async def test_lights_and_groups(hass):
"""Test that lights or groups entities are created.""" """Test that lights or groups entities are created."""
await setup_bridge(hass, {"lights": LIGHT, "groups": GROUP}) with patch('pydeconz.DeconzSession.async_put_state',
assert "light.light_1_name" in hass.data[deconz.DATA_DECONZ_ID] return_value=mock_coro(True)):
assert "light.group_1_name" in hass.data[deconz.DATA_DECONZ_ID] await setup_gateway(hass, {"lights": LIGHT, "groups": GROUP})
assert "light.group_2_name" not in hass.data[deconz.DATA_DECONZ_ID] assert "light.light_1_name" in hass.data[deconz.DOMAIN].deconz_ids
assert len(hass.states.async_all()) == 3 assert "light.light_2_name" in hass.data[deconz.DOMAIN].deconz_ids
assert "light.group_1_name" in hass.data[deconz.DOMAIN].deconz_ids
assert "light.group_2_name" not in hass.data[deconz.DOMAIN].deconz_ids
assert len(hass.states.async_all()) == 4
lamp_1 = hass.states.get('light.light_1_name')
assert lamp_1 is not None
assert lamp_1.state == 'on'
assert lamp_1.attributes['brightness'] == 255
assert lamp_1.attributes['hs_color'] == (224.235, 100.0)
light_2 = hass.states.get('light.light_2_name')
assert light_2 is not None
assert light_2.state == 'on'
assert light_2.attributes['color_temp'] == 2500
hass.data[deconz.DOMAIN].api.lights['1'].async_update({})
await hass.services.async_call('light', 'turn_on', {
'entity_id': 'light.light_1_name',
'color_temp': 2500,
'brightness': 200,
'transition': 5,
'flash': 'short',
'effect': 'colorloop'
}, blocking=True)
await hass.services.async_call('light', 'turn_on', {
'entity_id': 'light.light_1_name',
'hs_color': (20, 30),
'flash': 'long',
'effect': 'None'
}, blocking=True)
await hass.services.async_call('light', 'turn_off', {
'entity_id': 'light.light_1_name',
'transition': 5,
'flash': 'short'
}, blocking=True)
await hass.services.async_call('light', 'turn_off', {
'entity_id': 'light.light_1_name',
'flash': 'long'
}, blocking=True)
async def test_add_new_light(hass): async def test_add_new_light(hass):
"""Test successful creation of light entities.""" """Test successful creation of light entities."""
data = {} await setup_gateway(hass, {})
await setup_bridge(hass, data)
light = Mock() light = Mock()
light.name = 'name' light.name = 'name'
light.register_async_callback = Mock() light.register_async_callback = Mock()
async_dispatcher_send(hass, 'deconz_new_light', [light]) async_dispatcher_send(hass, 'deconz_new_light', [light])
await hass.async_block_till_done() await hass.async_block_till_done()
assert "light.name" in hass.data[deconz.DATA_DECONZ_ID] assert "light.name" in hass.data[deconz.DOMAIN].deconz_ids
async def test_add_new_group(hass): async def test_add_new_group(hass):
"""Test successful creation of group entities.""" """Test successful creation of group entities."""
data = {} await setup_gateway(hass, {})
await setup_bridge(hass, data)
group = Mock() group = Mock()
group.name = 'name' group.name = 'name'
group.register_async_callback = Mock() group.register_async_callback = Mock()
async_dispatcher_send(hass, 'deconz_new_group', [group]) async_dispatcher_send(hass, 'deconz_new_group', [group])
await hass.async_block_till_done() await hass.async_block_till_done()
assert "light.name" in hass.data[deconz.DATA_DECONZ_ID] assert "light.name" in hass.data[deconz.DOMAIN].deconz_ids
async def test_do_not_add_deconz_groups(hass): async def test_do_not_add_deconz_groups(hass):
"""Test that clip sensors can be ignored.""" """Test that clip sensors can be ignored."""
data = {} await setup_gateway(hass, {}, allow_deconz_groups=False)
await setup_bridge(hass, data, allow_deconz_groups=False)
group = Mock() group = Mock()
group.name = 'name' group.name = 'name'
group.register_async_callback = Mock() group.register_async_callback = Mock()
async_dispatcher_send(hass, 'deconz_new_group', [group]) async_dispatcher_send(hass, 'deconz_new_group', [group])
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0
async def test_no_switch(hass): async def test_no_switch(hass):
"""Test that a switch doesn't get created as a light entity.""" """Test that a switch doesn't get created as a light entity."""
await setup_bridge(hass, {"lights": SWITCH}) await setup_gateway(hass, {"lights": SWITCH})
assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0
assert len(hass.states.async_all()) == 0 assert len(hass.states.async_all()) == 0
async def test_unload_light(hass):
"""Test that it works to unload switch entities."""
await setup_gateway(hass, {"lights": LIGHT, "groups": GROUP})
await hass.data[deconz.DOMAIN].async_reset()
# Group.all_lights will not be removed
assert len(hass.states.async_all()) == 1

View File

@ -1,8 +1,11 @@
"""deCONZ scenes platform tests.""" """deCONZ scene platform tests."""
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import deconz from homeassistant.components import deconz
from homeassistant.setup import async_setup_component
import homeassistant.components.scene as scene
from tests.common import mock_coro from tests.common import mock_coro
@ -21,39 +24,73 @@ GROUP = {
} }
async def setup_bridge(hass, data): ENTRY_CONFIG = {
deconz.const.CONF_ALLOW_CLIP_SENSOR: True,
deconz.const.CONF_ALLOW_DECONZ_GROUPS: True,
deconz.config_flow.CONF_API_KEY: "ABCDEF",
deconz.config_flow.CONF_BRIDGEID: "0123456789",
deconz.config_flow.CONF_HOST: "1.2.3.4",
deconz.config_flow.CONF_PORT: 80
}
async def setup_gateway(hass, data):
"""Load the deCONZ scene platform.""" """Load the deCONZ scene platform."""
from pydeconz import DeconzSession from pydeconz import DeconzSession
loop = Mock() loop = Mock()
session = Mock() session = Mock()
entry = Mock()
entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} config_entry = config_entries.ConfigEntry(
bridge = DeconzSession(loop, session, **entry.data) 1, deconz.DOMAIN, 'Mock Title', ENTRY_CONFIG, 'test',
config_entries.CONN_CLASS_LOCAL_PUSH)
gateway = deconz.DeconzGateway(hass, config_entry)
gateway.api = DeconzSession(loop, session, **config_entry.data)
gateway.api.config = Mock()
hass.data[deconz.DOMAIN] = gateway
with patch('pydeconz.DeconzSession.async_get_state', with patch('pydeconz.DeconzSession.async_get_state',
return_value=mock_coro(data)): return_value=mock_coro(data)):
await bridge.async_load_parameters() await gateway.api.async_load_parameters()
hass.data[deconz.DOMAIN] = bridge
hass.data[deconz.DATA_DECONZ_UNSUB] = []
hass.data[deconz.DATA_DECONZ_ID] = {}
config_entry = config_entries.ConfigEntry(
1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test',
config_entries.CONN_CLASS_LOCAL_PUSH)
await hass.config_entries.async_forward_entry_setup(config_entry, 'scene') await hass.config_entries.async_forward_entry_setup(config_entry, 'scene')
# To flush out the service call to update the group # To flush out the service call to update the group
await hass.async_block_till_done() await hass.async_block_till_done()
async def test_platform_manually_configured(hass):
"""Test that we do not discover anything or try to set up a gateway."""
assert await async_setup_component(hass, scene.DOMAIN, {
'scene': {
'platform': deconz.DOMAIN
}
}) is True
assert deconz.DOMAIN not in hass.data
async def test_no_scenes(hass): async def test_no_scenes(hass):
"""Test the update_lights function with some lights.""" """Test that scenes can be loaded without scenes being available."""
data = {} await setup_gateway(hass, {})
await setup_bridge(hass, data) assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0
assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0
assert len(hass.states.async_all()) == 0 assert len(hass.states.async_all()) == 0
async def test_scenes(hass): async def test_scenes(hass):
"""Test the update_lights function with some lights.""" """Test that scenes works."""
data = {"groups": GROUP} with patch('pydeconz.DeconzSession.async_put_state',
await setup_bridge(hass, data) return_value=mock_coro(True)):
assert "scene.group_1_name_scene_1" in hass.data[deconz.DATA_DECONZ_ID] await setup_gateway(hass, {"groups": GROUP})
assert "scene.group_1_name_scene_1" in hass.data[deconz.DOMAIN].deconz_ids
assert len(hass.states.async_all()) == 1 assert len(hass.states.async_all()) == 1
await hass.services.async_call('scene', 'turn_on', {
'entity_id': 'scene.group_1_name_scene_1'
}, blocking=True)
async def test_unload_scene(hass):
"""Test that it works to unload scene entities."""
await setup_gateway(hass, {"groups": GROUP})
await hass.data[deconz.DOMAIN].async_reset()
assert len(hass.states.async_all()) == 0

View File

@ -1,10 +1,12 @@
"""deCONZ sensor platform tests.""" """deCONZ sensor platform tests."""
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import deconz from homeassistant.components import deconz
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.setup import async_setup_component
import homeassistant.components.sensor as sensor
from tests.common import mock_coro from tests.common import mock_coro
@ -13,9 +15,10 @@ SENSOR = {
"1": { "1": {
"id": "Sensor 1 id", "id": "Sensor 1 id",
"name": "Sensor 1 name", "name": "Sensor 1 name",
"type": "ZHATemperature", "type": "ZHALightLevel",
"state": {"temperature": False}, "state": {"lightlevel": 30000, "dark": False},
"config": {} "config": {"reachable": True},
"uniqueid": "00:00:00:00:00:00:00:00-00"
}, },
"2": { "2": {
"id": "Sensor 2 id", "id": "Sensor 2 id",
@ -36,80 +39,134 @@ SENSOR = {
"name": "Sensor 4 name", "name": "Sensor 4 name",
"type": "ZHASwitch", "type": "ZHASwitch",
"state": {"buttonevent": 1000}, "state": {"buttonevent": 1000},
"config": {"battery": 100} "config": {"battery": 100},
"uniqueid": "00:00:00:00:00:00:00:01-00"
},
"5": {
"id": "Sensor 5 id",
"name": "Sensor 5 name",
"type": "ZHASwitch",
"state": {"buttonevent": 1000},
"config": {"battery": 100},
"uniqueid": "00:00:00:00:00:00:00:02:00-00"
},
"6": {
"id": "Sensor 6 id",
"name": "Sensor 6 name",
"type": "Daylight",
"state": {"daylight": True},
"config": {}
},
"7": {
"id": "Sensor 7 id",
"name": "Sensor 7 name",
"type": "ZHAPower",
"state": {"current": 2, "power": 6, "voltage": 3},
"config": {"reachable": True}
} }
} }
async def setup_bridge(hass, data, allow_clip_sensor=True): ENTRY_CONFIG = {
deconz.const.CONF_ALLOW_CLIP_SENSOR: True,
deconz.const.CONF_ALLOW_DECONZ_GROUPS: True,
deconz.config_flow.CONF_API_KEY: "ABCDEF",
deconz.config_flow.CONF_BRIDGEID: "0123456789",
deconz.config_flow.CONF_HOST: "1.2.3.4",
deconz.config_flow.CONF_PORT: 80
}
async def setup_gateway(hass, data, allow_clip_sensor=True):
"""Load the deCONZ sensor platform.""" """Load the deCONZ sensor platform."""
from pydeconz import DeconzSession from pydeconz import DeconzSession
loop = Mock() loop = Mock()
session = Mock() session = Mock()
entry = Mock()
entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} ENTRY_CONFIG[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor
bridge = DeconzSession(loop, session, **entry.data)
bridge.config = Mock() config_entry = config_entries.ConfigEntry(
1, deconz.DOMAIN, 'Mock Title', ENTRY_CONFIG, 'test',
config_entries.CONN_CLASS_LOCAL_PUSH)
gateway = deconz.DeconzGateway(hass, config_entry)
gateway.api = DeconzSession(loop, session, **config_entry.data)
gateway.api.config = Mock()
hass.data[deconz.DOMAIN] = gateway
with patch('pydeconz.DeconzSession.async_get_state', with patch('pydeconz.DeconzSession.async_get_state',
return_value=mock_coro(data)): return_value=mock_coro(data)):
await bridge.async_load_parameters() await gateway.api.async_load_parameters()
hass.data[deconz.DOMAIN] = bridge
hass.data[deconz.DATA_DECONZ_UNSUB] = [] await hass.config_entries.async_forward_entry_setup(
hass.data[deconz.DATA_DECONZ_EVENT] = [] config_entry, 'sensor')
hass.data[deconz.DATA_DECONZ_ID] = {}
config_entry = config_entries.ConfigEntry(
1, deconz.DOMAIN, 'Mock Title',
{'host': 'mock-host', 'allow_clip_sensor': allow_clip_sensor}, 'test',
config_entries.CONN_CLASS_LOCAL_PUSH)
await hass.config_entries.async_forward_entry_setup(config_entry, 'sensor')
# To flush out the service call to update the group # To flush out the service call to update the group
await hass.async_block_till_done() await hass.async_block_till_done()
async def test_platform_manually_configured(hass):
"""Test that we do not discover anything or try to set up a gateway."""
assert await async_setup_component(hass, sensor.DOMAIN, {
'sensor': {
'platform': deconz.DOMAIN
}
}) is True
assert deconz.DOMAIN not in hass.data
async def test_no_sensors(hass): async def test_no_sensors(hass):
"""Test that no sensors in deconz results in no sensor entities.""" """Test that no sensors in deconz results in no sensor entities."""
data = {} await setup_gateway(hass, {})
await setup_bridge(hass, data) assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0
assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0
assert len(hass.states.async_all()) == 0 assert len(hass.states.async_all()) == 0
async def test_sensors(hass): async def test_sensors(hass):
"""Test successful creation of sensor entities.""" """Test successful creation of sensor entities."""
data = {"sensors": SENSOR} await setup_gateway(hass, {"sensors": SENSOR})
await setup_bridge(hass, data) assert "sensor.sensor_1_name" in hass.data[deconz.DOMAIN].deconz_ids
assert "sensor.sensor_1_name" in hass.data[deconz.DATA_DECONZ_ID] assert "sensor.sensor_2_name" not in hass.data[deconz.DOMAIN].deconz_ids
assert "sensor.sensor_2_name" not in hass.data[deconz.DATA_DECONZ_ID] assert "sensor.sensor_3_name" not in hass.data[deconz.DOMAIN].deconz_ids
assert "sensor.sensor_3_name" not in hass.data[deconz.DATA_DECONZ_ID]
assert "sensor.sensor_3_name_battery_level" not in \ assert "sensor.sensor_3_name_battery_level" not in \
hass.data[deconz.DATA_DECONZ_ID] hass.data[deconz.DOMAIN].deconz_ids
assert "sensor.sensor_4_name" not in hass.data[deconz.DATA_DECONZ_ID] assert "sensor.sensor_4_name" not in hass.data[deconz.DOMAIN].deconz_ids
assert "sensor.sensor_4_name_battery_level" in \ assert "sensor.sensor_4_name_battery_level" in \
hass.data[deconz.DATA_DECONZ_ID] hass.data[deconz.DOMAIN].deconz_ids
assert len(hass.states.async_all()) == 2 assert len(hass.states.async_all()) == 5
hass.data[deconz.DOMAIN].api.sensors['1'].async_update(
{'state': {'on': False}})
hass.data[deconz.DOMAIN].api.sensors['4'].async_update(
{'config': {'battery': 75}})
async def test_add_new_sensor(hass): async def test_add_new_sensor(hass):
"""Test successful creation of sensor entities.""" """Test successful creation of sensor entities."""
data = {} await setup_gateway(hass, {})
await setup_bridge(hass, data)
sensor = Mock() sensor = Mock()
sensor.name = 'name' sensor.name = 'name'
sensor.type = 'ZHATemperature' sensor.type = 'ZHATemperature'
sensor.register_async_callback = Mock() sensor.register_async_callback = Mock()
async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) async_dispatcher_send(hass, 'deconz_new_sensor', [sensor])
await hass.async_block_till_done() await hass.async_block_till_done()
assert "sensor.name" in hass.data[deconz.DATA_DECONZ_ID] assert "sensor.name" in hass.data[deconz.DOMAIN].deconz_ids
async def test_do_not_allow_clipsensor(hass): async def test_do_not_allow_clipsensor(hass):
"""Test that clip sensors can be ignored.""" """Test that clip sensors can be ignored."""
data = {} await setup_gateway(hass, {}, allow_clip_sensor=False)
await setup_bridge(hass, data, allow_clip_sensor=False)
sensor = Mock() sensor = Mock()
sensor.name = 'name' sensor.name = 'name'
sensor.type = 'CLIPTemperature' sensor.type = 'CLIPTemperature'
sensor.register_async_callback = Mock() sensor.register_async_callback = Mock()
async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) async_dispatcher_send(hass, 'deconz_new_sensor', [sensor])
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0
async def test_unload_sensor(hass):
"""Test that it works to unload sensor entities."""
await setup_gateway(hass, {"sensors": SENSOR})
await hass.data[deconz.DOMAIN].async_reset()
assert len(hass.states.async_all()) == 0

View File

@ -5,6 +5,9 @@ from homeassistant import config_entries
from homeassistant.components import deconz from homeassistant.components import deconz
from homeassistant.components.deconz.const import SWITCH_TYPES from homeassistant.components.deconz.const import SWITCH_TYPES
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.setup import async_setup_component
import homeassistant.components.switch as switch
from tests.common import mock_coro from tests.common import mock_coro
@ -13,19 +16,20 @@ SUPPORTED_SWITCHES = {
"id": "Switch 1 id", "id": "Switch 1 id",
"name": "Switch 1 name", "name": "Switch 1 name",
"type": "On/Off plug-in unit", "type": "On/Off plug-in unit",
"state": {} "state": {"on": True, "reachable": True},
"uniqueid": "00:00:00:00:00:00:00:00-00"
}, },
"2": { "2": {
"id": "Switch 2 id", "id": "Switch 2 id",
"name": "Switch 2 name", "name": "Switch 2 name",
"type": "Smart plug", "type": "Smart plug",
"state": {} "state": {"on": True, "reachable": True}
}, },
"3": { "3": {
"id": "Switch 3 id", "id": "Switch 3 id",
"name": "Switch 3 name", "name": "Switch 3 name",
"type": "Warning device", "type": "Warning device",
"state": {} "state": {"alert": "lselect", "reachable": True}
} }
} }
@ -39,61 +43,113 @@ UNSUPPORTED_SWITCH = {
} }
async def setup_bridge(hass, data): ENTRY_CONFIG = {
deconz.const.CONF_ALLOW_CLIP_SENSOR: True,
deconz.const.CONF_ALLOW_DECONZ_GROUPS: True,
deconz.config_flow.CONF_API_KEY: "ABCDEF",
deconz.config_flow.CONF_BRIDGEID: "0123456789",
deconz.config_flow.CONF_HOST: "1.2.3.4",
deconz.config_flow.CONF_PORT: 80
}
async def setup_gateway(hass, data):
"""Load the deCONZ switch platform.""" """Load the deCONZ switch platform."""
from pydeconz import DeconzSession from pydeconz import DeconzSession
loop = Mock() loop = Mock()
session = Mock() session = Mock()
entry = Mock()
entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} config_entry = config_entries.ConfigEntry(
bridge = DeconzSession(loop, session, **entry.data) 1, deconz.DOMAIN, 'Mock Title', ENTRY_CONFIG, 'test',
bridge.config = Mock() config_entries.CONN_CLASS_LOCAL_PUSH)
gateway = deconz.DeconzGateway(hass, config_entry)
gateway.api = DeconzSession(loop, session, **config_entry.data)
gateway.api.config = Mock()
hass.data[deconz.DOMAIN] = gateway
with patch('pydeconz.DeconzSession.async_get_state', with patch('pydeconz.DeconzSession.async_get_state',
return_value=mock_coro(data)): return_value=mock_coro(data)):
await bridge.async_load_parameters() await gateway.api.async_load_parameters()
hass.data[deconz.DOMAIN] = bridge
hass.data[deconz.DATA_DECONZ_UNSUB] = []
hass.data[deconz.DATA_DECONZ_ID] = {}
config_entry = config_entries.ConfigEntry(
1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test',
config_entries.CONN_CLASS_LOCAL_PUSH)
await hass.config_entries.async_forward_entry_setup(config_entry, 'switch') await hass.config_entries.async_forward_entry_setup(config_entry, 'switch')
# To flush out the service call to update the group # To flush out the service call to update the group
await hass.async_block_till_done() await hass.async_block_till_done()
async def test_platform_manually_configured(hass):
"""Test that we do not discover anything or try to set up a gateway."""
assert await async_setup_component(hass, switch.DOMAIN, {
'switch': {
'platform': deconz.DOMAIN
}
}) is True
assert deconz.DOMAIN not in hass.data
async def test_no_switches(hass): async def test_no_switches(hass):
"""Test that no switch entities are created.""" """Test that no switch entities are created."""
data = {} await setup_gateway(hass, {})
await setup_bridge(hass, data) assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0
assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0
assert len(hass.states.async_all()) == 0 assert len(hass.states.async_all()) == 0
async def test_switch(hass): async def test_switches(hass):
"""Test that all supported switch entities are created.""" """Test that all supported switch entities are created."""
await setup_bridge(hass, {"lights": SUPPORTED_SWITCHES}) with patch('pydeconz.DeconzSession.async_put_state',
assert "switch.switch_1_name" in hass.data[deconz.DATA_DECONZ_ID] return_value=mock_coro(True)):
assert "switch.switch_2_name" in hass.data[deconz.DATA_DECONZ_ID] await setup_gateway(hass, {"lights": SUPPORTED_SWITCHES})
assert "switch.switch_3_name" in hass.data[deconz.DATA_DECONZ_ID] assert "switch.switch_1_name" in hass.data[deconz.DOMAIN].deconz_ids
assert "switch.switch_2_name" in hass.data[deconz.DOMAIN].deconz_ids
assert "switch.switch_3_name" in hass.data[deconz.DOMAIN].deconz_ids
assert len(SUPPORTED_SWITCHES) == len(SWITCH_TYPES) assert len(SUPPORTED_SWITCHES) == len(SWITCH_TYPES)
assert len(hass.states.async_all()) == 4 assert len(hass.states.async_all()) == 4
switch_1 = hass.states.get('switch.switch_1_name')
assert switch_1 is not None
assert switch_1.state == 'on'
switch_3 = hass.states.get('switch.switch_3_name')
assert switch_3 is not None
assert switch_3.state == 'on'
hass.data[deconz.DOMAIN].api.lights['1'].async_update({})
await hass.services.async_call('switch', 'turn_on', {
'entity_id': 'switch.switch_1_name'
}, blocking=True)
await hass.services.async_call('switch', 'turn_off', {
'entity_id': 'switch.switch_1_name'
}, blocking=True)
await hass.services.async_call('switch', 'turn_on', {
'entity_id': 'switch.switch_3_name'
}, blocking=True)
await hass.services.async_call('switch', 'turn_off', {
'entity_id': 'switch.switch_3_name'
}, blocking=True)
async def test_add_new_switch(hass): async def test_add_new_switch(hass):
"""Test successful creation of switch entity.""" """Test successful creation of switch entity."""
data = {} await setup_gateway(hass, {})
await setup_bridge(hass, data)
switch = Mock() switch = Mock()
switch.name = 'name' switch.name = 'name'
switch.type = "Smart plug" switch.type = "Smart plug"
switch.register_async_callback = Mock() switch.register_async_callback = Mock()
async_dispatcher_send(hass, 'deconz_new_light', [switch]) async_dispatcher_send(hass, 'deconz_new_light', [switch])
await hass.async_block_till_done() await hass.async_block_till_done()
assert "switch.name" in hass.data[deconz.DATA_DECONZ_ID] assert "switch.name" in hass.data[deconz.DOMAIN].deconz_ids
async def test_unsupported_switch(hass): async def test_unsupported_switch(hass):
"""Test that unsupported switches are not created.""" """Test that unsupported switches are not created."""
await setup_bridge(hass, {"lights": UNSUPPORTED_SWITCH}) await setup_gateway(hass, {"lights": UNSUPPORTED_SWITCH})
assert len(hass.states.async_all()) == 0 assert len(hass.states.async_all()) == 0
async def test_unload_switch(hass):
"""Test that it works to unload switch entities."""
await setup_gateway(hass, {"lights": SUPPORTED_SWITCHES})
await hass.data[deconz.DOMAIN].async_reset()
assert len(hass.states.async_all()) == 1