mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 21:57:51 +00:00
Backend support for themes (#8419)
* Backend support for themes * Fix test * Add theme_updated event * Shorten name * Add tests
This commit is contained in:
parent
bb9db28c95
commit
a65f22378e
@ -6,7 +6,11 @@ import logging
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from aiohttp import web
|
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.core import callback
|
||||||
from homeassistant.components import api
|
from homeassistant.components import api
|
||||||
from homeassistant.components.http import HomeAssistantView
|
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/')
|
STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static/')
|
||||||
|
|
||||||
|
ATTR_THEMES = 'themes'
|
||||||
|
DEFAULT_THEME_COLOR = '#03A9F4'
|
||||||
MANIFEST_JSON = {
|
MANIFEST_JSON = {
|
||||||
'background_color': '#FFFFFF',
|
'background_color': '#FFFFFF',
|
||||||
'description': 'Open-source home automation platform running on Python 3.',
|
'description': 'Open-source home automation platform running on Python 3.',
|
||||||
@ -32,7 +38,7 @@ MANIFEST_JSON = {
|
|||||||
'name': 'Home Assistant',
|
'name': 'Home Assistant',
|
||||||
'short_name': 'Assistant',
|
'short_name': 'Assistant',
|
||||||
'start_url': '/',
|
'start_url': '/',
|
||||||
'theme_color': '#03A9F4'
|
'theme_color': DEFAULT_THEME_COLOR
|
||||||
}
|
}
|
||||||
|
|
||||||
for size in (192, 384, 512, 1024):
|
for size in (192, 384, 512, 1024):
|
||||||
@ -44,11 +50,30 @@ for size in (192, 384, 512, 1024):
|
|||||||
|
|
||||||
DATA_PANELS = 'frontend_panels'
|
DATA_PANELS = 'frontend_panels'
|
||||||
DATA_INDEX_VIEW = 'frontend_index_view'
|
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)
|
# To keep track we don't register a component twice (gives a warning)
|
||||||
_REGISTERED_COMPONENTS = set()
|
_REGISTERED_COMPONENTS = set()
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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,
|
def register_built_in_panel(hass, component_name, sidebar_title=None,
|
||||||
sidebar_icon=None, url_path=None, config=None):
|
sidebar_icon=None, url_path=None, config=None):
|
||||||
@ -186,9 +211,65 @@ def setup(hass, config):
|
|||||||
'dev-template'):
|
'dev-template'):
|
||||||
register_built_in_panel(hass, panel)
|
register_built_in_panel(hass, panel)
|
||||||
|
|
||||||
|
themes = config.get(DOMAIN, {}).get(ATTR_THEMES)
|
||||||
|
if themes:
|
||||||
|
setup_themes(hass, themes)
|
||||||
|
|
||||||
return True
|
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):
|
class BootstrapView(HomeAssistantView):
|
||||||
"""View to bootstrap frontend with all needed data."""
|
"""View to bootstrap frontend with all needed data."""
|
||||||
|
|
||||||
@ -291,3 +372,21 @@ class ManifestJSONView(HomeAssistantView):
|
|||||||
"""Return the manifest.json."""
|
"""Return the manifest.json."""
|
||||||
msg = json.dumps(MANIFEST_JSON, sort_keys=True).encode('UTF-8')
|
msg = json.dumps(MANIFEST_JSON, sort_keys=True).encode('UTF-8')
|
||||||
return web.Response(body=msg, content_type="application/manifest+json")
|
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],
|
||||||
|
})
|
||||||
|
11
homeassistant/components/frontend/services.yaml
Normal file
11
homeassistant/components/frontend/services.yaml
Normal 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.
|
@ -179,6 +179,7 @@ EVENT_COMPONENT_LOADED = 'component_loaded'
|
|||||||
EVENT_SERVICE_REGISTERED = 'service_registered'
|
EVENT_SERVICE_REGISTERED = 'service_registered'
|
||||||
EVENT_SERVICE_REMOVED = 'service_removed'
|
EVENT_SERVICE_REMOVED = 'service_removed'
|
||||||
EVENT_LOGBOOK_ENTRY = 'logbook_entry'
|
EVENT_LOGBOOK_ENTRY = 'logbook_entry'
|
||||||
|
EVENT_THEMES_UPDATED = 'themes_updated'
|
||||||
|
|
||||||
# #### STATES ####
|
# #### STATES ####
|
||||||
STATE_ON = 'on'
|
STATE_ON = 'on'
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
"""The tests for Home Assistant frontend."""
|
"""The tests for Home Assistant frontend."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import re
|
import re
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
from homeassistant.components.frontend import DOMAIN, ATTR_THEMES
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -14,6 +16,20 @@ def mock_http_client(hass, test_client):
|
|||||||
return hass.loop.run_until_complete(test_client(hass.http.app))
|
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
|
@asyncio.coroutine
|
||||||
def test_frontend_and_static(mock_http_client):
|
def test_frontend_and_static(mock_http_client):
|
||||||
"""Test if we can get the frontend."""
|
"""Test if we can get the frontend."""
|
||||||
@ -56,10 +72,64 @@ def test_we_cannot_POST_to_root(mock_http_client):
|
|||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def test_states_routes(hass, mock_http_client):
|
def test_states_routes(mock_http_client):
|
||||||
"""All served by index."""
|
"""All served by index."""
|
||||||
resp = yield from mock_http_client.get('/states')
|
resp = yield from mock_http_client.get('/states')
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
|
|
||||||
resp = yield from mock_http_client.get('/states/group.existing')
|
resp = yield from mock_http_client.get('/states/group.existing')
|
||||||
assert resp.status == 200
|
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…
x
Reference in New Issue
Block a user