From 4c8ea3669ca68bf80bd1f03a291bb410f49becfa Mon Sep 17 00:00:00 2001 From: Renaud Allard Date: Fri, 10 Apr 2026 10:38:17 +0200 Subject: [PATCH] Load lovelace resource collection eagerly during setup (#165773) --- .../components/lovelace/resources.py | 26 ++++++++++---- tests/components/lovelace/test_resources.py | 36 +++++++++++++++++++ 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/lovelace/resources.py b/homeassistant/components/lovelace/resources.py index 96f84ccbc60..b2f1c80dda2 100644 --- a/homeassistant/components/lovelace/resources.py +++ b/homeassistant/components/lovelace/resources.py @@ -62,14 +62,32 @@ class ResourceStorageCollection(collection.DictStorageCollection): ) self.ll_config = ll_config - async def async_get_info(self) -> dict[str, int]: - """Return the resources info for YAML mode.""" + async def _async_ensure_loaded(self) -> None: + """Ensure the collection has been loaded from storage.""" if not self.loaded: await self.async_load() self.loaded = True + async def async_get_info(self) -> dict[str, int]: + """Return the resources info for YAML mode.""" + await self._async_ensure_loaded() return {"resources": len(self.async_items() or [])} + async def async_create_item(self, data: dict) -> dict: + """Create a new item.""" + await self._async_ensure_loaded() + return await super().async_create_item(data) + + async def async_update_item(self, item_id: str, updates: dict) -> dict: + """Update item.""" + await self._async_ensure_loaded() + return await super().async_update_item(item_id, updates) + + async def async_delete_item(self, item_id: str) -> None: + """Delete item.""" + await self._async_ensure_loaded() + await super().async_delete_item(item_id) + async def _async_load_data(self) -> collection.SerializedStorageCollection | None: """Load the data.""" if (store_data := await self.store.async_load()) is not None: @@ -118,10 +136,6 @@ class ResourceStorageCollection(collection.DictStorageCollection): async def _update_data(self, item: dict, update_data: dict) -> dict: """Return a new updated data object.""" - if not self.loaded: - await self.async_load() - self.loaded = True - update_data = self.UPDATE_SCHEMA(update_data) if CONF_RESOURCE_TYPE_WS in update_data: update_data[CONF_TYPE] = update_data.pop(CONF_RESOURCE_TYPE_WS) diff --git a/tests/components/lovelace/test_resources.py b/tests/components/lovelace/test_resources.py index 281fb001fc2..2b248161b3e 100644 --- a/tests/components/lovelace/test_resources.py +++ b/tests/components/lovelace/test_resources.py @@ -8,6 +8,7 @@ import uuid import pytest from homeassistant.components.lovelace import dashboard, resources +from homeassistant.components.lovelace.const import LOVELACE_DATA from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -278,6 +279,41 @@ async def test_storage_resources_import_invalid( ) +async def test_storage_resources_create_preserves_existing( + hass: HomeAssistant, + hass_storage: dict[str, Any], +) -> None: + """Test async_create_item lazy-loads before writing. + + Custom integrations may call async_create_item() during startup before the + frontend triggers a resource listing. Without a lazy-load guard, the + collection is empty and async_create_item() overwrites all existing + resources on disk. + """ + resource_config = [{**item, "id": uuid.uuid4().hex} for item in RESOURCE_EXAMPLES] + hass_storage[resources.RESOURCE_STORAGE_KEY] = { + "key": resources.RESOURCE_STORAGE_KEY, + "version": 1, + "data": {"items": resource_config}, + } + assert await async_setup_component(hass, "lovelace", {}) + + resource_collection = hass.data[LOVELACE_DATA].resources + + # Directly call async_create_item before any websocket listing + await resource_collection.async_create_item( + {"res_type": "module", "url": "/local/new.js"} + ) + + # Existing resources must still be present + items = resource_collection.async_items() + assert len(items) == len(resource_config) + 1 + urls = [item["url"] for item in items] + for original in resource_config: + assert original["url"] in urls + assert "/local/new.js" in urls + + @pytest.mark.parametrize("list_cmd", ["lovelace/resources", "lovelace/resources/list"]) async def test_storage_resources_safe_mode( hass: HomeAssistant,