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
from homeassistant import config_entries
from homeassistant.core import callback, CoreState
from homeassistant.loader import get_platform
from homeassistant.helpers import discovery
@ -28,7 +29,13 @@ from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send)
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 . import workaround
from .discovery_schemas import DISCOVERY_SCHEMAS
@ -40,12 +47,10 @@ REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.9']
_LOGGER = logging.getLogger(__name__)
CLASS_ID = 'class_id'
CONF_AUTOHEAL = 'autoheal'
CONF_DEBUG = 'debug'
ATTR_POWER = 'power_consumption'
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_INVERT_OPENCLOSE_BUTTONS = 'invert_openclose_buttons'
CONF_REFRESH_VALUE = 'refresh_value'
@ -53,14 +58,9 @@ CONF_REFRESH_DELAY = 'delay'
CONF_DEVICE_CONFIG = 'device_config'
CONF_DEVICE_CONFIG_GLOB = 'device_config_glob'
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_INVERT_OPENCLOSE_BUTTONS = 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):
"""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.
"""
@ -240,27 +260,31 @@ async def async_setup(hass, config):
from openzwave.network import ZWaveNetwork
from openzwave.group import ZWaveGroup
config = {}
if DATA_ZWAVE_CONFIG in hass.data:
config = hass.data[DATA_ZWAVE_CONFIG]
# Load configuration
use_debug = config[DOMAIN].get(CONF_DEBUG)
autoheal = config[DOMAIN].get(CONF_AUTOHEAL)
use_debug = config.get(CONF_DEBUG, DEFAULT_DEBUG)
autoheal = config.get(CONF_AUTOHEAL,
DEFAULT_CONF_AUTOHEAL)
device_config = EntityValues(
config[DOMAIN][CONF_DEVICE_CONFIG],
config[DOMAIN][CONF_DEVICE_CONFIG_DOMAIN],
config[DOMAIN][CONF_DEVICE_CONFIG_GLOB])
config.get(CONF_DEVICE_CONFIG),
config.get(CONF_DEVICE_CONFIG_DOMAIN),
config.get(CONF_DEVICE_CONFIG_GLOB))
# Setup options
options = ZWaveOption(
config[DOMAIN].get(CONF_USB_STICK_PATH),
config_entry.data[CONF_USB_STICK_PATH],
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)
if CONF_NETWORK_KEY in config[DOMAIN]:
options.addOption("NetworkKey", config[DOMAIN][CONF_NETWORK_KEY])
options.lock()
if CONF_NETWORK_KEY in config_entry.data:
options.addOption("NetworkKey", config_entry.data[CONF_NETWORK_KEY])
await hass.async_add_executor_job(options.lock)
network = hass.data[DATA_NETWORK] = ZWaveNetwork(options, autostart=False)
hass.data[DATA_DEVICES] = {}
hass.data[DATA_ENTITY_VALUES] = []
@ -666,7 +690,7 @@ async def async_setup(hass, config):
def _finalize_start():
"""Perform final initializations after Z-Wave network is awaked."""
polling_interval = convert(
config[DOMAIN].get(CONF_POLLING_INTERVAL), int)
config.get(CONF_POLLING_INTERVAL), int)
if polling_interval is not None:
network.set_poll_interval(polling_interval, False)
@ -691,8 +715,6 @@ async def async_setup(hass, config):
test_network)
hass.services.register(DOMAIN, const.SERVICE_STOP_NETWORK,
stop_network)
hass.services.register(DOMAIN, const.SERVICE_START_NETWORK,
start_zwave)
hass.services.register(DOMAIN, const.SERVICE_RENAME_NODE, rename_node,
schema=RENAME_NODE_SCHEMA)
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.services.async_register(DOMAIN, const.SERVICE_START_NETWORK,
start_zwave)
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
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'
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',
'zone',
'upnp',
'zwave'
]

View file

