mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 14:17:45 +00:00
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
@ -74,9 +74,16 @@ DATA_EXTRA_HTML_URL = "frontend_extra_html_url"
|
|||||||
DATA_EXTRA_HTML_URL_ES5 = "frontend_extra_html_url_es5"
|
DATA_EXTRA_HTML_URL_ES5 = "frontend_extra_html_url_es5"
|
||||||
DATA_EXTRA_MODULE_URL = "frontend_extra_module_url"
|
DATA_EXTRA_MODULE_URL = "frontend_extra_module_url"
|
||||||
DATA_EXTRA_JS_URL_ES5 = "frontend_extra_js_url_es5"
|
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_THEMES = "frontend_themes"
|
||||||
DATA_DEFAULT_THEME = "frontend_default_theme"
|
DATA_DEFAULT_THEME = "frontend_default_theme"
|
||||||
|
DATA_DEFAULT_DARK_THEME = "frontend_default_dark_theme"
|
||||||
DEFAULT_THEME = "default"
|
DEFAULT_THEME = "default"
|
||||||
|
VALUE_NO_THEME = "none"
|
||||||
|
|
||||||
PRIMARY_COLOR = "primary-color"
|
PRIMARY_COLOR = "primary-color"
|
||||||
|
|
||||||
@ -114,6 +121,7 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
|
|
||||||
SERVICE_SET_THEME = "set_theme"
|
SERVICE_SET_THEME = "set_theme"
|
||||||
SERVICE_RELOAD_THEMES = "reload_themes"
|
SERVICE_RELOAD_THEMES = "reload_themes"
|
||||||
|
CONF_MODE = "mode"
|
||||||
|
|
||||||
|
|
||||||
class Panel:
|
class Panel:
|
||||||
@ -321,17 +329,31 @@ async def async_setup(hass, config):
|
|||||||
for url in conf.get(CONF_EXTRA_JS_URL_ES5, []):
|
for url in conf.get(CONF_EXTRA_JS_URL_ES5, []):
|
||||||
add_extra_js_url(hass, url, True)
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
@callback
|
async def _async_setup_themes(hass, themes):
|
||||||
def _async_setup_themes(hass, themes):
|
|
||||||
"""Set up themes data and services."""
|
"""Set up themes data and services."""
|
||||||
hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME
|
|
||||||
hass.data[DATA_THEMES] = themes or {}
|
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
|
@callback
|
||||||
def update_theme_and_fire_event():
|
def update_theme_and_fire_event():
|
||||||
"""Update theme_color in manifest."""
|
"""Update theme_color in manifest."""
|
||||||
@ -348,14 +370,35 @@ def _async_setup_themes(hass, themes):
|
|||||||
@callback
|
@callback
|
||||||
def set_theme(call):
|
def set_theme(call):
|
||||||
"""Set backend-preferred theme."""
|
"""Set backend-preferred theme."""
|
||||||
data = call.data
|
name = call.data[CONF_NAME]
|
||||||
name = data[CONF_NAME]
|
mode = call.data.get("mode", "light")
|
||||||
if name == DEFAULT_THEME or name in hass.data[DATA_THEMES]:
|
|
||||||
_LOGGER.info("Theme %s set as default", name)
|
if (
|
||||||
hass.data[DATA_DEFAULT_THEME] = name
|
name not in (DEFAULT_THEME, VALUE_NO_THEME)
|
||||||
update_theme_and_fire_event()
|
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:
|
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(_):
|
async def reload_themes(_):
|
||||||
"""Reload themes."""
|
"""Reload themes."""
|
||||||
@ -364,6 +407,11 @@ def _async_setup_themes(hass, themes):
|
|||||||
hass.data[DATA_THEMES] = new_themes
|
hass.data[DATA_THEMES] = new_themes
|
||||||
if hass.data[DATA_DEFAULT_THEME] not in new_themes:
|
if hass.data[DATA_DEFAULT_THEME] not in new_themes:
|
||||||
hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME
|
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()
|
update_theme_and_fire_event()
|
||||||
|
|
||||||
service.async_register_admin_service(
|
service.async_register_admin_service(
|
||||||
@ -371,7 +419,12 @@ def _async_setup_themes(hass, themes):
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_SET_THEME,
|
SERVICE_SET_THEME,
|
||||||
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(
|
service.async_register_admin_service(
|
||||||
@ -536,6 +589,7 @@ def websocket_get_themes(hass, connection, msg):
|
|||||||
{
|
{
|
||||||
"themes": hass.data[DATA_THEMES],
|
"themes": hass.data[DATA_THEMES],
|
||||||
"default_theme": hass.data[DATA_DEFAULT_THEME],
|
"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.
|
description: Set a theme unless the client selected per-device theme.
|
||||||
fields:
|
fields:
|
||||||
name:
|
name:
|
||||||
description: Name of a predefined theme or 'default'.
|
description: Name of a predefined theme, 'default' or 'none'.
|
||||||
example: "light"
|
example: "light"
|
||||||
|
mode:
|
||||||
|
description: The mode the theme is for, either 'dark' or 'light' (default).
|
||||||
|
example: "dark"
|
||||||
|
|
||||||
reload_themes:
|
reload_themes:
|
||||||
description: Reload themes from yaml configuration.
|
description: Reload themes from yaml configuration.
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
"""The tests for Home Assistant frontend."""
|
"""The tests for Home Assistant frontend."""
|
||||||
|
from datetime import timedelta
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -10,16 +11,25 @@ from homeassistant.components.frontend import (
|
|||||||
CONF_THEMES,
|
CONF_THEMES,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
EVENT_PANELS_UPDATED,
|
EVENT_PANELS_UPDATED,
|
||||||
|
THEMES_STORAGE_KEY,
|
||||||
)
|
)
|
||||||
from homeassistant.components.websocket_api.const import TYPE_RESULT
|
from homeassistant.components.websocket_api.const import TYPE_RESULT
|
||||||
from homeassistant.const import HTTP_NOT_FOUND
|
from homeassistant.const import HTTP_NOT_FOUND
|
||||||
from homeassistant.loader import async_get_integration
|
from homeassistant.loader import async_get_integration
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
from homeassistant.util import dt
|
||||||
|
|
||||||
from tests.async_mock import patch
|
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
|
@pytest.fixture
|
||||||
@ -117,7 +127,11 @@ async def test_themes_api(hass, hass_ws_client):
|
|||||||
msg = await client.receive_json()
|
msg = await client.receive_json()
|
||||||
|
|
||||||
assert msg["result"]["default_theme"] == "default"
|
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
|
# safe mode
|
||||||
hass.config.safe_mode = True
|
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):
|
async def test_themes_set_theme(hass, hass_ws_client):
|
||||||
"""Test frontend.set_theme service."""
|
"""Test frontend.set_theme service."""
|
||||||
assert await async_setup_component(hass, "frontend", CONFIG_THEMES)
|
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"
|
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):
|
async def test_themes_set_theme_wrong_name(hass, hass_ws_client):
|
||||||
"""Test frontend.set_theme service called with wrong name."""
|
"""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"
|
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):
|
async def test_themes_reload_themes(hass, hass_ws_client):
|
||||||
"""Test frontend.reload_themes service."""
|
"""Test frontend.reload_themes service."""
|
||||||
assert await async_setup_component(hass, "frontend", CONFIG_THEMES)
|
assert await async_setup_component(hass, "frontend", CONFIG_THEMES)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user