Add voice agent and session ID options, update changelog to version 0.1.61
- Introduced `voice_agent_id` and `assist_session_id` options for enhanced integration. - Updated request handling to support new headers for voice interactions. - Preserved existing behavior when new options are left blank. - Updated changelog for version 0.1.61 to reflect these changes.
This commit is contained in:
@@ -2,6 +2,22 @@
|
|||||||
|
|
||||||
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.61] - 2026-03-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Added an optional dedicated `voice_agent_id` integration option so Assist and microphone-originated chat requests can be routed to a separate OpenClaw agent without changing the default text/chat agent.
|
||||||
|
- Added an optional `assist_session_id` integration option so the native Home Assistant conversation agent can reuse a fixed OpenClaw session when desired.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Leaving either of the new options blank preserves the existing behavior: voice requests still use the configured default agent, and Assist still uses the Home Assistant conversation or fallback session ID.
|
||||||
|
|
||||||
|
## [0.1.60] - 2026-03-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Added voice-origin request headers for Home Assistant Assist / voice pipeline traffic: `x-openclaw-source: voice` and `x-ha-voice: true`.
|
||||||
|
- Added matching voice-origin support for microphone-triggered messages from the OpenClaw chat card, so card voice input and Assist voice pipeline requests are both marked as spoken interactions.
|
||||||
|
- This allows OpenClaw agents and hooks to detect spoken interactions and return TTS-friendly responses without affecting regular typed chat requests.
|
||||||
|
|
||||||
## [0.1.59] - 2026-03-07
|
## [0.1.59] - 2026-03-07
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -46,9 +46,11 @@ from .const import (
|
|||||||
ATTR_DRY_RUN,
|
ATTR_DRY_RUN,
|
||||||
ATTR_MESSAGE_CHANNEL,
|
ATTR_MESSAGE_CHANNEL,
|
||||||
ATTR_ACCOUNT_ID,
|
ATTR_ACCOUNT_ID,
|
||||||
|
ATTR_SOURCE,
|
||||||
ATTR_TIMESTAMP,
|
ATTR_TIMESTAMP,
|
||||||
CONF_ADDON_CONFIG_PATH,
|
CONF_ADDON_CONFIG_PATH,
|
||||||
CONF_AGENT_ID,
|
CONF_AGENT_ID,
|
||||||
|
CONF_VOICE_AGENT_ID,
|
||||||
CONF_GATEWAY_HOST,
|
CONF_GATEWAY_HOST,
|
||||||
CONF_GATEWAY_PORT,
|
CONF_GATEWAY_PORT,
|
||||||
CONF_GATEWAY_TOKEN,
|
CONF_GATEWAY_TOKEN,
|
||||||
@@ -66,6 +68,7 @@ from .const import (
|
|||||||
CONF_THINKING_TIMEOUT,
|
CONF_THINKING_TIMEOUT,
|
||||||
CONTEXT_STRATEGY_TRUNCATE,
|
CONTEXT_STRATEGY_TRUNCATE,
|
||||||
DEFAULT_AGENT_ID,
|
DEFAULT_AGENT_ID,
|
||||||
|
DEFAULT_VOICE_AGENT_ID,
|
||||||
DEFAULT_CONTEXT_MAX_CHARS,
|
DEFAULT_CONTEXT_MAX_CHARS,
|
||||||
DEFAULT_CONTEXT_STRATEGY,
|
DEFAULT_CONTEXT_STRATEGY,
|
||||||
DEFAULT_ENABLE_TOOL_CALLS,
|
DEFAULT_ENABLE_TOOL_CALLS,
|
||||||
@@ -92,13 +95,18 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
_MAX_CHAT_HISTORY = 200
|
_MAX_CHAT_HISTORY = 200
|
||||||
|
|
||||||
|
_VOICE_REQUEST_HEADERS = {
|
||||||
|
"x-openclaw-source": "voice",
|
||||||
|
"x-ha-voice": "true",
|
||||||
|
}
|
||||||
|
|
||||||
# Path to the chat card JS inside the integration package (custom_components/openclaw/www/)
|
# Path to the chat card JS inside the integration package (custom_components/openclaw/www/)
|
||||||
_CARD_FILENAME = "openclaw-chat-card.js"
|
_CARD_FILENAME = "openclaw-chat-card.js"
|
||||||
_CARD_PATH = Path(__file__).parent / "www" / _CARD_FILENAME
|
_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.56"
|
_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.60"
|
||||||
|
|
||||||
OpenClawConfigEntry = ConfigEntry
|
OpenClawConfigEntry = ConfigEntry
|
||||||
|
|
||||||
@@ -107,6 +115,7 @@ OpenClawConfigEntry = ConfigEntry
|
|||||||
SEND_MESSAGE_SCHEMA = vol.Schema(
|
SEND_MESSAGE_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(ATTR_MESSAGE): cv.string,
|
vol.Required(ATTR_MESSAGE): cv.string,
|
||||||
|
vol.Optional(ATTR_SOURCE): cv.string,
|
||||||
vol.Optional(ATTR_SESSION_ID): cv.string,
|
vol.Optional(ATTR_SESSION_ID): cv.string,
|
||||||
vol.Optional(ATTR_ATTACHMENTS): vol.All(cv.ensure_list, [cv.string]),
|
vol.Optional(ATTR_ATTACHMENTS): vol.All(cv.ensure_list, [cv.string]),
|
||||||
vol.Optional(ATTR_AGENT_ID): cv.string,
|
vol.Optional(ATTR_AGENT_ID): cv.string,
|
||||||
@@ -390,11 +399,19 @@ async def _async_add_lovelace_resource(hass: HomeAssistant, url: str) -> bool:
|
|||||||
def _async_register_services(hass: HomeAssistant) -> None:
|
def _async_register_services(hass: HomeAssistant) -> None:
|
||||||
"""Register openclaw.send_message and openclaw.clear_history services."""
|
"""Register openclaw.send_message and openclaw.clear_history services."""
|
||||||
|
|
||||||
|
def _normalize_optional_text(value: Any) -> str | None:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
return None
|
||||||
|
cleaned = value.strip()
|
||||||
|
return cleaned or None
|
||||||
|
|
||||||
async def handle_send_message(call: ServiceCall) -> None:
|
async def handle_send_message(call: ServiceCall) -> None:
|
||||||
"""Handle the openclaw.send_message service call."""
|
"""Handle the openclaw.send_message service call."""
|
||||||
message: str = call.data[ATTR_MESSAGE]
|
message: str = call.data[ATTR_MESSAGE]
|
||||||
|
source: str | None = call.data.get(ATTR_SOURCE)
|
||||||
session_id: str = call.data.get(ATTR_SESSION_ID) or "default"
|
session_id: str = call.data.get(ATTR_SESSION_ID) or "default"
|
||||||
call_agent_id: str | None = call.data.get(ATTR_AGENT_ID)
|
call_agent_id = _normalize_optional_text(call.data.get(ATTR_AGENT_ID))
|
||||||
|
extra_headers = _VOICE_REQUEST_HEADERS if source == "voice" else None
|
||||||
|
|
||||||
entry_data = _get_first_entry_data(hass)
|
entry_data = _get_first_entry_data(hass)
|
||||||
if not entry_data:
|
if not entry_data:
|
||||||
@@ -404,6 +421,12 @@ def _async_register_services(hass: HomeAssistant) -> None:
|
|||||||
client: OpenClawApiClient = entry_data["client"]
|
client: OpenClawApiClient = entry_data["client"]
|
||||||
coordinator: OpenClawCoordinator = entry_data["coordinator"]
|
coordinator: OpenClawCoordinator = entry_data["coordinator"]
|
||||||
options = _get_entry_options(hass, entry_data)
|
options = _get_entry_options(hass, entry_data)
|
||||||
|
voice_agent_id = _normalize_optional_text(
|
||||||
|
options.get(CONF_VOICE_AGENT_ID, DEFAULT_VOICE_AGENT_ID)
|
||||||
|
)
|
||||||
|
resolved_agent_id = call_agent_id
|
||||||
|
if resolved_agent_id is None and source == "voice":
|
||||||
|
resolved_agent_id = voice_agent_id
|
||||||
|
|
||||||
try:
|
try:
|
||||||
include_context = options.get(
|
include_context = options.get(
|
||||||
@@ -430,7 +453,8 @@ def _async_register_services(hass: HomeAssistant) -> None:
|
|||||||
message=message,
|
message=message,
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
system_prompt=system_prompt,
|
system_prompt=system_prompt,
|
||||||
agent_id=call_agent_id,
|
agent_id=resolved_agent_id,
|
||||||
|
extra_headers=extra_headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
if options.get(CONF_ENABLE_TOOL_CALLS, DEFAULT_ENABLE_TOOL_CALLS):
|
if options.get(CONF_ENABLE_TOOL_CALLS, DEFAULT_ENABLE_TOOL_CALLS):
|
||||||
@@ -444,7 +468,8 @@ def _async_register_services(hass: HomeAssistant) -> None:
|
|||||||
),
|
),
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
system_prompt=system_prompt,
|
system_prompt=system_prompt,
|
||||||
agent_id=call_agent_id,
|
agent_id=resolved_agent_id,
|
||||||
|
extra_headers=extra_headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
assistant_message = _extract_assistant_message(response)
|
assistant_message = _extract_assistant_message(response)
|
||||||
|
|||||||
@@ -85,14 +85,21 @@ class OpenClawApiClient:
|
|||||||
"""Update the authentication token (e.g., after addon restart)."""
|
"""Update the authentication token (e.g., after addon restart)."""
|
||||||
self._token = token
|
self._token = token
|
||||||
|
|
||||||
def _headers(self, agent_id: str | None = None) -> dict[str, str]:
|
def _headers(
|
||||||
|
self,
|
||||||
|
agent_id: str | None = None,
|
||||||
|
extra_headers: dict[str, str] | None = None,
|
||||||
|
) -> dict[str, str]:
|
||||||
"""Build request headers with auth token and agent ID."""
|
"""Build request headers with auth token and agent ID."""
|
||||||
effective_agent = agent_id or self._agent_id or "main"
|
effective_agent = agent_id or self._agent_id or "main"
|
||||||
return {
|
headers = {
|
||||||
"Authorization": f"Bearer {self._token}",
|
"Authorization": f"Bearer {self._token}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"x-openclaw-agent-id": effective_agent,
|
"x-openclaw-agent-id": effective_agent,
|
||||||
}
|
}
|
||||||
|
if extra_headers:
|
||||||
|
headers.update(extra_headers)
|
||||||
|
return headers
|
||||||
|
|
||||||
async def _get_session(self) -> aiohttp.ClientSession:
|
async def _get_session(self) -> aiohttp.ClientSession:
|
||||||
"""Get or create an aiohttp session."""
|
"""Get or create an aiohttp session."""
|
||||||
@@ -188,6 +195,7 @@ class OpenClawApiClient:
|
|||||||
system_prompt: str | None = None,
|
system_prompt: str | None = None,
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
agent_id: str | None = None,
|
agent_id: str | None = None,
|
||||||
|
extra_headers: dict[str, str] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Send a chat message (non-streaming).
|
"""Send a chat message (non-streaming).
|
||||||
|
|
||||||
@@ -197,6 +205,7 @@ class OpenClawApiClient:
|
|||||||
model: Optional model override.
|
model: Optional model override.
|
||||||
stream: If True, raises ValueError (use async_stream_message).
|
stream: If True, raises ValueError (use async_stream_message).
|
||||||
agent_id: Optional per-call agent ID override.
|
agent_id: Optional per-call agent ID override.
|
||||||
|
extra_headers: Optional additional headers for gateway-side routing or hints.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Complete chat completion response.
|
Complete chat completion response.
|
||||||
@@ -220,7 +229,7 @@ class OpenClawApiClient:
|
|||||||
payload["model"] = model
|
payload["model"] = model
|
||||||
|
|
||||||
# Pass session_id as a custom header or param if supported by gateway
|
# Pass session_id as a custom header or param if supported by gateway
|
||||||
headers = self._headers(agent_id=agent_id)
|
headers = self._headers(agent_id=agent_id, extra_headers=extra_headers)
|
||||||
if session_id:
|
if session_id:
|
||||||
headers["X-Session-Id"] = session_id
|
headers["X-Session-Id"] = session_id
|
||||||
headers["x-openclaw-session-key"] = session_id
|
headers["x-openclaw-session-key"] = session_id
|
||||||
@@ -255,6 +264,7 @@ class OpenClawApiClient:
|
|||||||
model: str | None = None,
|
model: str | None = None,
|
||||||
system_prompt: str | None = None,
|
system_prompt: str | None = None,
|
||||||
agent_id: str | None = None,
|
agent_id: str | None = None,
|
||||||
|
extra_headers: dict[str, str] | None = None,
|
||||||
) -> AsyncIterator[str]:
|
) -> AsyncIterator[str]:
|
||||||
"""Send a chat message and stream the response via SSE.
|
"""Send a chat message and stream the response via SSE.
|
||||||
|
|
||||||
@@ -265,6 +275,7 @@ class OpenClawApiClient:
|
|||||||
session_id: Optional session/conversation ID.
|
session_id: Optional session/conversation ID.
|
||||||
model: Optional model override.
|
model: Optional model override.
|
||||||
agent_id: Optional per-call agent ID override.
|
agent_id: Optional per-call agent ID override.
|
||||||
|
extra_headers: Optional additional headers for gateway-side routing or hints.
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
Content delta strings from the streaming response.
|
Content delta strings from the streaming response.
|
||||||
@@ -284,7 +295,7 @@ class OpenClawApiClient:
|
|||||||
if model:
|
if model:
|
||||||
payload["model"] = model
|
payload["model"] = model
|
||||||
|
|
||||||
headers = self._headers(agent_id=agent_id)
|
headers = self._headers(agent_id=agent_id, extra_headers=extra_headers)
|
||||||
if session_id:
|
if session_id:
|
||||||
headers["X-Session-Id"] = session_id
|
headers["X-Session-Id"] = session_id
|
||||||
headers["x-openclaw-session-key"] = session_id
|
headers["x-openclaw-session-key"] = session_id
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ from .const import (
|
|||||||
ADDON_SLUG_FRAGMENTS,
|
ADDON_SLUG_FRAGMENTS,
|
||||||
CONF_ADDON_CONFIG_PATH,
|
CONF_ADDON_CONFIG_PATH,
|
||||||
CONF_AGENT_ID,
|
CONF_AGENT_ID,
|
||||||
|
CONF_ASSIST_SESSION_ID,
|
||||||
CONF_GATEWAY_HOST,
|
CONF_GATEWAY_HOST,
|
||||||
CONF_GATEWAY_PORT,
|
CONF_GATEWAY_PORT,
|
||||||
CONF_GATEWAY_TOKEN,
|
CONF_GATEWAY_TOKEN,
|
||||||
@@ -48,6 +49,7 @@ from .const import (
|
|||||||
CONF_INCLUDE_EXPOSED_CONTEXT,
|
CONF_INCLUDE_EXPOSED_CONTEXT,
|
||||||
CONF_WAKE_WORD,
|
CONF_WAKE_WORD,
|
||||||
CONF_WAKE_WORD_ENABLED,
|
CONF_WAKE_WORD_ENABLED,
|
||||||
|
CONF_VOICE_AGENT_ID,
|
||||||
CONF_ALLOW_BRAVE_WEBSPEECH,
|
CONF_ALLOW_BRAVE_WEBSPEECH,
|
||||||
CONF_BROWSER_VOICE_LANGUAGE,
|
CONF_BROWSER_VOICE_LANGUAGE,
|
||||||
CONF_VOICE_PROVIDER,
|
CONF_VOICE_PROVIDER,
|
||||||
@@ -56,6 +58,7 @@ from .const import (
|
|||||||
CONTEXT_STRATEGY_CLEAR,
|
CONTEXT_STRATEGY_CLEAR,
|
||||||
CONTEXT_STRATEGY_TRUNCATE,
|
CONTEXT_STRATEGY_TRUNCATE,
|
||||||
DEFAULT_AGENT_ID,
|
DEFAULT_AGENT_ID,
|
||||||
|
DEFAULT_ASSIST_SESSION_ID,
|
||||||
DEFAULT_GATEWAY_HOST,
|
DEFAULT_GATEWAY_HOST,
|
||||||
DEFAULT_GATEWAY_PORT,
|
DEFAULT_GATEWAY_PORT,
|
||||||
DEFAULT_CONTEXT_MAX_CHARS,
|
DEFAULT_CONTEXT_MAX_CHARS,
|
||||||
@@ -68,6 +71,7 @@ from .const import (
|
|||||||
DEFAULT_BROWSER_VOICE_LANGUAGE,
|
DEFAULT_BROWSER_VOICE_LANGUAGE,
|
||||||
DEFAULT_VOICE_PROVIDER,
|
DEFAULT_VOICE_PROVIDER,
|
||||||
DEFAULT_THINKING_TIMEOUT,
|
DEFAULT_THINKING_TIMEOUT,
|
||||||
|
DEFAULT_VOICE_AGENT_ID,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
OPENCLAW_CONFIG_REL_PATH,
|
OPENCLAW_CONFIG_REL_PATH,
|
||||||
)
|
)
|
||||||
@@ -466,6 +470,20 @@ class OpenClawOptionsFlow(OptionsFlowWithReload):
|
|||||||
self._config_entry.data.get(CONF_AGENT_ID, DEFAULT_AGENT_ID),
|
self._config_entry.data.get(CONF_AGENT_ID, DEFAULT_AGENT_ID),
|
||||||
),
|
),
|
||||||
): str,
|
): str,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_VOICE_AGENT_ID,
|
||||||
|
default=options.get(
|
||||||
|
CONF_VOICE_AGENT_ID,
|
||||||
|
DEFAULT_VOICE_AGENT_ID,
|
||||||
|
),
|
||||||
|
): str,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_ASSIST_SESSION_ID,
|
||||||
|
default=options.get(
|
||||||
|
CONF_ASSIST_SESSION_ID,
|
||||||
|
DEFAULT_ASSIST_SESSION_ID,
|
||||||
|
),
|
||||||
|
): str,
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_INCLUDE_EXPOSED_CONTEXT,
|
CONF_INCLUDE_EXPOSED_CONTEXT,
|
||||||
default=options.get(
|
default=options.get(
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ CONF_USE_SSL = "use_ssl"
|
|||||||
CONF_VERIFY_SSL = "verify_ssl"
|
CONF_VERIFY_SSL = "verify_ssl"
|
||||||
CONF_ADDON_CONFIG_PATH = "addon_config_path"
|
CONF_ADDON_CONFIG_PATH = "addon_config_path"
|
||||||
CONF_AGENT_ID = "agent_id"
|
CONF_AGENT_ID = "agent_id"
|
||||||
|
CONF_VOICE_AGENT_ID = "voice_agent_id"
|
||||||
|
CONF_ASSIST_SESSION_ID = "assist_session_id"
|
||||||
|
|
||||||
# Options
|
# Options
|
||||||
CONF_INCLUDE_EXPOSED_CONTEXT = "include_exposed_context"
|
CONF_INCLUDE_EXPOSED_CONTEXT = "include_exposed_context"
|
||||||
@@ -38,6 +40,8 @@ CONF_BROWSER_VOICE_LANGUAGE = "browser_voice_language"
|
|||||||
CONF_THINKING_TIMEOUT = "thinking_timeout"
|
CONF_THINKING_TIMEOUT = "thinking_timeout"
|
||||||
|
|
||||||
DEFAULT_AGENT_ID = "main"
|
DEFAULT_AGENT_ID = "main"
|
||||||
|
DEFAULT_VOICE_AGENT_ID = ""
|
||||||
|
DEFAULT_ASSIST_SESSION_ID = ""
|
||||||
DEFAULT_INCLUDE_EXPOSED_CONTEXT = True
|
DEFAULT_INCLUDE_EXPOSED_CONTEXT = True
|
||||||
DEFAULT_CONTEXT_MAX_CHARS = 13000
|
DEFAULT_CONTEXT_MAX_CHARS = 13000
|
||||||
DEFAULT_CONTEXT_STRATEGY = "truncate"
|
DEFAULT_CONTEXT_STRATEGY = "truncate"
|
||||||
@@ -121,6 +125,7 @@ SERVICE_INVOKE_TOOL = "invoke_tool"
|
|||||||
|
|
||||||
# Attributes
|
# Attributes
|
||||||
ATTR_MESSAGE = "message"
|
ATTR_MESSAGE = "message"
|
||||||
|
ATTR_SOURCE = "source"
|
||||||
ATTR_SESSION_ID = "session_id"
|
ATTR_SESSION_ID = "session_id"
|
||||||
ATTR_ATTACHMENTS = "attachments"
|
ATTR_ATTACHMENTS = "attachments"
|
||||||
ATTR_MODEL = "model"
|
ATTR_MODEL = "model"
|
||||||
|
|||||||
@@ -22,9 +22,12 @@ from .const import (
|
|||||||
ATTR_MODEL,
|
ATTR_MODEL,
|
||||||
ATTR_SESSION_ID,
|
ATTR_SESSION_ID,
|
||||||
ATTR_TIMESTAMP,
|
ATTR_TIMESTAMP,
|
||||||
|
CONF_ASSIST_SESSION_ID,
|
||||||
CONF_CONTEXT_MAX_CHARS,
|
CONF_CONTEXT_MAX_CHARS,
|
||||||
CONF_CONTEXT_STRATEGY,
|
CONF_CONTEXT_STRATEGY,
|
||||||
CONF_INCLUDE_EXPOSED_CONTEXT,
|
CONF_INCLUDE_EXPOSED_CONTEXT,
|
||||||
|
CONF_VOICE_AGENT_ID,
|
||||||
|
DEFAULT_ASSIST_SESSION_ID,
|
||||||
DEFAULT_CONTEXT_MAX_CHARS,
|
DEFAULT_CONTEXT_MAX_CHARS,
|
||||||
DEFAULT_CONTEXT_STRATEGY,
|
DEFAULT_CONTEXT_STRATEGY,
|
||||||
DEFAULT_INCLUDE_EXPOSED_CONTEXT,
|
DEFAULT_INCLUDE_EXPOSED_CONTEXT,
|
||||||
@@ -37,6 +40,11 @@ from .exposure import apply_context_policy, build_exposed_entities_context
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_VOICE_REQUEST_HEADERS = {
|
||||||
|
"x-openclaw-source": "voice",
|
||||||
|
"x-ha-voice": "true",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -110,6 +118,9 @@ class OpenClawConversationAgent(conversation.AbstractConversationAgent):
|
|||||||
conversation_id = self._resolve_conversation_id(user_input)
|
conversation_id = self._resolve_conversation_id(user_input)
|
||||||
assistant_id = "conversation"
|
assistant_id = "conversation"
|
||||||
options = self.entry.options
|
options = self.entry.options
|
||||||
|
voice_agent_id = self._normalize_optional_text(
|
||||||
|
options.get(CONF_VOICE_AGENT_ID)
|
||||||
|
)
|
||||||
include_context = options.get(
|
include_context = options.get(
|
||||||
CONF_INCLUDE_EXPOSED_CONTEXT,
|
CONF_INCLUDE_EXPOSED_CONTEXT,
|
||||||
DEFAULT_INCLUDE_EXPOSED_CONTEXT,
|
DEFAULT_INCLUDE_EXPOSED_CONTEXT,
|
||||||
@@ -136,6 +147,7 @@ class OpenClawConversationAgent(conversation.AbstractConversationAgent):
|
|||||||
client,
|
client,
|
||||||
message,
|
message,
|
||||||
conversation_id,
|
conversation_id,
|
||||||
|
voice_agent_id,
|
||||||
system_prompt,
|
system_prompt,
|
||||||
)
|
)
|
||||||
except OpenClawApiError as err:
|
except OpenClawApiError as err:
|
||||||
@@ -151,6 +163,7 @@ class OpenClawConversationAgent(conversation.AbstractConversationAgent):
|
|||||||
client,
|
client,
|
||||||
message,
|
message,
|
||||||
conversation_id,
|
conversation_id,
|
||||||
|
voice_agent_id,
|
||||||
system_prompt,
|
system_prompt,
|
||||||
)
|
)
|
||||||
except OpenClawApiError as retry_err:
|
except OpenClawApiError as retry_err:
|
||||||
@@ -191,6 +204,15 @@ class OpenClawConversationAgent(conversation.AbstractConversationAgent):
|
|||||||
|
|
||||||
def _resolve_conversation_id(self, user_input: conversation.ConversationInput) -> str:
|
def _resolve_conversation_id(self, user_input: conversation.ConversationInput) -> str:
|
||||||
"""Return conversation id from HA or a stable Assist fallback session key."""
|
"""Return conversation id from HA or a stable Assist fallback session key."""
|
||||||
|
configured_session_id = self._normalize_optional_text(
|
||||||
|
self.entry.options.get(
|
||||||
|
CONF_ASSIST_SESSION_ID,
|
||||||
|
DEFAULT_ASSIST_SESSION_ID,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if configured_session_id:
|
||||||
|
return configured_session_id
|
||||||
|
|
||||||
if user_input.conversation_id:
|
if user_input.conversation_id:
|
||||||
return user_input.conversation_id
|
return user_input.conversation_id
|
||||||
|
|
||||||
@@ -205,11 +227,19 @@ class OpenClawConversationAgent(conversation.AbstractConversationAgent):
|
|||||||
|
|
||||||
return "assist_default"
|
return "assist_default"
|
||||||
|
|
||||||
|
def _normalize_optional_text(self, value: Any) -> str | None:
|
||||||
|
"""Return a stripped string or None for blank values."""
|
||||||
|
if not isinstance(value, str):
|
||||||
|
return None
|
||||||
|
cleaned = value.strip()
|
||||||
|
return cleaned or None
|
||||||
|
|
||||||
async def _get_response(
|
async def _get_response(
|
||||||
self,
|
self,
|
||||||
client: OpenClawApiClient,
|
client: OpenClawApiClient,
|
||||||
message: str,
|
message: str,
|
||||||
conversation_id: str,
|
conversation_id: str,
|
||||||
|
agent_id: str | None = None,
|
||||||
system_prompt: str | None = None,
|
system_prompt: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Get a response from OpenClaw, trying streaming first."""
|
"""Get a response from OpenClaw, trying streaming first."""
|
||||||
@@ -219,6 +249,8 @@ class OpenClawConversationAgent(conversation.AbstractConversationAgent):
|
|||||||
message=message,
|
message=message,
|
||||||
session_id=conversation_id,
|
session_id=conversation_id,
|
||||||
system_prompt=system_prompt,
|
system_prompt=system_prompt,
|
||||||
|
agent_id=agent_id,
|
||||||
|
extra_headers=_VOICE_REQUEST_HEADERS,
|
||||||
):
|
):
|
||||||
full_response += chunk
|
full_response += chunk
|
||||||
|
|
||||||
@@ -230,6 +262,8 @@ class OpenClawConversationAgent(conversation.AbstractConversationAgent):
|
|||||||
message=message,
|
message=message,
|
||||||
session_id=conversation_id,
|
session_id=conversation_id,
|
||||||
system_prompt=system_prompt,
|
system_prompt=system_prompt,
|
||||||
|
agent_id=agent_id,
|
||||||
|
extra_headers=_VOICE_REQUEST_HEADERS,
|
||||||
)
|
)
|
||||||
extracted = self._extract_text_recursive(response)
|
extracted = self._extract_text_recursive(response)
|
||||||
return extracted or ""
|
return extracted or ""
|
||||||
|
|||||||
@@ -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.59",
|
"version": "0.1.61",
|
||||||
"dependencies": ["conversation"],
|
"dependencies": ["conversation"],
|
||||||
"after_dependencies": ["hassio", "lovelace"]
|
"after_dependencies": ["hassio", "lovelace"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@
|
|||||||
"description": "Configure context and tool-calling behavior.",
|
"description": "Configure context and tool-calling behavior.",
|
||||||
"data": {
|
"data": {
|
||||||
"agent_id": "Agent ID (e.g. main)",
|
"agent_id": "Agent ID (e.g. main)",
|
||||||
|
"voice_agent_id": "Voice agent ID (optional)",
|
||||||
|
"assist_session_id": "Assist session ID override (optional)",
|
||||||
"include_exposed_context": "Include exposed entities context",
|
"include_exposed_context": "Include exposed entities context",
|
||||||
"context_max_chars": "Max context characters",
|
"context_max_chars": "Max context characters",
|
||||||
"context_strategy": "When context exceeds max",
|
"context_strategy": "When context exceeds max",
|
||||||
|
|||||||
@@ -41,6 +41,8 @@
|
|||||||
"description": "Configure context and tool-calling behavior.",
|
"description": "Configure context and tool-calling behavior.",
|
||||||
"data": {
|
"data": {
|
||||||
"agent_id": "Agent ID (e.g. main)",
|
"agent_id": "Agent ID (e.g. main)",
|
||||||
|
"voice_agent_id": "Voice agent ID (optional)",
|
||||||
|
"assist_session_id": "Assist session ID override (optional)",
|
||||||
"include_exposed_context": "Include exposed entities context",
|
"include_exposed_context": "Include exposed entities context",
|
||||||
"context_max_chars": "Max context characters",
|
"context_max_chars": "Max context characters",
|
||||||
"context_strategy": "When context exceeds max",
|
"context_strategy": "When context exceeds max",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* + subscribes to openclaw_message_received events.
|
* + subscribes to openclaw_message_received events.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CARD_VERSION = "0.3.12";
|
const CARD_VERSION = "0.3.13";
|
||||||
|
|
||||||
// Max time (ms) to show the thinking indicator before falling back to an error (default; overridable via card config `thinking_timeout` in seconds)
|
// Max time (ms) to show the thinking indicator before falling back to an error (default; overridable via card config `thinking_timeout` in seconds)
|
||||||
const THINKING_TIMEOUT_MS = 120_000;
|
const THINKING_TIMEOUT_MS = 120_000;
|
||||||
@@ -516,7 +516,7 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
this._persistMessages();
|
this._persistMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
async _sendMessage(text) {
|
async _sendMessage(text, source = null) {
|
||||||
if (!text || !text.trim() || !this._hass) return;
|
if (!text || !text.trim() || !this._hass) return;
|
||||||
|
|
||||||
await this._subscribeToEvents();
|
await this._subscribeToEvents();
|
||||||
@@ -541,6 +541,7 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
try {
|
try {
|
||||||
await this._hass.callService("openclaw", "send_message", {
|
await this._hass.callService("openclaw", "send_message", {
|
||||||
message: message,
|
message: message,
|
||||||
|
source: source || undefined,
|
||||||
session_id: this._config.session_id || undefined,
|
session_id: this._config.session_id || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -948,7 +949,7 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
|
|
||||||
this._voiceStatus = "Sending…";
|
this._voiceStatus = "Sending…";
|
||||||
this._render();
|
this._render();
|
||||||
this._sendMessage(transcript);
|
this._sendMessage(transcript, "voice");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("OpenClaw: Assist STT transcription failed", err);
|
console.error("OpenClaw: Assist STT transcription failed", err);
|
||||||
this._voiceStatus = "Assist STT transcription failed.";
|
this._voiceStatus = "Assist STT transcription failed.";
|
||||||
@@ -1161,13 +1162,13 @@ class OpenClawChatCard extends HTMLElement {
|
|||||||
}
|
}
|
||||||
this._voiceStatus = "Sending…";
|
this._voiceStatus = "Sending…";
|
||||||
this._render();
|
this._render();
|
||||||
this._sendMessage(command);
|
this._sendMessage(command, "voice");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._voiceStatus = "Sending…";
|
this._voiceStatus = "Sending…";
|
||||||
this._render();
|
this._render();
|
||||||
this._sendMessage(text);
|
this._sendMessage(text, "voice");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user