diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index c78356e0dd6..e7c309be719 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components import frontend from homeassistant.const import CONF_FILENAME, CONF_ICON from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import collection, config_validation as cv from homeassistant.util import sanitize_filename, slugify from . import dashboard, resources, websocket @@ -17,7 +17,9 @@ from .const import ( LOVELACE_CONFIG_FILE, MODE_STORAGE, MODE_YAML, + RESOURCE_CREATE_FIELDS, RESOURCE_SCHEMA, + RESOURCE_UPDATE_FIELDS, ) _LOGGER = logging.getLogger(__name__) @@ -111,6 +113,14 @@ async def async_setup(hass, config): resource_collection = resources.ResourceStorageCollection(hass, default_config) + collection.StorageCollectionWebsocket( + resource_collection, + "lovelace/resources", + "resource", + RESOURCE_CREATE_FIELDS, + RESOURCE_UPDATE_FIELDS, + ).async_setup(hass, create_list=False) + hass.components.websocket_api.async_register_command( websocket.websocket_lovelace_config ) diff --git a/homeassistant/components/lovelace/const.py b/homeassistant/components/lovelace/const.py index 2bf2b34098c..1e984b3d82d 100644 --- a/homeassistant/components/lovelace/const.py +++ b/homeassistant/components/lovelace/const.py @@ -14,13 +14,27 @@ MODE_STORAGE = "storage" LOVELACE_CONFIG_FILE = "ui-lovelace.yaml" CONF_RESOURCES = "resources" CONF_URL_PATH = "url_path" +CONF_RESOURCE_TYPE_WS = "res_type" + +RESOURCE_TYPES = ["js", "css", "module", "html"] RESOURCE_FIELDS = { - CONF_TYPE: vol.In(["js", "css", "module", "html"]), + CONF_TYPE: vol.In(RESOURCE_TYPES), CONF_URL: cv.string, } + RESOURCE_SCHEMA = vol.Schema(RESOURCE_FIELDS) +RESOURCE_CREATE_FIELDS = { + vol.Required(CONF_RESOURCE_TYPE_WS): vol.In(RESOURCE_TYPES), + vol.Required(CONF_URL): cv.string, +} + +RESOURCE_UPDATE_FIELDS = { + vol.Optional(CONF_RESOURCE_TYPE_WS): vol.In(RESOURCE_TYPES), + vol.Optional(CONF_URL): cv.string, +} + class ConfigNotFound(HomeAssistantError): """When no config available.""" diff --git a/homeassistant/components/lovelace/resources.py b/homeassistant/components/lovelace/resources.py index 4244feb26dd..57acaa487bd 100644 --- a/homeassistant/components/lovelace/resources.py +++ b/homeassistant/components/lovelace/resources.py @@ -5,11 +5,19 @@ import uuid import voluptuous as vol +from homeassistant.const import CONF_TYPE from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, storage -from .const import CONF_RESOURCES, DOMAIN, RESOURCE_SCHEMA +from .const import ( + CONF_RESOURCE_TYPE_WS, + CONF_RESOURCES, + DOMAIN, + RESOURCE_CREATE_FIELDS, + RESOURCE_SCHEMA, + RESOURCE_UPDATE_FIELDS, +) from .dashboard import LovelaceConfig RESOURCE_STORAGE_KEY = f"{DOMAIN}_resources" @@ -36,6 +44,8 @@ class ResourceStorageCollection(collection.StorageCollection): """Collection to store resources.""" loaded = False + CREATE_SCHEMA = vol.Schema(RESOURCE_CREATE_FIELDS) + UPDATE_SCHEMA = vol.Schema(RESOURCE_UPDATE_FIELDS) def __init__(self, hass: HomeAssistant, ll_config: LovelaceConfig): """Initialize the storage collection.""" @@ -84,13 +94,23 @@ class ResourceStorageCollection(collection.StorageCollection): async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" - raise NotImplementedError + data = self.CREATE_SCHEMA(data) + data[CONF_TYPE] = data.pop(CONF_RESOURCE_TYPE_WS) + return data @callback def _get_suggested_id(self, info: dict) -> str: - """Suggest an ID based on the config.""" - raise NotImplementedError + """Return unique ID.""" + return uuid.uuid4().hex async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" - raise NotImplementedError + 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) + + return {**data, **update_data} diff --git a/tests/components/lovelace/test_resources.py b/tests/components/lovelace/test_resources.py index 89464d95350..a44af14d3a0 100644 --- a/tests/components/lovelace/test_resources.py +++ b/tests/components/lovelace/test_resources.py @@ -90,6 +90,67 @@ async def test_storage_resources_import(hass, hass_ws_client, hass_storage): not in hass_storage[dashboard.CONFIG_STORAGE_KEY_DEFAULT]["data"]["config"] ) + # Add a resource + await client.send_json( + { + "id": 6, + "type": "lovelace/resources/create", + "res_type": "module", + "url": "/local/yo.js", + } + ) + response = await client.receive_json() + assert response["success"] + + await client.send_json({"id": 7, "type": "lovelace/resources"}) + response = await client.receive_json() + assert response["success"] + + last_item = response["result"][-1] + assert last_item["type"] == "module" + assert last_item["url"] == "/local/yo.js" + + # Update a resource + first_item = response["result"][0] + + await client.send_json( + { + "id": 8, + "type": "lovelace/resources/update", + "resource_id": first_item["id"], + "res_type": "css", + "url": "/local/updated.css", + } + ) + response = await client.receive_json() + assert response["success"] + + await client.send_json({"id": 9, "type": "lovelace/resources"}) + response = await client.receive_json() + assert response["success"] + + first_item = response["result"][0] + assert first_item["type"] == "css" + assert first_item["url"] == "/local/updated.css" + + # Delete resources + await client.send_json( + { + "id": 10, + "type": "lovelace/resources/delete", + "resource_id": first_item["id"], + } + ) + response = await client.receive_json() + assert response["success"] + + await client.send_json({"id": 11, "type": "lovelace/resources"}) + response = await client.receive_json() + assert response["success"] + + assert len(response["result"]) == 2 + assert first_item["id"] not in (item["id"] for item in response["result"]) + async def test_storage_resources_import_invalid(hass, hass_ws_client, hass_storage): """Test importing resources from storage config."""