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:
parent
d0d0403664
commit
d66ddeb69e
3 changed files with 199 additions and 16 deletions
|
@ -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),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue