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:
parent
5167658a1d
commit
cf249e3e5e
7 changed files with 227 additions and 40 deletions
22
homeassistant/components/zwave/.translations/en.json
Normal file
22
homeassistant/components/zwave/.translations/en.json
Normal 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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
||||
|
|
95
homeassistant/components/zwave/config_flow.py
Normal file
95
homeassistant/components/zwave/config_flow.py
Normal 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),
|
||||
},
|
||||
)
|
|
@ -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'
|
||||
|
|
22
homeassistant/components/zwave/strings.json
Normal file
22
homeassistant/components/zwave/strings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -151,6 +151,7 @@ FLOWS = [
|
|||
'tradfri',
|
||||
'zone',
|
||||
'upnp',
|
||||
'zwave'
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue