Z-Wave Config Entry Support (#17119)

* Initial Z-Wave Config Entry Support

* Use conf.get() for config import

* Uncomment test

* Re-add line breaks

* tabs -> space

* Unused import cleanup & lint fixes

* Remove unused config flow link step

* Address comments

* Remove unused import

* Fix tests

* Check for valid usb_path

* Test for Z-Stick in config flow

* Pass config dir to ZWaveOption

* Auto-generate Network Key if none provided

* Test fixes

* Address comments & more start network service registration

* add_executor_job for options.lock()
This commit is contained in:
Charles Garwood 2018-10-09 10:30:55 -04:00 committed by Paulus Schoutsen
parent 5167658a1d
commit cf249e3e5e
7 changed files with 227 additions and 40 deletions

View file

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "Z-Wave is already configured",
"one_instance_only": "Component only supports one Z-Wave instance"
},
"error": {
"option_error": "Z-Wave validation failed. Is the path to the USB stick correct?"
},
"step": {
"user": {
"data": {
"network_key": "Network Key (leave blank to auto-generate)",
"usb_path": "USB Path"
},
"description": "See https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables",
"title": "Set up Z-Wave"
}
},
"title": "Z-Wave"
}
}

View file

@ -11,6 +11,7 @@ from pprint import pprint
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import callback, CoreState from homeassistant.core import callback, CoreState
from homeassistant.loader import get_platform from homeassistant.loader import get_platform
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
@ -28,7 +29,13 @@ from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send) async_dispatcher_connect, async_dispatcher_send)
from . import const from . import const
from .const import DOMAIN, DATA_DEVICES, DATA_NETWORK, DATA_ENTITY_VALUES from . import config_flow # noqa # pylint: disable=unused-import
from .const import (
CONF_AUTOHEAL, CONF_DEBUG, CONF_POLLING_INTERVAL,
CONF_USB_STICK_PATH, CONF_CONFIG_PATH, CONF_NETWORK_KEY,
DEFAULT_CONF_AUTOHEAL, DEFAULT_CONF_USB_STICK_PATH,
DEFAULT_POLLING_INTERVAL, DEFAULT_DEBUG, DOMAIN,
DATA_DEVICES, DATA_NETWORK, DATA_ENTITY_VALUES)
from .node_entity import ZWaveBaseEntity, ZWaveNodeEntity from .node_entity import ZWaveBaseEntity, ZWaveNodeEntity
from . import workaround from . import workaround
from .discovery_schemas import DISCOVERY_SCHEMAS from .discovery_schemas import DISCOVERY_SCHEMAS
@ -40,12 +47,10 @@ REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.9']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CLASS_ID = 'class_id' CLASS_ID = 'class_id'
CONF_AUTOHEAL = 'autoheal'
CONF_DEBUG = 'debug' ATTR_POWER = 'power_consumption'
CONF_POLLING_INTENSITY = 'polling_intensity' CONF_POLLING_INTENSITY = 'polling_intensity'
CONF_POLLING_INTERVAL = 'polling_interval'
CONF_USB_STICK_PATH = 'usb_path'
CONF_CONFIG_PATH = 'config_path'
CONF_IGNORED = 'ignored' CONF_IGNORED = 'ignored'
CONF_INVERT_OPENCLOSE_BUTTONS = 'invert_openclose_buttons' CONF_INVERT_OPENCLOSE_BUTTONS = 'invert_openclose_buttons'
CONF_REFRESH_VALUE = 'refresh_value' CONF_REFRESH_VALUE = 'refresh_value'
@ -53,14 +58,9 @@ CONF_REFRESH_DELAY = 'delay'
CONF_DEVICE_CONFIG = 'device_config' CONF_DEVICE_CONFIG = 'device_config'
CONF_DEVICE_CONFIG_GLOB = 'device_config_glob' CONF_DEVICE_CONFIG_GLOB = 'device_config_glob'
CONF_DEVICE_CONFIG_DOMAIN = 'device_config_domain' CONF_DEVICE_CONFIG_DOMAIN = 'device_config_domain'
CONF_NETWORK_KEY = 'network_key'
ATTR_POWER = 'power_consumption' DATA_ZWAVE_CONFIG = 'zwave_config'
DEFAULT_CONF_AUTOHEAL = True
DEFAULT_CONF_USB_STICK_PATH = '/zwaveusbstick'
DEFAULT_POLLING_INTERVAL = 60000
DEFAULT_DEBUG = False
DEFAULT_CONF_IGNORED = False DEFAULT_CONF_IGNORED = False
DEFAULT_CONF_INVERT_OPENCLOSE_BUTTONS = False DEFAULT_CONF_INVERT_OPENCLOSE_BUTTONS = False
DEFAULT_CONF_REFRESH_VALUE = False DEFAULT_CONF_REFRESH_VALUE = False
@ -230,7 +230,27 @@ async def async_setup_platform(hass, config, async_add_entities,
async def async_setup(hass, config): async def async_setup(hass, config):
"""Set up Z-Wave. """Set up Z-Wave components."""
if DOMAIN not in config:
return True
conf = config[DOMAIN]
hass.data[DATA_ZWAVE_CONFIG] = conf
if not hass.config_entries.async_entries(DOMAIN):
hass.async_create_task(hass.config_entries.flow.async_init(
DOMAIN, context={'source': config_entries.SOURCE_IMPORT},
data={
CONF_USB_STICK_PATH: conf[CONF_USB_STICK_PATH],
CONF_NETWORK_KEY: conf.get(CONF_NETWORK_KEY),
}
))
return True
async def async_setup_entry(hass, config_entry):
"""Set up Z-Wave from a config entry.
Will automatically load components to support devices found on the network. Will automatically load components to support devices found on the network.
""" """
@ -240,27 +260,31 @@ async def async_setup(hass, config):
from openzwave.network import ZWaveNetwork from openzwave.network import ZWaveNetwork
from openzwave.group import ZWaveGroup from openzwave.group import ZWaveGroup
config = {}
if DATA_ZWAVE_CONFIG in hass.data:
config = hass.data[DATA_ZWAVE_CONFIG]
# Load configuration # Load configuration
use_debug = config[DOMAIN].get(CONF_DEBUG) use_debug = config.get(CONF_DEBUG, DEFAULT_DEBUG)
autoheal = config[DOMAIN].get(CONF_AUTOHEAL) autoheal = config.get(CONF_AUTOHEAL,
DEFAULT_CONF_AUTOHEAL)
device_config = EntityValues( device_config = EntityValues(
config[DOMAIN][CONF_DEVICE_CONFIG], config.get(CONF_DEVICE_CONFIG),
config[DOMAIN][CONF_DEVICE_CONFIG_DOMAIN], config.get(CONF_DEVICE_CONFIG_DOMAIN),
config[DOMAIN][CONF_DEVICE_CONFIG_GLOB]) config.get(CONF_DEVICE_CONFIG_GLOB))
# Setup options # Setup options
options = ZWaveOption( options = ZWaveOption(
config[DOMAIN].get(CONF_USB_STICK_PATH), config_entry.data[CONF_USB_STICK_PATH],
user_path=hass.config.config_dir, user_path=hass.config.config_dir,
config_path=config[DOMAIN].get(CONF_CONFIG_PATH)) config_path=config.get(CONF_CONFIG_PATH))
options.set_console_output(use_debug) options.set_console_output(use_debug)
if CONF_NETWORK_KEY in config[DOMAIN]: if CONF_NETWORK_KEY in config_entry.data:
options.addOption("NetworkKey", config[DOMAIN][CONF_NETWORK_KEY]) options.addOption("NetworkKey", config_entry.data[CONF_NETWORK_KEY])
options.lock()
await hass.async_add_executor_job(options.lock)
network = hass.data[DATA_NETWORK] = ZWaveNetwork(options, autostart=False) network = hass.data[DATA_NETWORK] = ZWaveNetwork(options, autostart=False)
hass.data[DATA_DEVICES] = {} hass.data[DATA_DEVICES] = {}
hass.data[DATA_ENTITY_VALUES] = [] hass.data[DATA_ENTITY_VALUES] = []
@ -666,7 +690,7 @@ async def async_setup(hass, config):
def _finalize_start(): def _finalize_start():
"""Perform final initializations after Z-Wave network is awaked.""" """Perform final initializations after Z-Wave network is awaked."""
polling_interval = convert( polling_interval = convert(
config[DOMAIN].get(CONF_POLLING_INTERVAL), int) config.get(CONF_POLLING_INTERVAL), int)
if polling_interval is not None: if polling_interval is not None:
network.set_poll_interval(polling_interval, False) network.set_poll_interval(polling_interval, False)
@ -691,8 +715,6 @@ async def async_setup(hass, config):
test_network) test_network)
hass.services.register(DOMAIN, const.SERVICE_STOP_NETWORK, hass.services.register(DOMAIN, const.SERVICE_STOP_NETWORK,
stop_network) stop_network)
hass.services.register(DOMAIN, const.SERVICE_START_NETWORK,
start_zwave)
hass.services.register(DOMAIN, const.SERVICE_RENAME_NODE, rename_node, hass.services.register(DOMAIN, const.SERVICE_RENAME_NODE, rename_node,
schema=RENAME_NODE_SCHEMA) schema=RENAME_NODE_SCHEMA)
hass.services.register(DOMAIN, const.SERVICE_RENAME_VALUE, hass.services.register(DOMAIN, const.SERVICE_RENAME_VALUE,
@ -752,6 +774,9 @@ async def async_setup(hass, config):
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_zwave) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_zwave)
hass.services.async_register(DOMAIN, const.SERVICE_START_NETWORK,
start_zwave)
return True return True

