Add initial Z-Wave config panel (#5937)
* Add Z-Wave config panel * Add config to Z-Wave dependencies * Lint * lint * Add tests * Remove temp workaround * Lint * Fix tests * Address comments * Fix tests under Py34pull/5982/merge
parent
6005933451
commit
36c196f9e8
homeassistant
components
config
zwave
util
tests
components
|
@ -1,29 +1,55 @@
|
|||
"""Component to interact with Hassbian tools."""
|
||||
"""Component to configure Home Assistant via an API."""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.bootstrap import async_prepare_setup_platform
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED
|
||||
from homeassistant.bootstrap import (
|
||||
async_prepare_setup_platform, ATTR_COMPONENT)
|
||||
from homeassistant.components.frontend import register_built_in_panel
|
||||
|
||||
DOMAIN = 'config'
|
||||
DEPENDENCIES = ['http']
|
||||
SECTIONS = ('core', 'hassbian')
|
||||
ON_DEMAND = ('zwave', )
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup the hassbian component."""
|
||||
"""Setup the config component."""
|
||||
register_built_in_panel(hass, 'config', 'Configuration', 'mdi:settings')
|
||||
|
||||
for panel_name in SECTIONS:
|
||||
@asyncio.coroutine
|
||||
def setup_panel(panel_name):
|
||||
"""Setup a panel."""
|
||||
panel = yield from async_prepare_setup_platform(hass, config, DOMAIN,
|
||||
panel_name)
|
||||
|
||||
if not panel:
|
||||
continue
|
||||
return
|
||||
|
||||
success = yield from panel.async_setup(hass)
|
||||
|
||||
if success:
|
||||
hass.config.components.add('{}.{}'.format(DOMAIN, panel_name))
|
||||
key = '{}.{}'.format(DOMAIN, panel_name)
|
||||
hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: key})
|
||||
hass.config.components.add(key)
|
||||
|
||||
tasks = [setup_panel(panel_name) for panel_name in SECTIONS]
|
||||
|
||||
for panel_name in ON_DEMAND:
|
||||
if panel_name in hass.config.components:
|
||||
tasks.append(setup_panel(panel_name))
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@callback
|
||||
def component_loaded(event):
|
||||
"""Respond to components being loaded."""
|
||||
panel_name = event.data.get(ATTR_COMPONENT)
|
||||
if panel_name in ON_DEMAND:
|
||||
hass.async_add_job(setup_panel(panel_name))
|
||||
|
||||
hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded)
|
||||
|
||||
return True
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
"""Provide configuration end points for Z-Wave."""
|
||||
import asyncio
|
||||
import os
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.zwave import DEVICE_CONFIG_SCHEMA_ENTRY
|
||||
from homeassistant.util.yaml import load_yaml, dump
|
||||
|
||||
|
||||
DEVICE_CONFIG = 'zwave_device_config.yml'
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass):
|
||||
"""Setup the hassbian config."""
|
||||
hass.http.register_view(DeviceConfigView)
|
||||
return True
|
||||
|
||||
|
||||
class DeviceConfigView(HomeAssistantView):
|
||||
"""Configure a Z-Wave device endpoint."""
|
||||
|
||||
url = '/api/config/zwave/device_config/{entity_id}'
|
||||
name = 'api:config:zwave:device_config:update'
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request, entity_id):
|
||||
"""Fetch device specific config."""
|
||||
hass = request.app['hass']
|
||||
current = yield from hass.loop.run_in_executor(
|
||||
None, _read, hass.config.path(DEVICE_CONFIG))
|
||||
return self.json(current.get(entity_id, {}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request, entity_id):
|
||||
"""Validate config and return results."""
|
||||
try:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
return self.json_message('Invalid JSON specified', 400)
|
||||
|
||||
try:
|
||||
# We just validate, we don't store that data because
|
||||
# we don't want to store the defaults.
|
||||
DEVICE_CONFIG_SCHEMA_ENTRY(data)
|
||||
except vol.Invalid as err:
|
||||
print(data, err)
|
||||
return self.json_message('Message malformed: {}'.format(err), 400)
|
||||
|
||||
hass = request.app['hass']
|
||||
path = hass.config.path(DEVICE_CONFIG)
|
||||
current = yield from hass.loop.run_in_executor(
|
||||
None, _read, hass.config.path(DEVICE_CONFIG))
|
||||
current.setdefault(entity_id, {}).update(data)
|
||||
|
||||
yield from hass.loop.run_in_executor(
|
||||
None, _write, hass.config.path(path), current)
|
||||
|
||||
return self.json({
|
||||
'result': 'ok',
|
||||
})
|
||||
|
||||
|
||||
def _read(path):
|
||||
"""Read YAML helper."""
|
||||
if not os.path.isfile(path):
|
||||
with open(path, 'w'):
|
||||
pass
|
||||
return {}
|
||||
|
||||
return load_yaml(path)
|
||||
|
||||
|
||||
def _write(path, data):
|
||||
"""Write YAML helper."""
|
||||
with open(path, 'w') as outfile:
|
||||
dump(data, outfile)
|
|
@ -160,7 +160,7 @@ SET_WAKEUP_SCHEMA = vol.Schema({
|
|||
vol.All(vol.Coerce(int), cv.positive_int),
|
||||
})
|
||||
|
||||
_DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({
|
||||
DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({
|
||||
vol.Optional(CONF_POLLING_INTENSITY): cv.positive_int,
|
||||
vol.Optional(CONF_IGNORED, default=DEFAULT_CONF_IGNORED): cv.boolean,
|
||||
vol.Optional(CONF_REFRESH_VALUE, default=DEFAULT_CONF_REFRESH_VALUE):
|
||||
|
@ -174,7 +174,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
vol.Optional(CONF_AUTOHEAL, default=DEFAULT_CONF_AUTOHEAL): cv.boolean,
|
||||
vol.Optional(CONF_CONFIG_PATH): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CONFIG, default={}):
|
||||
vol.Schema({cv.entity_id: _DEVICE_CONFIG_SCHEMA_ENTRY}),
|
||||
vol.Schema({cv.entity_id: DEVICE_CONFIG_SCHEMA_ENTRY}),
|
||||
vol.Optional(CONF_DEBUG, default=DEFAULT_DEBUG): cv.boolean,
|
||||
vol.Optional(CONF_POLLING_INTERVAL, default=DEFAULT_POLLING_INTERVAL):
|
||||
cv.positive_int,
|
||||
|
|
|
@ -69,9 +69,9 @@ def load_yaml(fname: str) -> Union[List, Dict]:
|
|||
raise HomeAssistantError(exc)
|
||||
|
||||
|
||||
def dump(_dict: dict) -> str:
|
||||
def dump(_dict: dict, outfile=None) -> str:
|
||||
"""Dump yaml to a string and remove null."""
|
||||
return yaml.safe_dump(_dict, default_flow_style=False) \
|
||||
return yaml.safe_dump(_dict, outfile, default_flow_style=False) \
|
||||
.replace(': null\n', ':\n')
|
||||
|
||||
|
||||
|
@ -272,3 +272,37 @@ yaml.SafeLoader.add_constructor('!include_dir_merge_list',
|
|||
yaml.SafeLoader.add_constructor('!include_dir_named', _include_dir_named_yaml)
|
||||
yaml.SafeLoader.add_constructor('!include_dir_merge_named',
|
||||
_include_dir_merge_named_yaml)
|
||||
|
||||
|
||||
# From: https://gist.github.com/miracle2k/3184458
|
||||
# pylint: disable=redefined-outer-name
|
||||
def represent_odict(dump, tag, mapping, flow_style=None):
|
||||
"""Like BaseRepresenter.represent_mapping but does not issue the sort()."""
|
||||
value = []
|
||||
node = yaml.MappingNode(tag, value, flow_style=flow_style)
|
||||
if dump.alias_key is not None:
|
||||
dump.represented_objects[dump.alias_key] = node
|
||||
best_style = True
|
||||
if hasattr(mapping, 'items'):
|
||||
mapping = mapping.items()
|
||||
for item_key, item_value in mapping:
|
||||
node_key = dump.represent_data(item_key)
|
||||
node_value = dump.represent_data(item_value)
|
||||
if not (isinstance(node_key, yaml.ScalarNode) and not node_key.style):
|
||||
best_style = False
|
||||
if not (isinstance(node_value, yaml.ScalarNode) and
|
||||
not node_value.style):
|
||||
best_style = False
|
||||
value.append((node_key, node_value))
|
||||
if flow_style is None:
|
||||
if dump.default_flow_style is not None:
|
||||
node.flow_style = dump.default_flow_style
|
||||
else:
|
||||
node.flow_style = best_style
|
||||
return node
|
||||
|
||||
|
||||
yaml.SafeDumper.add_representer(
|
||||
OrderedDict,
|
||||
lambda dumper, value:
|
||||
represent_odict(dumper, u'tag:yaml.org,2002:map', value))
|
||||
|
|
|
@ -401,6 +401,16 @@ def mock_generator(return_value=None):
|
|||
return mock_coro(return_value)()
|
||||
|
||||
|
||||
def mock_coro_func(return_value=None):
|
||||
"""Helper method to return a coro that returns a value."""
|
||||
@asyncio.coroutine
|
||||
def coro(*args, **kwargs):
|
||||
"""Fake coroutine."""
|
||||
return return_value
|
||||
|
||||
return coro
|
||||
|
||||
|
||||
@contextmanager
|
||||
def assert_setup_component(count, domain=None):
|
||||
"""Collect valid configuration from setup_component.
|
||||
|
|
|
@ -10,7 +10,7 @@ from tests.common import mock_http_component_app, mock_coro
|
|||
|
||||
@asyncio.coroutine
|
||||
def test_validate_config_ok(hass, test_client):
|
||||
"""Test getting suites."""
|
||||
"""Test checking config."""
|
||||
app = mock_http_component_app(hass)
|
||||
with patch.object(config, 'SECTIONS', ['core']):
|
||||
yield from async_setup_component(hass, 'config', {})
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
"""Test config init."""
|
||||
import asyncio
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.bootstrap import async_setup_component
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED
|
||||
from homeassistant.bootstrap import async_setup_component, ATTR_COMPONENT
|
||||
from homeassistant.components import config
|
||||
|
||||
from tests.common import mock_http_component
|
||||
from tests.common import mock_http_component, mock_coro
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
@ -12,7 +17,43 @@ def stub_http(hass):
|
|||
mock_http_component(hass)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_config_setup(hass, loop):
|
||||
"""Test it sets up hassbian."""
|
||||
loop.run_until_complete(async_setup_component(hass, 'config', {}))
|
||||
yield from async_setup_component(hass, 'config', {})
|
||||
assert 'config' in hass.config.components
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_load_on_demand_already_loaded(hass, test_client):
|
||||
"""Test getting suites."""
|
||||
hass.config.components.add('zwave')
|
||||
|
||||
with patch.object(config, 'SECTIONS', []), \
|
||||
patch.object(config, 'ON_DEMAND', ['zwave']), \
|
||||
patch('homeassistant.components.config.zwave.async_setup') as stp:
|
||||
stp.return_value = mock_coro(True)()
|
||||
|
||||
yield from async_setup_component(hass, 'config', {})
|
||||
|
||||
yield from hass.async_block_till_done()
|
||||
assert 'config.zwave' in hass.config.components
|
||||
assert stp.called
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_load_on_demand_on_load(hass, test_client):
|
||||
"""Test getting suites."""
|
||||
with patch.object(config, 'SECTIONS', []), \
|
||||
patch.object(config, 'ON_DEMAND', ['zwave']):
|
||||
yield from async_setup_component(hass, 'config', {})
|
||||
|
||||
assert 'config.zwave' not in hass.config.components
|
||||
|
||||
with patch('homeassistant.components.config.zwave.async_setup') as stp:
|
||||
stp.return_value = mock_coro(True)()
|
||||
hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: 'zwave'})
|
||||
yield from hass.async_block_till_done()
|
||||
|
||||
assert 'config.zwave' in hass.config.components
|
||||
assert stp.called
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
"""Test Z-Wave config panel."""
|
||||
import asyncio
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.bootstrap import async_setup_component
|
||||
from homeassistant.components import config
|
||||
from homeassistant.components.config.zwave import DeviceConfigView
|
||||
from tests.common import mock_http_component_app
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_get_device_config(hass, test_client):
|
||||
"""Test getting device config."""
|
||||
app = mock_http_component_app(hass)
|
||||
|
||||
with patch.object(config, 'SECTIONS', ['zwave']):
|
||||
yield from async_setup_component(hass, 'config', {})
|
||||
|
||||
hass.http.views[DeviceConfigView.name].register(app.router)
|
||||
|
||||
client = yield from test_client(app)
|
||||
|
||||
def mock_read(path):
|
||||
"""Mock reading data."""
|
||||
return {
|
||||
'hello.beer': {
|
||||
'free': 'beer',
|
||||
},
|
||||
'other.entity': {
|
||||
'do': 'something',
|
||||
},
|
||||
}
|
||||
|
||||
with patch('homeassistant.components.config.zwave._read', mock_read):
|
||||
resp = yield from client.get(
|
||||
'/api/config/zwave/device_config/hello.beer')
|
||||
|
||||
assert resp.status == 200
|
||||
result = yield from resp.json()
|
||||
|
||||
assert result == {'free': 'beer'}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_update_device_config(hass, test_client):
|
||||
"""Test updating device config."""
|
||||
app = mock_http_component_app(hass)
|
||||
|
||||
with patch.object(config, 'SECTIONS', ['zwave']):
|
||||
yield from async_setup_component(hass, 'config', {})
|
||||
|
||||
hass.http.views[DeviceConfigView.name].register(app.router)
|
||||
|
||||
client = yield from test_client(app)
|
||||
|
||||
orig_data = {
|
||||
'hello.beer': {
|
||||
'ignored': True,
|
||||
},
|
||||
'other.entity': {
|
||||
'polling_intensity': 2,
|
||||
},
|
||||
}
|
||||
|
||||
def mock_read(path):
|
||||
"""Mock reading data."""
|
||||
return orig_data
|
||||
|
||||
written = []
|
||||
|
||||
def mock_write(path, data):
|
||||
"""Mock writing data."""
|
||||
written.append(data)
|
||||
|
||||
with patch('homeassistant.components.config.zwave._read', mock_read), \
|
||||
patch('homeassistant.components.config.zwave._write', mock_write):
|
||||
resp = yield from client.post(
|
||||
'/api/config/zwave/device_config/hello.beer', data=json.dumps({
|
||||
'polling_intensity': 2
|
||||
}))
|
||||
|
||||
assert resp.status == 200
|
||||
result = yield from resp.json()
|
||||
assert result == {'result': 'ok'}
|
||||
|
||||
orig_data['hello.beer']['polling_intensity'] = 2
|
||||
|
||||
assert written[0] == orig_data
|
|
@ -48,10 +48,8 @@ class TestComponentsDeviceTracker(unittest.TestCase):
|
|||
# pylint: disable=invalid-name
|
||||
def tearDown(self):
|
||||
"""Stop everything that was started."""
|
||||
try:
|
||||
if os.path.isfile(self.yaml_devices):
|
||||
os.remove(self.yaml_devices)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
self.hass.stop()
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import pytest
|
|||
|
||||
from homeassistant.bootstrap import async_setup_component
|
||||
from homeassistant.components.zwave import (
|
||||
DATA_DEVICE_CONFIG, _DEVICE_CONFIG_SCHEMA_ENTRY)
|
||||
DATA_DEVICE_CONFIG, DEVICE_CONFIG_SCHEMA_ENTRY)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
@ -39,7 +39,7 @@ def test_device_config(hass):
|
|||
assert DATA_DEVICE_CONFIG in hass.data
|
||||
|
||||
test_data = {
|
||||
key: _DEVICE_CONFIG_SCHEMA_ENTRY(value)
|
||||
key: DEVICE_CONFIG_SCHEMA_ENTRY(value)
|
||||
for key, value in device_config.items()
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue