Add json cache to lovelace config (#117843)
This commit is contained in:
parent
2c09f72c33
commit
2308ff2cbf
3 changed files with 110 additions and 34 deletions
|
@ -7,14 +7,16 @@ import logging
|
|||
import os
|
||||
from pathlib import Path
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.frontend import DATA_PANELS
|
||||
from homeassistant.const import CONF_FILENAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import collection, storage
|
||||
from homeassistant.helpers.json import json_bytes, json_fragment
|
||||
from homeassistant.util.yaml import Secrets, load_yaml_dict
|
||||
|
||||
from .const import (
|
||||
|
@ -42,11 +44,13 @@ _LOGGER = logging.getLogger(__name__)
|
|||
class LovelaceConfig(ABC):
|
||||
"""Base class for Lovelace config."""
|
||||
|
||||
def __init__(self, hass, url_path, config):
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, url_path: str | None, config: dict[str, Any] | None
|
||||
) -> None:
|
||||
"""Initialize Lovelace config."""
|
||||
self.hass = hass
|
||||
if config:
|
||||
self.config = {**config, CONF_URL_PATH: url_path}
|
||||
self.config: dict[str, Any] | None = {**config, CONF_URL_PATH: url_path}
|
||||
else:
|
||||
self.config = None
|
||||
|
||||
|
@ -65,7 +69,7 @@ class LovelaceConfig(ABC):
|
|||
"""Return the config info."""
|
||||
|
||||
@abstractmethod
|
||||
async def async_load(self, force):
|
||||
async def async_load(self, force: bool) -> dict[str, Any]:
|
||||
"""Load config."""
|
||||
|
||||
async def async_save(self, config):
|
||||
|
@ -77,7 +81,7 @@ class LovelaceConfig(ABC):
|
|||
raise HomeAssistantError("Not supported")
|
||||
|
||||
@callback
|
||||
def _config_updated(self):
|
||||
def _config_updated(self) -> None:
|
||||
"""Fire config updated event."""
|
||||
self.hass.bus.async_fire(EVENT_LOVELACE_UPDATED, {"url_path": self.url_path})
|
||||
|
||||
|
@ -85,10 +89,10 @@ class LovelaceConfig(ABC):
|
|||
class LovelaceStorage(LovelaceConfig):
|
||||
"""Class to handle Storage based Lovelace config."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
def __init__(self, hass: HomeAssistant, config: dict[str, Any] | None) -> None:
|
||||
"""Initialize Lovelace config based on storage helper."""
|
||||
if config is None:
|
||||
url_path = None
|
||||
url_path: str | None = None
|
||||
storage_key = CONFIG_STORAGE_KEY_DEFAULT
|
||||
else:
|
||||
url_path = config[CONF_URL_PATH]
|
||||
|
@ -96,8 +100,11 @@ class LovelaceStorage(LovelaceConfig):
|
|||
|
||||
super().__init__(hass, url_path, config)
|
||||
|
||||
self._store = storage.Store(hass, CONFIG_STORAGE_VERSION, storage_key)
|
||||
self._data = None
|
||||
self._store = storage.Store[dict[str, Any]](
|
||||
hass, CONFIG_STORAGE_VERSION, storage_key
|
||||
)
|
||||
self._data: dict[str, Any] | None = None
|
||||
self._json_config: json_fragment | None = None
|
||||
|
||||
@property
|
||||
def mode(self) -> str:
|
||||
|
@ -106,27 +113,30 @@ class LovelaceStorage(LovelaceConfig):
|
|||
|
||||
async def async_get_info(self):
|
||||
"""Return the Lovelace storage info."""
|
||||
if self._data is None:
|
||||
await self._load()
|
||||
|
||||
if self._data["config"] is None:
|
||||
data = self._data or await self._load()
|
||||
if data["config"] is None:
|
||||
return {"mode": "auto-gen"}
|
||||
return _config_info(self.mode, data["config"])
|
||||
|
||||
return _config_info(self.mode, self._data["config"])
|
||||
|
||||
async def async_load(self, force):
|
||||
async def async_load(self, force: bool) -> dict[str, Any]:
|
||||
"""Load config."""
|
||||
if self.hass.config.recovery_mode:
|
||||
raise ConfigNotFound
|
||||
|
||||
if self._data is None:
|
||||
await self._load()
|
||||
|
||||
if (config := self._data["config"]) is None:
|
||||
data = self._data or await self._load()
|
||||
if (config := data["config"]) is None:
|
||||
raise ConfigNotFound
|
||||
|
||||
return config
|
||||
|
||||
async def async_json(self, force: bool) -> json_fragment:
|
||||
"""Return JSON representation of the config."""
|
||||
if self.hass.config.recovery_mode:
|
||||
raise ConfigNotFound
|
||||
if self._data is None:
|
||||
await self._load()
|
||||
return self._json_config or self._async_build_json()
|
||||
|
||||
async def async_save(self, config):
|
||||
"""Save config."""
|
||||
if self.hass.config.recovery_mode:
|
||||
|
@ -135,6 +145,7 @@ class LovelaceStorage(LovelaceConfig):
|
|||
if self._data is None:
|
||||
await self._load()
|
||||
self._data["config"] = config
|
||||
self._json_config = None
|
||||
self._config_updated()
|
||||
await self._store.async_save(self._data)
|
||||
|
||||
|
@ -145,25 +156,37 @@ class LovelaceStorage(LovelaceConfig):
|
|||
|
||||
await self._store.async_remove()
|
||||
self._data = None
|
||||
self._json_config = None
|
||||
self._config_updated()
|
||||
|
||||
async def _load(self):
|
||||
async def _load(self) -> dict[str, Any]:
|
||||
"""Load the config."""
|
||||
data = await self._store.async_load()
|
||||
self._data = data if data else {"config": None}
|
||||
return self._data
|
||||
|
||||
@callback
|
||||
def _async_build_json(self) -> json_fragment:
|
||||
"""Build JSON representation of the config."""
|
||||
if self._data is None or self._data["config"] is None:
|
||||
raise ConfigNotFound
|
||||
self._json_config = json_fragment(json_bytes(self._data["config"]))
|
||||
return self._json_config
|
||||
|
||||
|
||||
class LovelaceYAML(LovelaceConfig):
|
||||
"""Class to handle YAML-based Lovelace config."""
|
||||
|
||||
def __init__(self, hass, url_path, config):
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, url_path: str | None, config: dict[str, Any] | None
|
||||
) -> None:
|
||||
"""Initialize the YAML config."""
|
||||
super().__init__(hass, url_path, config)
|
||||
|
||||
self.path = hass.config.path(
|
||||
config[CONF_FILENAME] if config else LOVELACE_CONFIG_FILE
|
||||
)
|
||||
self._cache = None
|
||||
self._cache: tuple[dict[str, Any], float, json_fragment] | None = None
|
||||
|
||||
@property
|
||||
def mode(self) -> str:
|
||||
|
@ -182,23 +205,35 @@ class LovelaceYAML(LovelaceConfig):
|
|||
|
||||
return _config_info(self.mode, config)
|
||||
|
||||
async def async_load(self, force):
|
||||
async def async_load(self, force: bool) -> dict[str, Any]:
|
||||
"""Load config."""
|
||||
is_updated, config = await self.hass.async_add_executor_job(
|
||||
config, json = await self._async_load_or_cached(force)
|
||||
return config
|
||||
|
||||
async def async_json(self, force: bool) -> json_fragment:
|
||||
"""Return JSON representation of the config."""
|
||||
config, json = await self._async_load_or_cached(force)
|
||||
return json
|
||||
|
||||
async def _async_load_or_cached(
|
||||
self, force: bool
|
||||
) -> tuple[dict[str, Any], json_fragment]:
|
||||
"""Load the config or return a cached version."""
|
||||
is_updated, config, json = await self.hass.async_add_executor_job(
|
||||
self._load_config, force
|
||||
)
|
||||
if is_updated:
|
||||
self._config_updated()
|
||||
return config
|
||||
return config, json
|
||||
|
||||
def _load_config(self, force):
|
||||
def _load_config(self, force: bool) -> tuple[bool, dict[str, Any], json_fragment]:
|
||||
"""Load the actual config."""
|
||||
# Check for a cached version of the config
|
||||
if not force and self._cache is not None:
|
||||
config, last_update = self._cache
|
||||
config, last_update, json = self._cache
|
||||
modtime = os.path.getmtime(self.path)
|
||||
if config and last_update > modtime:
|
||||
return False, config
|
||||
return False, config, json
|
||||
|
||||
is_updated = self._cache is not None
|
||||
|
||||
|
@ -209,8 +244,9 @@ class LovelaceYAML(LovelaceConfig):
|
|||
except FileNotFoundError:
|
||||
raise ConfigNotFound from None
|
||||
|
||||
self._cache = (config, time.time())
|
||||
return is_updated, config
|
||||
json = json_fragment(json_bytes(config))
|
||||
self._cache = (config, time.time(), json)
|
||||
return is_updated, config, json
|
||||
|
||||
|
||||
def _config_info(mode, config):
|
||||
|
|
|
@ -11,6 +11,7 @@ from homeassistant.components import websocket_api
|
|||
from homeassistant.core import HomeAssistant, callback
|
||||
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, DOMAIN, ConfigNotFound
|
||||
from .dashboard import LovelaceStorage
|
||||
|
@ -86,9 +87,9 @@ async def websocket_lovelace_config(
|
|||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
config: LovelaceStorage,
|
||||
) -> None:
|
||||
) -> json_fragment:
|
||||
"""Send Lovelace UI config over WebSocket configuration."""
|
||||
return await config.async_load(msg["force"])
|
||||
return await config.async_json(msg["force"])
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
|
@ -137,7 +138,7 @@ def websocket_lovelace_dashboards(
|
|||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Delete Lovelace UI configuration."""
|
||||
"""Send Lovelace dashboard configuration."""
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
[
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""Test the Lovelace initialization."""
|
||||
|
||||
from collections.abc import Generator
|
||||
import time
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
@ -180,6 +181,44 @@ async def test_lovelace_from_yaml(
|
|||
|
||||
assert len(events) == 1
|
||||
|
||||
# Make sure when the mtime changes, we reload the config
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.lovelace.dashboard.load_yaml_dict",
|
||||
return_value={"hello": "yo3"},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.lovelace.dashboard.os.path.getmtime",
|
||||
return_value=time.time(),
|
||||
),
|
||||
):
|
||||
await client.send_json({"id": 9, "type": "lovelace/config", "force": False})
|
||||
response = await client.receive_json()
|
||||
|
||||
assert response["success"]
|
||||
assert response["result"] == {"hello": "yo3"}
|
||||
|
||||
assert len(events) == 2
|
||||
|
||||
# If the mtime is lower, preserve the cache
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.lovelace.dashboard.load_yaml_dict",
|
||||
return_value={"hello": "yo4"},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.lovelace.dashboard.os.path.getmtime",
|
||||
return_value=0,
|
||||
),
|
||||
):
|
||||
await client.send_json({"id": 10, "type": "lovelace/config", "force": False})
|
||||
response = await client.receive_json()
|
||||
|
||||
assert response["success"]
|
||||
assert response["result"] == {"hello": "yo3"}
|
||||
|
||||
assert len(events) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("url_path", ["test-panel", "test-panel-no-sidebar"])
|
||||
async def test_dashboard_from_yaml(
|
||||
|
|
Loading…
Add table
Reference in a new issue