Backend changes for customize config panel. (#9134)

* Backend changes for customize config panel.

* Backend changes for customize config panel.

* Add customize.yaml to default config

* Precreate customize.yaml

* Add tests
This commit is contained in:
Andrey 2017-08-26 20:02:32 +03:00 committed by Paulus Schoutsen
parent c537770786
commit c73338bf3e
6 changed files with 196 additions and 15 deletions

View File

@ -101,6 +101,12 @@ def reload_core_config(hass):
hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
@asyncio.coroutine
def async_reload_core_config(hass):
"""Reload the core config."""
yield from hass.services.async_call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
@asyncio.coroutine
def async_setup(hass, config):
"""Set up general services related to Home Assistant."""

View File

@ -14,7 +14,7 @@ from homeassistant.util.yaml import load_yaml, dump
DOMAIN = 'config'
DEPENDENCIES = ['http']
SECTIONS = ('core', 'group', 'hassbian', 'automation', 'script')
SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script')
ON_DEMAND = ('zwave')
@ -77,11 +77,11 @@ class BaseEditConfigView(HomeAssistantView):
"""Empty config if file not found."""
raise NotImplementedError
def _get_value(self, data, config_key):
def _get_value(self, hass, data, config_key):
"""Get value."""
raise NotImplementedError
def _write_value(self, data, config_key, new_value):
def _write_value(self, hass, data, config_key, new_value):
"""Set value."""
raise NotImplementedError
@ -90,7 +90,7 @@ class BaseEditConfigView(HomeAssistantView):
"""Fetch device specific config."""
hass = request.app['hass']
current = yield from self.read_config(hass)
value = self._get_value(current, config_key)
value = self._get_value(hass, current, config_key)
if value is None:
return self.json_message('Resource not found', 404)
@ -121,7 +121,7 @@ class BaseEditConfigView(HomeAssistantView):
path = hass.config.path(self.path)
current = yield from self.read_config(hass)
self._write_value(current, config_key, data)
self._write_value(hass, current, config_key, data)
yield from hass.async_add_job(_write, path, current)
@ -149,11 +149,11 @@ class EditKeyBasedConfigView(BaseEditConfigView):
"""Return an empty config."""
return {}
def _get_value(self, data, config_key):
def _get_value(self, hass, data, config_key):
"""Get value."""
return data.get(config_key, {})
def _write_value(self, data, config_key, new_value):
def _write_value(self, hass, data, config_key, new_value):
"""Set value."""
data.setdefault(config_key, {}).update(new_value)
@ -165,14 +165,14 @@ class EditIdBasedConfigView(BaseEditConfigView):
"""Return an empty config."""
return []
def _get_value(self, data, config_key):
def _get_value(self, hass, data, config_key):
"""Get value."""
return next(
(val for val in data if val.get(CONF_ID) == config_key), None)
def _write_value(self, data, config_key, new_value):
def _write_value(self, hass, data, config_key, new_value):
"""Set value."""
value = self._get_value(data, config_key)
value = self._get_value(hass, data, config_key)
if value is None:
value = {CONF_ID: config_key}

View File

@ -0,0 +1,39 @@
"""Provide configuration end points for Customize."""
import asyncio
from homeassistant.components.config import EditKeyBasedConfigView
from homeassistant.components import async_reload_core_config
from homeassistant.config import DATA_CUSTOMIZE
import homeassistant.helpers.config_validation as cv
CONFIG_PATH = 'customize.yaml'
@asyncio.coroutine
def async_setup(hass):
"""Set up the Customize config API."""
hass.http.register_view(CustomizeConfigView(
'customize', 'config', CONFIG_PATH, cv.entity_id, dict,
post_write_hook=async_reload_core_config
))
return True
class CustomizeConfigView(EditKeyBasedConfigView):
"""Configure a list of entries."""
def _get_value(self, hass, data, config_key):
"""Get value."""
customize = hass.data.get(DATA_CUSTOMIZE, {}).get(config_key) or {}
return {'global': customize, 'local': data.get(config_key, {})}
def _write_value(self, hass, data, config_key, new_value):
"""Set value."""
data[config_key] = new_value
state = hass.states.get(config_key)
state_attributes = dict(state.attributes)
state_attributes.update(new_value)
hass.states.async_set(config_key, state.state, state_attributes)

View File

@ -57,6 +57,7 @@ DEFAULT_CORE_CONFIG = (
CONF_UNIT_SYSTEM_IMPERIAL)),
(CONF_TIME_ZONE, 'UTC', 'time_zone', 'Pick yours from here: http://en.wiki'
'pedia.org/wiki/List_of_tz_database_time_zones'),
(CONF_CUSTOMIZE, '!include customize.yaml', None, 'Customization file'),
) # type: Tuple[Tuple[str, Any, Any, str], ...]
DEFAULT_CONFIG = """
# Show links to resources in log and frontend
@ -176,12 +177,15 @@ def create_default_config(config_dir, detect_location=True):
CONFIG_PATH as AUTOMATION_CONFIG_PATH)
from homeassistant.components.config.script import (
CONFIG_PATH as SCRIPT_CONFIG_PATH)
from homeassistant.components.config.customize import (
CONFIG_PATH as CUSTOMIZE_CONFIG_PATH)
config_path = os.path.join(config_dir, YAML_CONFIG_FILE)
version_path = os.path.join(config_dir, VERSION_FILE)
group_yaml_path = os.path.join(config_dir, GROUP_CONFIG_PATH)
automation_yaml_path = os.path.join(config_dir, AUTOMATION_CONFIG_PATH)
script_yaml_path = os.path.join(config_dir, SCRIPT_CONFIG_PATH)
customize_yaml_path = os.path.join(config_dir, CUSTOMIZE_CONFIG_PATH)
info = {attr: default for attr, default, _, _ in DEFAULT_CORE_CONFIG}
@ -229,6 +233,9 @@ def create_default_config(config_dir, detect_location=True):
with open(script_yaml_path, 'wt'):
pass
with open(customize_yaml_path, 'wt'):
pass
return config_path
except IOError:

View File

@ -0,0 +1,118 @@
"""Test Customize 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.config import DATA_CUSTOMIZE
@asyncio.coroutine
def test_get_entity(hass, test_client):
"""Test getting entity."""
with patch.object(config, 'SECTIONS', ['customize']):
yield from async_setup_component(hass, 'config', {})
client = yield from test_client(hass.http.app)
def mock_read(path):
"""Mock reading data."""
return {
'hello.beer': {
'free': 'beer',
},
'other.entity': {
'do': 'something',
},
}
hass.data[DATA_CUSTOMIZE] = {'hello.beer': {'cold': 'beer'}}
with patch('homeassistant.components.config._read', mock_read):
resp = yield from client.get(
'/api/config/customize/config/hello.beer')
assert resp.status == 200
result = yield from resp.json()
assert result == {'local': {'free': 'beer'}, 'global': {'cold': 'beer'}}
@asyncio.coroutine
def test_update_entity(hass, test_client):
"""Test updating entity."""
with patch.object(config, 'SECTIONS', ['customize']):
yield from async_setup_component(hass, 'config', {})
client = yield from test_client(hass.http.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)
hass.states.async_set('hello.world', 'state', {'a': 'b'})
with patch('homeassistant.components.config._read', mock_read), \
patch('homeassistant.components.config._write', mock_write):
resp = yield from client.post(
'/api/config/customize/config/hello.world', data=json.dumps({
'name': 'Beer',
'entities': ['light.top', 'light.bottom'],
}))
assert resp.status == 200
result = yield from resp.json()
assert result == {'result': 'ok'}
state = hass.states.get('hello.world')
assert state.state == 'state'
assert dict(state.attributes) == {
'a': 'b', 'name': 'Beer', 'entities': ['light.top', 'light.bottom']}
orig_data['hello.world']['name'] = 'Beer'
orig_data['hello.world']['entities'] = ['light.top', 'light.bottom']
assert written[0] == orig_data
@asyncio.coroutine
def test_update_entity_invalid_key(hass, test_client):
"""Test updating entity."""
with patch.object(config, 'SECTIONS', ['customize']):
yield from async_setup_component(hass, 'config', {})
client = yield from test_client(hass.http.app)
resp = yield from client.post(
'/api/config/customize/config/not_entity', data=json.dumps({
'name': 'YO',
}))
assert resp.status == 400
@asyncio.coroutine
def test_update_entity_invalid_json(hass, test_client):
"""Test updating entity."""
with patch.object(config, 'SECTIONS', ['customize']):
yield from async_setup_component(hass, 'config', {})
client = yield from test_client(hass.http.app)
resp = yield from client.post(
'/api/config/customize/config/hello.beer', data='not json')
assert resp.status == 400

