From b50afec5f11e07352f40f9bdbbaf5bff7a6b11ff Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 5 Apr 2019 02:48:24 +0200 Subject: [PATCH] Support multiple deCONZ gateways (#22449) * Store gateways inside a dict in deconz domain * Make reachable events gateway specific * Gateway shall always exist * Adapt new device signalling to support multiple gateways * Services follow gateway master * Working on unload entry * Make unload and master handover work Improve tests for init * Fix config flow * Fix linting * Clean up init tests * Clean up hassio discovery to fit with the rest * Store gateways inside a dict in deconz domain * Make reachable events gateway specific * Gateway shall always exist * Adapt new device signalling to support multiple gateways * Services follow gateway master * Working on unload entry * Make unload and master handover work Improve tests for init * Fix config flow * Fix linting * Clean up init tests * Clean up hassio discovery to fit with the rest * Add support for services to specify bridgeid --- homeassistant/components/deconz/__init__.py | 96 +++++--- .../components/deconz/binary_sensor.py | 19 +- homeassistant/components/deconz/climate.py | 19 +- .../components/deconz/config_flow.py | 115 ++++------ homeassistant/components/deconz/const.py | 19 +- homeassistant/components/deconz/cover.py | 14 +- .../components/deconz/deconz_device.py | 5 +- homeassistant/components/deconz/gateway.py | 73 +++++- homeassistant/components/deconz/light.py | 22 +- homeassistant/components/deconz/scene.py | 12 +- homeassistant/components/deconz/sensor.py | 20 +- homeassistant/components/deconz/services.yaml | 8 +- homeassistant/components/deconz/switch.py | 13 +- tests/components/deconz/test_binary_sensor.py | 35 +-- tests/components/deconz/test_climate.py | 47 ++-- tests/components/deconz/test_config_flow.py | 73 ++---- tests/components/deconz/test_cover.py | 24 +- tests/components/deconz/test_init.py | 208 ++++++++++++------ tests/components/deconz/test_light.py | 49 +++-- tests/components/deconz/test_scene.py | 15 +- tests/components/deconz/test_sensor.py | 47 ++-- tests/components/deconz/test_switch.py | 28 +-- 22 files changed, 535 insertions(+), 426 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 8bdd946e2ef..ff1ee2bf06e 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -4,12 +4,14 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC # Loading the config flow file will register the flow -from .config_flow import configured_hosts -from .const import DEFAULT_PORT, DOMAIN, _LOGGER +from .config_flow import get_master_gateway +from .const import ( + CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_DECONZ_GROUPS, CONF_BRIDGEID, + CONF_MASTER_GATEWAY, DEFAULT_PORT, DOMAIN, _LOGGER) from .gateway import DeconzGateway REQUIREMENTS = ['pydeconz==54'] @@ -32,26 +34,27 @@ SERVICE_SCHEMA = vol.All(vol.Schema({ vol.Optional(SERVICE_ENTITY): cv.entity_id, vol.Optional(SERVICE_FIELD): cv.matches_regex('/.*'), vol.Required(SERVICE_DATA): dict, + vol.Optional(CONF_BRIDGEID): str }), cv.has_at_least_one_key(SERVICE_ENTITY, SERVICE_FIELD)) SERVICE_DEVICE_REFRESH = 'device_refresh' +SERVICE_DEVICE_REFRESCH_SCHEMA = vol.All(vol.Schema({ + vol.Optional(CONF_BRIDGEID): str +})) + async def async_setup(hass, config): """Load configuration for deCONZ component. Discovery has loaded the component if DOMAIN is not present in config. """ - if DOMAIN in config: - deconz_config = None - if CONF_HOST in config[DOMAIN]: - deconz_config = config[DOMAIN] - if deconz_config and not configured_hosts(hass): - hass.async_add_job(hass.config_entries.flow.async_init( - DOMAIN, - context={'source': config_entries.SOURCE_IMPORT}, - data=deconz_config - )) + if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config: + deconz_config = config[DOMAIN] + hass.async_add_job(hass.config_entries.flow.async_init( + DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, + data=deconz_config + )) return True @@ -61,26 +64,20 @@ 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. """ - if DOMAIN in hass.data: - _LOGGER.error( - "Config entry failed since one deCONZ instance already exists") - return False + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + + if not config_entry.options: + await async_populate_options(hass, config_entry) gateway = DeconzGateway(hass, config_entry) if not await gateway.async_setup(): return False - hass.data[DOMAIN] = gateway + hass.data[DOMAIN][gateway.bridgeid] = gateway - 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, 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) + await gateway.async_update_device_registry() async def async_configure(call): """Set attribute of device in deCONZ. @@ -100,8 +97,11 @@ 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) - gateway = hass.data[DOMAIN] + data = call.data[SERVICE_DATA] + + gateway = get_master_gateway(hass) + if CONF_BRIDGEID in call.data: + gateway = hass.data[DOMAIN][call.data[CONF_BRIDGEID]] if entity_id: try: @@ -117,7 +117,9 @@ async def async_setup_entry(hass, config_entry): async def async_refresh_devices(call): """Refresh available devices from deCONZ.""" - gateway = hass.data[DOMAIN] + gateway = get_master_gateway(hass) + if CONF_BRIDGEID in call.data: + gateway = hass.data[DOMAIN][call.data[CONF_BRIDGEID]] groups = set(gateway.api.groups.keys()) lights = set(gateway.api.lights.keys()) @@ -151,7 +153,8 @@ async def async_setup_entry(hass, config_entry): ) hass.services.async_register( - DOMAIN, SERVICE_DEVICE_REFRESH, async_refresh_devices) + DOMAIN, SERVICE_DEVICE_REFRESH, async_refresh_devices, + schema=SERVICE_DEVICE_REFRESCH_SCHEMA) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gateway.shutdown) return True @@ -159,7 +162,34 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload deCONZ config entry.""" - gateway = hass.data.pop(DOMAIN) - hass.services.async_remove(DOMAIN, SERVICE_DECONZ) - hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH) + gateway = hass.data[DOMAIN].pop(config_entry.data[CONF_BRIDGEID]) + + if not hass.data[DOMAIN]: + hass.services.async_remove(DOMAIN, SERVICE_DECONZ) + hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH) + elif gateway.master: + await async_populate_options(hass, config_entry) + new_master_gateway = next(iter(hass.data[DOMAIN].values())) + await async_populate_options(hass, new_master_gateway.config_entry) + return await gateway.async_reset() + + +@callback +async def async_populate_options(hass, config_entry): + """Populate default options for gateway. + + Called by setup_entry and unload_entry. + Makes sure there is always one master available. + """ + master = not get_master_gateway(hass) + + options = { + CONF_MASTER_GATEWAY: master, + CONF_ALLOW_CLIP_SENSOR: config_entry.data.get( + CONF_ALLOW_CLIP_SENSOR, False), + CONF_ALLOW_DECONZ_GROUPS: config_entry.data.get( + CONF_ALLOW_DECONZ_GROUPS, True) + } + + hass.config_entries.async_update_entry(config_entry, options=options) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 2b0c2037248..70de1fd7cf4 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -4,10 +4,9 @@ from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import ( - ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DECONZ_DOMAIN, - NEW_SENSOR) +from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice +from .gateway import get_gateway_from_config_entry DEPENDENCIES = ['deconz'] @@ -24,22 +23,26 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ binary sensor.""" - gateway = hass.data[DECONZ_DOMAIN] + gateway = get_gateway_from_config_entry(hass, config_entry) @callback def async_add_sensor(sensors): """Add binary sensor from deCONZ.""" from pydeconz.sensor import DECONZ_BINARY_SENSOR entities = [] - allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True) + for sensor in sensors: + if sensor.type in DECONZ_BINARY_SENSOR and \ - not (not allow_clip_sensor and sensor.type.startswith('CLIP')): + not (not gateway.allow_clip_sensor and + sensor.type.startswith('CLIP')): + entities.append(DeconzBinarySensor(sensor, gateway)) + async_add_entities(entities, True) - gateway.listeners.append( - async_dispatcher_connect(hass, NEW_SENSOR, async_add_sensor)) + gateway.listeners.append(async_dispatcher_connect( + hass, gateway.async_event_new_device(NEW_SENSOR), async_add_sensor)) async_add_sensor(gateway.api.sensors.values()) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 1f39b8705c7..c4327d3c497 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -7,10 +7,9 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import ( - ATTR_OFFSET, ATTR_VALVE, CONF_ALLOW_CLIP_SENSOR, - DOMAIN as DECONZ_DOMAIN, NEW_SENSOR) +from .const import ATTR_OFFSET, ATTR_VALVE, NEW_SENSOR from .deconz_device import DeconzDevice +from .gateway import get_gateway_from_config_entry DEPENDENCIES = ['deconz'] @@ -20,22 +19,26 @@ async def async_setup_entry(hass, config_entry, async_add_entities): Thermostats are based on the same device class as sensors in deCONZ. """ - gateway = hass.data[DECONZ_DOMAIN] + gateway = get_gateway_from_config_entry(hass, config_entry) @callback def async_add_climate(sensors): """Add climate devices from deCONZ.""" from pydeconz.sensor import THERMOSTAT entities = [] - allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True) + for sensor in sensors: + if sensor.type in THERMOSTAT and \ - not (not allow_clip_sensor and sensor.type.startswith('CLIP')): + not (not gateway.allow_clip_sensor and + sensor.type.startswith('CLIP')): + entities.append(DeconzThermostat(sensor, gateway)) + async_add_entities(entities, True) - gateway.listeners.append( - async_dispatcher_connect(hass, NEW_SENSOR, async_add_climate)) + gateway.listeners.append(async_dispatcher_connect( + hass, gateway.async_event_new_device(NEW_SENSOR), async_add_climate)) async_add_climate(gateway.api.sensors.values()) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 38849fb37b3..1ecfee7ada5 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -9,10 +9,7 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.helpers import aiohttp_client -from .const import ( - CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_DECONZ_GROUPS, CONF_BRIDGEID, - DEFAULT_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_DECONZ_GROUPS, DEFAULT_PORT, - DOMAIN) +from .const import CONF_BRIDGEID, DEFAULT_PORT, DOMAIN @callback @@ -22,6 +19,14 @@ def configured_hosts(hass): in hass.config_entries.async_entries(DOMAIN)) +@callback +def get_master_gateway(hass): + """Return a bool telling if this is the master gateway.""" + for gateway in hass.data[DOMAIN].values(): + if gateway.master: + return gateway + + @config_entries.HANDLERS.register(DOMAIN) class DeconzFlowHandler(config_entries.ConfigFlow): """Handle a deCONZ config flow.""" @@ -39,16 +44,12 @@ class DeconzFlowHandler(config_entries.ConfigFlow): async def async_step_user(self, user_input=None): """Handle a deCONZ config flow start. - Only allows one instance to be set up. If only one bridge is found go to link step. If more than one bridge is found let user choose bridge to link. If no bridge is found allow user to manually input configuration. """ from pydeconz.utils import async_discovery - if configured_hosts(self.hass): - return self.async_abort(reason='one_instance_only') - if user_input is not None: for bridge in self.bridges: if bridge[CONF_HOST] == user_input[CONF_HOST]: @@ -99,9 +100,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow): errors = {} if user_input is not None: - if configured_hosts(self.hass): - return self.async_abort(reason='one_instance_only') - session = aiohttp_client.async_get_clientsession(self.hass) try: @@ -114,51 +112,32 @@ class DeconzFlowHandler(config_entries.ConfigFlow): else: self.deconz_config[CONF_API_KEY] = api_key - return await self.async_step_options() + return await self._create_entry() return self.async_show_form( step_id='link', errors=errors, ) - async def async_step_options(self, user_input=None): - """Extra options for deCONZ. - - CONF_CLIP_SENSOR -- Allow user to choose if they want clip sensors. - CONF_DECONZ_GROUPS -- Allow user to choose if they want deCONZ groups. - """ + async def _create_entry(self): + """Create entry for gateway.""" from pydeconz.utils import async_get_bridgeid - if user_input is not None: - self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = \ - user_input[CONF_ALLOW_CLIP_SENSOR] - self.deconz_config[CONF_ALLOW_DECONZ_GROUPS] = \ - user_input[CONF_ALLOW_DECONZ_GROUPS] + if CONF_BRIDGEID not in self.deconz_config: + session = aiohttp_client.async_get_clientsession(self.hass) - if CONF_BRIDGEID not in self.deconz_config: - session = aiohttp_client.async_get_clientsession(self.hass) - try: - with async_timeout.timeout(10): - self.deconz_config[CONF_BRIDGEID] = \ - await async_get_bridgeid( - session, **self.deconz_config) + try: + with async_timeout.timeout(10): + self.deconz_config[CONF_BRIDGEID] = \ + await async_get_bridgeid( + session, **self.deconz_config) - except asyncio.TimeoutError: - return self.async_abort(reason='no_bridges') + except asyncio.TimeoutError: + return self.async_abort(reason='no_bridges') - return self.async_create_entry( - title='deCONZ-' + self.deconz_config[CONF_BRIDGEID], - data=self.deconz_config - ) - - return self.async_show_form( - step_id='options', - data_schema=vol.Schema({ - vol.Optional(CONF_ALLOW_CLIP_SENSOR, - default=DEFAULT_ALLOW_CLIP_SENSOR): bool, - vol.Optional(CONF_ALLOW_DECONZ_GROUPS, - default=DEFAULT_ALLOW_DECONZ_GROUPS): bool, - }), + return self.async_create_entry( + title='deCONZ-' + self.deconz_config[CONF_BRIDGEID], + data=self.deconz_config ) async def async_step_discovery(self, discovery_info): @@ -166,10 +145,14 @@ class DeconzFlowHandler(config_entries.ConfigFlow): This flow is triggered by the discovery component. """ - deconz_config = {} - deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST) - deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT) - deconz_config[CONF_BRIDGEID] = discovery_info.get('serial') + deconz_config = { + CONF_HOST: discovery_info[CONF_HOST], + CONF_PORT: discovery_info[CONF_PORT], + CONF_BRIDGEID: discovery_info['serial'] + } + + if deconz_config[CONF_HOST] in configured_hosts(self.hass): + return self.async_abort(reason='one_instance_only') return await self.async_step_import(deconz_config) @@ -186,16 +169,11 @@ class DeconzFlowHandler(config_entries.ConfigFlow): Otherwise we will delegate to `link` step which will ask user to link the bridge. """ - if configured_hosts(self.hass): - return self.async_abort(reason='one_instance_only') - self.deconz_config = import_config if CONF_API_KEY not in import_config: return await self.async_step_link() - user_input = {CONF_ALLOW_CLIP_SENSOR: True, - CONF_ALLOW_DECONZ_GROUPS: True} - return await self.async_step_options(user_input=user_input) + return await self._create_entry() async def async_step_hassio(self, user_input=None): """Prepare configuration for a Hass.io deCONZ bridge. @@ -212,29 +190,18 @@ class DeconzFlowHandler(config_entries.ConfigFlow): async def async_step_hassio_confirm(self, user_input=None): """Confirm a Hass.io discovery.""" if user_input is not None: - data = self._hassio_discovery + self.deconz_config = { + CONF_HOST: self._hassio_discovery[CONF_HOST], + CONF_PORT: self._hassio_discovery[CONF_PORT], + CONF_BRIDGEID: self._hassio_discovery['serial'], + CONF_API_KEY: self._hassio_discovery[CONF_API_KEY] + } - return self.async_create_entry( - title=data['addon'], data={ - CONF_HOST: data[CONF_HOST], - CONF_PORT: data[CONF_PORT], - CONF_BRIDGEID: data['serial'], - CONF_API_KEY: data[CONF_API_KEY], - CONF_ALLOW_CLIP_SENSOR: - user_input[CONF_ALLOW_CLIP_SENSOR], - CONF_ALLOW_DECONZ_GROUPS: - user_input[CONF_ALLOW_DECONZ_GROUPS], - }) + return await self._create_entry() return self.async_show_form( step_id='hassio_confirm', description_placeholders={ 'addon': self._hassio_discovery['addon'] - }, - data_schema=vol.Schema({ - vol.Optional(CONF_ALLOW_CLIP_SENSOR, - default=DEFAULT_ALLOW_CLIP_SENSOR): bool, - vol.Optional(CONF_ALLOW_DECONZ_GROUPS, - default=DEFAULT_ALLOW_DECONZ_GROUPS): bool, - }) + } ) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index b26fddd9147..bf0f5884073 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -12,22 +12,21 @@ DEFAULT_ALLOW_DECONZ_GROUPS = False CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor' CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups' CONF_BRIDGEID = 'bridgeid' +CONF_MASTER_GATEWAY = 'master' SUPPORTED_PLATFORMS = ['binary_sensor', 'climate', 'cover', 'light', 'scene', 'sensor', 'switch'] -DECONZ_REACHABLE = 'deconz_reachable' - -NEW_GROUP = 'deconz_new_group' -NEW_LIGHT = 'deconz_new_light' -NEW_SCENE = 'deconz_new_scene' -NEW_SENSOR = 'deconz_new_sensor' +NEW_GROUP = 'group' +NEW_LIGHT = 'light' +NEW_SCENE = 'scene' +NEW_SENSOR = 'sensor' NEW_DEVICE = { - 'group': NEW_GROUP, - 'light': NEW_LIGHT, - 'scene': NEW_SCENE, - 'sensor': NEW_SENSOR + NEW_GROUP: 'deconz_new_group_{}', + NEW_LIGHT: 'deconz_new_light_{}', + NEW_SCENE: 'deconz_new_scene_{}', + NEW_SENSOR: 'deconz_new_sensor_{}' } ATTR_DARK = 'dark' diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index fda4fe4309c..903c1160eb8 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -5,9 +5,9 @@ from homeassistant.components.cover import ( from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import ( - COVER_TYPES, DAMPERS, DOMAIN as DECONZ_DOMAIN, NEW_LIGHT, WINDOW_COVERS) +from .const import COVER_TYPES, DAMPERS, NEW_LIGHT, WINDOW_COVERS from .deconz_device import DeconzDevice +from .gateway import get_gateway_from_config_entry DEPENDENCIES = ['deconz'] @@ -25,22 +25,26 @@ async def async_setup_entry(hass, config_entry, async_add_entities): Covers are based on same device class as lights in deCONZ. """ - gateway = hass.data[DECONZ_DOMAIN] + gateway = get_gateway_from_config_entry(hass, config_entry) @callback def async_add_cover(lights): """Add cover from deCONZ.""" entities = [] + for light in lights: + if light.type in COVER_TYPES: if light.modelid in ZIGBEE_SPEC: entities.append(DeconzCoverZigbeeSpec(light, gateway)) + else: entities.append(DeconzCover(light, gateway)) + async_add_entities(entities, True) - gateway.listeners.append( - async_dispatcher_connect(hass, NEW_LIGHT, async_add_cover)) + gateway.listeners.append(async_dispatcher_connect( + hass, gateway.async_event_new_device(NEW_LIGHT), async_add_cover)) async_add_cover(gateway.api.lights.values()) diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index bfcbd158b9f..0c5cbeef1fb 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -4,7 +4,7 @@ from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from .const import DECONZ_REACHABLE, DOMAIN as DECONZ_DOMAIN +from .const import DOMAIN as DECONZ_DOMAIN class DeconzDevice(Entity): @@ -21,7 +21,8 @@ class DeconzDevice(Entity): self._device.register_async_callback(self.async_update_callback) self.gateway.deconz_ids[self.entity_id] = self._device.deconz_id self.unsub_dispatcher = async_dispatcher_connect( - self.hass, DECONZ_REACHABLE, self.async_update_callback) + self.hass, self.gateway.event_reachable, + self.async_update_callback) async def async_will_remove_from_hass(self) -> None: """Disconnect device object when removed.""" diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 11fb247a6f4..4d9e1503902 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -6,16 +6,23 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.const import CONF_EVENT, CONF_HOST, CONF_ID from homeassistant.core import EventOrigin, callback from homeassistant.helpers import aiohttp_client +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 .const import ( - _LOGGER, DECONZ_REACHABLE, CONF_ALLOW_CLIP_SENSOR, NEW_DEVICE, NEW_SENSOR, - SUPPORTED_PLATFORMS) + _LOGGER, CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_DECONZ_GROUPS, CONF_BRIDGEID, + CONF_MASTER_GATEWAY, DOMAIN, NEW_DEVICE, NEW_SENSOR, SUPPORTED_PLATFORMS) from .errors import AuthenticationRequired, CannotConnect +@callback +def get_gateway_from_config_entry(hass, config_entry): + """Return gateway with a matching bridge id.""" + return hass.data[DOMAIN][config_entry.data[CONF_BRIDGEID]] + + class DeconzGateway: """Manages a single deCONZ gateway.""" @@ -30,6 +37,40 @@ class DeconzGateway: self.events = [] self.listeners = [] + @property + def bridgeid(self) -> str: + """Return the unique identifier of the gateway.""" + return self.config_entry.data[CONF_BRIDGEID] + + @property + def master(self) -> bool: + """Gateway which is used with deCONZ services without defining id.""" + return self.config_entry.options[CONF_MASTER_GATEWAY] + + @property + def allow_clip_sensor(self) -> bool: + """Allow loading clip sensor from gateway.""" + return self.config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True) + + @property + def allow_deconz_groups(self) -> bool: + """Allow loading deCONZ groups from gateway.""" + return self.config_entry.data.get(CONF_ALLOW_DECONZ_GROUPS, True) + + async def async_update_device_registry(self): + """Update device registry.""" + device_registry = await \ + self.hass.helpers.device_registry.async_get_registry() + device_registry.async_get_or_create( + config_entry_id=self.config_entry.entry_id, + connections={(CONNECTION_NETWORK_MAC, self.api.config.mac)}, + identifiers={(DOMAIN, self.api.config.bridgeid)}, + manufacturer='Dresden Elektronik', + model=self.api.config.modelid, + name=self.api.config.name, + sw_version=self.api.config.swversion + ) + async def async_setup(self): """Set up a deCONZ gateway.""" hass = self.hass @@ -52,9 +93,9 @@ class DeconzGateway: hass.config_entries.async_forward_entry_setup( self.config_entry, component)) - self.listeners.append( - async_dispatcher_connect( - hass, NEW_SENSOR, self.async_add_remote)) + self.listeners.append(async_dispatcher_connect( + hass, self.async_event_new_device(NEW_SENSOR), + self.async_add_remote)) self.async_add_remote(self.api.sensors.values()) @@ -62,29 +103,39 @@ class DeconzGateway: return True + @property + def event_reachable(self): + """Gateway specific event to signal a change in connection status.""" + return 'deconz_reachable_{}'.format(self.bridgeid) + @callback def async_connection_status_callback(self, available): """Handle signals of gateway connection status.""" self.available = available - async_dispatcher_send( - self.hass, DECONZ_REACHABLE, {'state': True, 'attr': 'reachable'}) + async_dispatcher_send(self.hass, self.event_reachable, + {'state': True, 'attr': 'reachable'}) + + @callback + def async_event_new_device(self, device_type): + """Gateway specific event to signal new device.""" + return NEW_DEVICE[device_type].format(self.bridgeid) @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, NEW_DEVICE[device_type], device) + async_dispatcher_send( + self.hass, self.async_event_new_device(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')): + not (not self.allow_clip_sensor and + sensor.type.startswith('CLIP')): self.events.append(DeconzEvent(self.hass, sensor)) @callback diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 3b63da8d9f8..b5a2b075f75 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -8,10 +8,9 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util -from .const import ( - CONF_ALLOW_DECONZ_GROUPS, DOMAIN as DECONZ_DOMAIN, COVER_TYPES, NEW_GROUP, - NEW_LIGHT, SWITCH_TYPES) +from .const import COVER_TYPES, NEW_GROUP, NEW_LIGHT, SWITCH_TYPES from .deconz_device import DeconzDevice +from .gateway import get_gateway_from_config_entry DEPENDENCIES = ['deconz'] @@ -24,32 +23,35 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ lights and groups from a config entry.""" - gateway = hass.data[DECONZ_DOMAIN] + gateway = get_gateway_from_config_entry(hass, config_entry) @callback def async_add_light(lights): """Add light from deCONZ.""" entities = [] + for light in lights: if light.type not in COVER_TYPES + SWITCH_TYPES: entities.append(DeconzLight(light, gateway)) + async_add_entities(entities, True) - gateway.listeners.append( - async_dispatcher_connect(hass, NEW_LIGHT, async_add_light)) + gateway.listeners.append(async_dispatcher_connect( + hass, gateway.async_event_new_device(NEW_LIGHT), async_add_light)) @callback def async_add_group(groups): """Add group from deCONZ.""" entities = [] - allow_group = config_entry.data.get(CONF_ALLOW_DECONZ_GROUPS, True) + for group in groups: - if group.lights and allow_group: + if group.lights and gateway.allow_deconz_groups: entities.append(DeconzLight(group, gateway)) + async_add_entities(entities, True) - gateway.listeners.append( - async_dispatcher_connect(hass, NEW_GROUP, async_add_group)) + gateway.listeners.append(async_dispatcher_connect( + hass, gateway.async_event_new_device(NEW_GROUP), async_add_group)) async_add_light(gateway.api.lights.values()) async_add_group(gateway.api.groups.values()) diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index 22b4c47f2ab..1ae1e079daa 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -3,7 +3,8 @@ from homeassistant.components.scene import Scene from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import DOMAIN as DECONZ_DOMAIN, NEW_SCENE +from .const import NEW_SCENE +from .gateway import get_gateway_from_config_entry DEPENDENCIES = ['deconz'] @@ -16,17 +17,20 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up scenes for deCONZ component.""" - gateway = hass.data[DECONZ_DOMAIN] + gateway = get_gateway_from_config_entry(hass, config_entry) @callback def async_add_scene(scenes): """Add scene from deCONZ.""" entities = [] + for scene in scenes: entities.append(DeconzScene(scene, gateway)) + async_add_entities(entities) - gateway.listeners.append( - async_dispatcher_connect(hass, NEW_SCENE, async_add_scene)) + + gateway.listeners.append(async_dispatcher_connect( + hass, gateway.async_event_new_device(NEW_SCENE), async_add_scene)) async_add_scene(gateway.api.scenes.values()) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index e6b033906e7..7c3109e1f59 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -5,10 +5,9 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import slugify -from .const import ( - ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DECONZ_DOMAIN, - NEW_SENSOR) +from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice +from .gateway import get_gateway_from_config_entry DEPENDENCIES = ['deconz'] @@ -25,7 +24,7 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ sensors.""" - gateway = hass.data[DECONZ_DOMAIN] + gateway = get_gateway_from_config_entry(hass, config_entry) @callback def async_add_sensor(sensors): @@ -33,19 +32,24 @@ async def async_setup_entry(hass, config_entry, async_add_entities): from pydeconz.sensor import ( DECONZ_SENSOR, SWITCH as DECONZ_REMOTE) entities = [] - allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True) + for sensor in sensors: + if sensor.type in DECONZ_SENSOR and \ - not (not allow_clip_sensor and sensor.type.startswith('CLIP')): + not (not gateway.allow_clip_sensor and + sensor.type.startswith('CLIP')): + if sensor.type in DECONZ_REMOTE: if sensor.battery: entities.append(DeconzBattery(sensor, gateway)) + else: entities.append(DeconzSensor(sensor, gateway)) + async_add_entities(entities, True) - gateway.listeners.append( - async_dispatcher_connect(hass, NEW_SENSOR, async_add_sensor)) + gateway.listeners.append(async_dispatcher_connect( + hass, gateway.async_event_new_device(NEW_SENSOR), async_add_sensor)) async_add_sensor(gateway.api.sensors.values()) diff --git a/homeassistant/components/deconz/services.yaml b/homeassistant/components/deconz/services.yaml index cde7ac79f4c..a39bbc01ea1 100644 --- a/homeassistant/components/deconz/services.yaml +++ b/homeassistant/components/deconz/services.yaml @@ -13,6 +13,12 @@ configure: data: description: Data is a json object with what data you want to alter. example: '{"on": true}' + bridgeid: + description: (Optional) Bridgeid is a string unique for each deCONZ hardware. It can be found as part of the integration name. + example: '00212EFFFF012345' device_refresh: - description: Refresh device lists from deCONZ. \ No newline at end of file + description: Refresh device lists from deCONZ. + bridgeid: + description: (Optional) Bridgeid is a string unique for each deCONZ hardware. It can be found as part of the integration name. + example: '00212EFFFF012345' \ No newline at end of file diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index 56d37d504cb..b9f959766fc 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -3,8 +3,9 @@ from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import DOMAIN as DECONZ_DOMAIN, NEW_LIGHT, POWER_PLUGS, SIRENS +from .const import NEW_LIGHT, POWER_PLUGS, SIRENS from .deconz_device import DeconzDevice +from .gateway import get_gateway_from_config_entry DEPENDENCIES = ['deconz'] @@ -20,21 +21,25 @@ async def async_setup_entry(hass, config_entry, async_add_entities): Switches are based same device class as lights in deCONZ. """ - gateway = hass.data[DECONZ_DOMAIN] + gateway = get_gateway_from_config_entry(hass, config_entry) @callback def async_add_switch(lights): """Add switch from deCONZ.""" entities = [] + for light in lights: + if light.type in POWER_PLUGS: entities.append(DeconzPowerPlug(light, gateway)) + elif light.type in SIRENS: entities.append(DeconzSiren(light, gateway)) + async_add_entities(entities, True) - gateway.listeners.append( - async_dispatcher_connect(hass, NEW_LIGHT, async_add_switch)) + gateway.listeners.append(async_dispatcher_connect( + hass, gateway.async_event_new_device(NEW_LIGHT), async_add_switch)) async_add_switch(gateway.api.lights.values()) diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index ba39afa0e88..1aee53f43c2 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -54,7 +54,7 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) gateway.api.config = Mock() - hass.data[deconz.DOMAIN] = gateway + hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway} with patch('pydeconz.DeconzSession.async_get_state', return_value=mock_coro(data)): @@ -64,6 +64,7 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): config_entry, 'binary_sensor') # To flush out the service call to update the group await hass.async_block_till_done() + return gateway async def test_platform_manually_configured(hass): @@ -79,56 +80,56 @@ async def test_platform_manually_configured(hass): async def test_no_binary_sensors(hass): """Test that no sensors in deconz results in no sensor entities.""" data = {} - await setup_gateway(hass, data) - assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0 + gateway = await setup_gateway(hass, data) + assert len(hass.data[deconz.DOMAIN][gateway.bridgeid].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_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.DOMAIN].deconz_ids + gateway = await setup_gateway(hass, data) + assert "binary_sensor.sensor_1_name" in gateway.deconz_ids + assert "binary_sensor.sensor_2_name" not in gateway.deconz_ids assert len(hass.states.async_all()) == 1 - hass.data[deconz.DOMAIN].api.sensors['1'].async_update( + hass.data[deconz.DOMAIN][gateway.bridgeid].api.sensors['1'].async_update( {'state': {'on': False}}) async def test_add_new_sensor(hass): """Test successful creation of sensor entities.""" data = {} - await setup_gateway(hass, data) + gateway = 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]) + async_dispatcher_send( + hass, gateway.async_event_new_device('sensor'), [sensor]) await hass.async_block_till_done() - assert "binary_sensor.name" in hass.data[deconz.DOMAIN].deconz_ids + assert "binary_sensor.name" in gateway.deconz_ids async def test_do_not_allow_clip_sensor(hass): """Test that clip sensors can be ignored.""" data = {} - await setup_gateway(hass, data, allow_clip_sensor=False) + gateway = 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]) + async_dispatcher_send( + hass, gateway.async_event_new_device('sensor'), [sensor]) await hass.async_block_till_done() - assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0 + assert len(gateway.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) + gateway = await setup_gateway(hass, data) - await hass.data[deconz.DOMAIN].async_reset() + await gateway.async_reset() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 953bb776419..a5f4d2bb79b 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -65,7 +65,7 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(hass.loop, session, **config_entry.data) gateway.api.config = Mock() - hass.data[deconz.DOMAIN] = gateway + hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway} with patch('pydeconz.DeconzSession.async_get_state', return_value=mock_coro(data)): @@ -75,6 +75,7 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): config_entry, 'climate') # To flush out the service call to update the group await hass.async_block_till_done() + return gateway async def test_platform_manually_configured(hass): @@ -89,26 +90,26 @@ async def test_platform_manually_configured(hass): async def test_no_sensors(hass): """Test that no sensors in deconz results in no climate entities.""" - await setup_gateway(hass, {}) - assert not hass.data[deconz.DOMAIN].deconz_ids + gateway = await setup_gateway(hass, {}) + assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids assert not hass.states.async_all() async def test_climate_devices(hass): """Test successful creation of sensor entities.""" - await setup_gateway(hass, {"sensors": SENSOR}) - assert "climate.climate_1_name" in hass.data[deconz.DOMAIN].deconz_ids - assert "sensor.sensor_2_name" not in hass.data[deconz.DOMAIN].deconz_ids + gateway = await setup_gateway(hass, {"sensors": SENSOR}) + assert "climate.climate_1_name" in gateway.deconz_ids + assert "sensor.sensor_2_name" not in gateway.deconz_ids assert len(hass.states.async_all()) == 1 - hass.data[deconz.DOMAIN].api.sensors['1'].async_update( + gateway.api.sensors['1'].async_update( {'state': {'on': False}}) await hass.services.async_call( 'climate', 'turn_on', {'entity_id': 'climate.climate_1_name'}, blocking=True ) - hass.data[deconz.DOMAIN].api.session.put.assert_called_with( + gateway.api.session.put.assert_called_with( 'http://1.2.3.4:80/api/ABCDEF/sensors/1/config', data='{"mode": "auto"}' ) @@ -117,7 +118,7 @@ async def test_climate_devices(hass): 'climate', 'turn_off', {'entity_id': 'climate.climate_1_name'}, blocking=True ) - hass.data[deconz.DOMAIN].api.session.put.assert_called_with( + gateway.api.session.put.assert_called_with( 'http://1.2.3.4:80/api/ABCDEF/sensors/1/config', data='{"mode": "off"}' ) @@ -127,18 +128,18 @@ async def test_climate_devices(hass): {'entity_id': 'climate.climate_1_name', 'temperature': 20}, blocking=True ) - hass.data[deconz.DOMAIN].api.session.put.assert_called_with( + gateway.api.session.put.assert_called_with( 'http://1.2.3.4:80/api/ABCDEF/sensors/1/config', data='{"heatsetpoint": 2000.0}' ) - assert len(hass.data[deconz.DOMAIN].api.session.put.mock_calls) == 3 + assert len(gateway.api.session.put.mock_calls) == 3 async def test_verify_state_update(hass): """Test that state update properly.""" - await setup_gateway(hass, {"sensors": SENSOR}) - assert "climate.climate_1_name" in hass.data[deconz.DOMAIN].deconz_ids + gateway = await setup_gateway(hass, {"sensors": SENSOR}) + assert "climate.climate_1_name" in gateway.deconz_ids thermostat = hass.states.get('climate.climate_1_name') assert thermostat.state == 'on' @@ -150,7 +151,7 @@ async def test_verify_state_update(hass): "id": "1", "config": {"on": False} } - hass.data[deconz.DOMAIN].api.async_event_handler(state_update) + gateway.api.async_event_handler(state_update) await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 @@ -161,32 +162,34 @@ async def test_verify_state_update(hass): async def test_add_new_climate_device(hass): """Test successful creation of climate entities.""" - await setup_gateway(hass, {}) + gateway = await setup_gateway(hass, {}) sensor = Mock() sensor.name = 'name' sensor.type = 'ZHAThermostat' sensor.register_async_callback = Mock() - async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) + async_dispatcher_send( + hass, gateway.async_event_new_device('sensor'), [sensor]) await hass.async_block_till_done() - assert "climate.name" in hass.data[deconz.DOMAIN].deconz_ids + assert "climate.name" in gateway.deconz_ids async def test_do_not_allow_clipsensor(hass): """Test that clip sensors can be ignored.""" - await setup_gateway(hass, {}, allow_clip_sensor=False) + gateway = await setup_gateway(hass, {}, allow_clip_sensor=False) sensor = Mock() sensor.name = 'name' sensor.type = 'CLIPThermostat' sensor.register_async_callback = Mock() - async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) + async_dispatcher_send( + hass, gateway.async_event_new_device('sensor'), [sensor]) await hass.async_block_till_done() - assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0 + assert len(gateway.deconz_ids) == 0 async def test_unload_sensor(hass): """Test that it works to unload sensor entities.""" - await setup_gateway(hass, {"sensors": SENSOR}) + gateway = await setup_gateway(hass, {"sensors": SENSOR}) - await hass.data[deconz.DOMAIN].async_reset() + await gateway.async_reset() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 863e4e93fc5..09510b136c4 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -22,10 +22,7 @@ async def test_flow_works(hass, aioclient_mock): flow.hass = hass 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}) + result = await flow.async_step_link(user_input={}) assert result['type'] == 'create_entry' assert result['title'] == 'deCONZ-id' @@ -33,25 +30,10 @@ async def test_flow_works(hass, aioclient_mock): 'bridgeid': 'id', 'host': '1.2.3.4', 'port': 80, - 'api_key': '1234567890ABCDEF', - 'allow_clip_sensor': True, - 'allow_deconz_groups': True + 'api_key': '1234567890ABCDEF' } -async def test_flow_already_registered_bridge(hass): - """Test config flow don't allow more than one bridge to be registered.""" - MockConfigEntry(domain='deconz', data={ - 'host': '1.2.3.4' - }).add_to_hass(hass) - - flow = config_flow.DeconzFlowHandler() - flow.hass = hass - - result = await flow.async_step_user() - assert result['type'] == 'abort' - - async def test_flow_bridge_discovery_fails(hass, aioclient_mock): """Test config flow works when discovery fails.""" flow = config_flow.DeconzFlowHandler() @@ -153,24 +135,6 @@ async def test_link_no_api_key(hass): assert result['errors'] == {'base': 'no_key'} -async def test_link_already_registered_bridge(hass): - """Test that link verifies to only allow one config entry to complete. - - This is possible with discovery which will allow the user to complete - a second config entry and then complete the discovered config entry. - """ - MockConfigEntry(domain='deconz', data={ - 'host': '1.2.3.4' - }).add_to_hass(hass) - - flow = config_flow.DeconzFlowHandler() - flow.hass = hass - flow.deconz_config = {'host': '1.2.3.4', 'port': 80} - - result = await flow.async_step_link(user_input={}) - assert result['type'] == 'abort' - - async def test_bridge_discovery(hass): """Test a bridge being discovered.""" flow = config_flow.DeconzFlowHandler() @@ -197,6 +161,7 @@ async def test_bridge_discovery_already_configured(hass): result = await flow.async_step_discovery({ 'host': '1.2.3.4', + 'port': 80, 'serial': 'id' }) @@ -234,14 +199,12 @@ async def test_import_with_api_key(hass): 'bridgeid': 'id', 'host': '1.2.3.4', 'port': 80, - 'api_key': '1234567890ABCDEF', - 'allow_clip_sensor': True, - 'allow_deconz_groups': True + 'api_key': '1234567890ABCDEF' } -async def test_options(hass, aioclient_mock): - """Test that options work and that bridgeid can be requested.""" +async def test_create_entry(hass, aioclient_mock): + """Test that _create_entry work and that bridgeid can be requested.""" aioclient_mock.get('http://1.2.3.4:80/api/1234567890ABCDEF/config', json={"bridgeid": "id"}, headers={'content-type': 'application/json'}) @@ -252,8 +215,7 @@ async def test_options(hass, aioclient_mock): 'port': 80, 'api_key': '1234567890ABCDEF'} - result = await flow.async_step_options( - user_input={'allow_clip_sensor': False, 'allow_deconz_groups': False}) + result = await flow._create_entry() assert result['type'] == 'create_entry' assert result['title'] == 'deCONZ-id' @@ -261,9 +223,7 @@ async def test_options(hass, aioclient_mock): 'bridgeid': 'id', 'host': '1.2.3.4', 'port': 80, - 'api_key': '1234567890ABCDEF', - 'allow_clip_sensor': False, - 'allow_deconz_groups': False + 'api_key': '1234567890ABCDEF' } @@ -286,8 +246,8 @@ async def test_hassio_confirm(hass): data={ 'addon': 'Mock Addon', 'host': 'mock-deconz', - 'port': 8080, - 'serial': 'aa:bb', + 'port': 80, + 'serial': 'id', 'api_key': '1234567890ABCDEF', }, context={'source': 'hassio'} @@ -299,18 +259,13 @@ async def test_hassio_confirm(hass): } result = await hass.config_entries.flow.async_configure( - result['flow_id'], { - 'allow_clip_sensor': True, - 'allow_deconz_groups': True, - } + result['flow_id'], user_input={} ) assert result['type'] == 'create_entry' assert result['result'].data == { 'host': 'mock-deconz', - 'port': 8080, - 'bridgeid': 'aa:bb', - 'api_key': '1234567890ABCDEF', - 'allow_clip_sensor': True, - 'allow_deconz_groups': True, + 'port': 80, + 'bridgeid': 'id', + 'api_key': '1234567890ABCDEF' } diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index b021bcb8d51..73e3d411958 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -61,7 +61,7 @@ async def setup_gateway(hass, data): gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) gateway.api.config = Mock() - hass.data[deconz.DOMAIN] = gateway + hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway} with patch('pydeconz.DeconzSession.async_get_state', return_value=mock_coro(data)): @@ -70,6 +70,7 @@ async def setup_gateway(hass, data): 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() + return gateway async def test_platform_manually_configured(hass): @@ -84,8 +85,8 @@ async def test_platform_manually_configured(hass): async def test_no_covers(hass): """Test that no cover entities are created.""" - await setup_gateway(hass, {}) - assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0 + gateway = await setup_gateway(hass, {}) + assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids assert len(hass.states.async_all()) == 0 @@ -93,8 +94,8 @@ async def test_cover(hass): """Test that all supported cover entities are created.""" 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 + gateway = await setup_gateway(hass, {"lights": SUPPORTED_COVERS}) + assert "cover.cover_1_name" in gateway.deconz_ids assert len(SUPPORTED_COVERS) == len(COVER_TYPES) assert len(hass.states.async_all()) == 3 @@ -102,7 +103,7 @@ async def test_cover(hass): assert cover_1 is not None assert cover_1.state == 'closed' - hass.data[deconz.DOMAIN].api.lights['1'].async_update({}) + gateway.api.lights['1'].async_update({}) await hass.services.async_call('cover', 'open_cover', { 'entity_id': 'cover.cover_1_name' @@ -122,14 +123,15 @@ async def test_cover(hass): async def test_add_new_cover(hass): """Test successful creation of cover entity.""" data = {} - await setup_gateway(hass, data) + gateway = 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]) + async_dispatcher_send( + hass, gateway.async_event_new_device('light'), [cover]) await hass.async_block_till_done() - assert "cover.name" in hass.data[deconz.DOMAIN].deconz_ids + assert "cover.name" in gateway.deconz_ids async def test_unsupported_cover(hass): @@ -140,8 +142,8 @@ async def test_unsupported_cover(hass): async def test_unload_cover(hass): """Test that it works to unload switch entities.""" - await setup_gateway(hass, {"lights": SUPPORTED_COVERS}) + gateway = await setup_gateway(hass, {"lights": SUPPORTED_COVERS}) - await hass.data[deconz.DOMAIN].async_reset() + await gateway.async_reset() assert len(hass.states.async_all()) == 1 diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index e0afadccc81..da37f4a9652 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -11,56 +11,62 @@ from homeassistant.components import deconz from tests.common import mock_coro, MockConfigEntry +ENTRY1_HOST = '1.2.3.4' +ENTRY1_PORT = 80 +ENTRY1_API_KEY = '1234567890ABCDEF' +ENTRY1_BRIDGEID = '12345ABC' -CONFIG = { - "config": { - "bridgeid": "0123456789ABCDEF", - "mac": "12:34:56:78:90:ab", - "modelid": "deCONZ", - "name": "Phoscon", - "swversion": "2.05.35" - } -} +ENTRY2_HOST = '2.3.4.5' +ENTRY2_PORT = 80 +ENTRY2_API_KEY = '1234567890ABCDEF' +ENTRY2_BRIDGEID = '23456DEF' + + +async def setup_entry(hass, entry): + """Test that setup entry works.""" + with patch.object(deconz.DeconzGateway, 'async_setup', + return_value=mock_coro(True)), \ + patch.object(deconz.DeconzGateway, 'async_update_device_registry', + return_value=mock_coro(True)): + assert await deconz.async_setup_entry(hass, entry) is True async def test_config_with_host_passed_to_config_entry(hass): """Test that configured options for a host are loaded via config entry.""" - with patch.object(hass, 'config_entries') as mock_config_entries, \ - patch.object(deconz, 'configured_hosts', return_value=[]): + with patch.object(hass.config_entries, 'flow') as mock_config_flow: assert await async_setup_component(hass, deconz.DOMAIN, { deconz.DOMAIN: { - deconz.CONF_HOST: '1.2.3.4', - deconz.CONF_PORT: 80 + deconz.CONF_HOST: ENTRY1_HOST, + deconz.CONF_PORT: ENTRY1_PORT } }) is True # Import flow started - assert len(mock_config_entries.flow.mock_calls) == 2 + assert len(mock_config_flow.mock_calls) == 2 async def test_config_without_host_not_passed_to_config_entry(hass): """Test that a configuration without a host does not initiate an import.""" - with patch.object(hass, 'config_entries') as mock_config_entries, \ - patch.object(deconz, 'configured_hosts', return_value=[]): + MockConfigEntry(domain=deconz.DOMAIN, data={}).add_to_hass(hass) + with patch.object(hass.config_entries, 'flow') as mock_config_flow: assert await async_setup_component(hass, deconz.DOMAIN, { deconz.DOMAIN: {} }) is True # No flow started - assert len(mock_config_entries.flow.mock_calls) == 0 + assert len(mock_config_flow.mock_calls) == 0 -async def test_config_already_registered_not_passed_to_config_entry(hass): +async def test_config_import_entry_fails_when_entries_exist(hass): """Test that an already registered host does not initiate an import.""" - with patch.object(hass, 'config_entries') as mock_config_entries, \ - patch.object(deconz, 'configured_hosts', - return_value=['1.2.3.4']): + MockConfigEntry(domain=deconz.DOMAIN, data={}).add_to_hass(hass) + with patch.object(hass.config_entries, 'flow') as mock_config_flow: assert await async_setup_component(hass, deconz.DOMAIN, { deconz.DOMAIN: { - deconz.CONF_HOST: '1.2.3.4', - deconz.CONF_PORT: 80 + deconz.CONF_HOST: ENTRY1_HOST, + deconz.CONF_PORT: ENTRY1_PORT } }) is True # No flow started - assert len(mock_config_entries.flow.mock_calls) == 0 + assert len(mock_config_flow.mock_calls) == 0 async def test_config_discovery(hass): @@ -71,16 +77,14 @@ async def test_config_discovery(hass): assert len(mock_config_entries.flow.mock_calls) == 0 -async def test_setup_entry_already_registered_bridge(hass): - """Test setup entry doesn't allow more than one instance of deCONZ.""" - hass.data[deconz.DOMAIN] = True - assert await deconz.async_setup_entry(hass, {}) is False - - async def test_setup_entry_fails(hass): """Test setup entry fails if deCONZ is not available.""" entry = Mock() - entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + entry.data = { + deconz.CONF_HOST: ENTRY1_HOST, + deconz.CONF_PORT: ENTRY1_PORT, + deconz.CONF_API_KEY: ENTRY1_API_KEY + } with patch('pydeconz.DeconzSession.async_load_parameters', side_effect=Exception): await deconz.async_setup_entry(hass, entry) @@ -89,61 +93,121 @@ async def test_setup_entry_fails(hass): async def test_setup_entry_no_available_bridge(hass): """Test setup entry fails if deCONZ is not available.""" entry = Mock() - entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} - with patch( - 'pydeconz.DeconzSession.async_load_parameters', - side_effect=asyncio.TimeoutError - ), pytest.raises(ConfigEntryNotReady): + entry.data = { + deconz.CONF_HOST: ENTRY1_HOST, + deconz.CONF_PORT: ENTRY1_PORT, + deconz.CONF_API_KEY: ENTRY1_API_KEY + } + with patch('pydeconz.DeconzSession.async_load_parameters', + side_effect=asyncio.TimeoutError),\ + pytest.raises(ConfigEntryNotReady): await deconz.async_setup_entry(hass, entry) async def test_setup_entry_successful(hass): """Test setup entry is successful.""" entry = MockConfigEntry(domain=deconz.DOMAIN, data={ - 'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF' + deconz.CONF_HOST: ENTRY1_HOST, + deconz.CONF_PORT: ENTRY1_PORT, + deconz.CONF_API_KEY: ENTRY1_API_KEY, + deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID }) 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 hass.data[deconz.DOMAIN] + + await setup_entry(hass, entry) + + assert ENTRY1_BRIDGEID in hass.data[deconz.DOMAIN] + assert hass.data[deconz.DOMAIN][ENTRY1_BRIDGEID].master + + +async def test_setup_entry_multiple_gateways(hass): + """Test setup entry is successful with multiple gateways.""" + entry = MockConfigEntry(domain=deconz.DOMAIN, data={ + deconz.CONF_HOST: ENTRY1_HOST, + deconz.CONF_PORT: ENTRY1_PORT, + deconz.CONF_API_KEY: ENTRY1_API_KEY, + deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID + }) + entry.add_to_hass(hass) + + entry2 = MockConfigEntry(domain=deconz.DOMAIN, data={ + deconz.CONF_HOST: ENTRY2_HOST, + deconz.CONF_PORT: ENTRY2_PORT, + deconz.CONF_API_KEY: ENTRY2_API_KEY, + deconz.CONF_BRIDGEID: ENTRY2_BRIDGEID + }) + entry2.add_to_hass(hass) + + await setup_entry(hass, entry) + await setup_entry(hass, entry2) + + assert ENTRY1_BRIDGEID in hass.data[deconz.DOMAIN] + assert hass.data[deconz.DOMAIN][ENTRY1_BRIDGEID].master + assert ENTRY2_BRIDGEID in hass.data[deconz.DOMAIN] + assert not hass.data[deconz.DOMAIN][ENTRY2_BRIDGEID].master async def test_unload_entry(hass): """Test being able to unload an entry.""" entry = MockConfigEntry(domain=deconz.DOMAIN, data={ - 'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF' + deconz.CONF_HOST: ENTRY1_HOST, + deconz.CONF_PORT: ENTRY1_PORT, + deconz.CONF_API_KEY: ENTRY1_API_KEY, + deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID }) 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 - 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 + await setup_entry(hass, entry) + + with patch.object(deconz.DeconzGateway, 'async_reset', + return_value=mock_coro(True)): + assert await deconz.async_unload_entry(hass, entry) + + assert not hass.data[deconz.DOMAIN] + + +async def test_unload_entry_multiple_gateways(hass): + """Test being able to unload an entry and master gateway gets moved.""" + entry = MockConfigEntry(domain=deconz.DOMAIN, data={ + deconz.CONF_HOST: ENTRY1_HOST, + deconz.CONF_PORT: ENTRY1_PORT, + deconz.CONF_API_KEY: ENTRY1_API_KEY, + deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID + }) + entry.add_to_hass(hass) + + entry2 = MockConfigEntry(domain=deconz.DOMAIN, data={ + deconz.CONF_HOST: ENTRY2_HOST, + deconz.CONF_PORT: ENTRY2_PORT, + deconz.CONF_API_KEY: ENTRY2_API_KEY, + deconz.CONF_BRIDGEID: ENTRY2_BRIDGEID + }) + entry2.add_to_hass(hass) + + await setup_entry(hass, entry) + await setup_entry(hass, entry2) + + with patch.object(deconz.DeconzGateway, 'async_reset', + return_value=mock_coro(True)): + assert await deconz.async_unload_entry(hass, entry) + + assert ENTRY2_BRIDGEID in hass.data[deconz.DOMAIN] + assert hass.data[deconz.DOMAIN][ENTRY2_BRIDGEID].master async def test_service_configure(hass): """Test that service invokes pydeconz with the correct path and data.""" entry = MockConfigEntry(domain=deconz.DOMAIN, data={ - 'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF' + deconz.CONF_HOST: ENTRY1_HOST, + deconz.CONF_PORT: ENTRY1_PORT, + deconz.CONF_API_KEY: ENTRY1_API_KEY, + deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID }) 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 - hass.data[deconz.DOMAIN].deconz_ids = { + await setup_entry(hass, entry) + + hass.data[deconz.DOMAIN][ENTRY1_BRIDGEID].deconz_ids = { 'light.test': '/light/1' } data = {'on': True, 'attr1': 10, 'attr2': 20} @@ -191,25 +255,23 @@ async def test_service_configure(hass): 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' + deconz.CONF_HOST: ENTRY1_HOST, + deconz.CONF_PORT: ENTRY1_PORT, + deconz.CONF_API_KEY: ENTRY1_API_KEY, + deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID }) 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 + await setup_entry(hass, entry) - with patch.object(hass.data[deconz.DOMAIN].api, 'async_load_parameters', - return_value=mock_coro(True)): + with patch('pydeconz.DeconzSession.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)): + with patch('pydeconz.DeconzSession.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/deconz/test_light.py b/tests/components/deconz/test_light.py index 49c3f280d8a..d9f6927fe2c 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -87,7 +87,7 @@ async def setup_gateway(hass, data, allow_deconz_groups=True): gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) gateway.api.config = Mock() - hass.data[deconz.DOMAIN] = gateway + hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway} with patch('pydeconz.DeconzSession.async_get_state', return_value=mock_coro(data)): @@ -96,6 +96,7 @@ async def setup_gateway(hass, data, allow_deconz_groups=True): 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() + return gateway async def test_platform_manually_configured(hass): @@ -110,8 +111,8 @@ async def test_platform_manually_configured(hass): async def test_no_lights_or_groups(hass): """Test that no lights or groups entities are created.""" - await setup_gateway(hass, {}) - assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0 + gateway = await setup_gateway(hass, {}) + assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids assert len(hass.states.async_all()) == 0 @@ -119,11 +120,12 @@ async def test_lights_and_groups(hass): """Test that lights or groups entities are created.""" 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 + gateway = await setup_gateway( + hass, {"lights": LIGHT, "groups": GROUP}) + assert "light.light_1_name" in gateway.deconz_ids + assert "light.light_2_name" in gateway.deconz_ids + assert "light.group_1_name" in gateway.deconz_ids + assert "light.group_2_name" not in gateway.deconz_ids assert len(hass.states.async_all()) == 4 lamp_1 = hass.states.get('light.light_1_name') @@ -137,7 +139,7 @@ async def test_lights_and_groups(hass): assert light_2.state == 'on' assert light_2.attributes['color_temp'] == 2500 - hass.data[deconz.DOMAIN].api.lights['1'].async_update({}) + gateway.api.lights['1'].async_update({}) await hass.services.async_call('light', 'turn_on', { 'entity_id': 'light.light_1_name', @@ -166,49 +168,52 @@ async def test_lights_and_groups(hass): async def test_add_new_light(hass): """Test successful creation of light entities.""" - await setup_gateway(hass, {}) + gateway = await setup_gateway(hass, {}) light = Mock() light.name = 'name' light.register_async_callback = Mock() - async_dispatcher_send(hass, 'deconz_new_light', [light]) + async_dispatcher_send( + hass, gateway.async_event_new_device('light'), [light]) await hass.async_block_till_done() - assert "light.name" in hass.data[deconz.DOMAIN].deconz_ids + assert "light.name" in gateway.deconz_ids async def test_add_new_group(hass): """Test successful creation of group entities.""" - await setup_gateway(hass, {}) + gateway = await setup_gateway(hass, {}) group = Mock() group.name = 'name' group.register_async_callback = Mock() - async_dispatcher_send(hass, 'deconz_new_group', [group]) + async_dispatcher_send( + hass, gateway.async_event_new_device('group'), [group]) await hass.async_block_till_done() - assert "light.name" in hass.data[deconz.DOMAIN].deconz_ids + assert "light.name" in gateway.deconz_ids async def test_do_not_add_deconz_groups(hass): """Test that clip sensors can be ignored.""" - await setup_gateway(hass, {}, allow_deconz_groups=False) + gateway = 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]) + async_dispatcher_send( + hass, gateway.async_event_new_device('group'), [group]) await hass.async_block_till_done() - assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0 + assert len(gateway.deconz_ids) == 0 async def test_no_switch(hass): """Test that a switch doesn't get created as a light entity.""" - await setup_gateway(hass, {"lights": SWITCH}) - assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0 + gateway = await setup_gateway(hass, {"lights": SWITCH}) + assert len(gateway.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}) + gateway = await setup_gateway(hass, {"lights": LIGHT, "groups": GROUP}) - await hass.data[deconz.DOMAIN].async_reset() + await gateway.async_reset() # Group.all_lights will not be removed assert len(hass.states.async_all()) == 1 diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index 963f1064b35..0feac24f22a 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -47,7 +47,7 @@ async def setup_gateway(hass, data): gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) gateway.api.config = Mock() - hass.data[deconz.DOMAIN] = gateway + hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway} with patch('pydeconz.DeconzSession.async_get_state', return_value=mock_coro(data)): @@ -56,6 +56,7 @@ async def setup_gateway(hass, data): 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() + return gateway async def test_platform_manually_configured(hass): @@ -70,8 +71,8 @@ async def test_platform_manually_configured(hass): async def test_no_scenes(hass): """Test that scenes can be loaded without scenes being available.""" - await setup_gateway(hass, {}) - assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0 + gateway = await setup_gateway(hass, {}) + assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids assert len(hass.states.async_all()) == 0 @@ -79,8 +80,8 @@ async def test_scenes(hass): """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 + gateway = await setup_gateway(hass, {"groups": GROUP}) + assert "scene.group_1_name_scene_1" in gateway.deconz_ids assert len(hass.states.async_all()) == 1 await hass.services.async_call('scene', 'turn_on', { @@ -90,8 +91,8 @@ async def test_scenes(hass): async def test_unload_scene(hass): """Test that it works to unload scene entities.""" - await setup_gateway(hass, {"groups": GROUP}) + gateway = await setup_gateway(hass, {"groups": GROUP}) - await hass.data[deconz.DOMAIN].async_reset() + await gateway.async_reset() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index f5cfbe2c183..41bb7b362f5 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -91,7 +91,7 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) gateway.api.config = Mock() - hass.data[deconz.DOMAIN] = gateway + hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway} with patch('pydeconz.DeconzSession.async_get_state', return_value=mock_coro(data)): @@ -101,6 +101,7 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): config_entry, 'sensor') # To flush out the service call to update the group await hass.async_block_till_done() + return gateway async def test_platform_manually_configured(hass): @@ -115,58 +116,56 @@ async def test_platform_manually_configured(hass): async def test_no_sensors(hass): """Test that no sensors in deconz results in no sensor entities.""" - await setup_gateway(hass, {}) - assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0 + gateway = await setup_gateway(hass, {}) + assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids assert len(hass.states.async_all()) == 0 async def test_sensors(hass): """Test successful creation of sensor entities.""" - 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.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.DOMAIN].deconz_ids + gateway = await setup_gateway(hass, {"sensors": SENSOR}) + assert "sensor.sensor_1_name" in gateway.deconz_ids + assert "sensor.sensor_2_name" not in gateway.deconz_ids + assert "sensor.sensor_3_name" not in gateway.deconz_ids + assert "sensor.sensor_3_name_battery_level" not in gateway.deconz_ids + assert "sensor.sensor_4_name" not in gateway.deconz_ids + assert "sensor.sensor_4_name_battery_level" in gateway.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}}) + gateway.api.sensors['1'].async_update({'state': {'on': False}}) + gateway.api.sensors['4'].async_update({'config': {'battery': 75}}) async def test_add_new_sensor(hass): """Test successful creation of sensor entities.""" - await setup_gateway(hass, {}) + gateway = 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]) + async_dispatcher_send( + hass, gateway.async_event_new_device('sensor'), [sensor]) await hass.async_block_till_done() - assert "sensor.name" in hass.data[deconz.DOMAIN].deconz_ids + assert "sensor.name" in gateway.deconz_ids async def test_do_not_allow_clipsensor(hass): """Test that clip sensors can be ignored.""" - await setup_gateway(hass, {}, allow_clip_sensor=False) + gateway = 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]) + async_dispatcher_send( + hass, gateway.async_event_new_device('sensor'), [sensor]) await hass.async_block_till_done() - assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0 + assert len(gateway.deconz_ids) == 0 async def test_unload_sensor(hass): """Test that it works to unload sensor entities.""" - await setup_gateway(hass, {"sensors": SENSOR}) + gateway = await setup_gateway(hass, {"sensors": SENSOR}) - await hass.data[deconz.DOMAIN].async_reset() + await gateway.async_reset() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 245be27961d..e05362953a1 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -65,7 +65,7 @@ async def setup_gateway(hass, data): gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) gateway.api.config = Mock() - hass.data[deconz.DOMAIN] = gateway + hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway} with patch('pydeconz.DeconzSession.async_get_state', return_value=mock_coro(data)): @@ -74,6 +74,7 @@ async def setup_gateway(hass, data): 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() + return gateway async def test_platform_manually_configured(hass): @@ -88,8 +89,8 @@ async def test_platform_manually_configured(hass): async def test_no_switches(hass): """Test that no switch entities are created.""" - await setup_gateway(hass, {}) - assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0 + gateway = await setup_gateway(hass, {}) + assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids assert len(hass.states.async_all()) == 0 @@ -97,10 +98,10 @@ async def test_switches(hass): """Test that all supported switch entities are created.""" 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 + gateway = await setup_gateway(hass, {"lights": SUPPORTED_SWITCHES}) + assert "switch.switch_1_name" in gateway.deconz_ids + assert "switch.switch_2_name" in gateway.deconz_ids + assert "switch.switch_3_name" in gateway.deconz_ids assert len(SUPPORTED_SWITCHES) == len(SWITCH_TYPES) assert len(hass.states.async_all()) == 4 @@ -111,7 +112,7 @@ async def test_switches(hass): assert switch_3 is not None assert switch_3.state == 'on' - hass.data[deconz.DOMAIN].api.lights['1'].async_update({}) + gateway.api.lights['1'].async_update({}) await hass.services.async_call('switch', 'turn_on', { 'entity_id': 'switch.switch_1_name' @@ -130,14 +131,15 @@ async def test_switches(hass): async def test_add_new_switch(hass): """Test successful creation of switch entity.""" - await setup_gateway(hass, {}) + gateway = 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]) + async_dispatcher_send( + hass, gateway.async_event_new_device('light'), [switch]) await hass.async_block_till_done() - assert "switch.name" in hass.data[deconz.DOMAIN].deconz_ids + assert "switch.name" in gateway.deconz_ids async def test_unsupported_switch(hass): @@ -148,8 +150,8 @@ async def test_unsupported_switch(hass): async def test_unload_switch(hass): """Test that it works to unload switch entities.""" - await setup_gateway(hass, {"lights": SUPPORTED_SWITCHES}) + gateway = await setup_gateway(hass, {"lights": SUPPORTED_SWITCHES}) - await hass.data[deconz.DOMAIN].async_reset() + await gateway.async_reset() assert len(hass.states.async_all()) == 1