From bee2fd5a458ba19d9011cda801c9c206c4536be8 Mon Sep 17 00:00:00 2001 From: techartdev Date: Sat, 21 Feb 2026 19:07:53 +0200 Subject: [PATCH] Update version to 0.1.50 and enhance voice behavior, chat consistency, and auto-scroll functionality --- CHANGELOG.md | 5 ++ custom_components/openclaw/__init__.py | 2 +- custom_components/openclaw/manifest.json | 2 +- .../openclaw/www/openclaw-chat-card.js | 54 +++++++++++++++++-- www/openclaw-chat-card.js | 2 +- 5 files changed, 59 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d39f97..92a9929 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to the OpenClaw Home Assistant Integration will be documented in this file. +## [0.1.50] - 2026-02-21 + +### Fixed +- Improved Android Home Assistant app voice behavior by preferring Home Assistant TTS first and handling browser-TTS-unavailable cases correctly. + ## [0.1.49] - 2026-02-21 ### Fixed diff --git a/custom_components/openclaw/__init__.py b/custom_components/openclaw/__init__.py index 4aebcfd..50692aa 100644 --- a/custom_components/openclaw/__init__.py +++ b/custom_components/openclaw/__init__.py @@ -92,7 +92,7 @@ _CARD_PATH = Path(__file__).parent / "www" / _CARD_FILENAME # URL at which the card JS is served (registered via register_static_path) _CARD_STATIC_URL = f"/openclaw/{_CARD_FILENAME}" # Versioned URL used for Lovelace resource registration to avoid stale browser cache -_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.49" +_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.50" OpenClawConfigEntry = ConfigEntry diff --git a/custom_components/openclaw/manifest.json b/custom_components/openclaw/manifest.json index 24c4b6e..79487b9 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.49", + "version": "0.1.50", "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 ff2e5fe..b679937 100644 --- a/custom_components/openclaw/www/openclaw-chat-card.js +++ b/custom_components/openclaw/www/openclaw-chat-card.js @@ -13,7 +13,7 @@ * + subscribes to openclaw_message_received events. */ -const CARD_VERSION = "0.3.8"; +const CARD_VERSION = "0.3.9"; // Max time (ms) to show the thinking indicator before falling back to an error const THINKING_TIMEOUT_MS = 120_000; @@ -641,6 +641,11 @@ class OpenClawChatCard extends HTMLElement { return brandMatch || ua.includes("brave") || !!navigator.brave; } + _isHomeAssistantAndroidApp() { + const ua = (navigator.userAgent || "").toLowerCase(); + return ua.includes("home assistant") && ua.includes("android"); + } + _getVoiceProvider() { const configured = this._config.voice_provider; if (configured === "assist_stt" || configured === "browser") { @@ -1218,6 +1223,7 @@ class OpenClawChatCard extends HTMLElement { if (this._voiceIdleRestartTimer) { clearTimeout(this._voiceIdleRestartTimer); } + const idleRestartDelayMs = this._isHomeAssistantAndroidApp() ? 3500 : 1500; this._voiceIdleRestartTimer = setTimeout(() => { this._voiceIdleRestartTimer = null; if (!this._isVoiceMode || !this._recognition) { @@ -1228,7 +1234,7 @@ class OpenClawChatCard extends HTMLElement { } catch (e) { // Ignore — may already be started } - }, 1500); + }, idleRestartDelayMs); return; } @@ -1348,8 +1354,10 @@ class OpenClawChatCard extends HTMLElement { } _speak(text) { - if (!("speechSynthesis" in window)) return; + const hasBrowserTts = "speechSynthesis" in window; this._pauseVoiceInputForTts(); + this._voiceStatus = "Speaking…"; + this._render(); // Strip markdown for TTS const plain = text.replace(/[*_`#\[\]()]/g, ""); const preferredLanguage = this._getSpeechSynthesisLanguage(); @@ -1357,6 +1365,25 @@ class OpenClawChatCard extends HTMLElement { this._hass?.locale?.language || this._hass?.selectedLanguage || this._hass?.language || navigator.language || "en-US" ); + if (!hasBrowserTts) { + this._voiceStatus = "Browser TTS unavailable, trying Home Assistant TTS…"; + this._render(); + this._speakViaHomeAssistantTts(plain, preferredLanguage).then((ok) => { + if (ok) { + this._voiceStatus = ""; + this._render(); + this._resumeVoiceInputAfterTts(); + return; + } + + const reason = this._lastHaTtsAttempt ? ` (${this._lastHaTtsAttempt})` : ""; + this._voiceStatus = `TTS unavailable${reason}`; + this._render(); + this._resumeVoiceInputAfterTts(); + }); + return; + } + const pickVoice = (targetLanguage, voices) => { const normalizedTarget = String(targetLanguage || "").toLowerCase(); const targetBase = normalizedTarget.split("-")[0]; @@ -1468,6 +1495,8 @@ class OpenClawChatCard extends HTMLElement { }; utterance.onend = () => { + this._voiceStatus = ""; + this._render(); this._resumeVoiceInputAfterTts(); }; @@ -1479,6 +1508,25 @@ class OpenClawChatCard extends HTMLElement { speechSynthesis.speak(utterance); }; + if (this._isHomeAssistantAndroidApp()) { + this._voiceStatus = "Speaking with Home Assistant TTS…"; + this._render(); + this._speakViaHomeAssistantTts(plain, preferredLanguage).then((ok) => { + if (ok) { + this._voiceStatus = ""; + this._render(); + this._resumeVoiceInputAfterTts(); + return; + } + + const reason = this._lastHaTtsAttempt ? ` (${this._lastHaTtsAttempt})` : ""; + this._voiceStatus = `Home Assistant TTS unavailable${reason}; trying browser TTS…`; + this._render(); + speakNow(preferredLanguage); + }); + return; + } + const loadedVoices = speechSynthesis.getVoices() || []; if (loadedVoices.length) { speakNow(preferredLanguage); diff --git a/www/openclaw-chat-card.js b/www/openclaw-chat-card.js index 6eda381..ecb3cc7 100644 --- a/www/openclaw-chat-card.js +++ b/www/openclaw-chat-card.js @@ -1,7 +1,7 @@ (async () => { try { if (!customElements.get("openclaw-chat-card")) { - const src = "/openclaw/openclaw-chat-card.js?v=0.1.49"; + const src = "/openclaw/openclaw-chat-card.js?v=0.1.50"; console.info("OpenClaw loader importing", src); await import(src); }