Add initial group config (#6135)

This commit is contained in:
Paulus Schoutsen 2017-02-20 21:53:55 -08:00 committed by GitHub
parent 1910440a3c
commit 32873508b7
6 changed files with 335 additions and 80 deletions

View File

@ -1,15 +1,20 @@
"""Component to configure Home Assistant via an API.""" """Component to configure Home Assistant via an API."""
import asyncio import asyncio
import os
import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import EVENT_COMPONENT_LOADED from homeassistant.const import EVENT_COMPONENT_LOADED
from homeassistant.bootstrap import ( from homeassistant.bootstrap import (
async_prepare_setup_platform, ATTR_COMPONENT) async_prepare_setup_platform, ATTR_COMPONENT)
from homeassistant.components.frontend import register_built_in_panel 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' DOMAIN = 'config'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
SECTIONS = ('core', 'hassbian') SECTIONS = ('core', 'group', 'hassbian')
ON_DEMAND = ('zwave', ) ON_DEMAND = ('zwave', )
@ -53,3 +58,76 @@ def async_setup(hass, config):
hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded) hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded)
return True 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))

View File

@ -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

View File

@ -1,78 +1,19 @@
"""Provide configuration end points for Z-Wave.""" """Provide configuration end points for Z-Wave."""
import asyncio 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.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 @asyncio.coroutine
def async_setup(hass): def async_setup(hass):
"""Setup the Z-Wave config API.""" """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 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))

View File

@ -57,14 +57,16 @@ def _conf_preprocess(value):
return 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({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: cv.ordered_dict(vol.All(_conf_preprocess, { DOMAIN: cv.ordered_dict(vol.All(_conf_preprocess, GROUP_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,
}, cv.match_all))
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
# List of ON/OFF state tuples for groupable states # List of ON/OFF state tuples for groupable states
@ -99,6 +101,12 @@ def reload(hass):
hass.services.call(DOMAIN, SERVICE_RELOAD) 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): def set_visibility(hass, entity_id=None, visible=True):
"""Hide or shows a group.""" """Hide or shows a group."""
data = {ATTR_ENTITY_ID: entity_id, ATTR_VISIBLE: visible} data = {ATTR_ENTITY_ID: entity_id, ATTR_VISIBLE: visible}

View File

@ -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

View File

@ -5,10 +5,12 @@ from unittest.mock import patch
from homeassistant.bootstrap import async_setup_component from homeassistant.bootstrap import async_setup_component
from homeassistant.components import config from homeassistant.components import config
from homeassistant.components.config.zwave import DeviceConfigView
from tests.common import mock_http_component_app from tests.common import mock_http_component_app
VIEW_NAME = 'api:config:zwave:device_config'
@asyncio.coroutine @asyncio.coroutine
def test_get_device_config(hass, test_client): def test_get_device_config(hass, test_client):
"""Test getting device config.""" """Test getting device config."""
@ -17,7 +19,7 @@ def test_get_device_config(hass, test_client):
with patch.object(config, 'SECTIONS', ['zwave']): with patch.object(config, 'SECTIONS', ['zwave']):
yield from async_setup_component(hass, 'config', {}) 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) 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( resp = yield from client.get(
'/api/config/zwave/device_config/hello.beer') '/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']): with patch.object(config, 'SECTIONS', ['zwave']):
yield from async_setup_component(hass, 'config', {}) 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) client = yield from test_client(app)
@ -73,8 +75,8 @@ def test_update_device_config(hass, test_client):
"""Mock writing data.""" """Mock writing data."""
written.append(data) written.append(data)
with patch('homeassistant.components.config.zwave._read', mock_read), \ with patch('homeassistant.components.config._read', mock_read), \
patch('homeassistant.components.config.zwave._write', mock_write): patch('homeassistant.components.config._write', mock_write):
resp = yield from client.post( resp = yield from client.post(
'/api/config/zwave/device_config/hello.beer', data=json.dumps({ '/api/config/zwave/device_config/hello.beer', data=json.dumps({
'polling_intensity': 2 'polling_intensity': 2
@ -87,3 +89,61 @@ def test_update_device_config(hass, test_client):
orig_data['hello.beer']['polling_intensity'] = 2 orig_data['hello.beer']['polling_intensity'] = 2
assert written[0] == orig_data 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