diff --git a/CHANGELOG.md b/CHANGELOG.md index fac2d81..9c2a126 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to the OpenClaw Home Assistant Integration will be documented in this file. +## [0.1.22] - 2026-02-20 + +### Fixed +- Improved speech-recognition language handling by normalizing language tags (e.g. `bg` → `bg-BG`). +- Added automatic fallback retry with browser locale on `SpeechRecognition` `network` errors. +- Updated versioned card resource URL to force clients to load the latest voice handling logic. + ## [0.1.21] - 2026-02-20 ### Fixed diff --git a/custom_components/openclaw/__init__.py b/custom_components/openclaw/__init__.py index 40f9024..d9afead 100644 --- a/custom_components/openclaw/__init__.py +++ b/custom_components/openclaw/__init__.py @@ -74,7 +74,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.20" +_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.22" type OpenClawConfigEntry = ConfigEntry diff --git a/custom_components/openclaw/manifest.json b/custom_components/openclaw/manifest.json index 477ebe0..382421d 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.21", + "version": "0.1.22", "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 193965e..a654351 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.2.1"; +const CARD_VERSION = "0.2.2"; // Max time (ms) to show the thinking indicator before falling back to an error const THINKING_TIMEOUT_MS = 120_000; @@ -66,6 +66,7 @@ class OpenClawChatCard extends HTMLElement { this._voiceStatus = ""; this._voiceRetryTimer = null; this._voiceRetryCount = 0; + this._speechLangOverride = null; } // ── HA card interface ─────────────────────────────────────────────── @@ -395,6 +396,52 @@ class OpenClawChatCard extends HTMLElement { // ── Voice ─────────────────────────────────────────────────────────── + _normalizeSpeechLanguage(lang) { + if (!lang) return "en-US"; + + const cleaned = String(lang).trim().replace(/_/g, "-").toLowerCase(); + if (!cleaned) return "en-US"; + + if (cleaned.includes("-")) { + const [base, region] = cleaned.split("-", 2); + if (base && region) { + return `${base}-${region.toUpperCase()}`; + } + } + + const languageMap = { + bg: "bg-BG", + en: "en-US", + de: "de-DE", + fr: "fr-FR", + es: "es-ES", + it: "it-IT", + pt: "pt-PT", + ru: "ru-RU", + nl: "nl-NL", + pl: "pl-PL", + tr: "tr-TR", + uk: "uk-UA", + cs: "cs-CZ", + ro: "ro-RO", + el: "el-GR", + }; + + return languageMap[cleaned] || `${cleaned}-${cleaned.toUpperCase()}`; + } + + _getSpeechRecognitionLanguage() { + if (this._speechLangOverride) { + return this._speechLangOverride; + } + + const configuredLang = this._config.voice_language; + const hassLang = this._hass?.selectedLanguage || this._hass?.language; + const browserLang = navigator.language; + const preferred = configuredLang || hassLang || browserLang || "en-US"; + return this._normalizeSpeechLanguage(preferred); + } + _startVoiceRecognition() { if (!("webkitSpeechRecognition" in window) && !("SpeechRecognition" in window)) { console.warn("OpenClaw: Speech recognition not supported in this browser"); @@ -407,10 +454,10 @@ class OpenClawChatCard extends HTMLElement { this._recognition = new SpeechRecognition(); this._recognition.continuous = this._isVoiceMode; this._recognition.interimResults = true; - this._recognition.lang = this._hass?.language || "en-US"; + this._recognition.lang = this._getSpeechRecognitionLanguage(); this._voiceStatus = this._isVoiceMode - ? `Listening (wake word: ${this._wakeWord || "hey openclaw"})` - : "Listening…"; + ? `Listening (${this._recognition.lang}, wake word: ${this._wakeWord || "hey openclaw"})` + : `Listening (${this._recognition.lang})…`; this._recognition.onresult = (event) => { const result = event.results[event.results.length - 1]; @@ -454,7 +501,12 @@ class OpenClawChatCard extends HTMLElement { const err = event?.error || "unknown"; if (err === "network") { this._voiceStatus = - "Voice network error: browser speech service unavailable. Check internet and retry."; + "Voice network error: browser speech service unavailable. Retrying with fallback locale…"; + + const browserLocale = this._normalizeSpeechLanguage(navigator.language || "en-US"); + if (!this._speechLangOverride && browserLocale !== this._recognition.lang) { + this._speechLangOverride = browserLocale; + } } else if (err === "not-allowed") { this._voiceStatus = "Microphone access denied. Allow mic permission for this site."; } else if (err === "audio-capture") { @@ -463,7 +515,7 @@ class OpenClawChatCard extends HTMLElement { this._voiceStatus = `Voice error: ${err}`; } - if (this._isVoiceMode && ["network", "audio-capture", "no-speech"].includes(err)) { + if (["network", "audio-capture", "no-speech"].includes(err)) { this._scheduleVoiceRetry(); } this._render(); @@ -502,7 +554,7 @@ class OpenClawChatCard extends HTMLElement { } _scheduleVoiceRetry() { - if (!this._isVoiceMode) return; + if (!this._isVoiceMode && !this._speechLangOverride) return; if (this._voiceRetryCount >= 6) { this._voiceStatus = "Voice retry stopped after repeated errors. Toggle voice mode to try again."; diff --git a/www/openclaw-chat-card.js b/www/openclaw-chat-card.js index ec37ca5..443be7c 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.20"; + const src = "/openclaw/openclaw-chat-card.js?v=0.1.22"; console.info("OpenClaw loader importing", src); await import(src); }