diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 1d437d35da7..6db147a5f59 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -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.""" diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 9e447c8936a..9ce7f30529b 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -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} diff --git a/homeassistant/components/config/customize.py b/homeassistant/components/config/customize.py new file mode 100644 index 00000000000..d25992ecc90 --- /dev/null +++ b/homeassistant/components/config/customize.py @@ -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) diff --git a/homeassistant/config.py b/homeassistant/config.py index c90c4517397..ee48ece67ab 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -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: diff --git a/tests/components/config/test_customize.py b/tests/components/config/test_customize.py new file mode 100644 index 00000000000..f12774c25d9 --- /dev/null +++ b/tests/components/config/test_customize.py @@ -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 diff --git a/tests/test_config.py b/tests/test_config.py index 8c889979a82..d1b9a052b72 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -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