Backend support for themes (#8419)

* Backend support for themes

* Fix test

* Add theme_updated event

* Shorten name

* Add tests
pull/8462/head
Andrey 2017-07-13 04:08:13 +03:00 committed by Paulus Schoutsen
parent bb9db28c95
commit a65f22378e
4 changed files with 183 additions and 2 deletions

View File

@ -6,7 +6,11 @@ import logging
import os
from aiohttp import web
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.config import find_config_file, load_yaml_config_file
from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback
from homeassistant.components import api
from homeassistant.components.http import HomeAssistantView
@ -22,6 +26,8 @@ URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static/')
ATTR_THEMES = 'themes'
DEFAULT_THEME_COLOR = '#03A9F4'
MANIFEST_JSON = {
'background_color': '#FFFFFF',
'description': 'Open-source home automation platform running on Python 3.',
@ -32,7 +38,7 @@ MANIFEST_JSON = {
'name': 'Home Assistant',
'short_name': 'Assistant',
'start_url': '/',
'theme_color': '#03A9F4'
'theme_color': DEFAULT_THEME_COLOR
}
for size in (192, 384, 512, 1024):
@ -44,11 +50,30 @@ for size in (192, 384, 512, 1024):
DATA_PANELS = 'frontend_panels'
DATA_INDEX_VIEW = 'frontend_index_view'
DATA_THEMES = 'frontend_themes'
DATA_DEFAULT_THEME = 'frontend_default_theme'
DEFAULT_THEME = 'default'
PRIMARY_COLOR = 'primary-color'
# To keep track we don't register a component twice (gives a warning)
_REGISTERED_COMPONENTS = set()
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(ATTR_THEMES): vol.Schema({
cv.string: {cv.string: cv.string}
}),
}),
}, extra=vol.ALLOW_EXTRA)
SERVICE_SET_THEME = 'set_theme'
SERVICE_RELOAD_THEMES = 'reload_themes'
SERVICE_SET_THEME_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
})
def register_built_in_panel(hass, component_name, sidebar_title=None,
sidebar_icon=None, url_path=None, config=None):
@ -186,9 +211,65 @@ def setup(hass, config):
'dev-template'):
register_built_in_panel(hass, panel)
themes = config.get(DOMAIN, {}).get(ATTR_THEMES)
if themes:
setup_themes(hass, themes)
return True
def setup_themes(hass, themes):
"""Set up themes data and services."""
hass.data[DATA_THEMES] = themes
hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME
hass.http.register_view(ThemesView)
@callback
def update_theme_and_fire_event():
"""Update theme_color in manifest."""
name = hass.data[DATA_DEFAULT_THEME]
themes = hass.data[DATA_THEMES]
if name != DEFAULT_THEME and PRIMARY_COLOR in themes[name]:
MANIFEST_JSON['theme_color'] = themes[name][PRIMARY_COLOR]
else:
MANIFEST_JSON['theme_color'] = DEFAULT_THEME_COLOR
hass.bus.async_fire(EVENT_THEMES_UPDATED, {
'themes': themes,
'default_theme': name,
})
@callback
def set_theme(call):
"""Set backend-prefered theme."""
data = call.data
name = data[CONF_NAME]
if name == DEFAULT_THEME or name in hass.data[DATA_THEMES]:
_LOGGER.info("Theme %s set as default", name)
hass.data[DATA_DEFAULT_THEME] = name
update_theme_and_fire_event()
else:
_LOGGER.warning("Theme %s is not defined.", name)
@callback
def reload_themes(_):
"""Reload themes."""
path = find_config_file(hass.config.config_dir)
new_themes = load_yaml_config_file(path)[DOMAIN].get(ATTR_THEMES, {})
hass.data[DATA_THEMES] = new_themes
if hass.data[DATA_DEFAULT_THEME] not in new_themes:
hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME
update_theme_and_fire_event()
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_SET_THEME,
set_theme,
descriptions[SERVICE_SET_THEME],
SERVICE_SET_THEME_SCHEMA)
hass.services.register(DOMAIN, SERVICE_RELOAD_THEMES, reload_themes,
descriptions[SERVICE_RELOAD_THEMES])
class BootstrapView(HomeAssistantView):
"""View to bootstrap frontend with all needed data."""
@ -291,3 +372,21 @@ class ManifestJSONView(HomeAssistantView):
"""Return the manifest.json."""
msg = json.dumps(MANIFEST_JSON, sort_keys=True).encode('UTF-8')
return web.Response(body=msg, content_type="application/manifest+json")
class ThemesView(HomeAssistantView):
"""View to return defined themes."""
requires_auth = False
url = '/api/themes'
name = 'api:themes'
@callback
def get(self, request):
"""Return themes."""
hass = request.app['hass']
return self.json({
'themes': hass.data[DATA_THEMES],
'default_theme': hass.data[DATA_DEFAULT_THEME],
})

