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."""
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))

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."""
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))

View File

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

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