diff --git a/.coveragerc b/.coveragerc index 72451dab531..5727ec1d43a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -76,9 +76,6 @@ omit = 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 diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index b0728ad167c..fe00402ec95 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -7,7 +7,7 @@ https://home-assistant.io/components/binary_sensor.deconz/ from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.deconz.const import ( 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.core import callback 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)) 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_add_sensor(hass.data[DATA_DECONZ].sensors.values()) + async_add_sensor(hass.data[DATA_DECONZ].api.sensors.values()) class DeconzBinarySensor(BinarySensorDevice): @@ -52,7 +52,8 @@ class DeconzBinarySensor(BinarySensorDevice): async def async_added_to_hass(self): """Subscribe sensors events.""" 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: """Disconnect sensor object when removed.""" @@ -127,7 +128,7 @@ class DeconzBinarySensor(BinarySensorDevice): self._sensor.uniqueid.count(':') != 7): return None 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 { 'connections': {(CONNECTION_ZIGBEE, serial)}, 'identifiers': {(DECONZ_DOMAIN, serial)}, diff --git a/homeassistant/components/cover/deconz.py b/homeassistant/components/cover/deconz.py index 89b29aa10a5..cd5871e153a 100644 --- a/homeassistant/components/cover/deconz.py +++ b/homeassistant/components/cover/deconz.py @@ -5,8 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.deconz/ """ from homeassistant.components.deconz.const import ( - COVER_TYPES, DAMPERS, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, - DATA_DECONZ_UNSUB, DECONZ_DOMAIN, WINDOW_COVERS) + COVER_TYPES, DAMPERS, DOMAIN as DATA_DECONZ, DECONZ_DOMAIN, WINDOW_COVERS) from homeassistant.components.cover import ( ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_STOP, SUPPORT_SET_POSITION) @@ -42,10 +41,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities.append(DeconzCover(light)) 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_add_cover(hass.data[DATA_DECONZ].lights.values()) + async_add_cover(hass.data[DATA_DECONZ].api.lights.values()) class DeconzCover(CoverDevice): @@ -62,7 +61,8 @@ class DeconzCover(CoverDevice): async def async_added_to_hass(self): """Subscribe to covers events.""" 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: """Disconnect cover object when removed.""" @@ -103,7 +103,6 @@ class DeconzCover(CoverDevice): return 'damper' if self._cover.type in WINDOW_COVERS: return 'window' - return None @property def supported_features(self): @@ -151,7 +150,7 @@ class DeconzCover(CoverDevice): self._cover.uniqueid.count(':') != 7): return None 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 { 'connections': {(CONNECTION_ZIGBEE, serial)}, 'identifiers': {(DECONZ_DOMAIN, serial)}, diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 648aebc8c89..c314a1191db 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -8,21 +8,15 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( - CONF_API_KEY, CONF_EVENT, CONF_HOST, - CONF_ID, CONF_PORT, EVENT_HOMEASSISTANT_STOP) -from homeassistant.core import EventOrigin, callback -from homeassistant.helpers import aiohttp_client, config_validation as cv + CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers import config_validation as cv 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 # Loading the config flow file will register the flow from .config_flow import configured_hosts -from .const import ( - CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT, - DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) +from .const import CONFIG_FILE, DOMAIN, _LOGGER +from .gateway import DeconzGateway 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. Start websocket for push notification of state changes from deCONZ. """ - from pydeconz import DeconzSession if DOMAIN in hass.data: _LOGGER.error( "Config entry failed since one deCONZ instance already exists") return False - @callback - 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) + gateway = DeconzGateway(hass, config_entry) - session = aiohttp_client.async_get_clientsession(hass) - deconz = DeconzSession(hass.loop, session, **config_entry.data, - async_add_device=async_add_device_callback) - result = await deconz.async_load_parameters() + hass.data[DOMAIN] = gateway - if result is False: + if not await gateway.async_setup(): 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 \ hass.helpers.device_registry.async_get_registry() device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={(CONNECTION_NETWORK_MAC, deconz.config.mac)}, - identifiers={(DOMAIN, deconz.config.bridgeid)}, - manufacturer='Dresden Elektronik', model=deconz.config.modelid, - name=deconz.config.name, sw_version=deconz.config.swversion) + connections={(CONNECTION_NETWORK_MAC, gateway.api.config.mac)}, + identifiers={(DOMAIN, gateway.api.config.bridgeid)}, + manufacturer='Dresden Elektronik', model=gateway.api.config.modelid, + name=gateway.api.config.name, sw_version=gateway.api.config.swversion) async def async_configure(call): """Set attribute of device in deCONZ. @@ -155,121 +114,66 @@ async def async_setup_entry(hass, config_entry): field = call.data.get(SERVICE_FIELD, '') entity_id = call.data.get(SERVICE_ENTITY) data = call.data.get(SERVICE_DATA) - deconz = hass.data[DOMAIN] + gateway = hass.data[DOMAIN] + if entity_id: try: - field = hass.data[DATA_DECONZ_ID][entity_id] + field + field = gateway.deconz_ids[entity_id] + field except KeyError: _LOGGER.error('Could not find the entity %s', entity_id) return - await deconz.async_put_state(field, data) + await gateway.api.async_put_state(field, data) hass.services.async_register( DOMAIN, SERVICE_DECONZ, async_configure, schema=SERVICE_SCHEMA) async def async_refresh_devices(call): """Refresh available devices from deCONZ.""" - deconz = hass.data[DOMAIN] + gateway = hass.data[DOMAIN] - groups = list(deconz.groups.keys()) - lights = list(deconz.lights.keys()) - scenes = list(deconz.scenes.keys()) - sensors = list(deconz.sensors.keys()) + groups = set(gateway.api.groups.keys()) + lights = set(gateway.api.lights.keys()) + scenes = set(gateway.api.scenes.keys()) + sensors = set(gateway.api.sensors.keys()) - if not await deconz.async_load_parameters(): + if not await gateway.api.async_load_parameters(): return - async_add_device_callback( + gateway.async_add_device_callback( '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] ) - async_add_device_callback( + gateway.async_add_device_callback( '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] ) - async_add_device_callback( + gateway.async_add_device_callback( '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] ) - async_add_device_callback( + gateway.async_add_device_callback( '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] ) hass.services.async_register( DOMAIN, SERVICE_DEVICE_REFRESH, async_refresh_devices) - @callback - 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) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gateway.shutdown) return True async def async_unload_entry(hass, config_entry): """Unload deCONZ config entry.""" - deconz = hass.data.pop(DOMAIN) + gateway = hass.data.pop(DOMAIN) hass.services.async_remove(DOMAIN, SERVICE_DECONZ) - deconz.close() - - 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) + hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH) + return await gateway.async_reset() diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 65fcf51b930..293b6c1b540 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -35,10 +35,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow): self.deconz_config = {} 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. Only allows one instance to be set up. @@ -67,7 +63,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow): for bridge in self.bridges: hosts.append(bridge[CONF_HOST]) return self.async_show_form( - step_id='init', + step_id='user', data_schema=vol.Schema({ vol.Required(CONF_HOST): vol.In(hosts) }) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 5462b5b61b9..ccd1eac77ea 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -13,6 +13,9 @@ DECONZ_DOMAIN = 'deconz' CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor' CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups' +SUPPORTED_PLATFORMS = ['binary_sensor', 'cover', + 'light', 'scene', 'sensor', 'switch'] + ATTR_DARK = 'dark' ATTR_ON = 'on' diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py new file mode 100644 index 00000000000..a64f9af886b --- /dev/null +++ b/homeassistant/components/deconz/gateway.py @@ -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) diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index d3bec079a4c..61f5ea39603 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -5,8 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/light.deconz/ """ from homeassistant.components.deconz.const import ( - CONF_ALLOW_DECONZ_GROUPS, DOMAIN as DATA_DECONZ, - DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN, + CONF_ALLOW_DECONZ_GROUPS, DOMAIN as DATA_DECONZ, DECONZ_DOMAIN, COVER_TYPES, SWITCH_TYPES) from homeassistant.components.light import ( 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)) 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)) @callback @@ -51,11 +50,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities.append(DeconzLight(group)) 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_add_light(hass.data[DATA_DECONZ].lights.values()) - async_add_group(hass.data[DATA_DECONZ].groups.values()) + async_add_light(hass.data[DATA_DECONZ].api.lights.values()) + async_add_group(hass.data[DATA_DECONZ].api.groups.values()) class DeconzLight(Light): @@ -81,7 +80,8 @@ class DeconzLight(Light): async def async_added_to_hass(self): """Subscribe to lights events.""" 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: """Disconnect light object when removed.""" @@ -214,7 +214,7 @@ class DeconzLight(Light): self._light.uniqueid.count(':') != 7): return None 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 { 'connections': {(CONNECTION_ZIGBEE, serial)}, 'identifiers': {(DECONZ_DOMAIN, serial)}, diff --git a/homeassistant/components/scene/deconz.py b/homeassistant/components/scene/deconz.py index b8fca6d8630..6319e52f6ef 100644 --- a/homeassistant/components/scene/deconz.py +++ b/homeassistant/components/scene/deconz.py @@ -4,8 +4,7 @@ Support for deCONZ scenes. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/scene.deconz/ """ -from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) +from homeassistant.components.deconz import DOMAIN as DATA_DECONZ from homeassistant.components.scene import Scene from homeassistant.core import callback 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: entities.append(DeconzScene(scene)) 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_add_scene(hass.data[DATA_DECONZ].scenes.values()) + async_add_scene(hass.data[DATA_DECONZ].api.scenes.values()) class DeconzScene(Scene): @@ -43,7 +42,8 @@ class DeconzScene(Scene): async def async_added_to_hass(self): """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: """Disconnect scene object when removed.""" diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index c66bda2bc1d..99f450d018e 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -6,7 +6,7 @@ https://home-assistant.io/components/sensor.deconz/ """ from homeassistant.components.deconz.const import ( 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, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY) from homeassistant.core import callback @@ -46,10 +46,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities.append(DeconzSensor(sensor)) 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_add_sensor(hass.data[DATA_DECONZ].sensors.values()) + async_add_sensor(hass.data[DATA_DECONZ].api.sensors.values()) class DeconzSensor(Entity): @@ -62,7 +62,8 @@ class DeconzSensor(Entity): async def async_added_to_hass(self): """Subscribe to sensors events.""" 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: """Disconnect sensor object when removed.""" @@ -147,7 +148,7 @@ class DeconzSensor(Entity): self._sensor.uniqueid.count(':') != 7): return None 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 { 'connections': {(CONNECTION_ZIGBEE, serial)}, 'identifiers': {(DECONZ_DOMAIN, serial)}, @@ -171,7 +172,8 @@ class DeconzBattery(Entity): async def async_added_to_hass(self): """Subscribe to sensors events.""" 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: """Disconnect sensor object when removed.""" @@ -181,7 +183,7 @@ class DeconzBattery(Entity): @callback def async_update_callback(self, reason): """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() @property @@ -229,7 +231,7 @@ class DeconzBattery(Entity): self._sensor.uniqueid.count(':') != 7): return None 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 { 'connections': {(CONNECTION_ZIGBEE, serial)}, 'identifiers': {(DECONZ_DOMAIN, serial)}, diff --git a/homeassistant/components/switch/deconz.py b/homeassistant/components/switch/deconz.py index f8911d65d98..4c2fcca052c 100644 --- a/homeassistant/components/switch/deconz.py +++ b/homeassistant/components/switch/deconz.py @@ -5,8 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.deconz/ """ from homeassistant.components.deconz.const import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, - DECONZ_DOMAIN, POWER_PLUGS, SIRENS) + DOMAIN as DATA_DECONZ, DECONZ_DOMAIN, POWER_PLUGS, SIRENS) from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback 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)) 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_add_switch(hass.data[DATA_DECONZ].lights.values()) + async_add_switch(hass.data[DATA_DECONZ].api.lights.values()) class DeconzSwitch(SwitchDevice): @@ -53,7 +52,8 @@ class DeconzSwitch(SwitchDevice): async def async_added_to_hass(self): """Subscribe to switches events.""" 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: """Disconnect switch object when removed.""" @@ -92,7 +92,7 @@ class DeconzSwitch(SwitchDevice): self._switch.uniqueid.count(':') != 7): return None 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 { 'connections': {(CONNECTION_ZIGBEE, serial)}, 'identifiers': {(DECONZ_DOMAIN, serial)}, diff --git a/tests/components/binary_sensor/test_deconz.py b/tests/components/binary_sensor/test_deconz.py index 5fd6e132e03..ba39afa0e88 100644 --- a/tests/components/binary_sensor/test_deconz.py +++ b/tests/components/binary_sensor/test_deconz.py @@ -4,6 +4,9 @@ from unittest.mock import Mock, patch from homeassistant import config_entries from homeassistant.components import deconz 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 @@ -14,7 +17,8 @@ SENSOR = { "name": "Sensor 1 name", "type": "ZHAPresence", "state": {"presence": False}, - "config": {} + "config": {}, + "uniqueid": "00:00:00:00:00:00:00:00-00" }, "2": { "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.""" from pydeconz import DeconzSession loop = Mock() session = Mock() - entry = Mock() - entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} - bridge = DeconzSession(loop, session, **entry.data) - bridge.config = Mock() + + ENTRY_CONFIG[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor + + 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', return_value=mock_coro(data)): - await bridge.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 gateway.api.async_load_parameters() + await hass.config_entries.async_forward_entry_setup( config_entry, 'binary_sensor') # To flush out the service call to update the group 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): """Test that no sensors in deconz results in no sensor entities.""" data = {} - await setup_bridge(hass, data) - assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + await setup_gateway(hass, data) + assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0 assert len(hass.states.async_all()) == 0 async def test_binary_sensors(hass): """Test successful creation of binary sensor entities.""" data = {"sensors": SENSOR} - await setup_bridge(hass, data) - assert "binary_sensor.sensor_1_name" in hass.data[deconz.DATA_DECONZ_ID] + await setup_gateway(hass, data) + assert "binary_sensor.sensor_1_name" in \ + hass.data[deconz.DOMAIN].deconz_ids 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 + hass.data[deconz.DOMAIN].api.sensors['1'].async_update( + {'state': {'on': False}}) + async def test_add_new_sensor(hass): """Test successful creation of sensor entities.""" data = {} - await setup_bridge(hass, data) + await setup_gateway(hass, data) sensor = Mock() sensor.name = 'name' sensor.type = 'ZHAPresence' sensor.register_async_callback = Mock() async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) 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): """Test that clip sensors can be ignored.""" data = {} - await setup_bridge(hass, data, allow_clip_sensor=False) + await setup_gateway(hass, data, allow_clip_sensor=False) sensor = Mock() sensor.name = 'name' sensor.type = 'CLIPPresence' sensor.register_async_callback = Mock() async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) 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 diff --git a/tests/components/cover/test_deconz.py b/tests/components/cover/test_deconz.py index e9c630823bd..b021bcb8d51 100644 --- a/tests/components/cover/test_deconz.py +++ b/tests/components/cover/test_deconz.py @@ -5,6 +5,9 @@ from homeassistant import config_entries from homeassistant.components import deconz from homeassistant.components.deconz.const import COVER_TYPES 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 @@ -13,14 +16,15 @@ SUPPORTED_COVERS = { "id": "Cover 1 id", "name": "Cover 1 name", "type": "Level controllable output", - "state": {}, - "modelid": "Not zigbee spec" + "state": {"bri": 255, "reachable": True}, + "modelid": "Not zigbee spec", + "uniqueid": "00:00:00:00:00:00:00:00-00" }, "2": { "id": "Cover 2 id", "name": "Cover 2 name", "type": "Window covering device", - "state": {}, + "state": {"bri": 255, "reachable": True}, "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.""" from pydeconz import DeconzSession loop = Mock() session = Mock() - entry = Mock() - entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} - bridge = DeconzSession(loop, session, **entry.data) + + 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', return_value=mock_coro(data)): - await bridge.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 gateway.api.async_load_parameters() + await hass.config_entries.async_forward_entry_setup(config_entry, 'cover') # To flush out the service call to update the group 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.""" - data = {} - await setup_bridge(hass, data) - assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + await setup_gateway(hass, {}) + assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0 assert len(hass.states.async_all()) == 0 async def test_cover(hass): """Test that all supported cover entities are created.""" - await setup_bridge(hass, {"lights": SUPPORTED_COVERS}) - assert "cover.cover_1_name" in hass.data[deconz.DATA_DECONZ_ID] + with patch('pydeconz.DeconzSession.async_put_state', + 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(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): """Test successful creation of cover entity.""" data = {} - await setup_bridge(hass, data) + await setup_gateway(hass, data) cover = Mock() cover.name = 'name' cover.type = "Level controllable output" cover.register_async_callback = Mock() async_dispatcher_send(hass, 'deconz_new_light', [cover]) 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): """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 + + +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 diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 111cfbe9697..20b7a88bc05 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -20,7 +20,7 @@ async def test_flow_works(hass, aioclient_mock): flow = config_flow.DeconzFlowHandler() flow.hass = hass - await flow.async_step_init() + await flow.async_step_user() await flow.async_step_link(user_input={}) result = await flow.async_step_options( 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.hass = hass - result = await flow.async_step_init() + result = await flow.async_step_user() assert result['type'] == 'abort' @@ -55,7 +55,7 @@ async def test_flow_no_discovered_bridges(hass, aioclient_mock): flow = config_flow.DeconzFlowHandler() flow.hass = hass - result = await flow.async_step_init() + result = await flow.async_step_user() assert result['type'] == 'abort' @@ -67,7 +67,7 @@ async def test_flow_one_bridge_discovered(hass, aioclient_mock): flow = config_flow.DeconzFlowHandler() flow.hass = hass - result = await flow.async_step_init() + result = await flow.async_step_user() assert result['type'] == 'form' assert result['step_id'] == 'link' @@ -81,9 +81,9 @@ async def test_flow_two_bridges_discovered(hass, aioclient_mock): flow = config_flow.DeconzFlowHandler() flow.hass = hass - result = await flow.async_step_init() + result = await flow.async_step_user() assert result['type'] == 'form' - assert result['step_id'] == 'init' + assert result['step_id'] == 'user' with pytest.raises(vol.Invalid): 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'}) +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): """Test config flow should abort if no API key was possible to retrieve.""" aioclient_mock.post('http://1.2.3.4:80/api', json=[]) diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index 8cc8c4bc242..3453dd86c12 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -1,11 +1,11 @@ """Test deCONZ component setup process.""" 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 tests.common import mock_coro +from homeassistant.components import deconz + +from tests.common import mock_coro, MockConfigEntry + CONFIG = { "config": { @@ -99,173 +99,113 @@ async def test_setup_entry_no_available_bridge(hass): async def test_setup_entry_successful(hass): """Test setup entry is successful.""" - entry = Mock() - entry.data = {'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, \ - patch('pydeconz.DeconzSession.async_get_state', - return_value=mock_coro(CONFIG)), \ - patch('pydeconz.DeconzSession.start', return_value=True), \ + 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())): + 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 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): """Test being able to unload an entry.""" - entry = Mock() - entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} - entry.async_unload.return_value = mock_coro(True) - deconzmock = Mock() - deconzmock.async_load_parameters.return_value = mock_coro(True) - deconzmock.sensors = {} - with patch('pydeconz.DeconzSession', return_value=deconzmock): + 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 - assert deconz.DATA_DECONZ_EVENT in hass.data - - hass.data[deconz.DATA_DECONZ_EVENT].append(Mock()) - hass.data[deconz.DATA_DECONZ_ID] = {'id': 'deconzid'} + mock_gateway.return_value.async_reset.return_value = mock_coro(True) assert await deconz.async_unload_entry(hass, entry) 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): """Test that service invokes pydeconz with the correct path and data.""" - entry = Mock() - entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} - with patch('pydeconz.DeconzSession.async_get_state', - return_value=mock_coro(CONFIG)), \ - patch('pydeconz.DeconzSession.start', return_value=True), \ + 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())): + 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 - hass.data[DATA_DECONZ_ID] = { + hass.data[deconz.DOMAIN].deconz_ids = { 'light.test': '/light/1' } data = {'on': True, 'attr1': 10, 'attr2': 20} # 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={ 'field': '/light/42', 'data': data }) await hass.async_block_till_done() - async_put_state.assert_called_with('/light/42', data) + # 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={ 'entity': 'light.test', 'data': data }) await hass.async_block_till_done() - async_put_state.assert_called_with('/light/1', data) + # 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={ 'entity': 'light.test', 'field': '/state', 'data': data}) await hass.async_block_till_done() - async_put_state.assert_called_with('/light/1/state', data) # 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={ 'entity': 'light.nonexisting', 'field': '/state', 'data': data}) await hass.async_block_till_done() - async_put_state.assert_not_called() + # 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={ 'entity': 'light.test', 'field': 'state', 'data': data}) 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() diff --git a/tests/components/light/test_deconz.py b/tests/components/light/test_deconz.py index 96f180505b8..081fd61ec4e 100644 --- a/tests/components/light/test_deconz.py +++ b/tests/components/light/test_deconz.py @@ -4,6 +4,9 @@ from unittest.mock import Mock, patch from homeassistant import config_entries from homeassistant.components import deconz 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 @@ -12,7 +15,18 @@ LIGHT = { "1": { "id": "Light 1 id", "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": { "id": "Group 1 id", "name": "Group 1 name", + "type": "LightGroup", "state": {}, "action": {}, "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.""" from pydeconz import DeconzSession loop = Mock() session = Mock() - entry = Mock() - entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} - bridge = DeconzSession(loop, session, **entry.data) - bridge.config = Mock() + + ENTRY_CONFIG[deconz.const.CONF_ALLOW_DECONZ_GROUPS] = allow_deconz_groups + + 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', return_value=mock_coro(data)): - await bridge.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 gateway.api.async_load_parameters() + await hass.config_entries.async_forward_entry_setup(config_entry, 'light') # To flush out the service call to update the group 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): """Test that no lights or groups entities are created.""" - data = {} - await setup_bridge(hass, data) - assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + await setup_gateway(hass, {}) + assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0 assert len(hass.states.async_all()) == 0 async def test_lights_and_groups(hass): """Test that lights or groups entities are created.""" - await setup_bridge(hass, {"lights": LIGHT, "groups": GROUP}) - assert "light.light_1_name" in hass.data[deconz.DATA_DECONZ_ID] - assert "light.group_1_name" in hass.data[deconz.DATA_DECONZ_ID] - assert "light.group_2_name" not in hass.data[deconz.DATA_DECONZ_ID] - assert len(hass.states.async_all()) == 3 + with patch('pydeconz.DeconzSession.async_put_state', + return_value=mock_coro(True)): + await setup_gateway(hass, {"lights": LIGHT, "groups": GROUP}) + assert "light.light_1_name" in hass.data[deconz.DOMAIN].deconz_ids + 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): """Test successful creation of light entities.""" - data = {} - await setup_bridge(hass, data) + await setup_gateway(hass, {}) light = Mock() light.name = 'name' light.register_async_callback = Mock() async_dispatcher_send(hass, 'deconz_new_light', [light]) 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): """Test successful creation of group entities.""" - data = {} - await setup_bridge(hass, data) + await setup_gateway(hass, {}) group = Mock() group.name = 'name' group.register_async_callback = Mock() async_dispatcher_send(hass, 'deconz_new_group', [group]) 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): """Test that clip sensors can be ignored.""" - data = {} - await setup_bridge(hass, data, allow_deconz_groups=False) + await setup_gateway(hass, {}, allow_deconz_groups=False) group = Mock() group.name = 'name' group.register_async_callback = Mock() async_dispatcher_send(hass, 'deconz_new_group', [group]) 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): """Test that a switch doesn't get created as a light entity.""" - await setup_bridge(hass, {"lights": SWITCH}) - assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + await setup_gateway(hass, {"lights": SWITCH}) + assert len(hass.data[deconz.DOMAIN].deconz_ids) == 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 diff --git a/tests/components/scene/test_deconz.py b/tests/components/scene/test_deconz.py index 89bb5297e78..788c6dc1c3e 100644 --- a/tests/components/scene/test_deconz.py +++ b/tests/components/scene/test_deconz.py @@ -1,8 +1,11 @@ -"""deCONZ scenes platform tests.""" +"""deCONZ scene platform tests.""" from unittest.mock import Mock, patch from homeassistant import config_entries from homeassistant.components import deconz +from homeassistant.setup import async_setup_component + +import homeassistant.components.scene as scene 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.""" from pydeconz import DeconzSession loop = Mock() session = Mock() - entry = Mock() - entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} - bridge = DeconzSession(loop, session, **entry.data) + + 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', return_value=mock_coro(data)): - await bridge.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 gateway.api.async_load_parameters() + await hass.config_entries.async_forward_entry_setup(config_entry, 'scene') # To flush out the service call to update the group 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): - """Test the update_lights function with some lights.""" - data = {} - await setup_bridge(hass, data) - assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + """Test that scenes can be loaded without scenes being available.""" + await setup_gateway(hass, {}) + assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0 assert len(hass.states.async_all()) == 0 async def test_scenes(hass): - """Test the update_lights function with some lights.""" - data = {"groups": GROUP} - await setup_bridge(hass, data) - assert "scene.group_1_name_scene_1" in hass.data[deconz.DATA_DECONZ_ID] + """Test that scenes works.""" + with patch('pydeconz.DeconzSession.async_put_state', + return_value=mock_coro(True)): + 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 + + 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 diff --git a/tests/components/sensor/test_deconz.py b/tests/components/sensor/test_deconz.py index ae9e75d6a41..f5cfbe2c183 100644 --- a/tests/components/sensor/test_deconz.py +++ b/tests/components/sensor/test_deconz.py @@ -1,10 +1,12 @@ """deCONZ sensor platform tests.""" from unittest.mock import Mock, patch - from homeassistant import config_entries from homeassistant.components import deconz 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 @@ -13,9 +15,10 @@ SENSOR = { "1": { "id": "Sensor 1 id", "name": "Sensor 1 name", - "type": "ZHATemperature", - "state": {"temperature": False}, - "config": {} + "type": "ZHALightLevel", + "state": {"lightlevel": 30000, "dark": False}, + "config": {"reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:00-00" }, "2": { "id": "Sensor 2 id", @@ -36,80 +39,134 @@ SENSOR = { "name": "Sensor 4 name", "type": "ZHASwitch", "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.""" from pydeconz import DeconzSession loop = Mock() session = Mock() - entry = Mock() - entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} - bridge = DeconzSession(loop, session, **entry.data) - bridge.config = Mock() + + ENTRY_CONFIG[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor + + 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', return_value=mock_coro(data)): - await bridge.async_load_parameters() - hass.data[deconz.DOMAIN] = bridge - hass.data[deconz.DATA_DECONZ_UNSUB] = [] - hass.data[deconz.DATA_DECONZ_EVENT] = [] - 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') + await gateway.api.async_load_parameters() + + await hass.config_entries.async_forward_entry_setup( + config_entry, 'sensor') # To flush out the service call to update the group 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): """Test that no sensors in deconz results in no sensor entities.""" - data = {} - await setup_bridge(hass, data) - assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + await setup_gateway(hass, {}) + assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0 assert len(hass.states.async_all()) == 0 async def test_sensors(hass): """Test successful creation of sensor entities.""" - data = {"sensors": SENSOR} - await setup_bridge(hass, data) - assert "sensor.sensor_1_name" in hass.data[deconz.DATA_DECONZ_ID] - assert "sensor.sensor_2_name" not in hass.data[deconz.DATA_DECONZ_ID] - assert "sensor.sensor_3_name" not in hass.data[deconz.DATA_DECONZ_ID] + await setup_gateway(hass, {"sensors": SENSOR}) + assert "sensor.sensor_1_name" in hass.data[deconz.DOMAIN].deconz_ids + assert "sensor.sensor_2_name" not in hass.data[deconz.DOMAIN].deconz_ids + assert "sensor.sensor_3_name" not in hass.data[deconz.DOMAIN].deconz_ids assert "sensor.sensor_3_name_battery_level" not in \ - hass.data[deconz.DATA_DECONZ_ID] - assert "sensor.sensor_4_name" not in hass.data[deconz.DATA_DECONZ_ID] + hass.data[deconz.DOMAIN].deconz_ids + assert "sensor.sensor_4_name" not in hass.data[deconz.DOMAIN].deconz_ids assert "sensor.sensor_4_name_battery_level" in \ - hass.data[deconz.DATA_DECONZ_ID] - assert len(hass.states.async_all()) == 2 + hass.data[deconz.DOMAIN].deconz_ids + 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): """Test successful creation of sensor entities.""" - data = {} - await setup_bridge(hass, data) + await setup_gateway(hass, {}) sensor = Mock() sensor.name = 'name' sensor.type = 'ZHATemperature' sensor.register_async_callback = Mock() async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) 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): """Test that clip sensors can be ignored.""" - data = {} - await setup_bridge(hass, data, allow_clip_sensor=False) + await setup_gateway(hass, {}, allow_clip_sensor=False) sensor = Mock() sensor.name = 'name' sensor.type = 'CLIPTemperature' sensor.register_async_callback = Mock() async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) 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 diff --git a/tests/components/switch/test_deconz.py b/tests/components/switch/test_deconz.py index 6833cab33d7..245be27961d 100644 --- a/tests/components/switch/test_deconz.py +++ b/tests/components/switch/test_deconz.py @@ -5,6 +5,9 @@ from homeassistant import config_entries from homeassistant.components import deconz from homeassistant.components.deconz.const import SWITCH_TYPES 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 @@ -13,19 +16,20 @@ SUPPORTED_SWITCHES = { "id": "Switch 1 id", "name": "Switch 1 name", "type": "On/Off plug-in unit", - "state": {} + "state": {"on": True, "reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:00-00" }, "2": { "id": "Switch 2 id", "name": "Switch 2 name", "type": "Smart plug", - "state": {} + "state": {"on": True, "reachable": True} }, "3": { "id": "Switch 3 id", "name": "Switch 3 name", "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.""" from pydeconz import DeconzSession loop = Mock() session = Mock() - entry = Mock() - entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} - 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', return_value=mock_coro(data)): - await bridge.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 gateway.api.async_load_parameters() + await hass.config_entries.async_forward_entry_setup(config_entry, 'switch') # To flush out the service call to update the group 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): """Test that no switch entities are created.""" - data = {} - await setup_bridge(hass, data) - assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + await setup_gateway(hass, {}) + assert len(hass.data[deconz.DOMAIN].deconz_ids) == 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.""" - await setup_bridge(hass, {"lights": SUPPORTED_SWITCHES}) - assert "switch.switch_1_name" in hass.data[deconz.DATA_DECONZ_ID] - assert "switch.switch_2_name" in hass.data[deconz.DATA_DECONZ_ID] - assert "switch.switch_3_name" in hass.data[deconz.DATA_DECONZ_ID] + with patch('pydeconz.DeconzSession.async_put_state', + return_value=mock_coro(True)): + await setup_gateway(hass, {"lights": SUPPORTED_SWITCHES}) + 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(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): """Test successful creation of switch entity.""" - data = {} - await setup_bridge(hass, data) + await setup_gateway(hass, {}) switch = Mock() switch.name = 'name' switch.type = "Smart plug" switch.register_async_callback = Mock() async_dispatcher_send(hass, 'deconz_new_light', [switch]) 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): """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 + + +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