This commit is contained in:
2026-06-14 02:01:49 -04:00
parent 16c1da4289
commit dcc2524785
132 changed files with 136000 additions and 4 deletions
+223
View File
@@ -0,0 +1,223 @@
"""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