diff --git a/CHANGELOG.md b/CHANGELOG.md index 65dd167..3b07552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to the OpenClaw Home Assistant Integration will be documented in this file. +## [0.1.7] - 2026-02-20 + +### Fixed +- Resolved chat card startup race where Lovelace resources were attempted before HTTP/Lovelace were ready, causing `Custom element not found: openclaw-chat-card`. +- Frontend registration now retries and waits for Home Assistant startup readiness before giving up. +- Static JS path registration is now idempotent and only marked successful after the path is actually registered. + +### Added +- Added MIT license file at repository root (`LICENSE`). + ## [0.1.6] - 2025-01-01 ### Fixed diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4c9e24b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 techartdev, Tech Art Ltd + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 0096082..6fa540f 100644 --- a/README.md +++ b/README.md @@ -81,4 +81,4 @@ action: ## License -See [LICENSE](../LICENSE). +See [LICENSE](LICENSE). diff --git a/custom_components/openclaw/__init__.py b/custom_components/openclaw/__init__.py index ebc1c7e..e0ef39d 100644 --- a/custom_components/openclaw/__init__.py +++ b/custom_components/openclaw/__init__.py @@ -5,6 +5,7 @@ Sets up the OpenClaw integration: API client, coordinator, platforms, and servic from __future__ import annotations +import asyncio from datetime import datetime, timezone import logging from pathlib import Path @@ -13,6 +14,7 @@ from typing import Any import voluptuous as vol from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -173,42 +175,60 @@ async def _async_register_frontend(hass: HomeAssistant) -> None: Called as a fire-and-forget task from async_setup_entry. Wrapped entirely in try/except so it can NEVER crash the integration. """ - frontend_key = f"{DOMAIN}_frontend_registered" + frontend_key = f"{DOMAIN}_frontend_registration_started" if hass.data.get(frontend_key): return hass.data[frontend_key] = True - url = _CARD_URL + async def _register_with_retries() -> None: + for _ in range(12): + url = await _async_register_static_path(hass) + if url and await _async_add_lovelace_resource(hass, url): + return + await asyncio.sleep(5) - # ── Step 1: Serve the JS via a static HTTP path ─────────────────── - if _CARD_PATH.exists() and hass.http is not None: - try: - # HA 2024.11+ — async_register_static_paths w/ StaticPathConfig - from homeassistant.components.http import StaticPathConfig # noqa: PLC0415 - await hass.http.async_register_static_paths( - [StaticPathConfig(url, str(_CARD_PATH), cache_headers=True)] - ) - _LOGGER.debug("Registered static path (new API): %s", url) - except (ImportError, AttributeError): - try: - # HA <2024.11 — synchronous register_static_path - hass.http.register_static_path(url, str(_CARD_PATH), True) - _LOGGER.debug("Registered static path (legacy API): %s", url) - except Exception as err: # noqa: BLE001 - _LOGGER.debug("Could not register static path '%s': %s", url, err) - url = f"/local/{_CARD_FILENAME}" # fall back to /local/ URL - except Exception as err: # noqa: BLE001 - _LOGGER.debug("Could not register static path '%s': %s", url, err) - url = f"/local/{_CARD_FILENAME}" - else: - # JS not in package (shouldn't happen) or HTTP not ready — try /local/ - url = f"/local/{_CARD_FILENAME}" + _LOGGER.warning( + "Could not auto-register OpenClaw chat card resource after retries. " + "Add it manually in Dashboard resources: %s", + _CARD_URL, + ) - # ── Step 2: Add to Lovelace resource store ─────────────────────── - await _async_add_lovelace_resource(hass, url) + hass.async_create_task(_register_with_retries()) + + async def _on_ha_started(_event) -> None: + await _register_with_retries() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _on_ha_started) -async def _async_add_lovelace_resource(hass: HomeAssistant, url: str) -> None: +async def _async_register_static_path(hass: HomeAssistant) -> str | None: + """Register the packaged chat-card JS as a static path when HTTP is ready.""" + static_key = f"{DOMAIN}_static_registered" + if hass.data.get(static_key): + return _CARD_URL + + if not _CARD_PATH.exists(): + _LOGGER.warning("Chat card JS not found at %s", _CARD_PATH) + return None + + if hass.http is None: + return None + + try: + from homeassistant.components.http import StaticPathConfig # noqa: PLC0415 + + await hass.http.async_register_static_paths( + [StaticPathConfig(_CARD_URL, str(_CARD_PATH), cache_headers=True)] + ) + except (ImportError, AttributeError): + hass.http.register_static_path(_CARD_URL, str(_CARD_PATH), True) + + hass.data[static_key] = True + _LOGGER.debug("Registered static path: %s", _CARD_URL) + return _CARD_URL + + +async def _async_add_lovelace_resource(hass: HomeAssistant, url: str) -> bool: """Add the card URL to Lovelace's resource store if not already present.""" # Lovelace stores resources in hass.data["lovelace"]["resources"]. # It is a ResourceStorageCollection with: @@ -216,22 +236,17 @@ async def _async_add_lovelace_resource(hass: HomeAssistant, url: str) -> None: # .async_create_item(data) → persists a new resource lovelace_data = hass.data.get("lovelace") if not lovelace_data: - _LOGGER.debug( - "Lovelace not loaded; resource '%s' must be added manually if needed", - url, - ) - return + return False resource_collection = lovelace_data.get("resources") if resource_collection is None: - _LOGGER.debug("Lovelace resource store not available") - return + return False try: existing_urls = {item["url"] for item in resource_collection.async_items()} if url in existing_urls: _LOGGER.debug("Lovelace resource already registered: %s", url) - return + return True await resource_collection.async_create_item( {"res_type": "module", "url": url} @@ -240,6 +255,7 @@ async def _async_add_lovelace_resource(hass: HomeAssistant, url: str) -> None: "Auto-registered Lovelace resource: %s — the chat card is ready to use.", url, ) + return True except Exception as err: # noqa: BLE001 _LOGGER.warning( "Could not auto-register Lovelace resource '%s': %s. " @@ -247,6 +263,7 @@ async def _async_add_lovelace_resource(hass: HomeAssistant, url: str) -> None: url, err, ) + return False # ── Service registration ────────────────────────────────────────────────────── diff --git a/custom_components/openclaw/manifest.json b/custom_components/openclaw/manifest.json index 697f9b8..43202cd 100644 --- a/custom_components/openclaw/manifest.json +++ b/custom_components/openclaw/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/techartdev/OpenClawHomeAssistant/issues", "requirements": [], - "version": "0.1.6", + "version": "0.1.7", "dependencies": ["conversation"], "after_dependencies": ["hassio", "lovelace"] }