Allow to set default dark theme and persist frontend default themes (#38548)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
Bram Kragten 2020-08-05 17:42:23 +02:00 committed by GitHub
parent d0d0403664
commit d66ddeb69e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 199 additions and 16 deletions

View file

@ -74,9 +74,16 @@ DATA_EXTRA_HTML_URL = "frontend_extra_html_url"
DATA_EXTRA_HTML_URL_ES5 = "frontend_extra_html_url_es5"
DATA_EXTRA_MODULE_URL = "frontend_extra_module_url"
DATA_EXTRA_JS_URL_ES5 = "frontend_extra_js_url_es5"
THEMES_STORAGE_KEY = f"{DOMAIN}_theme"
THEMES_STORAGE_VERSION = 1
THEMES_SAVE_DELAY = 60
DATA_THEMES_STORE = "frontend_themes_store"
DATA_THEMES = "frontend_themes"
DATA_DEFAULT_THEME = "frontend_default_theme"
DATA_DEFAULT_DARK_THEME = "frontend_default_dark_theme"
DEFAULT_THEME = "default"
VALUE_NO_THEME = "none"
PRIMARY_COLOR = "primary-color"
@ -114,6 +121,7 @@ CONFIG_SCHEMA = vol.Schema(
SERVICE_SET_THEME = "set_theme"
SERVICE_RELOAD_THEMES = "reload_themes"
CONF_MODE = "mode"
class Panel:
@ -321,17 +329,31 @@ async def async_setup(hass, config):
for url in conf.get(CONF_EXTRA_JS_URL_ES5, []):
add_extra_js_url(hass, url, True)
_async_setup_themes(hass, conf.get(CONF_THEMES))
await _async_setup_themes(hass, conf.get(CONF_THEMES))
return True
@callback
def _async_setup_themes(hass, themes):
async def _async_setup_themes(hass, themes):
"""Set up themes data and services."""
hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME
hass.data[DATA_THEMES] = themes or {}
store = hass.data[DATA_THEMES_STORE] = hass.helpers.storage.Store(
THEMES_STORAGE_VERSION, THEMES_STORAGE_KEY
)
theme_data = await store.async_load() or {}
theme_name = theme_data.get(DATA_DEFAULT_THEME, DEFAULT_THEME)
dark_theme_name = theme_data.get(DATA_DEFAULT_DARK_THEME)
if theme_name == DEFAULT_THEME or theme_name in hass.data[DATA_THEMES]:
hass.data[DATA_DEFAULT_THEME] = theme_name
else:
hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME
if dark_theme_name == DEFAULT_THEME or dark_theme_name in hass.data[DATA_THEMES]:
hass.data[DATA_DEFAULT_DARK_THEME] = dark_theme_name
@callback
def update_theme_and_fire_event():
"""Update theme_color in manifest."""
@ -348,14 +370,35 @@ def _async_setup_themes(hass, themes):
@callback
def set_theme(call):
"""Set backend-preferred 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()
name = call.data[CONF_NAME]
mode = call.data.get("mode", "light")
if (
name not in (DEFAULT_THEME, VALUE_NO_THEME)
and name not in hass.data[DATA_THEMES]
):
_LOGGER.warning("Theme %s not found", name)
return
light_mode = mode == "light"
theme_key = DATA_DEFAULT_THEME if light_mode else DATA_DEFAULT_DARK_THEME
if name == VALUE_NO_THEME:
to_set = DEFAULT_THEME if light_mode else None
else:
_LOGGER.warning("Theme %s is not defined", name)
_LOGGER.info("Theme %s set as default %s theme", name, mode)
to_set = name
hass.data[theme_key] = to_set
store.async_delay_save(
lambda: {
DATA_DEFAULT_THEME: hass.data[DATA_DEFAULT_THEME],
DATA_DEFAULT_DARK_THEME: hass.data.get(DATA_DEFAULT_DARK_THEME),
},
THEMES_SAVE_DELAY,
)
update_theme_and_fire_event()
async def reload_themes(_):
"""Reload themes."""
@ -364,6 +407,11 @@ def _async_setup_themes(hass, themes):
hass.data[DATA_THEMES] = new_themes
if hass.data[DATA_DEFAULT_THEME] not in new_themes:
hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME
if (
hass.data.get(DATA_DEFAULT_DARK_THEME)
and hass.data.get(DATA_DEFAULT_DARK_THEME) not in new_themes
):
hass.data[DATA_DEFAULT_DARK_THEME] = None
update_theme_and_fire_event()
service.async_register_admin_service(
@ -371,7 +419,12 @@ def _async_setup_themes(hass, themes):
DOMAIN,
SERVICE_SET_THEME,
set_theme,
vol.Schema({vol.Required(CONF_NAME): cv.string}),
vol.Schema(
{
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_MODE): vol.Any("dark", "light"),
}
),
)
service.async_register_admin_service(
@ -536,6 +589,7 @@ def websocket_get_themes(hass, connection, msg):
{
"themes": hass.data[DATA_THEMES],
"default_theme": hass.data[DATA_DEFAULT_THEME],
"default_dark_theme": hass.data.get(DATA_DEFAULT_DARK_THEME),
},
)
)

View file

@ -4,8 +4,11 @@ set_theme:
description: Set a theme unless the client selected per-device theme.
fields:
name:
description: Name of a predefined theme or 'default'.
description: Name of a predefined theme, 'default' or 'none'.
example: "light"
mode:
description: The mode the theme is for, either 'dark' or 'light' (default).
example: "dark"
reload_themes:
description: Reload themes from yaml configuration.

View file

@ -1,4 +1,5 @@
"""The tests for Home Assistant frontend."""
from datetime import timedelta
import re
import pytest
@ -10,16 +11,25 @@ from homeassistant.components.frontend import (
CONF_THEMES,
DOMAIN,
EVENT_PANELS_UPDATED,
THEMES_STORAGE_KEY,
)
from homeassistant.components.websocket_api.const import TYPE_RESULT
from homeassistant.const import HTTP_NOT_FOUND
from homeassistant.loader import async_get_integration
from homeassistant.setup import async_setup_component
from homeassistant.util import dt
from tests.async_mock import patch
from tests.common import async_capture_events
from tests.common import async_capture_events, async_fire_time_changed
CONFIG_THEMES = {DOMAIN: {CONF_THEMES: {"happy": {"primary-color": "red"}}}}
CONFIG_THEMES = {
DOMAIN: {
CONF_THEMES: {
"happy": {"primary-color": "red"},
"dark": {"primary-color": "black"},
}
}
}
@pytest.fixture
@ -117,7 +127,11 @@ async def test_themes_api(hass, hass_ws_client):
msg = await client.receive_json()
assert msg["result"]["default_theme"] == "default"
assert msg["result"]["themes"] == {"happy": {"primary-color": "red"}}
assert msg["result"]["default_dark_theme"] is None
assert msg["result"]["themes"] == {
"happy": {"primary-color": "red"},
"dark": {"primary-color": "black"},
}
# safe mode
hass.config.safe_mode = True
@ -130,6 +144,58 @@ async def test_themes_api(hass, hass_ws_client):
}
async def test_themes_persist(hass, hass_ws_client, hass_storage):
"""Test that theme settings are restores after restart."""
hass_storage[THEMES_STORAGE_KEY] = {
"key": THEMES_STORAGE_KEY,
"version": 1,
"data": {
"frontend_default_theme": "happy",
"frontend_default_dark_theme": "dark",
},
}
assert await async_setup_component(hass, "frontend", CONFIG_THEMES)
client = await hass_ws_client(hass)
await client.send_json({"id": 5, "type": "frontend/get_themes"})
msg = await client.receive_json()
assert msg["result"]["default_theme"] == "happy"
assert msg["result"]["default_dark_theme"] == "dark"
async def test_themes_save_storage(hass, hass_storage):
"""Test that theme settings are restores after restart."""
hass_storage[THEMES_STORAGE_KEY] = {
"key": THEMES_STORAGE_KEY,
"version": 1,
"data": {},
}
assert await async_setup_component(hass, "frontend", CONFIG_THEMES)
await hass.services.async_call(
DOMAIN, "set_theme", {"name": "happy"}, blocking=True
)
await hass.services.async_call(
DOMAIN, "set_theme", {"name": "dark", "mode": "dark"}, blocking=True
)
# To trigger the call_later
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=60))
# To execute the save
await hass.async_block_till_done()
assert hass_storage[THEMES_STORAGE_KEY]["data"] == {
"frontend_default_theme": "happy",
"frontend_default_dark_theme": "dark",
}
async def test_themes_set_theme(hass, hass_ws_client):
"""Test frontend.set_theme service."""
assert await async_setup_component(hass, "frontend", CONFIG_THEMES)
@ -153,6 +219,17 @@ async def test_themes_set_theme(hass, hass_ws_client):
assert msg["result"]["default_theme"] == "default"
await hass.services.async_call(
DOMAIN, "set_theme", {"name": "happy"}, blocking=True
)
await hass.services.async_call(DOMAIN, "set_theme", {"name": "none"}, blocking=True)
await client.send_json({"id": 7, "type": "frontend/get_themes"})
msg = await client.receive_json()
assert msg["result"]["default_theme"] == "default"
async def test_themes_set_theme_wrong_name(hass, hass_ws_client):
"""Test frontend.set_theme service called with wrong name."""
@ -170,6 +247,55 @@ async def test_themes_set_theme_wrong_name(hass, hass_ws_client):
assert msg["result"]["default_theme"] == "default"
async def test_themes_set_dark_theme(hass, hass_ws_client):
"""Test frontend.set_theme service called with dark mode."""
assert await async_setup_component(hass, "frontend", CONFIG_THEMES)
client = await hass_ws_client(hass)
await hass.services.async_call(
DOMAIN, "set_theme", {"name": "dark", "mode": "dark"}, blocking=True
)
await client.send_json({"id": 5, "type": "frontend/get_themes"})
msg = await client.receive_json()
assert msg["result"]["default_dark_theme"] == "dark"
await hass.services.async_call(
DOMAIN, "set_theme", {"name": "default", "mode": "dark"}, blocking=True
)
await client.send_json({"id": 6, "type": "frontend/get_themes"})
msg = await client.receive_json()
assert msg["result"]["default_dark_theme"] == "default"
await hass.services.async_call(
DOMAIN, "set_theme", {"name": "none", "mode": "dark"}, blocking=True
)
await client.send_json({"id": 7, "type": "frontend/get_themes"})
msg = await client.receive_json()
assert msg["result"]["default_dark_theme"] is None
async def test_themes_set_dark_theme_wrong_name(hass, hass_ws_client):
"""Test frontend.set_theme service called with mode dark and wrong name."""
assert await async_setup_component(hass, "frontend", CONFIG_THEMES)
client = await hass_ws_client(hass)
await hass.services.async_call(
DOMAIN, "set_theme", {"name": "wrong", "mode": "dark"}, blocking=True
)
await client.send_json({"id": 5, "type": "frontend/get_themes"})
msg = await client.receive_json()
assert msg["result"]["default_dark_theme"] is None
async def test_themes_reload_themes(hass, hass_ws_client):
"""Test frontend.reload_themes service."""
assert await async_setup_component(hass, "frontend", CONFIG_THEMES)