Backend support for themes (#8419)
* Backend support for themes * Fix test * Add theme_updated event * Shorten name * Add testspull/8462/head
parent
bb9db28c95
commit
a65f22378e
|
@ -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],
|
||||
})
|
||||
|
|
|
@ -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.
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in New Issue