mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Add initial group config (#6135)
This commit is contained in:
parent
1910440a3c
commit
32873508b7
@ -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))
|
||||
|
19
homeassistant/components/config/group.py
Normal file
19
homeassistant/components/config/group.py
Normal 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
|
@ -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))
|
||||
|
@ -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}
|
||||
|
149
tests/components/config/test_group.py
Normal file
149
tests/components/config/test_group.py
Normal 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
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user