From e6fb9e2914d4a35fd3fcd5538c0d697177fd79ad Mon Sep 17 00:00:00 2001 From: techartdev Date: Fri, 20 Feb 2026 16:26:59 +0200 Subject: [PATCH] Update changelog for version 0.1.11, enhance assistant message extraction, add fallback event emission on API errors, and improve custom card registration --- CHANGELOG.md | 14 +++ custom_components/openclaw/__init__.py | 88 +++++++++++++++---- custom_components/openclaw/manifest.json | 2 +- .../openclaw/www/openclaw-chat-card.js | 23 +++-- www/openclaw-chat-card.js | 23 +++-- 5 files changed, 118 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a40ed7a..e525dde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to the OpenClaw Home Assistant Integration will be documented in this file. +## [0.1.11] - 2026-02-20 + +### Fixed +- Chat card no longer waits forever when the gateway returns a non-`choices[0].message.content` response shape. +- `openclaw.send_message` now extracts assistant text from multiple OpenAI-compatible formats (`choices`, `output_text`, `response`, `message`, `content`, `answer`). +- Added fallback event emission on API errors so the frontend always receives a response and exits the typing state. + +## [0.1.10] - 2026-02-20 + +### Fixed +- Made custom card registration idempotent (`customElements.get(...)` guards) to prevent duplicate-load exceptions that can block card discovery. +- Prevented duplicate `window.customCards` entries for `openclaw-chat-card`. +- Synced registration hardening in both packaged and root `www/` card scripts. + ## [0.1.9] - 2026-02-20 ### Fixed diff --git a/custom_components/openclaw/__init__.py b/custom_components/openclaw/__init__.py index 8f43b29..773b7ec 100644 --- a/custom_components/openclaw/__init__.py +++ b/custom_components/openclaw/__init__.py @@ -318,26 +318,38 @@ def _async_register_services(hass: HomeAssistant) -> None: session_id=session_id, ) - choices = response.get("choices", []) - if choices: - assistant_message = ( - choices[0].get("message", {}).get("content", "") - ) - model_used = response.get("model", "unknown") + assistant_message = _extract_assistant_message(response) + model_used = response.get("model", "unknown") - hass.bus.async_fire( - EVENT_MESSAGE_RECEIVED, - { - ATTR_MESSAGE: assistant_message, - ATTR_SESSION_ID: session_id, - ATTR_MODEL: model_used, - ATTR_TIMESTAMP: datetime.now(timezone.utc).isoformat(), - }, + if not assistant_message: + assistant_message = "Response received, but no readable message content was found." + _LOGGER.warning( + "OpenClaw response had no parseable assistant message. Keys: %s", + list(response.keys()), ) - coordinator.update_last_activity() + + hass.bus.async_fire( + EVENT_MESSAGE_RECEIVED, + { + ATTR_MESSAGE: assistant_message, + ATTR_SESSION_ID: session_id, + ATTR_MODEL: model_used, + ATTR_TIMESTAMP: datetime.now(timezone.utc).isoformat(), + }, + ) + coordinator.update_last_activity() except OpenClawApiError as err: _LOGGER.error("Failed to send message to OpenClaw: %s", err) + hass.bus.async_fire( + EVENT_MESSAGE_RECEIVED, + { + ATTR_MESSAGE: f"OpenClaw error: {err}", + ATTR_SESSION_ID: session_id, + ATTR_MODEL: "unknown", + ATTR_TIMESTAMP: datetime.now(timezone.utc).isoformat(), + }, + ) async def handle_clear_history(call: ServiceCall) -> None: """Handle the openclaw.clear_history service call.""" @@ -366,3 +378,49 @@ def _get_first_entry_data(hass: HomeAssistant) -> dict[str, Any] | None: if isinstance(entry_data, dict) and "client" in entry_data: return entry_data return None + + +def _coerce_text(value: Any) -> str | None: + """Convert known message content shapes to plain text.""" + if isinstance(value, str): + text = value.strip() + return text or None + + if isinstance(value, list): + parts: list[str] = [] + for item in value: + if isinstance(item, str): + if item.strip(): + parts.append(item.strip()) + continue + if isinstance(item, dict): + text_part = item.get("text") or item.get("content") + if isinstance(text_part, str) and text_part.strip(): + parts.append(text_part.strip()) + if parts: + return "\n".join(parts) + + return None + + +def _extract_assistant_message(response: dict[str, Any]) -> str | None: + """Extract assistant text from multiple OpenAI-compatible response shapes.""" + choices = response.get("choices") + if isinstance(choices, list) and choices: + first = choices[0] + if isinstance(first, dict): + message = first.get("message") + if isinstance(message, dict): + extracted = _coerce_text(message.get("content")) + if extracted: + return extracted + extracted = _coerce_text(first.get("text")) + if extracted: + return extracted + + for key in ("output_text", "response", "message", "content", "answer"): + extracted = _coerce_text(response.get(key)) + if extracted: + return extracted + + return None diff --git a/custom_components/openclaw/manifest.json b/custom_components/openclaw/manifest.json index d84b33f..b1dd9e9 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.9", + "version": "0.1.11", "dependencies": ["conversation"], "after_dependencies": ["hassio", "lovelace"] } diff --git a/custom_components/openclaw/www/openclaw-chat-card.js b/custom_components/openclaw/www/openclaw-chat-card.js index 3532ae9..c9ae805 100644 --- a/custom_components/openclaw/www/openclaw-chat-card.js +++ b/custom_components/openclaw/www/openclaw-chat-card.js @@ -836,16 +836,23 @@ class OpenClawChatCardEditor extends HTMLElement { // ── Card registration ─────────────────────────────────────────────────────── -customElements.define("openclaw-chat-card-editor", OpenClawChatCardEditor); -customElements.define("openclaw-chat-card", OpenClawChatCard); +if (!customElements.get("openclaw-chat-card-editor")) { + customElements.define("openclaw-chat-card-editor", OpenClawChatCardEditor); +} + +if (!customElements.get("openclaw-chat-card")) { + customElements.define("openclaw-chat-card", OpenClawChatCard); +} window.customCards = window.customCards || []; -window.customCards.push({ - type: "openclaw-chat-card", - name: "OpenClaw Chat", - description: "Chat interface for OpenClaw AI Assistant with streaming, voice, and file support.", - preview: true, -}); +if (!window.customCards.some((card) => card?.type === "openclaw-chat-card")) { + window.customCards.push({ + type: "openclaw-chat-card", + name: "OpenClaw Chat", + description: "Chat interface for OpenClaw AI Assistant with streaming, voice, and file support.", + preview: true, + }); +} console.info( `%c OPENCLAW-CHAT-CARD %c v${CARD_VERSION} `, diff --git a/www/openclaw-chat-card.js b/www/openclaw-chat-card.js index 3532ae9..c9ae805 100644 --- a/www/openclaw-chat-card.js +++ b/www/openclaw-chat-card.js @@ -836,16 +836,23 @@ class OpenClawChatCardEditor extends HTMLElement { // ── Card registration ─────────────────────────────────────────────────────── -customElements.define("openclaw-chat-card-editor", OpenClawChatCardEditor); -customElements.define("openclaw-chat-card", OpenClawChatCard); +if (!customElements.get("openclaw-chat-card-editor")) { + customElements.define("openclaw-chat-card-editor", OpenClawChatCardEditor); +} + +if (!customElements.get("openclaw-chat-card")) { + customElements.define("openclaw-chat-card", OpenClawChatCard); +} window.customCards = window.customCards || []; -window.customCards.push({ - type: "openclaw-chat-card", - name: "OpenClaw Chat", - description: "Chat interface for OpenClaw AI Assistant with streaming, voice, and file support.", - preview: true, -}); +if (!window.customCards.some((card) => card?.type === "openclaw-chat-card")) { + window.customCards.push({ + type: "openclaw-chat-card", + name: "OpenClaw Chat", + description: "Chat interface for OpenClaw AI Assistant with streaming, voice, and file support.", + preview: true, + }); +} console.info( `%c OPENCLAW-CHAT-CARD %c v${CARD_VERSION} `,