View File

@ -22,6 +22,8 @@ from homeassistant.components.config.group import (
CONFIG_PATH as GROUP_CONFIG_PATH)
from homeassistant.components.config.automation import (
CONFIG_PATH as AUTOMATIONS_CONFIG_PATH)
from homeassistant.components.config.customize import (
CONFIG_PATH as CUSTOMIZE_CONFIG_PATH)
from tests.common import (
get_test_config_dir, get_test_home_assistant, mock_coro)
@ -31,6 +33,7 @@ YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE)
VERSION_PATH = os.path.join(CONFIG_DIR, config_util.VERSION_FILE)
GROUP_PATH = os.path.join(CONFIG_DIR, GROUP_CONFIG_PATH)
AUTOMATIONS_PATH = os.path.join(CONFIG_DIR, AUTOMATIONS_CONFIG_PATH)
CUSTOMIZE_PATH = os.path.join(CONFIG_DIR, CUSTOMIZE_CONFIG_PATH)
ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE
@ -65,8 +68,12 @@ class TestConfig(unittest.TestCase):
if os.path.isfile(AUTOMATIONS_PATH):
os.remove(AUTOMATIONS_PATH)
if os.path.isfile(CUSTOMIZE_PATH):
os.remove(CUSTOMIZE_PATH)
self.hass.stop()
# pylint: disable=no-self-use
def test_create_default_config(self):
"""Test creation of default config."""
config_util.create_default_config(CONFIG_DIR, False)
@ -75,6 +82,7 @@ class TestConfig(unittest.TestCase):
assert os.path.isfile(VERSION_PATH)
assert os.path.isfile(GROUP_PATH)
assert os.path.isfile(AUTOMATIONS_PATH)
assert os.path.isfile(CUSTOMIZE_PATH)
def test_find_config_file_yaml(self):
"""Test if it finds a YAML config file."""
@ -169,7 +177,8 @@ class TestConfig(unittest.TestCase):
CONF_ELEVATION: 101,
CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
CONF_NAME: 'Home',
CONF_TIME_ZONE: 'America/Los_Angeles'
CONF_TIME_ZONE: 'America/Los_Angeles',
CONF_CUSTOMIZE: OrderedDict(),
}
assert expected_values == ha_conf
@ -334,11 +343,12 @@ class TestConfig(unittest.TestCase):
mock_open = mock.mock_open()
def mock_isfile(filename):
def _mock_isfile(filename):
return True
with mock.patch('homeassistant.config.open', mock_open, create=True), \
mock.patch('homeassistant.config.os.path.isfile', mock_isfile):
mock.patch(
'homeassistant.config.os.path.isfile', _mock_isfile):
opened_file = mock_open.return_value
# pylint: disable=no-member
opened_file.readline.return_value = ha_version
@ -359,11 +369,12 @@ class TestConfig(unittest.TestCase):
mock_open = mock.mock_open()
def mock_isfile(filename):
def _mock_isfile(filename):
return False
with mock.patch('homeassistant.config.open', mock_open, create=True), \
mock.patch('homeassistant.config.os.path.isfile', mock_isfile):
mock.patch(
'homeassistant.config.os.path.isfile', _mock_isfile):
opened_file = mock_open.return_value
# pylint: disable=no-member
opened_file.readline.return_value = ha_version