Update version to 0.1.48 and improve chat-card gateway status detection

This commit is contained in:
techartdev
2026-02-21 01:47:43 +02:00
parent a5a1ed87c5
commit 2fe88a7170
5 changed files with 129 additions and 54 deletions
+5
View File
@@ -2,6 +2,11 @@
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.48] - 2026-02-21
### Fixed
- Improved chat-card gateway header status detection to handle suffixed entity IDs (for example `_2`) and common connected/offline state variants.
## [0.1.47] - 2026-02-21 ## [0.1.47] - 2026-02-21
### Fixed ### Fixed
+1 -1
View File
@@ -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) # URL at which the card JS is served (registered via register_static_path)
_CARD_STATIC_URL = f"/openclaw/{_CARD_FILENAME}" _CARD_STATIC_URL = f"/openclaw/{_CARD_FILENAME}"
# Versioned URL used for Lovelace resource registration to avoid stale browser cache # Versioned URL used for Lovelace resource registration to avoid stale browser cache
_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.47" _CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.48"
OpenClawConfigEntry = ConfigEntry OpenClawConfigEntry = ConfigEntry
+1 -1
View File
@@ -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.47", "version": "0.1.48",
"dependencies": ["conversation"], "dependencies": ["conversation"],
"after_dependencies": ["hassio", "lovelace"] "after_dependencies": ["hassio", "lovelace"]
} }
@@ -13,7 +13,7 @@
* + subscribes to openclaw_message_received events. * + subscribes to openclaw_message_received events.
*/ */
const CARD_VERSION = "0.3.6"; const CARD_VERSION = "0.3.7";
// Max time (ms) to show the thinking indicator before falling back to an error // Max time (ms) to show the thinking indicator before falling back to an error
const THINKING_TIMEOUT_MS = 120_000; const THINKING_TIMEOUT_MS = 120_000;
@@ -75,6 +75,8 @@ class OpenClawChatCard extends HTMLElement {
this._voiceProviderIntegration = "browser"; this._voiceProviderIntegration = "browser";
this._preferredAssistSttEngine = null; this._preferredAssistSttEngine = null;
this._preferredAssistTtsEngine = null; this._preferredAssistTtsEngine = null;
this._assistTtsEngines = [];
this._lastHaTtsAttempt = null;
this._voiceBackendBlocked = false; this._voiceBackendBlocked = false;
this._voiceStopRequested = false; this._voiceStopRequested = false;
this._assistRecordingActive = false; this._assistRecordingActive = false;
@@ -129,15 +131,34 @@ class OpenClawChatCard extends HTMLElement {
} }
_getGatewayConnectionState() { _getGatewayConnectionState() {
const statusEntity = this._hass?.states?.["sensor.openclaw_status"]; const states = this._hass?.states || {};
const binaryEntity = this._hass?.states?.["binary_sensor.openclaw_connected"]; const stateEntries = Object.entries(states);
const status = String(statusEntity?.state || "").toLowerCase();
const connected = String(binaryEntity?.state || "").toLowerCase() === "on";
if (connected || status === "online") { const exactStatus = states["sensor.openclaw_status"];
const exactConnected = states["binary_sensor.openclaw_connected"];
const statusEntity =
exactStatus ||
stateEntries.find(([entityId]) => entityId.startsWith("sensor.openclaw_status"))?.[1] ||
null;
const binaryEntity =
exactConnected ||
stateEntries.find(([entityId]) => entityId.startsWith("binary_sensor.openclaw_connected"))?.[1] ||
null;
const status = String(statusEntity?.state || "").toLowerCase();
const connectedState = String(binaryEntity?.state || "").toLowerCase();
const connected = ["on", "connected", "online", "true", "1"].includes(connectedState);
const disconnected = ["off", "disconnected", "offline", "false", "0"].includes(
connectedState
);
if (connected || ["online", "connected"].includes(status)) {
return { label: "Online", css: "online" }; return { label: "Online", css: "online" };
} }
if (status === "offline" || String(binaryEntity?.state || "").toLowerCase() === "off") { if (disconnected || ["offline", "disconnected"].includes(status)) {
return { label: "Offline", css: "offline" }; return { label: "Offline", css: "offline" };
} }
@@ -379,10 +400,15 @@ class OpenClawChatCard extends HTMLElement {
const pipelines = Array.isArray(pipelineResult?.pipelines) const pipelines = Array.isArray(pipelineResult?.pipelines)
? pipelineResult.pipelines ? pipelineResult.pipelines
: []; : [];
const discoveredTtsEngines = pipelines
.map((pipeline) => pipeline?.tts_engine)
.filter((engine) => typeof engine === "string" && engine.trim().length > 0)
.map((engine) => engine.trim());
this._assistTtsEngines = [...new Set(discoveredTtsEngines)];
const preferredPipeline = const preferredPipeline =
pipelines.find((pipeline) => pipeline?.id === preferredPipelineId) || pipelines[0]; pipelines.find((pipeline) => pipeline?.id === preferredPipelineId) || pipelines[0];
this._preferredAssistSttEngine = preferredPipeline?.stt_engine || null; this._preferredAssistSttEngine = preferredPipeline?.stt_engine || null;
this._preferredAssistTtsEngine = preferredPipeline?.tts_engine || null; this._preferredAssistTtsEngine = preferredPipeline?.tts_engine || this._assistTtsEngines[0] || null;
const sttLanguage = preferredPipeline?.stt_language || preferredPipeline?.language; const sttLanguage = preferredPipeline?.stt_language || preferredPipeline?.language;
const ttsLanguage = const ttsLanguage =
@@ -1319,8 +1345,9 @@ class OpenClawChatCard extends HTMLElement {
return; return;
} }
const reason = this._lastHaTtsAttempt ? ` (${this._lastHaTtsAttempt})` : "";
this._voiceStatus = this._voiceStatus =
"Home Assistant TTS unavailable; using browser default voice as fallback."; `Home Assistant TTS unavailable${reason}; using browser default voice as fallback.`;
this._render(); this._render();
try { try {
speechSynthesis.cancel(); speechSynthesis.cancel();
@@ -1394,8 +1421,24 @@ class OpenClawChatCard extends HTMLElement {
async _speakViaHomeAssistantTts(text, language) { async _speakViaHomeAssistantTts(text, language) {
if (!this._hass) return false; if (!this._hass) return false;
const engine = this._preferredAssistTtsEngine; this._lastHaTtsAttempt = null;
if (!engine) return false;
const configuredEngine =
typeof this._config?.ha_tts_engine === "string" ? this._config.ha_tts_engine.trim() : "";
const engineCandidates = [];
for (const candidate of [configuredEngine, this._preferredAssistTtsEngine, ...this._assistTtsEngines]) {
if (typeof candidate !== "string") continue;
const normalized = candidate.trim();
if (!normalized) continue;
if (!engineCandidates.includes(normalized)) {
engineCandidates.push(normalized);
}
}
if (!engineCandidates.length) {
this._lastHaTtsAttempt = "no_tts_engine_configured_in_assist_pipeline";
return false;
}
const token = this._hass?.auth?.data?.access_token; const token = this._hass?.auth?.data?.access_token;
const headers = { const headers = {
@@ -1403,6 +1446,12 @@ class OpenClawChatCard extends HTMLElement {
...(token ? { Authorization: `Bearer ${token}` } : {}), ...(token ? { Authorization: `Bearer ${token}` } : {}),
}; };
let audioPath = null;
let selectedEngine = null;
let lastError = "unknown_error";
for (const engine of engineCandidates) {
console.info("OpenClaw: trying HA TTS engine", engine, "language", language);
const payloads = [ const payloads = [
{ {
platform: engine, platform: engine,
@@ -1424,7 +1473,6 @@ class OpenClawChatCard extends HTMLElement {
}, },
]; ];
let audioPath = null;
for (const payload of payloads) { for (const payload of payloads) {
try { try {
const response = await fetch("/api/tts_get_url", { const response = await fetch("/api/tts_get_url", {
@@ -1432,30 +1480,50 @@ class OpenClawChatCard extends HTMLElement {
headers, headers,
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
if (!response.ok) continue;
if (!response.ok) {
lastError = `engine=${engine}, tts_get_url_http=${response.status}`;
continue;
}
const body = await response.json(); const body = await response.json();
const candidate = body?.url || body?.path || body?.audio_url || body?.audioUrl; const candidate = body?.url || body?.path || body?.audio_url || body?.audioUrl;
if (!candidate) continue; if (!candidate) {
lastError = `engine=${engine}, no_audio_url_in_response`;
continue;
}
audioPath = String(candidate).startsWith("http") audioPath = String(candidate).startsWith("http")
? String(candidate) ? String(candidate)
: String(candidate).startsWith("/") : String(candidate).startsWith("/")
? String(candidate) ? String(candidate)
: `/${String(candidate)}`; : `/${String(candidate)}`;
selectedEngine = engine;
break; break;
} catch (err) { } catch (err) {
console.debug("OpenClaw: HA TTS URL request failed", err); console.debug("OpenClaw: HA TTS URL request failed", err);
lastError = `engine=${engine}, tts_get_url_request_failed`;
} }
} }
if (!audioPath) return false; if (audioPath) {
break;
}
}
if (!audioPath || !selectedEngine) {
this._lastHaTtsAttempt = lastError;
return false;
}
try { try {
const audioResponse = await fetch(audioPath, { const audioResponse = await fetch(audioPath, {
headers: token ? { Authorization: `Bearer ${token}` } : {}, headers: token ? { Authorization: `Bearer ${token}` } : {},
}); });
if (!audioResponse.ok) return false; if (!audioResponse.ok) {
this._lastHaTtsAttempt = `engine=${selectedEngine}, audio_fetch_http=${audioResponse.status}`;
return false;
}
const audioBlob = await audioResponse.blob(); const audioBlob = await audioResponse.blob();
const objectUrl = URL.createObjectURL(audioBlob); const objectUrl = URL.createObjectURL(audioBlob);
@@ -1481,9 +1549,11 @@ class OpenClawChatCard extends HTMLElement {
} }
}); });
this._lastHaTtsAttempt = `engine=${selectedEngine}, ok`;
return true; return true;
} catch (err) { } catch (err) {
console.debug("OpenClaw: HA TTS playback failed", err); console.debug("OpenClaw: HA TTS playback failed", err);
this._lastHaTtsAttempt = `engine=${selectedEngine}, audio_playback_failed`;
return false; return false;
} }
} }
+1 -1
View File
@@ -1,7 +1,7 @@
(async () => { (async () => {
try { try {
if (!customElements.get("openclaw-chat-card")) { if (!customElements.get("openclaw-chat-card")) {
const src = "/openclaw/openclaw-chat-card.js?v=0.1.47"; const src = "/openclaw/openclaw-chat-card.js?v=0.1.48";
console.info("OpenClaw loader importing", src); console.info("OpenClaw loader importing", src);
await import(src); await import(src);
} }