@ -2,6 +2,7 @@
import asyncio
from collections import OrderedDict
from datetime import datetime
from pytz import utc
import unittest
from unittest.mock import patch, MagicMock
@ -22,13 +23,6 @@ from tests.common import (
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
def test_valid_device_config(hass, mock_openzwave):
"""Test valid device config."""
@ -41,6 +35,7 @@ def test_valid_device_config(hass, mock_openzwave):
'zwave': {
'device_config': device_config
}})
yield from hass.async_block_till_done()
assert result
@ -57,6 +52,7 @@ def test_invalid_device_config(hass, mock_openzwave):
'zwave': {
'device_config': device_config
}})
yield from hass.async_block_till_done()
assert not result
@ -81,6 +77,7 @@ def test_network_options(hass, mock_openzwave):
'usb_path': 'mock_usb_path',
'config_path': 'mock_config_path',
}})
yield from hass.async_block_till_done()
assert result
@ -92,14 +89,16 @@ def test_network_options(hass, mock_openzwave):
@asyncio.coroutine
def test_auto_heal_midnight(hass, mock_openzwave):
"""Test network auto-heal at midnight."""
assert (yield from async_setup_component(hass, 'zwave', {
yield from async_setup_component(hass, 'zwave', {
'zwave': {
'autoheal': True,
}}))
}})
yield from hass.async_block_till_done()
network = hass.data[zwave.DATA_NETWORK]
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)
yield from hass.async_block_till_done()
assert network.heal.called
@ -109,14 +108,16 @@ def test_auto_heal_midnight(hass, mock_openzwave):
@asyncio.coroutine
def test_auto_heal_disabled(hass, mock_openzwave):
"""Test network auto-heal disabled."""
assert (yield from async_setup_component(hass, 'zwave', {
yield from async_setup_component(hass, 'zwave', {
'zwave': {
'autoheal': False,
}}))
}})
yield from hass.async_block_till_done()
network = hass.data[zwave.DATA_NETWORK]
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)
yield from hass.async_block_till_done()
assert not network.heal.called
@ -215,6 +216,7 @@ def test_node_discovery(hass, mock_openzwave):
with patch('pydispatch.dispatcher.connect', new=mock_connect):
yield from async_setup_component(hass, 'zwave', {'zwave': {}})
yield from hass.async_block_till_done()
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):
await async_setup_component(hass, 'zwave', {'zwave': {}})
await hass.async_block_till_done()
assert len(mock_receivers) == 1
@ -282,6 +285,7 @@ def test_node_ignored(hass, mock_openzwave):
'zwave.mock_node': {
'ignored': True,
}}}})
yield from hass.async_block_till_done()
assert len(mock_receivers) == 1
@ -303,6 +307,7 @@ def test_value_discovery(hass, mock_openzwave):
with patch('pydispatch.dispatcher.connect', new=mock_connect):
yield from async_setup_component(hass, 'zwave', {'zwave': {}})
yield from hass.async_block_till_done()
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):
yield from async_setup_component(hass, 'zwave', {'zwave': {}})
yield from hass.async_block_till_done()
assert len(mock_receivers) == 1
@ -373,6 +379,7 @@ def test_power_schemes(hass, mock_openzwave):
with patch('pydispatch.dispatcher.connect', new=mock_connect):
yield from async_setup_component(hass, 'zwave', {'zwave': {}})
yield from hass.async_block_till_done()
assert len(mock_receivers) == 1
@ -415,6 +422,7 @@ def test_network_ready(hass, mock_openzwave):
with patch('pydispatch.dispatcher.connect', new=mock_connect):
yield from async_setup_component(hass, 'zwave', {'zwave': {}})
yield from hass.async_block_till_done()
assert len(mock_receivers) == 1
@ -442,6 +450,7 @@ def test_network_complete(hass, mock_openzwave):
with patch('pydispatch.dispatcher.connect', new=mock_connect):
yield from async_setup_component(hass, 'zwave', {'zwave': {}})
yield from hass.async_block_till_done()
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):
yield from async_setup_component(hass, 'zwave', {'zwave': {}})
yield from hass.async_block_till_done()
assert len(mock_receivers) == 1