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
This commit is contained in:
Robert Svensson 2018-05-29 16:09:53 +02:00 committed by Paulus Schoutsen
parent 3b38de63ea
commit 8c93b484c4
11 changed files with 165 additions and 42 deletions

View file

@ -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

View file

@ -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"
}
}

View file

@ -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))

View file

@ -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
)

View file

@ -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'

View file

@ -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": {

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -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