1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-08 17:49:37 +01:00

Allow lovelace path for dashboard in yaml and fix yaml dashboard migration (#161816)

This commit is contained in:
Paul Bottein
2026-01-29 23:19:37 +01:00
committed by GitHub
parent ef410c1e2a
commit 190fe10eed
9 changed files with 164 additions and 33 deletions
+43 -22
View File
@@ -33,6 +33,7 @@ from .const import ( # noqa: F401
CONF_ALLOW_SINGLE_WORD,
CONF_ICON,
CONF_REQUIRE_ADMIN,
CONF_RESOURCE_MODE,
CONF_SHOW_IN_SIDEBAR,
CONF_TITLE,
CONF_URL_PATH,
@@ -61,7 +62,7 @@ def _validate_url_slug(value: Any) -> str:
"""Validate value is a valid url slug."""
if value is None:
raise vol.Invalid("Slug should not be None")
if "-" not in value:
if value != "lovelace" and "-" not in value:
raise vol.Invalid("Url path needs to contain a hyphen (-)")
str_value = str(value)
slg = slugify(str_value, separator="-")
@@ -84,9 +85,13 @@ CONFIG_SCHEMA = vol.Schema(
{
vol.Optional(DOMAIN, default={}): vol.Schema(
{
# Deprecated - Remove in 2026.8
vol.Optional(CONF_MODE, default=MODE_STORAGE): vol.All(
vol.Lower, vol.In([MODE_YAML, MODE_STORAGE])
),
vol.Optional(CONF_RESOURCE_MODE): vol.All(
vol.Lower, vol.In([MODE_YAML, MODE_STORAGE])
),
vol.Optional(CONF_DASHBOARDS): cv.schema_with_slug_keys(
YAML_DASHBOARD_SCHEMA,
slug_validator=_validate_url_slug,
@@ -103,7 +108,7 @@ CONFIG_SCHEMA = vol.Schema(
class LovelaceData:
"""Dataclass to store information in hass.data."""
mode: str
resource_mode: str # The mode used for resources (yaml or storage)
dashboards: dict[str | None, dashboard.LovelaceConfig]
resources: resources.ResourceYAMLCollection | resources.ResourceStorageCollection
yaml_dashboards: dict[str | None, ConfigType]
@@ -114,18 +119,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
mode = config[DOMAIN][CONF_MODE]
yaml_resources = config[DOMAIN].get(CONF_RESOURCES)
# Deprecated - Remove in 2026.8
# For YAML mode, register the default panel in yaml mode (temporary until user migrates)
if mode == MODE_YAML:
frontend.async_register_built_in_panel(
hass,
DOMAIN,
config={"mode": mode},
sidebar_title="overview",
sidebar_icon="mdi:view-dashboard",
sidebar_default_visible=False,
)
_async_create_yaml_mode_repair(hass)
# resource_mode controls how resources are loaded (yaml vs storage)
# Deprecated - Remove mode fallback in 2026.8
resource_mode = config[DOMAIN].get(CONF_RESOURCE_MODE, mode)
async def reload_resources_service_handler(service_call: ServiceCall) -> None:
"""Reload yaml resources."""
@@ -149,12 +145,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
)
hass.data[LOVELACE_DATA].resources = resource_collection
default_config: dashboard.LovelaceConfig
resource_collection: (
resources.ResourceYAMLCollection | resources.ResourceStorageCollection
)
if mode == MODE_YAML:
default_config = dashboard.LovelaceYAML(hass, None, None)
default_config = dashboard.LovelaceStorage(hass, None)
# Load resources based on resource_mode
if resource_mode == MODE_YAML:
resource_collection = await create_yaml_resource_col(hass, yaml_resources)
async_register_admin_service(
@@ -177,8 +174,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
)
else:
default_config = dashboard.LovelaceStorage(hass, None)
if yaml_resources is not None:
_LOGGER.warning(
"Lovelace is running in storage mode. Define resources via user"
@@ -195,18 +190,44 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
RESOURCE_UPDATE_FIELDS,
).async_setup(hass)
websocket_api.async_register_command(hass, websocket.websocket_lovelace_info)
websocket_api.async_register_command(hass, websocket.websocket_lovelace_config)
websocket_api.async_register_command(hass, websocket.websocket_lovelace_save_config)
websocket_api.async_register_command(
hass, websocket.websocket_lovelace_delete_config
)
yaml_dashboards = config[DOMAIN].get(CONF_DASHBOARDS, {})
# Deprecated - Remove in 2026.8
# For YAML mode, add the default "lovelace" dashboard if not already defined
# This migrates the legacy yaml mode to a proper yaml dashboard entry
if mode == MODE_YAML and DOMAIN not in yaml_dashboards:
translations = await async_get_translations(
hass, hass.config.language, "dashboard", {onboarding.DOMAIN}
)
title = translations.get(
"component.onboarding.dashboard.overview.title", "Overview"
)
yaml_dashboards = {
DOMAIN: {
CONF_TITLE: title,
CONF_ICON: DEFAULT_ICON,
CONF_SHOW_IN_SIDEBAR: True,
CONF_REQUIRE_ADMIN: False,
CONF_MODE: MODE_YAML,
CONF_FILENAME: LOVELACE_CONFIG_FILE,
},
**yaml_dashboards,
}
_async_create_yaml_mode_repair(hass)
hass.data[LOVELACE_DATA] = LovelaceData(
mode=mode,
resource_mode=resource_mode,
# We store a dictionary mapping url_path: config. None is the default.
dashboards={None: default_config},
resources=resource_collection,
yaml_dashboards=config[DOMAIN].get(CONF_DASHBOARDS, {}),
yaml_dashboards=yaml_dashboards,
)
if hass.config.recovery_mode:
@@ -450,7 +471,7 @@ async def _async_migrate_default_config(
# Deprecated - Remove in 2026.8
@callback
def _async_create_yaml_mode_repair(hass: HomeAssistant) -> None:
"""Create repair issue for YAML mode migration."""
"""Create repair issue for YAML mode deprecation."""
ir.async_create_issue(
hass,
DOMAIN,
+9 -1
View File
@@ -158,7 +158,15 @@ async def _get_dashboard_info(
"""Load a dashboard and return info on views."""
if url_path == DEFAULT_DASHBOARD:
url_path = None
dashboard = hass.data[LOVELACE_DATA].dashboards.get(url_path)
# When url_path is None, prefer "lovelace" dashboard if it exists (for YAML mode)
# Otherwise fall back to dashboards[None] (storage mode default)
if url_path is None:
dashboard = hass.data[LOVELACE_DATA].dashboards.get(DOMAIN) or hass.data[
LOVELACE_DATA
].dashboards.get(None)
else:
dashboard = hass.data[LOVELACE_DATA].dashboards.get(url_path)
if dashboard is None:
raise ValueError("Invalid dashboard specified")
@@ -57,6 +57,7 @@ RESOURCE_UPDATE_FIELDS: VolDictType = {
SERVICE_RELOAD_RESOURCES = "reload_resources"
RESOURCE_RELOAD_SERVICE_SCHEMA = vol.Schema({})
CONF_RESOURCE_MODE = "resource_mode"
CONF_TITLE = "title"
CONF_REQUIRE_ADMIN = "require_admin"
CONF_SHOW_IN_SIDEBAR = "show_in_sidebar"
@@ -6,8 +6,8 @@
},
"issues": {
"yaml_mode_deprecated": {
"description": "Starting with Home Assistant 2026.8, the default Lovelace dashboard will no longer support YAML mode. To migrate:\n\n1. Remove `mode: yaml` from `lovelace:` in your `configuration.yaml`\n2. Rename `{config_file}` to a new filename (e.g., `my-dashboard.yaml`)\n3. Add a dashboard entry in your `configuration.yaml`:\n\n```yaml\nlovelace:\n dashboards:\n lovelace:\n mode: yaml\n filename: my-dashboard.yaml\n title: Overview\n icon: mdi:view-dashboard\n show_in_sidebar: true\n```\n\n4. Restart Home Assistant",
"title": "Lovelace YAML mode migration required"
"description": "The `mode` option in `lovelace:` configuration is deprecated and will be removed in Home Assistant 2026.8.\n\nTo migrate:\n\n1. Remove `mode: yaml` from `lovelace:` in your `configuration.yaml`\n2. If you have `resources:` declared in your lovelace configuration, add `resource_mode: yaml` to keep loading resources from YAML\n3. Add a dashboard entry in your `configuration.yaml`:\n\n ```yaml\n lovelace:\n resource_mode: yaml # Add this if you have resources declared\n dashboards:\n lovelace:\n mode: yaml\n filename: {config_file}\n title: Overview\n icon: mdi:view-dashboard\n show_in_sidebar: true\n ```\n\n4. Restart Home Assistant",
"title": "Lovelace YAML mode deprecated"
}
},
"services": {
@@ -42,9 +42,7 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
else:
health_info[key] = dashboard[key]
if hass.data[LOVELACE_DATA].mode == MODE_YAML:
health_info[CONF_MODE] = MODE_YAML
elif MODE_STORAGE in modes:
if MODE_STORAGE in modes:
health_info[CONF_MODE] = MODE_STORAGE
elif MODE_YAML in modes:
health_info[CONF_MODE] = MODE_YAML
+30 -2
View File
@@ -14,7 +14,13 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.json import json_fragment
from .const import CONF_URL_PATH, LOVELACE_DATA, ConfigNotFound
from .const import (
CONF_RESOURCE_MODE,
CONF_URL_PATH,
DOMAIN,
LOVELACE_DATA,
ConfigNotFound,
)
from .dashboard import LovelaceConfig
if TYPE_CHECKING:
@@ -38,7 +44,15 @@ def _handle_errors[_R](
msg: dict[str, Any],
) -> None:
url_path = msg.get(CONF_URL_PATH)
config = hass.data[LOVELACE_DATA].dashboards.get(url_path)
# When url_path is None, prefer "lovelace" dashboard if it exists (for YAML mode)
# Otherwise fall back to dashboards[None] (storage mode default)
if url_path is None:
config = hass.data[LOVELACE_DATA].dashboards.get(DOMAIN) or hass.data[
LOVELACE_DATA
].dashboards.get(None)
else:
config = hass.data[LOVELACE_DATA].dashboards.get(url_path)
if config is None:
connection.send_error(
@@ -100,6 +114,20 @@ async def websocket_lovelace_resources_impl(
connection.send_result(msg["id"], resources.async_items())
@websocket_api.websocket_command({"type": "lovelace/info"})
@websocket_api.async_response
async def websocket_lovelace_info(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Send Lovelace UI info over WebSocket connection."""
connection.send_result(
msg["id"],
{CONF_RESOURCE_MODE: hass.data[LOVELACE_DATA].resource_mode},
)
@websocket_api.websocket_command(
{
"type": "lovelace/config",
+45 -1
View File
@@ -368,7 +368,7 @@ async def test_lovelace_from_yaml_creates_repair_issue(
"""Test YAML mode creates a repair issue."""
assert await async_setup_component(hass, "lovelace", {"lovelace": {"mode": "YAML"}})
# Panel should still be registered for backwards compatibility
# Panel should be registered as a YAML dashboard
assert hass.data[frontend.DATA_PANELS]["lovelace"].config == {"mode": "yaml"}
# Repair issue should be created
@@ -803,3 +803,47 @@ async def test_lovelace_no_migration_no_default_panel_set(
response = await client.receive_json()
assert response["success"]
assert response["result"]["value"] is None
async def test_lovelace_info_default(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
"""Test lovelace/info returns default resource_mode."""
assert await async_setup_component(hass, "lovelace", {})
client = await hass_ws_client(hass)
await client.send_json({"id": 5, "type": "lovelace/info"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == {"resource_mode": "storage"}
async def test_lovelace_info_yaml_resource_mode(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
"""Test lovelace/info returns yaml resource_mode."""
assert await async_setup_component(
hass, "lovelace", {"lovelace": {"resource_mode": "yaml"}}
)
client = await hass_ws_client(hass)
await client.send_json({"id": 5, "type": "lovelace/info"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == {"resource_mode": "yaml"}
async def test_lovelace_info_yaml_mode_fallback(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
"""Test lovelace/info returns yaml resource_mode when mode is yaml."""
assert await async_setup_component(hass, "lovelace", {"lovelace": {"mode": "yaml"}})
client = await hass_ws_client(hass)
await client.send_json({"id": 5, "type": "lovelace/info"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == {"resource_mode": "yaml"}
+29
View File
@@ -5,7 +5,9 @@ from typing import Any
from unittest.mock import MagicMock, patch
import pytest
import voluptuous as vol
from homeassistant.components.lovelace import _validate_url_slug
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@@ -96,3 +98,30 @@ async def test_create_dashboards_when_not_onboarded(
response = await client.receive_json()
assert response["success"]
assert response["result"] == {"strategy": {"type": "map"}}
@pytest.mark.parametrize(
("value", "expected"),
[
("lovelace", "lovelace"),
("my-dashboard", "my-dashboard"),
("my-cool-dashboard", "my-cool-dashboard"),
],
)
def test_validate_url_slug_valid(value: str, expected: str) -> None:
"""Test _validate_url_slug with valid values."""
assert _validate_url_slug(value) == expected
@pytest.mark.parametrize(
("value", "error_message"),
[
(None, r"Slug should not be None"),
("nodash", r"Url path needs to contain a hyphen \(-\)"),
("my-dash board", r"invalid slug my-dash board \(try my-dash-board\)"),
],
)
def test_validate_url_slug_invalid(value: Any, error_message: str) -> None:
"""Test _validate_url_slug with invalid values."""
with pytest.raises(vol.Invalid, match=error_message):
_validate_url_slug(value)
@@ -62,7 +62,8 @@ async def test_system_health_info_yaml(hass: HomeAssistant) -> None:
return_value={"views": [{"cards": []}]},
):
info = await get_system_health_info(hass, "lovelace")
assert info == {"dashboards": 1, "mode": "yaml", "resources": 0, "views": 1}
# 2 dashboards: default storage (None) + yaml "lovelace" dashboard
assert info == {"dashboards": 2, "mode": "yaml", "resources": 0, "views": 1}
async def test_system_health_info_yaml_not_found(hass: HomeAssistant) -> None:
@@ -71,8 +72,9 @@ async def test_system_health_info_yaml_not_found(hass: HomeAssistant) -> None:
assert await async_setup_component(hass, "lovelace", {"lovelace": {"mode": "YAML"}})
await hass.async_block_till_done()
info = await get_system_health_info(hass, "lovelace")
# 2 dashboards: default storage (None) + yaml "lovelace" dashboard
assert info == {
"dashboards": 1,
"dashboards": 2,
"mode": "yaml",
"error": f"{hass.config.path('ui-lovelace.yaml')} not found",
"resources": 0,