View File

@ -0,0 +1,11 @@
# Describes the format for available frontend services
set_theme:
description: Set a theme unless the client selected per-device theme.
fields:
name:
description: Name of a predefined theme or 'default'.
example: 'light'
reload_themes:
description: Reload themes from yaml config.

View File

@ -179,6 +179,7 @@ EVENT_COMPONENT_LOADED = 'component_loaded'
EVENT_SERVICE_REGISTERED = 'service_registered'
EVENT_SERVICE_REMOVED = 'service_removed'
EVENT_LOGBOOK_ENTRY = 'logbook_entry'
EVENT_THEMES_UPDATED = 'themes_updated'
# #### STATES ####
STATE_ON = 'on'

View File

@ -1,10 +1,12 @@
"""The tests for Home Assistant frontend."""
import asyncio
import re
from unittest.mock import patch
import pytest
from homeassistant.setup import async_setup_component
from homeassistant.components.frontend import DOMAIN, ATTR_THEMES
@pytest.fixture
@ -14,6 +16,20 @@ def mock_http_client(hass, test_client):
return hass.loop.run_until_complete(test_client(hass.http.app))
@pytest.fixture
def mock_http_client_with_themes(hass, test_client):
"""Start the Hass HTTP component."""
hass.loop.run_until_complete(async_setup_component(hass, 'frontend', {
DOMAIN: {
ATTR_THEMES: {
'happy': {
'primary-color': 'red'
}
}
}}))
return hass.loop.run_until_complete(test_client(hass.http.app))
@asyncio.coroutine
def test_frontend_and_static(mock_http_client):
"""Test if we can get the frontend."""
@ -56,10 +72,64 @@ def test_we_cannot_POST_to_root(mock_http_client):
@asyncio.coroutine
def test_states_routes(hass, mock_http_client):
def test_states_routes(mock_http_client):
"""All served by index."""
resp = yield from mock_http_client.get('/states')
assert resp.status == 200
resp = yield from mock_http_client.get('/states/group.existing')
assert resp.status == 200
@asyncio.coroutine
def test_themes_api(mock_http_client_with_themes):
"""Test that /api/themes returns correct data."""
resp = yield from mock_http_client_with_themes.get('/api/themes')
json = yield from resp.json()
assert json['default_theme'] == 'default'
assert json['themes'] == {'happy': {'primary-color': 'red'}}
@asyncio.coroutine
def test_themes_set_theme(hass, mock_http_client_with_themes):
"""Test frontend.set_theme service."""
yield from hass.services.async_call(DOMAIN, 'set_theme', {'name': 'happy'})
yield from hass.async_block_till_done()
resp = yield from mock_http_client_with_themes.get('/api/themes')
json = yield from resp.json()
assert json['default_theme'] == 'happy'
yield from hass.services.async_call(
DOMAIN, 'set_theme', {'name': 'default'})
yield from hass.async_block_till_done()
resp = yield from mock_http_client_with_themes.get('/api/themes')
json = yield from resp.json()
assert json['default_theme'] == 'default'
@asyncio.coroutine
def test_themes_set_theme_wrong_name(hass, mock_http_client_with_themes):
"""Test frontend.set_theme service called with wrong name."""
yield from hass.services.async_call(DOMAIN, 'set_theme', {'name': 'wrong'})
yield from hass.async_block_till_done()
resp = yield from mock_http_client_with_themes.get('/api/themes')
json = yield from resp.json()
assert json['default_theme'] == 'default'
@asyncio.coroutine
def test_themes_reload_themes(hass, mock_http_client_with_themes):
"""Test frontend.reload_themes service."""
with patch('homeassistant.components.frontend.load_yaml_config_file',
return_value={DOMAIN: {
ATTR_THEMES: {
'sad': {'primary-color': 'blue'}
}}}):
yield from hass.services.async_call(DOMAIN, 'set_theme',
{'name': 'happy'})
yield from hass.services.async_call(DOMAIN, 'reload_themes')
yield from hass.async_block_till_done()
resp = yield from mock_http_client_with_themes.get('/api/themes')
json = yield from resp.json()
assert json['themes'] == {'sad': {'primary-color': 'blue'}}
assert json['default_theme'] == 'default'