diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 5b899eee287..77e7bdbff4e 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -36,7 +36,10 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration, bind_hass from homeassistant.util.hass_dict import HassKey -from .storage import async_setup_frontend_storage +from .storage import ( + async_setup_frontend_storage, + async_system_store as async_system_store, +) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 7120bf04c31..0519323d3d5 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -1,7 +1,9 @@ """Support for the Lovelace UI.""" +from contextlib import suppress from dataclasses import dataclass import logging +import os from typing import Any import voluptuous as vol @@ -14,9 +16,14 @@ from homeassistant.config import ( from homeassistant.const import CONF_FILENAME, CONF_MODE, CONF_RESOURCES from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import collection, config_validation as cv +from homeassistant.helpers import ( + collection, + config_validation as cv, + issue_registry as ir, +) from homeassistant.helpers.frame import report_usage from homeassistant.helpers.service import async_register_admin_service +from homeassistant.helpers.storage import Store from homeassistant.helpers.translation import async_get_translations from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration @@ -34,6 +41,7 @@ from .const import ( # noqa: F401 DEFAULT_ICON, DOMAIN, EVENT_LOVELACE_UPDATED, + LOVELACE_CONFIG_FILE, LOVELACE_DATA, MODE_STORAGE, MODE_YAML, @@ -135,14 +143,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: mode = config[DOMAIN][CONF_MODE] yaml_resources = config[DOMAIN].get(CONF_RESOURCES) - frontend.async_register_built_in_panel( - hass, - DOMAIN, - config={"mode": mode}, - sidebar_title="overview", - sidebar_icon="mdi:view-dashboard", - sidebar_default_visible=False, - ) + # 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) async def reload_resources_service_handler(service_call: ServiceCall) -> None: """Reload yaml resources.""" @@ -238,6 +250,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if change_type == collection.CHANGE_REMOVED: frontend.async_remove_panel(hass, url_path) await hass.data[LOVELACE_DATA].dashboards.pop(url_path).async_delete() + # Re-register default lovelace panel if the "lovelace" dashboard was deleted + if url_path == DOMAIN: + _async_ensure_default_panel(hass) return if change_type == collection.CHANGE_ADDED: @@ -282,6 +297,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: dashboards_collection.async_add_listener(storage_dashboard_changed) await dashboards_collection.async_load() + # Migrate default lovelace panel to dashboard entry (storage mode only) + if mode == MODE_STORAGE: + await _async_migrate_default_config(hass, dashboards_collection) + + # Ensure lovelace panel is always registered for backwards compatibility + _async_ensure_default_panel(hass) + dashboard.DashboardsCollectionWebSocket( dashboards_collection, "lovelace/dashboards", @@ -321,6 +343,16 @@ async def create_yaml_resource_col( return resources.ResourceYAMLCollection(yaml_resources or []) +@callback +def _async_ensure_default_panel(hass: HomeAssistant) -> None: + """Ensure a default lovelace panel is registered for backward compatibility.""" + if ( + frontend.DATA_PANELS not in hass.data + or DOMAIN not in hass.data[frontend.DATA_PANELS] + ): + frontend.async_register_built_in_panel(hass, DOMAIN) + + @callback def _register_panel( hass: HomeAssistant, url_path: str | None, mode: str, config: dict, update: bool @@ -360,3 +392,101 @@ async def _create_map_dashboard( map_store = hass.data[LOVELACE_DATA].dashboards["map"] await map_store.async_save({"strategy": {"type": "map"}}) + + +async def _async_migrate_default_config( + hass: HomeAssistant, dashboards_collection: dashboard.DashboardsCollection +) -> None: + """Migrate default lovelace storage config to a named dashboard entry. + + This migration: + 1. Skips if a dashboard with url_path "lovelace" already exists + 2. Skips if .storage/lovelace has no data + 3. Creates a new dashboard entry with url_path "lovelace" + 4. Handles storage files: + a. If .storage/lovelace.lovelace does not exist, copies data and removes old file + b. If .storage/lovelace.lovelace already exists, renames old file to lovelace_old as backup + 5. Sets the default panel to "lovelace" if not already configured + """ + # 1. Skip if already migrated (dashboard with url_path "lovelace" exists) + for item in dashboards_collection.async_items(): + if item.get(CONF_URL_PATH) == DOMAIN: + return + + # 2. Skip if old storage data does not exist + old_store = Store[dict[str, Any]]( + hass, dashboard.CONFIG_STORAGE_VERSION, dashboard.CONFIG_STORAGE_KEY_DEFAULT + ) + old_data = await old_store.async_load() + if old_data is None or old_data.get("config") is None: + return + + _LOGGER.info("Migrating default lovelace config to dashboard entry") + + # 3. Create dashboard entry + translations = await async_get_translations( + hass, hass.config.language, "dashboard", {onboarding.DOMAIN} + ) + title = translations.get( + "component.onboarding.dashboard.overview.title", "Overview" + ) + + try: + await dashboards_collection.async_create_item( + { + CONF_ALLOW_SINGLE_WORD: True, + CONF_ICON: DEFAULT_ICON, + CONF_TITLE: title, + CONF_URL_PATH: DOMAIN, + } + ) + except (HomeAssistantError, vol.Invalid): + _LOGGER.exception("Failed to create dashboard entry during migration") + return + + # 4. Handle storage files + new_store = Store[dict[str, Any]]( + hass, + dashboard.CONFIG_STORAGE_VERSION, + dashboard.CONFIG_STORAGE_KEY.format(DOMAIN), + ) + if await new_store.async_load() is None: + await new_store.async_save(old_data) + await old_store.async_remove() + else: + # This should not happen because "lovelace" was a forbidden dashboard + # url_path before this migration existed. Rename old file as backup + # to avoid data loss just in case. + with suppress(FileNotFoundError): + await hass.async_add_executor_job( + os.rename, + old_store.path, + old_store.path + "_old", + ) + + _LOGGER.info("Successfully migrated default lovelace config to dashboard entry") + + # 5. Set default panel to lovelace if not already configured + system_store = await frontend.async_system_store(hass) + core_data = system_store.data.get("core") + if core_data is None or core_data.get("default_panel") is None: + await system_store.async_set_item( + "core", {**(core_data or {}), "default_panel": DOMAIN} + ) + _LOGGER.info("Set default panel to 'lovelace' during migration") + + +# Deprecated - Remove in 2026.8 +@callback +def _async_create_yaml_mode_repair(hass: HomeAssistant) -> None: + """Create repair issue for YAML mode migration.""" + ir.async_create_issue( + hass, + DOMAIN, + "yaml_mode_deprecated", + breaks_in_ha_version="2026.8.0", + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key="yaml_mode_deprecated", + translation_placeholders={"config_file": LOVELACE_CONFIG_FILE}, + ) diff --git a/homeassistant/components/lovelace/dashboard.py b/homeassistant/components/lovelace/dashboard.py index 41e82291e44..222abafbefe 100644 --- a/homeassistant/components/lovelace/dashboard.py +++ b/homeassistant/components/lovelace/dashboard.py @@ -286,7 +286,7 @@ class DashboardsCollection(collection.DictStorageCollection): if not allow_single_word and "-" not in url_path: raise vol.Invalid("Url path needs to contain a hyphen (-)") - if url_path in self.hass.data[DATA_PANELS]: + if DATA_PANELS in self.hass.data and url_path in self.hass.data[DATA_PANELS]: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="url_already_exists", diff --git a/homeassistant/components/lovelace/strings.json b/homeassistant/components/lovelace/strings.json index 704e864c567..edb574ffd78 100644 --- a/homeassistant/components/lovelace/strings.json +++ b/homeassistant/components/lovelace/strings.json @@ -4,6 +4,12 @@ "message": "The URL \"{url}\" is already in use. Please choose a different one." } }, + "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" + } + }, "services": { "reload_resources": { "description": "Reloads dashboard resources from the YAML-configuration.", diff --git a/homeassistant/components/onboarding/strings.json b/homeassistant/components/onboarding/strings.json index cd354827065..c22bcc466db 100644 --- a/homeassistant/components/onboarding/strings.json +++ b/homeassistant/components/onboarding/strings.json @@ -5,6 +5,7 @@ "living_room": "Living Room" }, "dashboard": { - "map": { "title": "Map" } + "map": { "title": "Map" }, + "overview": { "title": "Overview" } } } diff --git a/tests/components/lovelace/test_dashboard.py b/tests/components/lovelace/test_dashboard.py index 5950fb7d62a..5679a8865bf 100644 --- a/tests/components/lovelace/test_dashboard.py +++ b/tests/components/lovelace/test_dashboard.py @@ -10,6 +10,7 @@ import pytest from homeassistant.components import frontend from homeassistant.components.lovelace import const, dashboard from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component from tests.common import assert_setup_component, async_capture_events @@ -29,111 +30,246 @@ def mock_onboarding_done() -> Generator[MagicMock]: yield mock_onboarding -async def test_lovelace_from_storage( +async def test_lovelace_from_storage_new_installation( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, hass_storage: dict[str, Any], ) -> None: - """Test we load lovelace config from storage.""" + """Test new installation has default lovelace panel but no dashboard entry.""" assert await async_setup_component(hass, "lovelace", {}) + + # Default lovelace panel is registered for backward compatibility + assert "lovelace" in hass.data[frontend.DATA_PANELS] + + client = await hass_ws_client(hass) + + # Dashboards list should be empty (no dashboard entry created) + await client.send_json({"id": 5, "type": "lovelace/dashboards/list"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [] + + +async def test_lovelace_from_storage_migration( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], +) -> None: + """Test we migrate existing lovelace config from storage to dashboard.""" + # Pre-populate storage with existing lovelace config + hass_storage[dashboard.CONFIG_STORAGE_KEY_DEFAULT] = { + "version": 1, + "key": dashboard.CONFIG_STORAGE_KEY_DEFAULT, + "data": {"config": {"views": [{"title": "Home"}]}}, + } + + assert await async_setup_component(hass, "lovelace", {}) + + # After migration, lovelace panel should be registered as a dashboard + assert "lovelace" in hass.data[frontend.DATA_PANELS] assert hass.data[frontend.DATA_PANELS]["lovelace"].config == {"mode": "storage"} client = await hass_ws_client(hass) - # Fetch data - await client.send_json({"id": 5, "type": "lovelace/config"}) + # Dashboard should be in the list + await client.send_json({"id": 5, "type": "lovelace/dashboards/list"}) response = await client.receive_json() - assert not response["success"] - assert response["error"]["code"] == "config_not_found" + assert response["success"] + assert len(response["result"]) == 1 + assert response["result"][0]["url_path"] == "lovelace" + assert response["result"][0]["title"] == "Overview" + + # Fetch migrated config + await client.send_json({"id": 6, "type": "lovelace/config", "url_path": "lovelace"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {"views": [{"title": "Home"}]} + + # Old storage key should be gone, new one should exist + assert dashboard.CONFIG_STORAGE_KEY_DEFAULT not in hass_storage + assert dashboard.CONFIG_STORAGE_KEY.format("lovelace") in hass_storage # Store new config events = async_capture_events(hass, const.EVENT_LOVELACE_UPDATED) await client.send_json( - {"id": 6, "type": "lovelace/config/save", "config": {"yo": "hello"}} + { + "id": 7, + "type": "lovelace/config/save", + "url_path": "lovelace", + "config": {"yo": "hello"}, + } ) response = await client.receive_json() assert response["success"] - assert hass_storage[dashboard.CONFIG_STORAGE_KEY_DEFAULT]["data"] == { + assert hass_storage[dashboard.CONFIG_STORAGE_KEY.format("lovelace")]["data"] == { "config": {"yo": "hello"} } assert len(events) == 1 # Load new config - await client.send_json({"id": 7, "type": "lovelace/config"}) + await client.send_json({"id": 8, "type": "lovelace/config", "url_path": "lovelace"}) response = await client.receive_json() assert response["success"] - assert response["result"] == {"yo": "hello"} # Test with recovery mode hass.config.recovery_mode = True - await client.send_json({"id": 8, "type": "lovelace/config"}) + await client.send_json({"id": 9, "type": "lovelace/config", "url_path": "lovelace"}) response = await client.receive_json() assert not response["success"] assert response["error"]["code"] == "config_not_found" await client.send_json( - {"id": 9, "type": "lovelace/config/save", "config": {"yo": "hello"}} + { + "id": 10, + "type": "lovelace/config/save", + "url_path": "lovelace", + "config": {"yo": "hello"}, + } ) response = await client.receive_json() assert not response["success"] - await client.send_json({"id": 10, "type": "lovelace/config/delete"}) + await client.send_json( + {"id": 11, "type": "lovelace/config/delete", "url_path": "lovelace"} + ) response = await client.receive_json() assert not response["success"] -async def test_lovelace_from_storage_save_before_load( +async def test_lovelace_dashboard_deleted_re_registers_panel( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, hass_storage: dict[str, Any], ) -> None: - """Test we can load lovelace config from storage.""" + """Test deleting the lovelace dashboard re-registers the default panel.""" + # Pre-populate storage with existing lovelace config (triggers migration) + hass_storage[dashboard.CONFIG_STORAGE_KEY_DEFAULT] = { + "version": 1, + "key": dashboard.CONFIG_STORAGE_KEY_DEFAULT, + "data": {"config": {"views": [{"title": "Home"}]}}, + } + assert await async_setup_component(hass, "lovelace", {}) + + # After migration, lovelace panel should be registered as a dashboard + assert "lovelace" in hass.data[frontend.DATA_PANELS] + client = await hass_ws_client(hass) - # Store new config + # Dashboard should be in the list + await client.send_json({"id": 5, "type": "lovelace/dashboards/list"}) + response = await client.receive_json() + assert response["success"] + assert len(response["result"]) == 1 + dashboard_id = response["result"][0]["id"] + + # Delete the lovelace dashboard await client.send_json( - {"id": 6, "type": "lovelace/config/save", "config": {"yo": "hello"}} + {"id": 6, "type": "lovelace/dashboards/delete", "dashboard_id": dashboard_id} ) response = await client.receive_json() assert response["success"] - assert hass_storage[dashboard.CONFIG_STORAGE_KEY_DEFAULT]["data"] == { - "config": {"yo": "hello"} - } + + # Dashboard should be gone from the list + await client.send_json({"id": 7, "type": "lovelace/dashboards/list"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [] + + # But the lovelace panel should still be registered (re-registered as default) + assert "lovelace" in hass.data[frontend.DATA_PANELS] -async def test_lovelace_from_storage_delete( +async def test_lovelace_migration_completes_when_both_files_exist( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, hass_storage: dict[str, Any], ) -> None: - """Test we delete lovelace config from storage.""" - assert await async_setup_component(hass, "lovelace", {}) - client = await hass_ws_client(hass) - - # Store new config - await client.send_json( - {"id": 6, "type": "lovelace/config/save", "config": {"yo": "hello"}} - ) - response = await client.receive_json() - assert response["success"] - assert hass_storage[dashboard.CONFIG_STORAGE_KEY_DEFAULT]["data"] == { - "config": {"yo": "hello"} + """Test migration completes when both old and new storage files exist.""" + # Pre-populate both old and new storage (simulating incomplete migration) + hass_storage[dashboard.CONFIG_STORAGE_KEY_DEFAULT] = { + "version": 1, + "key": dashboard.CONFIG_STORAGE_KEY_DEFAULT, + "data": {"config": {"views": [{"title": "Old"}]}}, + } + hass_storage[dashboard.CONFIG_STORAGE_KEY.format("lovelace")] = { + "version": 1, + "key": dashboard.CONFIG_STORAGE_KEY.format("lovelace"), + "data": {"config": {"views": [{"title": "New"}]}}, } - # Delete config - await client.send_json({"id": 7, "type": "lovelace/config/delete"}) + with patch("homeassistant.components.lovelace.os.rename") as mock_rename: + assert await async_setup_component(hass, "lovelace", {}) + + # Old file should be renamed as backup + old_path = hass.config.path(".storage", dashboard.CONFIG_STORAGE_KEY_DEFAULT) + mock_rename.assert_called_once_with(old_path, old_path + "_old") + + # Dashboard should be created, completing the incomplete migration + client = await hass_ws_client(hass) + await client.send_json({"id": 5, "type": "lovelace/dashboards/list"}) response = await client.receive_json() assert response["success"] - assert dashboard.CONFIG_STORAGE_KEY_DEFAULT not in hass_storage + assert len(response["result"]) == 1 + assert response["result"][0]["url_path"] == "lovelace" - # Fetch data - await client.send_json({"id": 8, "type": "lovelace/config"}) + # New storage data should be preserved (not overwritten with old data) + await client.send_json({"id": 6, "type": "lovelace/config", "url_path": "lovelace"}) response = await client.receive_json() - assert not response["success"] - assert response["error"]["code"] == "config_not_found" + assert response["success"] + assert response["result"] == {"views": [{"title": "New"}]} + + +async def test_lovelace_migration_skipped_when_already_migrated( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], +) -> None: + """Test migration is skipped when dashboard already exists.""" + # Pre-populate dashboards with existing lovelace dashboard + hass_storage[dashboard.DASHBOARDS_STORAGE_KEY] = { + "version": 1, + "key": dashboard.DASHBOARDS_STORAGE_KEY, + "data": { + "items": [ + { + "id": "lovelace", + "url_path": "lovelace", + "title": "Overview", + "icon": "mdi:view-dashboard", + "show_in_sidebar": True, + "require_admin": False, + "mode": "storage", + } + ] + }, + } + hass_storage[dashboard.CONFIG_STORAGE_KEY.format("lovelace")] = { + "version": 1, + "key": dashboard.CONFIG_STORAGE_KEY.format("lovelace"), + "data": {"config": {"views": [{"title": "Home"}]}}, + } + # Also have old file (should be ignored since dashboard exists) + hass_storage[dashboard.CONFIG_STORAGE_KEY_DEFAULT] = { + "version": 1, + "key": dashboard.CONFIG_STORAGE_KEY_DEFAULT, + "data": {"config": {"views": [{"title": "Old"}]}}, + } + + assert await async_setup_component(hass, "lovelace", {}) + + client = await hass_ws_client(hass) + await client.send_json({"id": 5, "type": "lovelace/dashboards/list"}) + response = await client.receive_json() + assert response["success"] + # Only the pre-existing dashboard, no duplicate + assert len(response["result"]) == 1 + assert response["result"][0]["url_path"] == "lovelace" + + # Old storage should still exist (not touched) + assert dashboard.CONFIG_STORAGE_KEY_DEFAULT in hass_storage async def test_lovelace_from_yaml( @@ -226,6 +362,24 @@ async def test_lovelace_from_yaml( assert len(events) == 2 +async def test_lovelace_from_yaml_creates_repair_issue( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """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 + assert hass.data[frontend.DATA_PANELS]["lovelace"].config == {"mode": "yaml"} + + # Repair issue should be created + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue("lovelace", "yaml_mode_deprecated") + assert issue is not None + assert issue.severity == ir.IssueSeverity.WARNING + assert issue.is_fixable is False + assert issue.breaks_in_ha_version == "2026.8.0" + + @pytest.mark.parametrize("url_path", ["test-panel", "test-panel-no-sidebar"]) async def test_dashboard_from_yaml( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, url_path @@ -364,7 +518,9 @@ async def test_storage_dashboards( ) -> None: """Test we load lovelace config from storage.""" assert await async_setup_component(hass, "lovelace", {}) - assert hass.data[frontend.DATA_PANELS]["lovelace"].config == {"mode": "storage"} + + # Default lovelace panel is registered for backward compatibility + assert "lovelace" in hass.data[frontend.DATA_PANELS] client = await hass_ws_client(hass) @@ -571,3 +727,79 @@ async def test_websocket_list_dashboards( assert without_sb["mode"] == "storage" assert without_sb["title"] == "Test Storage" assert without_sb["url_path"] == "created-url-path" + + +async def test_lovelace_migration_sets_default_panel( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], +) -> None: + """Test migration sets default_panel to lovelace when not configured.""" + # Pre-populate storage with existing lovelace config + hass_storage[dashboard.CONFIG_STORAGE_KEY_DEFAULT] = { + "version": 1, + "key": dashboard.CONFIG_STORAGE_KEY_DEFAULT, + "data": {"config": {"views": [{"title": "Home"}]}}, + } + + # Need to setup frontend to register the websocket commands + assert await async_setup_component(hass, "frontend", {}) + assert await async_setup_component(hass, "lovelace", {}) + + # Verify default_panel was set in frontend system storage via websocket + client = await hass_ws_client(hass) + await client.send_json({"id": 5, "type": "frontend/get_system_data", "key": "core"}) + response = await client.receive_json() + assert response["success"] + assert response["result"]["value"]["default_panel"] == "lovelace" + + +async def test_lovelace_migration_preserves_existing_default_panel( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], +) -> None: + """Test migration does not override existing default_panel.""" + # Pre-populate storage with existing lovelace config + hass_storage[dashboard.CONFIG_STORAGE_KEY_DEFAULT] = { + "version": 1, + "key": dashboard.CONFIG_STORAGE_KEY_DEFAULT, + "data": {"config": {"views": [{"title": "Home"}]}}, + } + # Pre-populate frontend system storage with existing default_panel + storage_key = f"{frontend.DOMAIN}.system_data" + hass_storage[storage_key] = { + "version": 1, + "key": storage_key, + "data": {"core": {"default_panel": "other-dashboard"}}, + } + + # Need to setup frontend to register the websocket commands + assert await async_setup_component(hass, "frontend", {}) + assert await async_setup_component(hass, "lovelace", {}) + + # Verify default_panel was NOT overwritten via websocket + client = await hass_ws_client(hass) + await client.send_json({"id": 5, "type": "frontend/get_system_data", "key": "core"}) + response = await client.receive_json() + assert response["success"] + assert response["result"]["value"]["default_panel"] == "other-dashboard" + + +async def test_lovelace_no_migration_no_default_panel_set( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], +) -> None: + """Test no default_panel is set when there's nothing to migrate.""" + # Need to setup frontend to register the websocket commands + assert await async_setup_component(hass, "frontend", {}) + # No pre-existing lovelace storage = no migration + assert await async_setup_component(hass, "lovelace", {}) + + # Verify default_panel was NOT set via websocket + client = await hass_ws_client(hass) + await client.send_json({"id": 5, "type": "frontend/get_system_data", "key": "core"}) + response = await client.receive_json() + assert response["success"] + assert response["result"]["value"] is None diff --git a/tests/components/lovelace/test_system_health.py b/tests/components/lovelace/test_system_health.py index 251153fe419..f05e948f41a 100644 --- a/tests/components/lovelace/test_system_health.py +++ b/tests/components/lovelace/test_system_health.py @@ -34,11 +34,12 @@ async def test_system_health_info_autogen(hass: HomeAssistant) -> None: assert info == {"dashboards": 1, "mode": "auto-gen", "resources": 0} -async def test_system_health_info_storage( +async def test_system_health_info_storage_migration( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: - """Test system health info endpoint.""" + """Test system health info endpoint after migration from old storage.""" assert await async_setup_component(hass, "system_health", {}) + # Pre-populate old storage format (triggers migration) hass_storage[dashboard.CONFIG_STORAGE_KEY_DEFAULT] = { "key": "lovelace", "version": 1, @@ -47,7 +48,8 @@ async def test_system_health_info_storage( assert await async_setup_component(hass, "lovelace", {}) await hass.async_block_till_done() info = await get_system_health_info(hass, "lovelace") - assert info == {"dashboards": 1, "mode": "storage", "resources": 0, "views": 0} + # After migration: default dashboard (auto-gen) + migrated "lovelace" dashboard (storage with data) + assert info == {"dashboards": 2, "mode": "storage", "resources": 0, "views": 0} async def test_system_health_info_yaml(hass: HomeAssistant) -> None: