Update changelog for version 0.1.18, add voice status feedback for recognition states, improve wake word handling, and enhance error reporting for unsupported browsers.
This commit is contained in:
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
All notable changes to the OpenClaw Home Assistant Integration will be documented in this file.
|
All notable changes to the OpenClaw Home Assistant Integration will be documented in this file.
|
||||||
|
|
||||||
|
## [0.1.18] - 2026-02-20
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Voice input now requires wake word only for continuous voice mode, not for manual mic usage.
|
||||||
|
- Added in-card voice status feedback (listening, wake-word wait, sending, error) to make microphone behavior visible.
|
||||||
|
- Improved handling for unsupported speech-recognition browsers with explicit UI status.
|
||||||
|
|
||||||
## [0.1.17] - 2026-02-20
|
## [0.1.17] - 2026-02-20
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"issue_tracker": "https://github.com/techartdev/OpenClawHomeAssistant/issues",
|
"issue_tracker": "https://github.com/techartdev/OpenClawHomeAssistant/issues",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"version": "0.1.17",
|
"version": "0.1.18",
|
||||||
"dependencies": ["conversation"],
|
"dependencies": ["conversation"],
|
||||||
"after_dependencies": ["hassio", "lovelace"]
|
"after_dependencies": ["hassio", "lovelace"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
this._wakeWordEnabled = false;
|
this._wakeWordEnabled = false;
|
||||||
this._wakeWord = "hey openclaw";
|
this._wakeWord = "hey openclaw";
|
||||||
this._alwaysVoiceMode = false;
|
this._alwaysVoiceMode = false;
|
||||||
|
this._voiceStatus = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── HA card interface ───────────────────────────────────────────────
|
// ── HA card interface ───────────────────────────────────────────────
|
||||||
@@ -391,6 +392,8 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
_startVoiceRecognition() {
|
_startVoiceRecognition() {
|
||||||
if (!("webkitSpeechRecognition" in window) && !("SpeechRecognition" in window)) {
|
if (!("webkitSpeechRecognition" in window) && !("SpeechRecognition" in window)) {
|
||||||
console.warn("OpenClaw: Speech recognition not supported in this browser");
|
console.warn("OpenClaw: Speech recognition not supported in this browser");
|
||||||
|
this._voiceStatus = "Speech recognition not supported by this browser.";
|
||||||
|
this._render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,6 +402,9 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
this._recognition.continuous = this._isVoiceMode;
|
this._recognition.continuous = this._isVoiceMode;
|
||||||
this._recognition.interimResults = true;
|
this._recognition.interimResults = true;
|
||||||
this._recognition.lang = this._hass?.language || "en-US";
|
this._recognition.lang = this._hass?.language || "en-US";
|
||||||
|
this._voiceStatus = this._isVoiceMode
|
||||||
|
? `Listening (wake word: ${this._wakeWord || "hey openclaw"})`
|
||||||
|
: "Listening…";
|
||||||
|
|
||||||
this._recognition.onresult = (event) => {
|
this._recognition.onresult = (event) => {
|
||||||
const result = event.results[event.results.length - 1];
|
const result = event.results[event.results.length - 1];
|
||||||
@@ -406,29 +412,40 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
const text = result[0].transcript?.trim();
|
const text = result[0].transcript?.trim();
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
||||||
if (this._wakeWordEnabled) {
|
const requireWakeWord = this._wakeWordEnabled && this._isVoiceMode;
|
||||||
|
|
||||||
|
if (requireWakeWord) {
|
||||||
const wake = this._wakeWord || "hey openclaw";
|
const wake = this._wakeWord || "hey openclaw";
|
||||||
const lower = text.toLowerCase();
|
const lower = text.toLowerCase();
|
||||||
const wakePos = lower.indexOf(wake);
|
const wakePos = lower.indexOf(wake);
|
||||||
if (wakePos < 0) {
|
if (wakePos < 0) {
|
||||||
|
this._voiceStatus = `Heard: \"${text}\" (waiting for wake word)`;
|
||||||
|
this._render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let command = text.slice(wakePos + wake.length).trim();
|
let command = text.slice(wakePos + wake.length).trim();
|
||||||
command = command.replace(/^[,:;.!?\-]+\s*/, "");
|
command = command.replace(/^[,:;.!?\-]+\s*/, "");
|
||||||
if (!command) {
|
if (!command) {
|
||||||
|
this._voiceStatus = "Wake word detected. Say command after wake word.";
|
||||||
|
this._render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this._voiceStatus = "Sending…";
|
||||||
|
this._render();
|
||||||
this._sendMessage(command);
|
this._sendMessage(command);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._voiceStatus = "Sending…";
|
||||||
|
this._render();
|
||||||
this._sendMessage(text);
|
this._sendMessage(text);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this._recognition.onerror = (event) => {
|
this._recognition.onerror = (event) => {
|
||||||
console.error("OpenClaw: Speech recognition error:", event.error);
|
console.error("OpenClaw: Speech recognition error:", event.error);
|
||||||
|
this._voiceStatus = `Voice error: ${event.error}`;
|
||||||
this._render();
|
this._render();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -441,6 +458,7 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
// Ignore — may already be started
|
// Ignore — may already be started
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
this._voiceStatus = "";
|
||||||
this._render();
|
this._render();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -455,6 +473,7 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
this._recognition = null;
|
this._recognition = null;
|
||||||
}
|
}
|
||||||
this._isVoiceMode = false;
|
this._isVoiceMode = false;
|
||||||
|
this._voiceStatus = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
_toggleVoiceMode() {
|
_toggleVoiceMode() {
|
||||||
@@ -740,6 +759,11 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
animation: pulse 1.5s infinite;
|
animation: pulse 1.5s infinite;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
.voice-status {
|
||||||
|
padding: 4px 16px 0 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--oc-text-secondary);
|
||||||
|
}
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% { opacity: 1; }
|
0%, 100% { opacity: 1; }
|
||||||
50% { opacity: 0.3; }
|
50% { opacity: 0.3; }
|
||||||
@@ -777,6 +801,8 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
${this._voiceStatus ? `<div class="voice-status">${this._escapeHtml(this._voiceStatus)}</div>` : ""}
|
||||||
|
|
||||||
<div class="input-area">
|
<div class="input-area">
|
||||||
<textarea
|
<textarea
|
||||||
id="input"
|
id="input"
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
this._wakeWordEnabled = false;
|
this._wakeWordEnabled = false;
|
||||||
this._wakeWord = "hey openclaw";
|
this._wakeWord = "hey openclaw";
|
||||||
this._alwaysVoiceMode = false;
|
this._alwaysVoiceMode = false;
|
||||||
|
this._voiceStatus = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── HA card interface ───────────────────────────────────────────────
|
// ── HA card interface ───────────────────────────────────────────────
|
||||||
@@ -391,6 +392,8 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
_startVoiceRecognition() {
|
_startVoiceRecognition() {
|
||||||
if (!("webkitSpeechRecognition" in window) && !("SpeechRecognition" in window)) {
|
if (!("webkitSpeechRecognition" in window) && !("SpeechRecognition" in window)) {
|
||||||
console.warn("OpenClaw: Speech recognition not supported in this browser");
|
console.warn("OpenClaw: Speech recognition not supported in this browser");
|
||||||
|
this._voiceStatus = "Speech recognition not supported by this browser.";
|
||||||
|
this._render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,6 +402,9 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
this._recognition.continuous = this._isVoiceMode;
|
this._recognition.continuous = this._isVoiceMode;
|
||||||
this._recognition.interimResults = true;
|
this._recognition.interimResults = true;
|
||||||
this._recognition.lang = this._hass?.language || "en-US";
|
this._recognition.lang = this._hass?.language || "en-US";
|
||||||
|
this._voiceStatus = this._isVoiceMode
|
||||||
|
? `Listening (wake word: ${this._wakeWord || "hey openclaw"})`
|
||||||
|
: "Listening…";
|
||||||
|
|
||||||
this._recognition.onresult = (event) => {
|
this._recognition.onresult = (event) => {
|
||||||
const result = event.results[event.results.length - 1];
|
const result = event.results[event.results.length - 1];
|
||||||
@@ -406,29 +412,40 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
const text = result[0].transcript?.trim();
|
const text = result[0].transcript?.trim();
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
||||||
if (this._wakeWordEnabled) {
|
const requireWakeWord = this._wakeWordEnabled && this._isVoiceMode;
|
||||||
|
|
||||||
|
if (requireWakeWord) {
|
||||||
const wake = this._wakeWord || "hey openclaw";
|
const wake = this._wakeWord || "hey openclaw";
|
||||||
const lower = text.toLowerCase();
|
const lower = text.toLowerCase();
|
||||||
const wakePos = lower.indexOf(wake);
|
const wakePos = lower.indexOf(wake);
|
||||||
if (wakePos < 0) {
|
if (wakePos < 0) {
|
||||||
|
this._voiceStatus = `Heard: \"${text}\" (waiting for wake word)`;
|
||||||
|
this._render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let command = text.slice(wakePos + wake.length).trim();
|
let command = text.slice(wakePos + wake.length).trim();
|
||||||
command = command.replace(/^[,:;.!?\-]+\s*/, "");
|
command = command.replace(/^[,:;.!?\-]+\s*/, "");
|
||||||
if (!command) {
|
if (!command) {
|
||||||
|
this._voiceStatus = "Wake word detected. Say command after wake word.";
|
||||||
|
this._render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this._voiceStatus = "Sending…";
|
||||||
|
this._render();
|
||||||
this._sendMessage(command);
|
this._sendMessage(command);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._voiceStatus = "Sending…";
|
||||||
|
this._render();
|
||||||
this._sendMessage(text);
|
this._sendMessage(text);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this._recognition.onerror = (event) => {
|
this._recognition.onerror = (event) => {
|
||||||
console.error("OpenClaw: Speech recognition error:", event.error);
|
console.error("OpenClaw: Speech recognition error:", event.error);
|
||||||
|
this._voiceStatus = `Voice error: ${event.error}`;
|
||||||
this._render();
|
this._render();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -441,6 +458,7 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
// Ignore — may already be started
|
// Ignore — may already be started
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
this._voiceStatus = "";
|
||||||
this._render();
|
this._render();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -455,6 +473,7 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
this._recognition = null;
|
this._recognition = null;
|
||||||
}
|
}
|
||||||
this._isVoiceMode = false;
|
this._isVoiceMode = false;
|
||||||
|
this._voiceStatus = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
_toggleVoiceMode() {
|
_toggleVoiceMode() {
|
||||||
@@ -740,6 +759,11 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
animation: pulse 1.5s infinite;
|
animation: pulse 1.5s infinite;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
.voice-status {
|
||||||
|
padding: 4px 16px 0 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--oc-text-secondary);
|
||||||
|
}
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% { opacity: 1; }
|
0%, 100% { opacity: 1; }
|
||||||
50% { opacity: 0.3; }
|
50% { opacity: 0.3; }
|
||||||
@@ -777,6 +801,8 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
${this._voiceStatus ? `<div class="voice-status">${this._escapeHtml(this._voiceStatus)}</div>` : ""}
|
||||||
|
|
||||||
<div class="input-area">
|
<div class="input-area">
|
||||||
<textarea
|
<textarea
|
||||||
id="input"
|
id="input"
|
||||||
|
|||||||
Reference in New Issue
Block a user