mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
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
@ -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."""
|
||||
|
@ -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}
|
||||
|
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_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:
|
||||
|
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)
|
||||
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user