diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cdd6c4..4256fca 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.41] - 2026-02-20 + +### Fixed +- Fixed voice-mode option sync so wake-word enabled/disabled changes are reloaded before toggling voice mode. +- Removed legacy always-on voice mode behavior that could force sticky voice-mode state. +- Added live gateway connection badge to the chat card header using existing OpenClaw status entities. + +### Changed +- Removed "Always-on voice mode" option from integration options UI and translations. + ## [0.1.40] - 2026-02-20 ### Added diff --git a/README.md b/README.md index df9779f..90eb7fe 100644 --- a/README.md +++ b/README.md @@ -28,15 +28,17 @@ OpenClaw is a Home Assistant custom integration that connects your HA instance t - `openclaw_message_received` - `openclaw_tool_invoked` - **Sensors / status entities** for model and connection state + - Includes tool telemetry sensors (`Last Tool`, `Last Tool Status`, `Last Tool Duration`, `Last Tool Invoked`) --- ## Requirements -- Home Assistant with Supervisor support for addon discovery +- Home Assistant Core `2025.1.0+` (declared minimum) +- Supervisor is optional (used for auto-discovery) - OpenClaw Assistant addon installed and running -The integration auto-detects the addon. You do not need to manually set an API base URL. +The integration can auto-detect the addon when Supervisor is available. You can always configure host/port/token manually. --- @@ -73,6 +75,8 @@ The integration auto-detects the addon. You do not need to manually set an API b The card is registered automatically by the integration. +The card header shows live gateway state (`Online` / `Offline`) using existing OpenClaw status entities. + ```yaml type: custom:openclaw-chat-card title: OpenClaw Chat @@ -125,7 +129,6 @@ When enabled, OpenClaw tool-call responses can execute Home Assistant services. - **Wake word enabled** - **Wake word** (default: `hey openclaw`) -- **Always voice mode** (continuous listening while card is open) - **Voice input provider** (`browser` or `assist_stt`) ### Voice provider usage @@ -138,7 +141,7 @@ When enabled, OpenClaw tool-call responses can execute Home Assistant services. - **`assist_stt`** - Uses Home Assistant STT provider via `/api/stt/`. - Intended for manual mic input (press mic, speak, auto-stop, transcribe, send). - - Continuous voice mode is not used in this provider. + - If continuous Voice Mode is enabled while this provider is selected, the card uses browser speech for continuous listening. For `assist_stt`, make sure an STT engine is configured in **Settings → Voice assistants**. @@ -291,6 +294,11 @@ action: - For `browser`: open browser console for `OpenClaw: Speech recognition error`; repeated `network` usually means browser speech backend failure - For `assist_stt`: check network calls to `/api/stt/` and verify Home Assistant Voice/STT provider is configured +### Tool sensors show `Unknown` + +- `Last Tool*` sensors stay `Unknown` until at least one `openclaw.invoke_tool` service call has executed. +- `Session Count` remains `0` if gateway policy blocks `sessions_list` for `/tools/invoke`. + ### Responses do not appear after sending - Verify `openclaw_message_received` is being fired in Developer Tools → Events diff --git a/custom_components/openclaw/__init__.py b/custom_components/openclaw/__init__.py index 27169de..bddc0d9 100644 --- a/custom_components/openclaw/__init__.py +++ b/custom_components/openclaw/__init__.py @@ -57,7 +57,6 @@ from .const import ( CONF_INCLUDE_EXPOSED_CONTEXT, CONF_WAKE_WORD, CONF_WAKE_WORD_ENABLED, - CONF_ALWAYS_VOICE_MODE, CONF_ALLOW_BRAVE_WEBSPEECH, CONF_VOICE_PROVIDER, CONTEXT_STRATEGY_TRUNCATE, @@ -67,7 +66,6 @@ from .const import ( DEFAULT_INCLUDE_EXPOSED_CONTEXT, DEFAULT_WAKE_WORD, DEFAULT_WAKE_WORD_ENABLED, - DEFAULT_ALWAYS_VOICE_MODE, DEFAULT_ALLOW_BRAVE_WEBSPEECH, DEFAULT_VOICE_PROVIDER, DOMAIN, @@ -92,7 +90,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.40" +_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.41" OpenClawConfigEntry = ConfigEntry @@ -802,10 +800,6 @@ def _async_register_websocket_api(hass: HomeAssistant) -> None: DEFAULT_WAKE_WORD_ENABLED, ), CONF_WAKE_WORD: options.get(CONF_WAKE_WORD, DEFAULT_WAKE_WORD), - CONF_ALWAYS_VOICE_MODE: options.get( - CONF_ALWAYS_VOICE_MODE, - DEFAULT_ALWAYS_VOICE_MODE, - ), CONF_ALLOW_BRAVE_WEBSPEECH: options.get( CONF_ALLOW_BRAVE_WEBSPEECH, DEFAULT_ALLOW_BRAVE_WEBSPEECH, diff --git a/custom_components/openclaw/__pycache__/__init__.cpython-312.pyc b/custom_components/openclaw/__pycache__/__init__.cpython-312.pyc index 217e612..4a5a231 100644 Binary files a/custom_components/openclaw/__pycache__/__init__.cpython-312.pyc and b/custom_components/openclaw/__pycache__/__init__.cpython-312.pyc differ diff --git a/custom_components/openclaw/__pycache__/config_flow.cpython-312.pyc b/custom_components/openclaw/__pycache__/config_flow.cpython-312.pyc index f3f35bb..c5b61b7 100644 Binary files a/custom_components/openclaw/__pycache__/config_flow.cpython-312.pyc and b/custom_components/openclaw/__pycache__/config_flow.cpython-312.pyc differ diff --git a/custom_components/openclaw/__pycache__/const.cpython-312.pyc b/custom_components/openclaw/__pycache__/const.cpython-312.pyc index 7cfc707..231a41e 100644 Binary files a/custom_components/openclaw/__pycache__/const.cpython-312.pyc and b/custom_components/openclaw/__pycache__/const.cpython-312.pyc differ diff --git a/custom_components/openclaw/config_flow.py b/custom_components/openclaw/config_flow.py index 6062c30..0c17b31 100644 --- a/custom_components/openclaw/config_flow.py +++ b/custom_components/openclaw/config_flow.py @@ -44,7 +44,6 @@ from .const import ( CONF_INCLUDE_EXPOSED_CONTEXT, CONF_WAKE_WORD, CONF_WAKE_WORD_ENABLED, - CONF_ALWAYS_VOICE_MODE, CONF_ALLOW_BRAVE_WEBSPEECH, CONF_VOICE_PROVIDER, CONTEXT_STRATEGY_CLEAR, @@ -57,7 +56,6 @@ from .const import ( DEFAULT_INCLUDE_EXPOSED_CONTEXT, DEFAULT_WAKE_WORD, DEFAULT_WAKE_WORD_ENABLED, - DEFAULT_ALWAYS_VOICE_MODE, DEFAULT_ALLOW_BRAVE_WEBSPEECH, DEFAULT_VOICE_PROVIDER, DOMAIN, @@ -462,13 +460,6 @@ class OpenClawOptionsFlow(OptionsFlow): DEFAULT_WAKE_WORD, ), ): str, - vol.Optional( - CONF_ALWAYS_VOICE_MODE, - default=options.get( - CONF_ALWAYS_VOICE_MODE, - DEFAULT_ALWAYS_VOICE_MODE, - ), - ): bool, vol.Optional( CONF_ALLOW_BRAVE_WEBSPEECH, default=options.get( diff --git a/custom_components/openclaw/const.py b/custom_components/openclaw/const.py index f71d5a5..b92b1b1 100644 --- a/custom_components/openclaw/const.py +++ b/custom_components/openclaw/const.py @@ -30,7 +30,6 @@ CONF_CONTEXT_STRATEGY = "context_strategy" CONF_ENABLE_TOOL_CALLS = "enable_tool_calls" CONF_WAKE_WORD_ENABLED = "wake_word_enabled" CONF_WAKE_WORD = "wake_word" -CONF_ALWAYS_VOICE_MODE = "always_voice_mode" CONF_ALLOW_BRAVE_WEBSPEECH = "allow_brave_webspeech" CONF_VOICE_PROVIDER = "voice_provider" @@ -40,7 +39,6 @@ DEFAULT_CONTEXT_STRATEGY = "truncate" DEFAULT_ENABLE_TOOL_CALLS = False DEFAULT_WAKE_WORD_ENABLED = False DEFAULT_WAKE_WORD = "hey openclaw" -DEFAULT_ALWAYS_VOICE_MODE = False DEFAULT_ALLOW_BRAVE_WEBSPEECH = False DEFAULT_VOICE_PROVIDER = "browser" diff --git a/custom_components/openclaw/manifest.json b/custom_components/openclaw/manifest.json index 2257a49..df68a06 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.40", + "version": "0.1.41", "dependencies": ["conversation"], "after_dependencies": ["hassio", "lovelace"] } diff --git a/custom_components/openclaw/strings.json b/custom_components/openclaw/strings.json index acc7940..dab5a33 100644 --- a/custom_components/openclaw/strings.json +++ b/custom_components/openclaw/strings.json @@ -43,7 +43,6 @@ "enable_tool_calls": "Enable tool calls (execute services)", "wake_word_enabled": "Require wake word", "wake_word": "Wake word", - "always_voice_mode": "Always-on voice mode", "allow_brave_webspeech": "Allow Web Speech in Brave (experimental)", "voice_provider": "Voice input provider (browser or HA STT)" } diff --git a/custom_components/openclaw/translations/en.json b/custom_components/openclaw/translations/en.json index acc7940..dab5a33 100644 --- a/custom_components/openclaw/translations/en.json +++ b/custom_components/openclaw/translations/en.json @@ -43,7 +43,6 @@ "enable_tool_calls": "Enable tool calls (execute services)", "wake_word_enabled": "Require wake word", "wake_word": "Wake word", - "always_voice_mode": "Always-on voice mode", "allow_brave_webspeech": "Allow Web Speech in Brave (experimental)", "voice_provider": "Voice input provider (browser or HA STT)" } diff --git a/custom_components/openclaw/www/openclaw-chat-card.js b/custom_components/openclaw/www/openclaw-chat-card.js index 0d76cab..8c26ce1 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.4"; +const CARD_VERSION = "0.3.5"; // Max time (ms) to show the thinking indicator before falling back to an error const THINKING_TIMEOUT_MS = 120_000; @@ -62,7 +62,6 @@ class OpenClawChatCard extends HTMLElement { this._historySyncRetryTimer = null; this._wakeWordEnabled = false; this._wakeWord = "hey openclaw"; - this._alwaysVoiceMode = false; this._voiceStatus = ""; this._voiceRetryTimer = null; this._voiceRetryCount = 0; @@ -123,8 +122,24 @@ class OpenClawChatCard extends HTMLElement { this._subscribeToEvents(); this._syncHistoryFromBackend(); this._loadIntegrationSettings(); - this._render(); } + this._render(); + } + + _getGatewayConnectionState() { + const statusEntity = this._hass?.states?.["sensor.openclaw_status"]; + const binaryEntity = this._hass?.states?.["binary_sensor.openclaw_connected"]; + const status = String(statusEntity?.state || "").toLowerCase(); + const connected = String(binaryEntity?.state || "").toLowerCase() === "on"; + + if (connected || status === "online") { + return { label: "Online", css: "online" }; + } + if (status === "offline" || String(binaryEntity?.state || "").toLowerCase() === "off") { + return { label: "Offline", css: "offline" }; + } + + return { label: "Unknown", css: "unknown" }; } getCardSize() { @@ -321,7 +336,7 @@ class OpenClawChatCard extends HTMLElement { } } - async _loadIntegrationSettings() { + async _loadIntegrationSettings(includePipeline = true) { if (!this._hass) return; try { @@ -336,7 +351,6 @@ class OpenClawChatCard extends HTMLElement { this._wakeWordEnabled = !!result?.wake_word_enabled; this._wakeWord = (result?.wake_word || "hey openclaw").toString().trim().toLowerCase(); - this._alwaysVoiceMode = !!result?.always_voice_mode; this._allowBraveWebSpeechIntegration = !!result?.allow_brave_webspeech; this._voiceProviderIntegration = result?.voice_provider === "assist_stt" ? "assist_stt" : "browser"; @@ -344,7 +358,8 @@ class OpenClawChatCard extends HTMLElement { ? this._normalizeSpeechLanguage(result.language) : null; - try { + if (includePipeline) { + try { let pipelineResult; if (typeof this._hass.callWS === "function") { pipelineResult = await this._hass.callWS({ type: "assist_pipeline/pipeline/list" }); @@ -372,13 +387,9 @@ class OpenClawChatCard extends HTMLElement { if (ttsLanguage) { this._integrationTtsLanguage = this._normalizeSpeechLanguage(ttsLanguage); } - } catch (pipelineErr) { - console.debug("OpenClaw: assist pipeline language detection skipped:", pipelineErr); - } - - if (this._alwaysVoiceMode && !this._isVoiceMode) { - this._isVoiceMode = true; - this._startVoiceRecognition(); + } catch (pipelineErr) { + console.debug("OpenClaw: assist pipeline language detection skipped:", pipelineErr); + } } this._render(); } catch (err) { @@ -1162,7 +1173,8 @@ class OpenClawChatCard extends HTMLElement { }, delayMs); } - _toggleVoiceMode() { + async _toggleVoiceMode() { + await this._loadIntegrationSettings(false); this._isVoiceMode = !this._isVoiceMode; if (this._isVoiceMode) { this._startVoiceRecognition(); @@ -1303,6 +1315,7 @@ class OpenClawChatCard extends HTMLElement { .join(""); const voiceActive = this._recognition !== null; + const gatewayState = this._getGatewayConnectionState(); this.shadowRoot.innerHTML = `