Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions homeassistant/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
20 changes: 10 additions & 10 deletions homeassistant/components/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

DOMAIN = 'config'
DEPENDENCIES = ['http']
SECTIONS = ('core', 'group', 'hassbian', 'automation', 'script')
SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script')
ON_DEMAND = ('zwave')


Expand Down Expand Up @@ -77,11 +77,11 @@ def _empty_config(self):
"""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

Expand All @@ -90,7 +90,7 @@ def get(self, request, config_key):
"""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)
Expand Down Expand Up @@ -121,7 +121,7 @@ def post(self, request, config_key):
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)

Expand Down Expand Up @@ -149,11 +149,11 @@ def _empty_config(self):
"""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)

Expand All @@ -165,14 +165,14 @@ def _empty_config(self):
"""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}
Expand Down
39 changes: 39 additions & 0 deletions homeassistant/components/config/customize.py
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 7 additions & 0 deletions homeassistant/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
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
Expand Down Expand Up @@ -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}

Expand Down Expand Up @@ -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:
Expand Down
118 changes: 118 additions & 0 deletions tests/components/config/test_customize.py
Original file line number Diff line number Diff line change
@@ -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
21 changes: 16 additions & 5 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
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)
Expand All @@ -31,6 +33,7 @@
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


Expand Down Expand Up @@ -65,8 +68,12 @@ def tearDown(self):
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)
Expand All @@ -75,6 +82,7 @@ def test_create_default_config(self):
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."""
Expand Down Expand Up @@ -169,7 +177,8 @@ def test_create_default_config_detect_location(self, mock_detect,
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
Expand Down Expand Up @@ -334,11 +343,12 @@ def test_migrate_file_on_upgrade(self, mock_os, mock_shutil):

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
Expand All @@ -359,11 +369,12 @@ def test_migrate_no_file_on_upgrade(self, mock_os, mock_shutil):

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
Expand Down