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:
parent
3b38de63ea
commit
8c93b484c4
11 changed files with 165 additions and 42 deletions
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue