224 lines
8.2 KiB
Python
224 lines
8.2 KiB
Python
"""Frontend card registration for WashData."""
|
|
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Any, Literal, TypedDict, cast
|
|
from homeassistant.core import HomeAssistant, Event
|
|
from homeassistant.const import EVENT_COMPONENT_LOADED
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
LOCAL_SUBDIR = "ha_washdata"
|
|
CARD_NAME = "ha-washdata-card.js"
|
|
INTEGRATION_URL = f"/{LOCAL_SUBDIR}/{CARD_NAME}"
|
|
CARD_REGISTERED = "registered"
|
|
CARD_DEFERRED = "deferred"
|
|
CARD_FAILED = "failed"
|
|
CardRegisterResult = Literal["registered", "deferred", "failed"]
|
|
|
|
|
|
class LovelaceResourceItem(TypedDict, total=False):
|
|
"""Known lovelace resource item shape used by this integration."""
|
|
|
|
id: str
|
|
url: str
|
|
res_type: str
|
|
|
|
|
|
def get_cache_buster() -> str:
|
|
"""Generate a stable cache buster based on card asset mtime."""
|
|
try:
|
|
src = Path(__file__).parent / "www" / CARD_NAME
|
|
return str(int(os.path.getmtime(src)))
|
|
except OSError:
|
|
# Deterministic fallback when file is unavailable.
|
|
return "1"
|
|
|
|
|
|
def _register_static_path(hass: HomeAssistant, url_path: str, path: str) -> None:
|
|
"""Register a static path with the HA HTTP component, compatible with multiple HA versions."""
|
|
try:
|
|
# pylint: disable=import-outside-toplevel
|
|
from homeassistant.components.http import StaticPathConfig
|
|
|
|
if hasattr(hass.http, "async_register_static_paths"):
|
|
|
|
async def _safe_register():
|
|
try:
|
|
await hass.http.async_register_static_paths(
|
|
[StaticPathConfig(url_path, path, True)]
|
|
)
|
|
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
_LOGGER.debug(
|
|
"Failed to async register static path %s -> %s: %s",
|
|
url_path,
|
|
path,
|
|
exc,
|
|
)
|
|
|
|
hass.async_create_task(_safe_register())
|
|
return
|
|
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
_LOGGER.debug(
|
|
"Async static path registration not available; falling back to "
|
|
"sync registration for %s -> %s (%s)",
|
|
url_path,
|
|
path,
|
|
exc,
|
|
)
|
|
|
|
# Fallback for older HA
|
|
try:
|
|
http_obj = cast(Any, hass.http)
|
|
register_static_path = getattr(http_obj, "register_static_path", None)
|
|
if callable(register_static_path):
|
|
register_static_path(url_path, path, cache_headers=True)
|
|
except Exception: # pylint: disable=broad-exception-caught
|
|
_LOGGER.debug("Failed to register static path %s -> %s", url_path, path)
|
|
|
|
|
|
async def _init_resource(hass: HomeAssistant, url: str, ver: str) -> bool:
|
|
"""Safely add or update a Lovelace resource for the given URL."""
|
|
try:
|
|
# pylint: disable=import-outside-toplevel
|
|
from homeassistant.components.frontend import add_extra_js_url
|
|
from homeassistant.components.lovelace.resources import (
|
|
ResourceStorageCollection,
|
|
)
|
|
except Exception: # pylint: disable=broad-exception-caught
|
|
_LOGGER.debug(
|
|
"Lovelace resource helpers unavailable; skipping auto resource init"
|
|
)
|
|
return False
|
|
|
|
lovelace = hass.data.get("lovelace")
|
|
if not lovelace:
|
|
_LOGGER.debug("Lovelace storage not available; skipping auto resource init")
|
|
return False
|
|
|
|
resources = (
|
|
lovelace.resources if hasattr(lovelace, "resources") else lovelace["resources"]
|
|
)
|
|
|
|
url2 = f"{url}?v={ver}"
|
|
|
|
if not isinstance(resources, ResourceStorageCollection):
|
|
_LOGGER.debug("Add extra JS module (non-storage): %s", url2)
|
|
add_extra_js_url(hass, url2)
|
|
return True
|
|
|
|
resources_obj = resources
|
|
await resources_obj.async_get_info()
|
|
|
|
for raw_item in resources_obj.async_items():
|
|
if not isinstance(raw_item, dict):
|
|
continue
|
|
|
|
item = cast(LovelaceResourceItem, raw_item)
|
|
item_url = item.get("url")
|
|
if not isinstance(item_url, str) or not item_url.startswith(url):
|
|
continue
|
|
|
|
if item_url == url2 and item.get("res_type") == "module":
|
|
return True
|
|
|
|
item_id = item.get("id")
|
|
if not isinstance(item_id, str):
|
|
continue
|
|
|
|
_LOGGER.debug("Update lovelace resource to: %s", url2)
|
|
await resources_obj.async_update_item(
|
|
item_id, {"res_type": "module", "url": url2}
|
|
)
|
|
|
|
return True
|
|
|
|
_LOGGER.debug("Add new lovelace resource: %s", url2)
|
|
await resources_obj.async_create_item({"res_type": "module", "url": url2})
|
|
|
|
return True
|
|
|
|
|
|
class WashDataCardRegistration:
|
|
"""Serve ha-washdata-card.js from the integration package."""
|
|
|
|
def __init__(self, hass: HomeAssistant) -> None:
|
|
self.hass = hass
|
|
|
|
def _src_path(self) -> Path:
|
|
return Path(__file__).parent / "www" / CARD_NAME
|
|
|
|
async def async_register(self) -> CardRegisterResult:
|
|
"""Register card assets/resources and report registration outcome."""
|
|
src = self._src_path()
|
|
if not src.exists():
|
|
_LOGGER.warning("Card file not found: %s", src)
|
|
return CARD_FAILED
|
|
|
|
_register_static_path(self.hass, INTEGRATION_URL, str(src))
|
|
|
|
version = get_cache_buster()
|
|
|
|
# Try auto-registration of the lovelace resource
|
|
# If lovelace is not yet loaded, wait for it
|
|
if not self.hass.data.get("lovelace"):
|
|
_LOGGER.debug("Lovelace not loaded yet; waiting for component loaded event")
|
|
|
|
unsubscribe_on_lovelace_loaded: Any = None
|
|
|
|
async def _on_lovelace_loaded(event: Event) -> None:
|
|
if event.data.get("component") == "lovelace":
|
|
_LOGGER.debug(
|
|
"Lovelace component loaded; retrying resource registration"
|
|
)
|
|
if unsubscribe_on_lovelace_loaded:
|
|
unsubscribe_on_lovelace_loaded()
|
|
try:
|
|
if await _init_resource(self.hass, INTEGRATION_URL, version):
|
|
self.hass.data["ha_washdata_card_registered"] = True
|
|
self.hass.data["ha_washdata_card_deferred"] = False
|
|
else:
|
|
self.hass.data["ha_washdata_card_deferred"] = False
|
|
except Exception: # pylint: disable=broad-exception-caught
|
|
self.hass.data["ha_washdata_card_deferred"] = False
|
|
_LOGGER.debug(
|
|
"Delayed auto-registration of lovelace resource failed for %s",
|
|
INTEGRATION_URL,
|
|
)
|
|
|
|
unsubscribe_on_lovelace_loaded = self.hass.bus.async_listen(EVENT_COMPONENT_LOADED, _on_lovelace_loaded)
|
|
|
|
# Re-check in case lovelace loaded between the initial check and listener registration.
|
|
if self.hass.data.get("lovelace"):
|
|
unsubscribe_on_lovelace_loaded()
|
|
_LOGGER.debug("Lovelace already loaded after deferred listener; registering now")
|
|
try:
|
|
if await _init_resource(self.hass, INTEGRATION_URL, version):
|
|
self.hass.data["ha_washdata_card_registered"] = True
|
|
self.hass.data["ha_washdata_card_deferred"] = False
|
|
return CARD_REGISTERED
|
|
self.hass.data["ha_washdata_card_deferred"] = False
|
|
return CARD_FAILED
|
|
except Exception: # pylint: disable=broad-exception-caught
|
|
self.hass.data["ha_washdata_card_deferred"] = False
|
|
return CARD_FAILED
|
|
|
|
return CARD_DEFERRED
|
|
|
|
# Lovelace is already loaded
|
|
try:
|
|
registered = await _init_resource(self.hass, INTEGRATION_URL, version)
|
|
except Exception as err: # pylint: disable=broad-exception-caught
|
|
_LOGGER.debug(
|
|
"Auto-registration of lovelace resource failed for %s: %s",
|
|
INTEGRATION_URL,
|
|
err,
|
|
)
|
|
return CARD_FAILED
|
|
|
|
if registered:
|
|
_LOGGER.debug("Auto-registered lovelace resource for %s", INTEGRATION_URL)
|
|
return CARD_REGISTERED
|
|
return CARD_FAILED
|