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:
parent
c537770786
commit
c73338bf3e
6 changed files with 196 additions and 15 deletions
|
@ -101,6 +101,12 @@ def reload_core_config(hass):
|
||||||
hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
|
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
|
@asyncio.coroutine
|
||||||
def async_setup(hass, config):
|
def async_setup(hass, config):
|
||||||
"""Set up general services related to Home Assistant."""
|
"""Set up general services related to Home Assistant."""
|
||||||
|
|
|
@ -14,7 +14,7 @@ from homeassistant.util.yaml import load_yaml, dump
|
||||||
|
|
||||||
DOMAIN = 'config'
|
DOMAIN = 'config'
|
||||||
DEPENDENCIES = ['http']
|
DEPENDENCIES = ['http']
|
||||||
SECTIONS = ('core', 'group', 'hassbian', 'automation', 'script')
|
SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script')
|
||||||
ON_DEMAND = ('zwave')
|
ON_DEMAND = ('zwave')
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,11 +77,11 @@ class BaseEditConfigView(HomeAssistantView):
|
||||||
"""Empty config if file not found."""
|
"""Empty config if file not found."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def _get_value(self, data, config_key):
|
def _get_value(self, hass, data, config_key):
|
||||||
"""Get value."""
|
"""Get value."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def _write_value(self, data, config_key, new_value):
|
def _write_value(self, hass, data, config_key, new_value):
|
||||||
"""Set value."""
|
"""Set value."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ class BaseEditConfigView(HomeAssistantView):
|
||||||
"""Fetch device specific config."""
|
"""Fetch device specific config."""
|
||||||
hass = request.app['hass']
|
hass = request.app['hass']
|
||||||
current = yield from self.read_config(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:
|
if value is None:
|
||||||
return self.json_message('Resource not found', 404)
|
return self.json_message('Resource not found', 404)
|
||||||
|
@ -121,7 +121,7 @@ class BaseEditConfigView(HomeAssistantView):
|
||||||
path = hass.config.path(self.path)
|
path = hass.config.path(self.path)
|
||||||
|
|
||||||
current = yield from self.read_config(hass)
|
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)
|
yield from hass.async_add_job(_write, path, current)
|
||||||
|
|
||||||
|
@ -149,11 +149,11 @@ class EditKeyBasedConfigView(BaseEditConfigView):
|
||||||
"""Return an empty config."""
|
"""Return an empty config."""
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def _get_value(self, data, config_key):
|
def _get_value(self, hass, data, config_key):
|
||||||
"""Get value."""
|
"""Get value."""
|
||||||
return data.get(config_key, {})
|
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."""
|
"""Set value."""
|
||||||
data.setdefault(config_key, {}).update(new_value)
|
data.setdefault(config_key, {}).update(new_value)
|
||||||
|
|
||||||
|
@ -165,14 +165,14 @@ class EditIdBasedConfigView(BaseEditConfigView):
|
||||||
"""Return an empty config."""
|
"""Return an empty config."""
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _get_value(self, data, config_key):
|
def _get_value(self, hass, data, config_key):
|
||||||
"""Get value."""
|
"""Get value."""
|
||||||
return next(
|
return next(
|
||||||
(val for val in data if val.get(CONF_ID) == config_key), None)
|
(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."""
|
"""Set value."""
|
||||||
value = self._get_value(data, config_key)
|
value = self._get_value(hass, data, config_key)
|
||||||
|
|
||||||
if value is None:
|
if value is None:
|
||||||
value = {CONF_ID: config_key}
|
value = {CONF_ID: config_key}
|
||||||
|
|
39
homeassistant/components/config/customize.py
Normal file
39
homeassistant/components/config/customize.py
Normal 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)
|
|
@ -57,6 +57,7 @@ DEFAULT_CORE_CONFIG = (
|
||||||
CONF_UNIT_SYSTEM_IMPERIAL)),
|
CONF_UNIT_SYSTEM_IMPERIAL)),
|
||||||
(CONF_TIME_ZONE, 'UTC', 'time_zone', 'Pick yours from here: http://en.wiki'
|
(CONF_TIME_ZONE, 'UTC', 'time_zone', 'Pick yours from here: http://en.wiki'
|
||||||
'pedia.org/wiki/List_of_tz_database_time_zones'),
|
'pedia.org/wiki/List_of_tz_database_time_zones'),
|
||||||
|
(CONF_CUSTOMIZE, '!include customize.yaml', None, 'Customization file'),
|
||||||
) # type: Tuple[Tuple[str, Any, Any, str], ...]
|
) # type: Tuple[Tuple[str, Any, Any, str], ...]
|
||||||
DEFAULT_CONFIG = """
|
DEFAULT_CONFIG = """
|
||||||
# Show links to resources in log and frontend
|
# 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)
|
CONFIG_PATH as AUTOMATION_CONFIG_PATH)
|
||||||
from homeassistant.components.config.script import (
|
from homeassistant.components.config.script import (
|
||||||
CONFIG_PATH as SCRIPT_CONFIG_PATH)
|
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)
|
config_path = os.path.join(config_dir, YAML_CONFIG_FILE)
|
||||||
version_path = os.path.join(config_dir, VERSION_FILE)
|
version_path = os.path.join(config_dir, VERSION_FILE)
|
||||||
group_yaml_path = os.path.join(config_dir, GROUP_CONFIG_PATH)
|
group_yaml_path = os.path.join(config_dir, GROUP_CONFIG_PATH)
|
||||||
automation_yaml_path = os.path.join(config_dir, AUTOMATION_CONFIG_PATH)
|
automation_yaml_path = os.path.join(config_dir, AUTOMATION_CONFIG_PATH)
|
||||||
script_yaml_path = os.path.join(config_dir, SCRIPT_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}
|
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'):
|
with open(script_yaml_path, 'wt'):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
with open(customize_yaml_path, 'wt'):
|
||||||
|
pass
|
||||||
|
|
||||||
return config_path
|
return config_path
|
||||||
|
|
||||||
except IOError:
|
except IOError:
|
||||||
|
|
118
tests/components/config/test_customize.py
Normal file
118
tests/components/config/test_customize.py
Normal 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
|
|
@ -22,6 +22,8 @@ from homeassistant.components.config.group import (
|
||||||
CONFIG_PATH as GROUP_CONFIG_PATH)
|
CONFIG_PATH as GROUP_CONFIG_PATH)
|
||||||
from homeassistant.components.config.automation import (
|
from homeassistant.components.config.automation import (
|
||||||
CONFIG_PATH as AUTOMATIONS_CONFIG_PATH)
|
CONFIG_PATH as AUTOMATIONS_CONFIG_PATH)
|
||||||
|
from homeassistant.components.config.customize import (
|
||||||
|
CONFIG_PATH as CUSTOMIZE_CONFIG_PATH)
|
||||||
|
|
||||||
from tests.common import (
|
from tests.common import (
|
||||||
get_test_config_dir, get_test_home_assistant, mock_coro)
|
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)
|
VERSION_PATH = os.path.join(CONFIG_DIR, config_util.VERSION_FILE)
|
||||||
GROUP_PATH = os.path.join(CONFIG_DIR, GROUP_CONFIG_PATH)
|
GROUP_PATH = os.path.join(CONFIG_DIR, GROUP_CONFIG_PATH)
|
||||||
AUTOMATIONS_PATH = os.path.join(CONFIG_DIR, AUTOMATIONS_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
|
ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE
|
||||||
|
|
||||||
|
|
||||||
|
@ -65,8 +68,12 @@ class TestConfig(unittest.TestCase):
|
||||||
if os.path.isfile(AUTOMATIONS_PATH):
|
if os.path.isfile(AUTOMATIONS_PATH):
|
||||||
os.remove(AUTOMATIONS_PATH)
|
os.remove(AUTOMATIONS_PATH)
|
||||||
|
|
||||||
|
if os.path.isfile(CUSTOMIZE_PATH):
|
||||||
|
os.remove(CUSTOMIZE_PATH)
|
||||||
|
|
||||||
self.hass.stop()
|
self.hass.stop()
|
||||||
|
|
||||||
|
# pylint: disable=no-self-use
|
||||||
def test_create_default_config(self):
|
def test_create_default_config(self):
|
||||||
"""Test creation of default config."""
|
"""Test creation of default config."""
|
||||||
config_util.create_default_config(CONFIG_DIR, False)
|
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(VERSION_PATH)
|
||||||
assert os.path.isfile(GROUP_PATH)
|
assert os.path.isfile(GROUP_PATH)
|
||||||
assert os.path.isfile(AUTOMATIONS_PATH)
|
assert os.path.isfile(AUTOMATIONS_PATH)
|
||||||
|
assert os.path.isfile(CUSTOMIZE_PATH)
|
||||||
|
|
||||||
def test_find_config_file_yaml(self):
|
def test_find_config_file_yaml(self):
|
||||||
"""Test if it finds a YAML config file."""
|
"""Test if it finds a YAML config file."""
|
||||||
|
@ -169,7 +177,8 @@ class TestConfig(unittest.TestCase):
|
||||||
CONF_ELEVATION: 101,
|
CONF_ELEVATION: 101,
|
||||||
CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
|
CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
|
||||||
CONF_NAME: 'Home',
|
CONF_NAME: 'Home',
|
||||||
CONF_TIME_ZONE: 'America/Los_Angeles'
|
CONF_TIME_ZONE: 'America/Los_Angeles',
|
||||||
|
CONF_CUSTOMIZE: OrderedDict(),
|
||||||
}
|
}
|
||||||
|
|
||||||
assert expected_values == ha_conf
|
assert expected_values == ha_conf
|
||||||
|
@ -334,11 +343,12 @@ class TestConfig(unittest.TestCase):
|
||||||
|
|
||||||
mock_open = mock.mock_open()
|
mock_open = mock.mock_open()
|
||||||
|
|
||||||
def mock_isfile(filename):
|
def _mock_isfile(filename):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
with mock.patch('homeassistant.config.open', mock_open, create=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
|
opened_file = mock_open.return_value
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
opened_file.readline.return_value = ha_version
|
opened_file.readline.return_value = ha_version
|
||||||
|
@ -359,11 +369,12 @@ class TestConfig(unittest.TestCase):
|
||||||
|
|
||||||
mock_open = mock.mock_open()
|
mock_open = mock.mock_open()
|
||||||
|
|
||||||
def mock_isfile(filename):
|
def _mock_isfile(filename):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with mock.patch('homeassistant.config.open', mock_open, create=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
|
opened_file = mock_open.return_value
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
opened_file.readline.return_value = ha_version
|
opened_file.readline.return_value = ha_version
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue