From 8c93b484c449f653191b616beb52ec77dd878251 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 29 May 2018 16:09:53 +0200 Subject: [PATCH] deCONZ - Option to load or not to load clip sensors on start (#14480) * Option to load or not to load clip sensors on start * Full flow * Fix config flow and add tests * Fix attribute dark reporting properly * Imported and properly configured deCONZ shouldn't need extra input to create config entry --- .../components/binary_sensor/deconz.py | 10 ++- .../components/deconz/.translations/en.json | 8 ++- homeassistant/components/deconz/__init__.py | 8 ++- .../components/deconz/config_flow.py | 69 +++++++++++++------ homeassistant/components/deconz/const.py | 2 + homeassistant/components/deconz/strings.json | 8 ++- homeassistant/components/sensor/deconz.py | 11 ++- tests/components/binary_sensor/test_deconz.py | 18 ++++- tests/components/deconz/test_config_flow.py | 37 ++++++++-- tests/components/deconz/test_init.py | 18 +++++ tests/components/sensor/test_deconz.py | 18 ++++- 11 files changed, 165 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index 9faa703d13c..6f59da0755a 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -6,7 +6,8 @@ https://home-assistant.io/components/binary_sensor.deconz/ """ from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) + CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, + DATA_DECONZ_UNSUB) from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -27,10 +28,13 @@ async def async_setup_entry(hass, config_entry, async_add_devices): """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: + if sensor.type in DECONZ_BINARY_SENSOR and \ + not (not allow_clip_sensor and sensor.type.startswith('CLIP')): entities.append(DeconzBinarySensor(sensor)) async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) @@ -103,6 +107,6 @@ class DeconzBinarySensor(BinarySensorDevice): attr = {} if self._sensor.battery: attr[ATTR_BATTERY_LEVEL] = self._sensor.battery - if self._sensor.type in PRESENCE and self._sensor.dark: + if self._sensor.type in PRESENCE and self._sensor.dark is not None: attr['dark'] = self._sensor.dark return attr diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 0009986d45f..a2f90e49e3a 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -19,8 +19,14 @@ "link": { "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button", "title": "Link with deCONZ" + }, + "options": { + "title": "Extra configuration options for deCONZ", + "data": { + "allow_clip_sensor": "Allow importing virtual sensors" + } } }, - "title": "deCONZ" + "title": "deCONZ Zigbee gateway" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index bbab4029d7e..850645225d0 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -19,8 +19,8 @@ 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 ( - CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, - DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) + CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT, + DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) REQUIREMENTS = ['pydeconz==38'] @@ -104,8 +104,10 @@ async def async_setup_entry(hass, config_entry): def async_add_remote(sensors): """Setup 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: + 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)) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index e900782ea65..cb7c3aad7fd 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -8,13 +8,15 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.helpers import aiohttp_client from homeassistant.util.json import load_json -from .const import CONFIG_FILE, DOMAIN +from .const import CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DOMAIN + +CONF_BRIDGEID = 'bridgeid' @callback def configured_hosts(hass): """Return a set of the configured hosts.""" - return set(entry.data['host'] for entry + return set(entry.data[CONF_HOST] for entry in hass.config_entries.async_entries(DOMAIN)) @@ -30,7 +32,12 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): self.deconz_config = {} async def async_step_init(self, user_input=None): - """Handle a deCONZ config flow start.""" + """Handle a deCONZ config flow start. + + Only allows one instance to be set up. + If only one bridge is found go to link step. + If more than one bridge is found let user choose bridge to link. + """ from pydeconz.utils import async_discovery if configured_hosts(self.hass): @@ -65,7 +72,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): async def async_step_link(self, user_input=None): """Attempt to link with the deCONZ bridge.""" - from pydeconz.utils import async_get_api_key, async_get_bridgeid + from pydeconz.utils import async_get_api_key errors = {} if user_input is not None: @@ -75,13 +82,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): api_key = await async_get_api_key(session, **self.deconz_config) if api_key: self.deconz_config[CONF_API_KEY] = api_key - if 'bridgeid' not in self.deconz_config: - self.deconz_config['bridgeid'] = await async_get_bridgeid( - session, **self.deconz_config) - return self.async_create_entry( - title='deCONZ-' + self.deconz_config['bridgeid'], - data=self.deconz_config - ) + return await self.async_step_options() errors['base'] = 'no_key' return self.async_show_form( @@ -89,6 +90,34 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): 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. + """ + 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] + + if CONF_BRIDGEID not in self.deconz_config: + session = aiohttp_client.async_get_clientsession(self.hass) + self.deconz_config[CONF_BRIDGEID] = await async_get_bridgeid( + session, **self.deconz_config) + + 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): bool, + }), + ) + async def async_step_discovery(self, discovery_info): """Prepare configuration for a discovered deCONZ bridge. @@ -97,7 +126,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): deconz_config = {} deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST) deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT) - deconz_config['bridgeid'] = discovery_info.get('serial') + deconz_config[CONF_BRIDGEID] = discovery_info.get('serial') config_file = await self.hass.async_add_job( load_json, self.hass.config.path(CONFIG_FILE)) @@ -121,19 +150,15 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): Otherwise we will delegate to `link` step which will ask user to link the bridge. """ - from pydeconz.utils import async_get_bridgeid - if configured_hosts(self.hass): return self.async_abort(reason='one_instance_only') - elif CONF_API_KEY not in import_config: - self.deconz_config = import_config + + self.deconz_config = import_config + if CONF_API_KEY not in import_config: return await self.async_step_link() - if 'bridgeid' not in import_config: - session = aiohttp_client.async_get_clientsession(self.hass) - import_config['bridgeid'] = await async_get_bridgeid( - session, **import_config) + self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = True return self.async_create_entry( - title='deCONZ-' + import_config['bridgeid'], - data=import_config + title='deCONZ-' + self.deconz_config[CONF_BRIDGEID], + data=self.deconz_config ) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 48e5ea75d68..43f3c6441da 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -8,3 +8,5 @@ CONFIG_FILE = 'deconz.conf' DATA_DECONZ_EVENT = 'deconz_events' DATA_DECONZ_ID = 'deconz_entities' DATA_DECONZ_UNSUB = 'deconz_dispatchers' + +CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor' diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 7ea68af01c1..cabe58694d2 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -1,6 +1,6 @@ { "config": { - "title": "deCONZ", + "title": "deCONZ Zigbee gateway", "step": { "init": { "title": "Define deCONZ gateway", @@ -12,6 +12,12 @@ "link": { "title": "Link with deCONZ", "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button" + }, + "options": { + "title": "Extra configuration options for deCONZ", + "data":{ + "allow_clip_sensor": "Allow importing virtual sensors" + } } }, "error": { diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index 221cdf2129e..0db06622ad8 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -5,7 +5,8 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/sensor.deconz/ """ from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) + CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, + DATA_DECONZ_UNSUB) from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY) from homeassistant.core import callback @@ -33,14 +34,17 @@ async def async_setup_entry(hass, config_entry, async_add_devices): """Add sensors from deCONZ.""" 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: + if sensor.type in DECONZ_SENSOR and \ + not (not allow_clip_sensor and sensor.type.startswith('CLIP')): if sensor.type in DECONZ_REMOTE: if sensor.battery: entities.append(DeconzBattery(sensor)) else: entities.append(DeconzSensor(sensor)) async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) @@ -114,9 +118,12 @@ class DeconzSensor(Entity): @property def device_state_attributes(self): """Return the state attributes of the sensor.""" + from pydeconz.sensor import LIGHTLEVEL attr = {} if self._sensor.battery: attr[ATTR_BATTERY_LEVEL] = self._sensor.battery + if self._sensor.type in LIGHTLEVEL and self._sensor.dark is not None: + attr['dark'] = self._sensor.dark if self.unit_of_measurement == 'Watts': attr[ATTR_CURRENT] = self._sensor.current attr[ATTR_VOLTAGE] = self._sensor.voltage diff --git a/tests/components/binary_sensor/test_deconz.py b/tests/components/binary_sensor/test_deconz.py index 88dd0dae737..2e33e28fa57 100644 --- a/tests/components/binary_sensor/test_deconz.py +++ b/tests/components/binary_sensor/test_deconz.py @@ -26,7 +26,7 @@ SENSOR = { } -async def setup_bridge(hass, data): +async def setup_bridge(hass, data, allow_clip_sensor=True): """Load the deCONZ binary sensor platform.""" from pydeconz import DeconzSession loop = Mock() @@ -41,7 +41,8 @@ async def setup_bridge(hass, data): 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') + 1, deconz.DOMAIN, 'Mock Title', + {'host': 'mock-host', 'allow_clip_sensor': allow_clip_sensor}, 'test') await hass.config_entries.async_forward_entry_setup( config_entry, 'binary_sensor') # To flush out the service call to update the group @@ -77,3 +78,16 @@ async def test_add_new_sensor(hass): 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] + + +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) + 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 diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index d86475b35ef..df3310f3d6f 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -21,7 +21,9 @@ async def test_flow_works(hass, aioclient_mock): flow = config_flow.DeconzFlowHandler() flow.hass = hass await flow.async_step_init() - result = await flow.async_step_link(user_input={}) + await flow.async_step_link(user_input={}) + result = await flow.async_step_options( + user_input={'allow_clip_sensor': True}) assert result['type'] == 'create_entry' assert result['title'] == 'deCONZ-id' @@ -29,7 +31,8 @@ async def test_flow_works(hass, aioclient_mock): 'bridgeid': 'id', 'host': '1.2.3.4', 'port': 80, - 'api_key': '1234567890ABCDEF' + 'api_key': '1234567890ABCDEF', + 'allow_clip_sensor': True } @@ -146,14 +149,14 @@ async def test_bridge_discovery_config_file(hass): 'port': 80, 'serial': 'id' }) - assert result['type'] == 'create_entry' assert result['title'] == 'deCONZ-id' assert result['data'] == { 'bridgeid': 'id', 'host': '1.2.3.4', 'port': 80, - 'api_key': '1234567890ABCDEF' + 'api_key': '1234567890ABCDEF', + 'allow_clip_sensor': True } @@ -214,12 +217,34 @@ async def test_import_with_api_key(hass): 'port': 80, 'api_key': '1234567890ABCDEF' }) - assert result['type'] == 'create_entry' assert result['title'] == 'deCONZ-id' assert result['data'] == { 'bridgeid': 'id', 'host': '1.2.3.4', 'port': 80, - 'api_key': '1234567890ABCDEF' + 'api_key': '1234567890ABCDEF', + 'allow_clip_sensor': True + } + + +async def test_options(hass, aioclient_mock): + """Test that options work and that bridgeid can be requested.""" + aioclient_mock.get('http://1.2.3.4:80/api/1234567890ABCDEF/config', + json={"bridgeid": "id"}) + flow = config_flow.DeconzFlowHandler() + flow.hass = hass + flow.deconz_config = {'host': '1.2.3.4', + 'port': 80, + 'api_key': '1234567890ABCDEF'} + result = await flow.async_step_options( + user_input={'allow_clip_sensor': False}) + assert result['type'] == 'create_entry' + assert result['title'] == 'deCONZ-id' + assert result['data'] == { + 'bridgeid': 'id', + 'host': '1.2.3.4', + 'port': 80, + 'api_key': '1234567890ABCDEF', + 'allow_clip_sensor': False } diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index 888094deea6..1cee08feb0a 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -172,3 +172,21 @@ async def test_add_new_remote(hass): 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_load_parameters', + return_value=mock_coro(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 diff --git a/tests/components/sensor/test_deconz.py b/tests/components/sensor/test_deconz.py index 8f6a53e6e65..d7cdb458646 100644 --- a/tests/components/sensor/test_deconz.py +++ b/tests/components/sensor/test_deconz.py @@ -41,7 +41,7 @@ SENSOR = { } -async def setup_bridge(hass, data): +async def setup_bridge(hass, data, allow_clip_sensor=True): """Load the deCONZ sensor platform.""" from pydeconz import DeconzSession loop = Mock() @@ -57,7 +57,8 @@ async def setup_bridge(hass, data): 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'}, 'test') + 1, deconz.DOMAIN, 'Mock Title', + {'host': 'mock-host', 'allow_clip_sensor': allow_clip_sensor}, 'test') 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() @@ -97,3 +98,16 @@ async def test_add_new_sensor(hass): async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) await hass.async_block_till_done() assert "sensor.name" in hass.data[deconz.DATA_DECONZ_ID] + + +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) + 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