View file

@ -0,0 +1,95 @@
"""Config flow to configure Z-Wave."""
from collections import OrderedDict
import logging
import voluptuous as vol
from homeassistant import config_entries
from .const import (
CONF_USB_STICK_PATH, CONF_NETWORK_KEY,
DEFAULT_CONF_USB_STICK_PATH, DOMAIN)
_LOGGER = logging.getLogger(__name__)
@config_entries.HANDLERS.register(DOMAIN)
class ZwaveFlowHandler(config_entries.ConfigFlow):
"""Handle a Z-Wave config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
def __init__(self):
"""Initialize the Z-Wave config flow."""
self.usb_path = CONF_USB_STICK_PATH
async def async_step_user(self, user_input=None):
"""Handle a flow start."""
if self._async_current_entries():
return self.async_abort(reason='one_instance_only')
errors = {}
fields = OrderedDict()
fields[vol.Required(CONF_USB_STICK_PATH,
default=DEFAULT_CONF_USB_STICK_PATH)] = str
fields[vol.Optional(CONF_NETWORK_KEY)] = str
if user_input is not None:
# Check if USB path is valid
from openzwave.option import ZWaveOption
from openzwave.object import ZWaveException
try:
from functools import partial
# pylint: disable=unused-variable
option = await self.hass.async_add_executor_job( # noqa: F841
partial(ZWaveOption,
user_input[CONF_USB_STICK_PATH],
user_path=self.hass.config.config_dir)
)
except ZWaveException:
errors['base'] = 'option_error'
return self.async_show_form(
step_id='user',
data_schema=vol.Schema(fields),
errors=errors
)
if user_input.get(CONF_NETWORK_KEY) is None:
# Generate a random key
from random import choice
key = str()
for i in range(16):
key += '0x'
key += choice('1234567890ABCDEF')
key += choice('1234567890ABCDEF')
if i < 15:
key += ', '
user_input[CONF_NETWORK_KEY] = key
return self.async_create_entry(
title='Z-Wave',
data={
CONF_USB_STICK_PATH: user_input[CONF_USB_STICK_PATH],
CONF_NETWORK_KEY: user_input[CONF_NETWORK_KEY],
},
)
return self.async_show_form(
step_id='user', data_schema=vol.Schema(fields)
)
async def async_step_import(self, info):
"""Import existing configuration from Z-Wave."""
if self._async_current_entries():
return self.async_abort(reason='already_setup')
return self.async_create_entry(
title="Z-Wave (import from configuration.yaml)",
data={
CONF_USB_STICK_PATH: info.get(CONF_USB_STICK_PATH),
CONF_NETWORK_KEY: info.get(CONF_NETWORK_KEY),
},
)

View file

@ -22,6 +22,18 @@ ATTR_VALUE_INSTANCE = "value_instance"
NETWORK_READY_WAIT_SECS = 300 NETWORK_READY_WAIT_SECS = 300
NODE_READY_WAIT_SECS = 30 NODE_READY_WAIT_SECS = 30
CONF_AUTOHEAL = 'autoheal'
CONF_DEBUG = 'debug'
CONF_POLLING_INTERVAL = 'polling_interval'
CONF_USB_STICK_PATH = 'usb_path'
CONF_CONFIG_PATH = 'config_path'
CONF_NETWORK_KEY = 'network_key'
DEFAULT_CONF_AUTOHEAL = True
DEFAULT_CONF_USB_STICK_PATH = '/zwaveusbstick'
DEFAULT_POLLING_INTERVAL = 60000
DEFAULT_DEBUG = False
DISCOVERY_DEVICE = 'device' DISCOVERY_DEVICE = 'device'
DATA_DEVICES = 'zwave_devices' DATA_DEVICES = 'zwave_devices'

View file

@ -0,0 +1,22 @@
{
"config": {
"title": "Z-Wave",
"step": {
"user": {
"title": "Set up Z-Wave",
"description": "See https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables",
"data": {
"usb_path": "USB Path",
"network_key": "Network Key (leave blank to auto-generate)"
}
}
},
"error": {
"option_error": "Z-Wave validation failed. Is the path to the USB stick correct?"
},
"abort": {
"already_configured": "Z-Wave is already configured",
"one_instance_only": "Component only supports one Z-Wave instance"
}
}
}

View file

@ -151,6 +151,7 @@ FLOWS = [
'tradfri', 'tradfri',
'zone', 'zone',
'upnp', 'upnp',
'zwave'
] ]

View file

@ -2,6 +2,7 @@
import asyncio import asyncio
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime from datetime import datetime
from pytz import utc
import unittest import unittest
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
@ -22,13 +23,6 @@ from tests.common import (
from tests.mock.zwave import MockNetwork, MockNode, MockValue, MockEntityValues from tests.mock.zwave import MockNetwork, MockNode, MockValue, MockEntityValues
@asyncio.coroutine
def test_missing_openzwave(hass):
"""Test that missing openzwave lib stops setup."""
result = yield from async_setup_component(hass, 'zwave', {'zwave': {}})
assert not result
@asyncio.coroutine @asyncio.coroutine
def test_valid_device_config(hass, mock_openzwave): def test_valid_device_config(hass, mock_openzwave):
"""Test valid device config.""" """Test valid device config."""
@ -41,6 +35,7 @@ def test_valid_device_config(hass, mock_openzwave):
'zwave': { 'zwave': {
'device_config': device_config 'device_config': device_config
}}) }})
yield from hass.async_block_till_done()
assert result assert result
@ -57,6 +52,7 @@ def test_invalid_device_config(hass, mock_openzwave):
'zwave': { 'zwave': {
'device_config': device_config 'device_config': device_config
}}) }})
yield from hass.async_block_till_done()
assert not result assert not result
@ -81,6 +77,7 @@ def test_network_options(hass, mock_openzwave):
'usb_path': 'mock_usb_path', 'usb_path': 'mock_usb_path',
'config_path': 'mock_config_path', 'config_path': 'mock_config_path',
}}) }})
yield from hass.async_block_till_done()
assert result assert result
@ -92,14 +89,16 @@ def test_network_options(hass, mock_openzwave):
@asyncio.coroutine @asyncio.coroutine
def test_auto_heal_midnight(hass, mock_openzwave): def test_auto_heal_midnight(hass, mock_openzwave):
"""Test network auto-heal at midnight.""" """Test network auto-heal at midnight."""
assert (yield from async_setup_component(hass, 'zwave', { yield from async_setup_component(hass, 'zwave', {
'zwave': { 'zwave': {
'autoheal': True, 'autoheal': True,
}})) }})
yield from hass.async_block_till_done()
network = hass.data[zwave.DATA_NETWORK] network = hass.data[zwave.DATA_NETWORK]
assert not network.heal.called assert not network.heal.called
time = datetime(2017, 5, 6, 0, 0, 0) time = utc.localize(datetime(2017, 5, 6, 0, 0, 0))
async_fire_time_changed(hass, time) async_fire_time_changed(hass, time)
yield from hass.async_block_till_done() yield from hass.async_block_till_done()
assert network.heal.called assert network.heal.called
@ -109,14 +108,16 @@ def test_auto_heal_midnight(hass, mock_openzwave):
@asyncio.coroutine @asyncio.coroutine
def test_auto_heal_disabled(hass, mock_openzwave): def test_auto_heal_disabled(hass, mock_openzwave):
"""Test network auto-heal disabled.""" """Test network auto-heal disabled."""
assert (yield from async_setup_component(hass, 'zwave', { yield from async_setup_component(hass, 'zwave', {
'zwave': { 'zwave': {
'autoheal': False, 'autoheal': False,
}})) }})
yield from hass.async_block_till_done()
network = hass.data[zwave.DATA_NETWORK] network = hass.data[zwave.DATA_NETWORK]
assert not network.heal.called assert not network.heal.called
time = datetime(2017, 5, 6, 0, 0, 0) time = utc.localize(datetime(2017, 5, 6, 0, 0, 0))
async_fire_time_changed(hass, time) async_fire_time_changed(hass, time)
yield from hass.async_block_till_done() yield from hass.async_block_till_done()
assert not network.heal.called assert not network.heal.called
@ -215,6 +216,7 @@ def test_node_discovery(hass, mock_openzwave):
with patch('pydispatch.dispatcher.connect', new=mock_connect): with patch('pydispatch.dispatcher.connect', new=mock_connect):
yield from async_setup_component(hass, 'zwave', {'zwave': {}}) yield from async_setup_component(hass, 'zwave', {'zwave': {}})
yield from hass.async_block_till_done()
assert len(mock_receivers) == 1 assert len(mock_receivers) == 1
@ -235,6 +237,7 @@ async def test_unparsed_node_discovery(hass, mock_openzwave):
with patch('pydispatch.dispatcher.connect', new=mock_connect): with patch('pydispatch.dispatcher.connect', new=mock_connect):
await async_setup_component(hass, 'zwave', {'zwave': {}}) await async_setup_component(hass, 'zwave', {'zwave': {}})
await hass.async_block_till_done()
assert len(mock_receivers) == 1 assert len(mock_receivers) == 1
@ -282,6 +285,7 @@ def test_node_ignored(hass, mock_openzwave):
'zwave.mock_node': { 'zwave.mock_node': {
'ignored': True, 'ignored': True,
}}}}) }}}})
yield from hass.async_block_till_done()
assert len(mock_receivers) == 1 assert len(mock_receivers) == 1
@ -303,6 +307,7 @@ def test_value_discovery(hass, mock_openzwave):
with patch('pydispatch.dispatcher.connect', new=mock_connect): with patch('pydispatch.dispatcher.connect', new=mock_connect):
yield from async_setup_component(hass, 'zwave', {'zwave': {}}) yield from async_setup_component(hass, 'zwave', {'zwave': {}})
yield from hass.async_block_till_done()
assert len(mock_receivers) == 1 assert len(mock_receivers) == 1
@ -328,6 +333,7 @@ def test_value_discovery_existing_entity(hass, mock_openzwave):
with patch('pydispatch.dispatcher.connect', new=mock_connect): with patch('pydispatch.dispatcher.connect', new=mock_connect):
yield from async_setup_component(hass, 'zwave', {'zwave': {}}) yield from async_setup_component(hass, 'zwave', {'zwave': {}})
yield from hass.async_block_till_done()
assert len(mock_receivers) == 1 assert len(mock_receivers) == 1
@ -373,6 +379,7 @@ def test_power_schemes(hass, mock_openzwave):
with patch('pydispatch.dispatcher.connect', new=mock_connect): with patch('pydispatch.dispatcher.connect', new=mock_connect):
yield from async_setup_component(hass, 'zwave', {'zwave': {}}) yield from async_setup_component(hass, 'zwave', {'zwave': {}})
yield from hass.async_block_till_done()
assert len(mock_receivers) == 1 assert len(mock_receivers) == 1
@ -415,6 +422,7 @@ def test_network_ready(hass, mock_openzwave):
with patch('pydispatch.dispatcher.connect', new=mock_connect): with patch('pydispatch.dispatcher.connect', new=mock_connect):
yield from async_setup_component(hass, 'zwave', {'zwave': {}}) yield from async_setup_component(hass, 'zwave', {'zwave': {}})
yield from hass.async_block_till_done()
assert len(mock_receivers) == 1 assert len(mock_receivers) == 1
@ -442,6 +450,7 @@ def test_network_complete(hass, mock_openzwave):
with patch('pydispatch.dispatcher.connect', new=mock_connect): with patch('pydispatch.dispatcher.connect', new=mock_connect):
yield from async_setup_component(hass, 'zwave', {'zwave': {}}) yield from async_setup_component(hass, 'zwave', {'zwave': {}})
yield from hass.async_block_till_done()
assert len(mock_receivers) == 1 assert len(mock_receivers) == 1
@ -469,6 +478,7 @@ def test_network_complete_some_dead(hass, mock_openzwave):
with patch('pydispatch.dispatcher.connect', new=mock_connect): with patch('pydispatch.dispatcher.connect', new=mock_connect):
yield from async_setup_component(hass, 'zwave', {'zwave': {}}) yield from async_setup_component(hass, 'zwave', {'zwave': {}})
yield from hass.async_block_till_done()
assert len(mock_receivers) == 1 assert len(mock_receivers) == 1