diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index a664d64a5e2..9fbb030e96e 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -1,15 +1,20 @@ """Component to configure Home Assistant via an API.""" import asyncio +import os + +import voluptuous as vol 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 +from homeassistant.components.http import HomeAssistantView +from homeassistant.util.yaml import load_yaml, dump DOMAIN = 'config' DEPENDENCIES = ['http'] -SECTIONS = ('core', 'hassbian') +SECTIONS = ('core', 'group', 'hassbian') ON_DEMAND = ('zwave', ) @@ -53,3 +58,76 @@ def async_setup(hass, config): hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded) return True + + +class EditKeyBasedConfigView(HomeAssistantView): + """Configure a Group endpoint.""" + + def __init__(self, component, config_type, path, key_schema, data_schema, + *, post_write_hook=None): + """Initialize a config view.""" + self.url = '/api/config/%s/%s/{config_key}' % (component, config_type) + self.name = 'api:config:%s:%s' % (component, config_type) + self.path = path + self.key_schema = key_schema + self.data_schema = data_schema + self.post_write_hook = post_write_hook + + @asyncio.coroutine + def get(self, request, config_key): + """Fetch device specific config.""" + hass = request.app['hass'] + current = yield from hass.loop.run_in_executor( + None, _read, hass.config.path(self.path)) + return self.json(current.get(config_key, {})) + + @asyncio.coroutine + def post(self, request, config_key): + """Validate config and return results.""" + try: + data = yield from request.json() + except ValueError: + return self.json_message('Invalid JSON specified', 400) + + try: + self.key_schema(config_key) + except vol.Invalid as err: + return self.json_message('Key malformed: {}'.format(err), 400) + + try: + # We just validate, we don't store that data because + # we don't want to store the defaults. + self.data_schema(data) + except vol.Invalid as err: + return self.json_message('Message malformed: {}'.format(err), 400) + + hass = request.app['hass'] + path = hass.config.path(self.path) + + current = yield from hass.loop.run_in_executor(None, _read, path) + current.setdefault(config_key, {}).update(data) + + yield from hass.loop.run_in_executor(None, _write, path, current) + + if self.post_write_hook is not None: + hass.async_add_job(self.post_write_hook(hass)) + + 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', encoding='utf-8') as outfile: + outfile.write(dump(data)) diff --git a/homeassistant/components/config/group.py b/homeassistant/components/config/group.py new file mode 100644 index 00000000000..3c719440001 --- /dev/null +++ b/homeassistant/components/config/group.py @@ -0,0 +1,19 @@ +"""Provide configuration end points for Groups.""" +import asyncio + +from homeassistant.components.config import EditKeyBasedConfigView +from homeassistant.components.group import GROUP_SCHEMA, async_reload +import homeassistant.helpers.config_validation as cv + + +CONFIG_PATH = 'groups.yaml' + + +@asyncio.coroutine +def async_setup(hass): + """Setup the Group config API.""" + hass.http.register_view(EditKeyBasedConfigView( + 'group', 'config', CONFIG_PATH, cv.slug, + GROUP_SCHEMA, post_write_hook=async_reload + )) + return True diff --git a/homeassistant/components/config/zwave.py b/homeassistant/components/config/zwave.py index 1f52e4f5e9a..5eca596c8f4 100644 --- a/homeassistant/components/config/zwave.py +++ b/homeassistant/components/config/zwave.py @@ -1,78 +1,19 @@ """Provide configuration end points for Z-Wave.""" import asyncio -import os -import voluptuous as vol -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.config import EditKeyBasedConfigView from homeassistant.components.zwave import DEVICE_CONFIG_SCHEMA_ENTRY -from homeassistant.util.yaml import load_yaml, dump +import homeassistant.helpers.config_validation as cv -DEVICE_CONFIG = 'zwave_device_config.yaml' +CONFIG_PATH = 'zwave_device_config.yaml' @asyncio.coroutine def async_setup(hass): """Setup the Z-Wave config API.""" - hass.http.register_view(DeviceConfigView) + hass.http.register_view(EditKeyBasedConfigView( + 'zwave', 'device_config', CONFIG_PATH, cv.entity_id, + DEVICE_CONFIG_SCHEMA_ENTRY + )) 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', encoding='utf-8') as outfile: - outfile.write(dump(data)) diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 25aa263d262..230e0e4567f 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -57,14 +57,16 @@ def _conf_preprocess(value): return value +GROUP_SCHEMA = vol.Schema({ + vol.Optional(CONF_ENTITIES): vol.Any(cv.entity_ids, None), + CONF_VIEW: cv.boolean, + CONF_NAME: cv.string, + CONF_ICON: cv.icon, + CONF_CONTROL: cv.string, +}) + CONFIG_SCHEMA = vol.Schema({ - DOMAIN: cv.ordered_dict(vol.All(_conf_preprocess, { - vol.Optional(CONF_ENTITIES): vol.Any(cv.entity_ids, None), - CONF_VIEW: cv.boolean, - CONF_NAME: cv.string, - CONF_ICON: cv.icon, - CONF_CONTROL: cv.string, - }, cv.match_all)) + DOMAIN: cv.ordered_dict(vol.All(_conf_preprocess, GROUP_SCHEMA)) }, extra=vol.ALLOW_EXTRA) # List of ON/OFF state tuples for groupable states @@ -99,6 +101,12 @@ def reload(hass): hass.services.call(DOMAIN, SERVICE_RELOAD) +@asyncio.coroutine +def async_reload(hass): + """Reload the automation from config.""" + yield from hass.services.async_call(DOMAIN, SERVICE_RELOAD) + + def set_visibility(hass, entity_id=None, visible=True): """Hide or shows a group.""" data = {ATTR_ENTITY_ID: entity_id, ATTR_VISIBLE: visible} diff --git a/tests/components/config/test_group.py b/tests/components/config/test_group.py new file mode 100644 index 00000000000..dc9b7f06c1f --- /dev/null +++ b/tests/components/config/test_group.py @@ -0,0 +1,149 @@ +"""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 tests.common import mock_http_component_app + + +VIEW_NAME = 'api:config:group:config' + + +@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', ['group']): + yield from async_setup_component(hass, 'config', {}) + + hass.http.views[VIEW_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._read', mock_read): + resp = yield from client.get( + '/api/config/group/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', ['group']): + yield from async_setup_component(hass, 'config', {}) + + hass.http.views[VIEW_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._read', mock_read), \ + patch('homeassistant.components.config._write', mock_write): + resp = yield from client.post( + '/api/config/group/config/hello_beer', data=json.dumps({ + 'name': 'Beer', + })) + + assert resp.status == 200 + result = yield from resp.json() + assert result == {'result': 'ok'} + + orig_data['hello_beer']['name'] = 'Beer' + + assert written[0] == orig_data + + +@asyncio.coroutine +def test_update_device_config_invalid_key(hass, test_client): + """Test updating device config.""" + app = mock_http_component_app(hass) + + with patch.object(config, 'SECTIONS', ['group']): + yield from async_setup_component(hass, 'config', {}) + + hass.http.views[VIEW_NAME].register(app.router) + + client = yield from test_client(app) + + resp = yield from client.post( + '/api/config/group/config/not a slug', data=json.dumps({ + 'name': 'YO', + })) + + assert resp.status == 400 + + +@asyncio.coroutine +def test_update_device_config_invalid_data(hass, test_client): + """Test updating device config.""" + app = mock_http_component_app(hass) + + with patch.object(config, 'SECTIONS', ['group']): + yield from async_setup_component(hass, 'config', {}) + + hass.http.views[VIEW_NAME].register(app.router) + + client = yield from test_client(app) + + resp = yield from client.post( + '/api/config/group/config/hello_beer', data=json.dumps({ + 'invalid_option': 2 + })) + + assert resp.status == 400 + + +@asyncio.coroutine +def test_update_device_config_invalid_json(hass, test_client): + """Test updating device config.""" + app = mock_http_component_app(hass) + + with patch.object(config, 'SECTIONS', ['group']): + yield from async_setup_component(hass, 'config', {}) + + hass.http.views[VIEW_NAME].register(app.router) + + client = yield from test_client(app) + + resp = yield from client.post( + '/api/config/group/config/hello_beer', data='not json') + + assert resp.status == 400 diff --git a/tests/components/config/test_zwave.py b/tests/components/config/test_zwave.py index 0a5d38b540a..0a136653070 100644 --- a/tests/components/config/test_zwave.py +++ b/tests/components/config/test_zwave.py @@ -5,10 +5,12 @@ 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 +VIEW_NAME = 'api:config:zwave:device_config' + + @asyncio.coroutine def test_get_device_config(hass, test_client): """Test getting device config.""" @@ -17,7 +19,7 @@ def test_get_device_config(hass, test_client): with patch.object(config, 'SECTIONS', ['zwave']): yield from async_setup_component(hass, 'config', {}) - hass.http.views[DeviceConfigView.name].register(app.router) + hass.http.views[VIEW_NAME].register(app.router) client = yield from test_client(app) @@ -32,7 +34,7 @@ def test_get_device_config(hass, test_client): }, } - with patch('homeassistant.components.config.zwave._read', mock_read): + with patch('homeassistant.components.config._read', mock_read): resp = yield from client.get( '/api/config/zwave/device_config/hello.beer') @@ -50,7 +52,7 @@ def test_update_device_config(hass, test_client): with patch.object(config, 'SECTIONS', ['zwave']): yield from async_setup_component(hass, 'config', {}) - hass.http.views[DeviceConfigView.name].register(app.router) + hass.http.views[VIEW_NAME].register(app.router) client = yield from test_client(app) @@ -73,8 +75,8 @@ def test_update_device_config(hass, test_client): """Mock writing data.""" written.append(data) - with patch('homeassistant.components.config.zwave._read', mock_read), \ - patch('homeassistant.components.config.zwave._write', mock_write): + with patch('homeassistant.components.config._read', mock_read), \ + patch('homeassistant.components.config._write', mock_write): resp = yield from client.post( '/api/config/zwave/device_config/hello.beer', data=json.dumps({ 'polling_intensity': 2 @@ -87,3 +89,61 @@ def test_update_device_config(hass, test_client): orig_data['hello.beer']['polling_intensity'] = 2 assert written[0] == orig_data + + +@asyncio.coroutine +def test_update_device_config_invalid_key(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[VIEW_NAME].register(app.router) + + client = yield from test_client(app) + + resp = yield from client.post( + '/api/config/zwave/device_config/invalid_entity', data=json.dumps({ + 'polling_intensity': 2 + })) + + assert resp.status == 400 + + +@asyncio.coroutine +def test_update_device_config_invalid_data(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[VIEW_NAME].register(app.router) + + client = yield from test_client(app) + + resp = yield from client.post( + '/api/config/zwave/device_config/hello.beer', data=json.dumps({ + 'invalid_option': 2 + })) + + assert resp.status == 400 + + +@asyncio.coroutine +def test_update_device_config_invalid_json(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[VIEW_NAME].register(app.router) + + client = yield from test_client(app) + + resp = yield from client.post( + '/api/config/zwave/device_config/hello.beer', data='not json') + + assert resp.status == 400