Skip to content

Commit 95bfe5e

Browse files
author
techartdev
committed
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.
1 parent e93c05f commit 95bfe5e

10 files changed

Lines changed: 128 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22

33
All notable changes to the OpenClaw Home Assistant Integration will be documented in this file.
44

5+
## [0.1.61] - 2026-03-07
6+
7+
### Added
8+
- 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.
9+
- Added an optional `assist_session_id` integration option so the native Home Assistant conversation agent can reuse a fixed OpenClaw session when desired.
10+
11+
### Changed
12+
- 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.
13+
14+
## [0.1.60] - 2026-03-07
15+
16+
### Added
17+
- Added voice-origin request headers for Home Assistant Assist / voice pipeline traffic: `x-openclaw-source: voice` and `x-ha-voice: true`.
18+
- 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.
19+
- This allows OpenClaw agents and hooks to detect spoken interactions and return TTS-friendly responses without affecting regular typed chat requests.
20+
521
## [0.1.59] - 2026-03-07
622

723
### Fixed

custom_components/openclaw/__init__.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,11 @@
4646
ATTR_DRY_RUN,
4747
ATTR_MESSAGE_CHANNEL,
4848
ATTR_ACCOUNT_ID,
49+
ATTR_SOURCE,
4950
ATTR_TIMESTAMP,
5051
CONF_ADDON_CONFIG_PATH,
5152
CONF_AGENT_ID,
53+
CONF_VOICE_AGENT_ID,
5254
CONF_GATEWAY_HOST,
5355
CONF_GATEWAY_PORT,
5456
CONF_GATEWAY_TOKEN,
@@ -66,6 +68,7 @@
6668
CONF_THINKING_TIMEOUT,
6769
CONTEXT_STRATEGY_TRUNCATE,
6870
DEFAULT_AGENT_ID,
71+
DEFAULT_VOICE_AGENT_ID,
6972
DEFAULT_CONTEXT_MAX_CHARS,
7073
DEFAULT_CONTEXT_STRATEGY,
7174
DEFAULT_ENABLE_TOOL_CALLS,
@@ -92,13 +95,18 @@
9295

9396
_MAX_CHAT_HISTORY = 200
9497

98+
_VOICE_REQUEST_HEADERS = {
99+
"x-openclaw-source": "voice",
100+
"x-ha-voice": "true",
101+
}
102+
95103
# Path to the chat card JS inside the integration package (custom_components/openclaw/www/)
96104
_CARD_FILENAME = "openclaw-chat-card.js"
97105
_CARD_PATH = Path(__file__).parent / "www" / _CARD_FILENAME
98106
# URL at which the card JS is served (registered via register_static_path)
99107
_CARD_STATIC_URL = f"/openclaw/{_CARD_FILENAME}"
100108
# Versioned URL used for Lovelace resource registration to avoid stale browser cache
101-
_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.56"
109+
_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.60"
102110

103111
OpenClawConfigEntry = ConfigEntry
104112

@@ -107,6 +115,7 @@
107115
SEND_MESSAGE_SCHEMA = vol.Schema(
108116
{
109117
vol.Required(ATTR_MESSAGE): cv.string,
118+
vol.Optional(ATTR_SOURCE): cv.string,
110119
vol.Optional(ATTR_SESSION_ID): cv.string,
111120
vol.Optional(ATTR_ATTACHMENTS): vol.All(cv.ensure_list, [cv.string]),
112121
vol.Optional(ATTR_AGENT_ID): cv.string,
@@ -390,11 +399,19 @@ async def _async_add_lovelace_resource(hass: HomeAssistant, url: str) -> bool:
390399
def _async_register_services(hass: HomeAssistant) -> None:
391400
"""Register openclaw.send_message and openclaw.clear_history services."""
392401

402+
def _normalize_optional_text(value: Any) -> str | None:
403+
if not isinstance(value, str):
404+
return None
405+
cleaned = value.strip()
406+
return cleaned or None
407+
393408
async def handle_send_message(call: ServiceCall) -> None:
394409
"""Handle the openclaw.send_message service call."""
395410
message: str = call.data[ATTR_MESSAGE]
411+
source: str | None = call.data.get(ATTR_SOURCE)
396412
session_id: str = call.data.get(ATTR_SESSION_ID) or "default"
397-
call_agent_id: str | None = call.data.get(ATTR_AGENT_ID)
413+
call_agent_id = _normalize_optional_text(call.data.get(ATTR_AGENT_ID))
414+
extra_headers = _VOICE_REQUEST_HEADERS if source == "voice" else None
398415

399416
entry_data = _get_first_entry_data(hass)
400417
if not entry_data:
@@ -404,6 +421,12 @@ async def handle_send_message(call: ServiceCall) -> None:
404421
client: OpenClawApiClient = entry_data["client"]
405422
coordinator: OpenClawCoordinator = entry_data["coordinator"]
406423
options = _get_entry_options(hass, entry_data)
424+
voice_agent_id = _normalize_optional_text(
425+
options.get(CONF_VOICE_AGENT_ID, DEFAULT_VOICE_AGENT_ID)
426+
)
427+
resolved_agent_id = call_agent_id
428+
if resolved_agent_id is None and source == "voice":
429+
resolved_agent_id = voice_agent_id
407430

408431
try:
409432
include_context = options.get(
@@ -430,7 +453,8 @@ async def handle_send_message(call: ServiceCall) -> None:
430453
message=message,
431454
session_id=session_id,
432455
system_prompt=system_prompt,
433-
agent_id=call_agent_id,
456+
agent_id=resolved_agent_id,
457+
extra_headers=extra_headers,
434458
)
435459

436460
if options.get(CONF_ENABLE_TOOL_CALLS, DEFAULT_ENABLE_TOOL_CALLS):
@@ -444,7 +468,8 @@ async def handle_send_message(call: ServiceCall) -> None:
444468
),
445469
session_id=session_id,
446470
system_prompt=system_prompt,
447-
agent_id=call_agent_id,
471+
agent_id=resolved_agent_id,
472+
extra_headers=extra_headers,
448473
)
449474

450475
assistant_message = _extract_assistant_message(response)

custom_components/openclaw/api.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,21 @@ def update_token(self, token: str) -> None:
8585
"""Update the authentication token (e.g., after addon restart)."""
8686
self._token = token
8787

88-
def _headers(self, agent_id: str | None = None) -> dict[str, str]:
88+
def _headers(
89+
self,
90+
agent_id: str | None = None,
91+
extra_headers: dict[str, str] | None = None,
92+
) -> dict[str, str]:
8993
"""Build request headers with auth token and agent ID."""
9094
effective_agent = agent_id or self._agent_id or "main"
91-
return {
95+
headers = {
9296
"Authorization": f"Bearer {self._token}",
9397
"Content-Type": "application/json",
9498
"x-openclaw-agent-id": effective_agent,
9599
}
100+
if extra_headers:
101+
headers.update(extra_headers)
102+
return headers
96103

97104
async def _get_session(self) -> aiohttp.ClientSession:
98105
"""Get or create an aiohttp session."""
@@ -188,6 +195,7 @@ async def async_send_message(
188195
system_prompt: str | None = None,
189196
stream: bool = False,
190197
agent_id: str | None = None,
198+
extra_headers: dict[str, str] | None = None,
191199
) -> dict[str, Any]:
192200
"""Send a chat message (non-streaming).
193201
@@ -197,6 +205,7 @@ async def async_send_message(
197205
model: Optional model override.
198206
stream: If True, raises ValueError (use async_stream_message).
199207
agent_id: Optional per-call agent ID override.
208+
extra_headers: Optional additional headers for gateway-side routing or hints.
200209
201210
Returns:
202211
Complete chat completion response.
@@ -220,7 +229,7 @@ async def async_send_message(
220229
payload["model"] = model
221230

222231
# Pass session_id as a custom header or param if supported by gateway
223-
headers = self._headers(agent_id=agent_id)
232+
headers = self._headers(agent_id=agent_id, extra_headers=extra_headers)
224233
if session_id:
225234
headers["X-Session-Id"] = session_id
226235
headers["x-openclaw-session-key"] = session_id
@@ -255,6 +264,7 @@ async def async_stream_message(
255264
model: str | None = None,
256265
system_prompt: str | None = None,
257266
agent_id: str | None = None,
267+
extra_headers: dict[str, str] | None = None,
258268
) -> AsyncIterator[str]:
259269
"""Send a chat message and stream the response via SSE.
260270
@@ -265,6 +275,7 @@ async def async_stream_message(
265275
session_id: Optional session/conversation ID.
266276
model: Optional model override.
267277
agent_id: Optional per-call agent ID override.
278+
extra_headers: Optional additional headers for gateway-side routing or hints.
268279
269280
Yields:
270281
Content delta strings from the streaming response.
@@ -284,7 +295,7 @@ async def async_stream_message(
284295
if model:
285296
payload["model"] = model
286297

287-
headers = self._headers(agent_id=agent_id)
298+
headers = self._headers(agent_id=agent_id, extra_headers=extra_headers)
288299
if session_id:
289300
headers["X-Session-Id"] = session_id
290301
headers["x-openclaw-session-key"] = session_id

custom_components/openclaw/config_flow.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
ADDON_SLUG_FRAGMENTS,
3838
CONF_ADDON_CONFIG_PATH,
3939
CONF_AGENT_ID,
40+
CONF_ASSIST_SESSION_ID,
4041
CONF_GATEWAY_HOST,
4142
CONF_GATEWAY_PORT,
4243
CONF_GATEWAY_TOKEN,
@@ -48,6 +49,7 @@
4849
CONF_INCLUDE_EXPOSED_CONTEXT,
4950
CONF_WAKE_WORD,
5051
CONF_WAKE_WORD_ENABLED,
52+
CONF_VOICE_AGENT_ID,
5153
CONF_ALLOW_BRAVE_WEBSPEECH,
5254
CONF_BROWSER_VOICE_LANGUAGE,
5355
CONF_VOICE_PROVIDER,
@@ -56,6 +58,7 @@
5658
CONTEXT_STRATEGY_CLEAR,
5759
CONTEXT_STRATEGY_TRUNCATE,
5860
DEFAULT_AGENT_ID,
61+
DEFAULT_ASSIST_SESSION_ID,
5962
DEFAULT_GATEWAY_HOST,
6063
DEFAULT_GATEWAY_PORT,
6164
DEFAULT_CONTEXT_MAX_CHARS,
@@ -68,6 +71,7 @@
6871
DEFAULT_BROWSER_VOICE_LANGUAGE,
6972
DEFAULT_VOICE_PROVIDER,
7073
DEFAULT_THINKING_TIMEOUT,
74+
DEFAULT_VOICE_AGENT_ID,
7175
DOMAIN,
7276
OPENCLAW_CONFIG_REL_PATH,
7377
)
@@ -466,6 +470,20 @@ async def async_step_init(
466470
self._config_entry.data.get(CONF_AGENT_ID, DEFAULT_AGENT_ID),
467471
),
468472
): str,
473+
vol.Optional(
474+
CONF_VOICE_AGENT_ID,
475+
default=options.get(
476+
CONF_VOICE_AGENT_ID,
477+
DEFAULT_VOICE_AGENT_ID,
478+
),
479+
): str,
480+
vol.Optional(
481+
CONF_ASSIST_SESSION_ID,
482+
default=options.get(
483+
CONF_ASSIST_SESSION_ID,
484+
DEFAULT_ASSIST_SESSION_ID,
485+
),
486+
): str,
469487
vol.Optional(
470488
CONF_INCLUDE_EXPOSED_CONTEXT,
471489
default=options.get(

custom_components/openclaw/const.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
CONF_VERIFY_SSL = "verify_ssl"
2525
CONF_ADDON_CONFIG_PATH = "addon_config_path"
2626
CONF_AGENT_ID = "agent_id"
27+
CONF_VOICE_AGENT_ID = "voice_agent_id"
28+
CONF_ASSIST_SESSION_ID = "assist_session_id"
2729

2830
# Options
2931
CONF_INCLUDE_EXPOSED_CONTEXT = "include_exposed_context"
@@ -38,6 +40,8 @@
3840
CONF_THINKING_TIMEOUT = "thinking_timeout"
3941

4042
DEFAULT_AGENT_ID = "main"
43+
DEFAULT_VOICE_AGENT_ID = ""
44+
DEFAULT_ASSIST_SESSION_ID = ""
4145
DEFAULT_INCLUDE_EXPOSED_CONTEXT = True
4246
DEFAULT_CONTEXT_MAX_CHARS = 13000
4347
DEFAULT_CONTEXT_STRATEGY = "truncate"
@@ -121,6 +125,7 @@
121125

122126
# Attributes
123127
ATTR_MESSAGE = "message"
128+
ATTR_SOURCE = "source"
124129
ATTR_SESSION_ID = "session_id"
125130
ATTR_ATTACHMENTS = "attachments"
126131
ATTR_MODEL = "model"

custom_components/openclaw/conversation.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@
2222
ATTR_MODEL,
2323
ATTR_SESSION_ID,
2424
ATTR_TIMESTAMP,
25+
CONF_ASSIST_SESSION_ID,
2526
CONF_CONTEXT_MAX_CHARS,
2627
CONF_CONTEXT_STRATEGY,
2728
CONF_INCLUDE_EXPOSED_CONTEXT,
29+
CONF_VOICE_AGENT_ID,
30+
DEFAULT_ASSIST_SESSION_ID,
2831
DEFAULT_CONTEXT_MAX_CHARS,
2932
DEFAULT_CONTEXT_STRATEGY,
3033
DEFAULT_INCLUDE_EXPOSED_CONTEXT,
@@ -37,6 +40,11 @@
3740

3841
_LOGGER = logging.getLogger(__name__)
3942

43+
_VOICE_REQUEST_HEADERS = {
44+
"x-openclaw-source": "voice",
45+
"x-ha-voice": "true",
46+
}
47+
4048

4149
async def async_setup_entry(
4250
hass: HomeAssistant,
@@ -110,6 +118,9 @@ async def async_process(
110118
conversation_id = self._resolve_conversation_id(user_input)
111119
assistant_id = "conversation"
112120
options = self.entry.options
121+
voice_agent_id = self._normalize_optional_text(
122+
options.get(CONF_VOICE_AGENT_ID)
123+
)
113124
include_context = options.get(
114125
CONF_INCLUDE_EXPOSED_CONTEXT,
115126
DEFAULT_INCLUDE_EXPOSED_CONTEXT,
@@ -136,6 +147,7 @@ async def async_process(
136147
client,
137148
message,
138149
conversation_id,
150+
voice_agent_id,
139151
system_prompt,
140152
)
141153
except OpenClawApiError as err:
@@ -151,6 +163,7 @@ async def async_process(
151163
client,
152164
message,
153165
conversation_id,
166+
voice_agent_id,
154167
system_prompt,
155168
)
156169
except OpenClawApiError as retry_err:
@@ -191,6 +204,15 @@ async def async_process(
191204

192205
def _resolve_conversation_id(self, user_input: conversation.ConversationInput) -> str:
193206
"""Return conversation id from HA or a stable Assist fallback session key."""
207+
configured_session_id = self._normalize_optional_text(
208+
self.entry.options.get(
209+
CONF_ASSIST_SESSION_ID,
210+
DEFAULT_ASSIST_SESSION_ID,
211+
)
212+
)
213+
if configured_session_id:
214+
return configured_session_id
215+
194216
if user_input.conversation_id:
195217
return user_input.conversation_id
196218

@@ -205,11 +227,19 @@ def _resolve_conversation_id(self, user_input: conversation.ConversationInput) -
205227

206228
return "assist_default"
207229

230+
def _normalize_optional_text(self, value: Any) -> str | None:
231+
"""Return a stripped string or None for blank values."""
232+
if not isinstance(value, str):
233+
return None
234+
cleaned = value.strip()
235+
return cleaned or None
236+
208237
async def _get_response(
209238
self,
210239
client: OpenClawApiClient,
211240
message: str,
212241
conversation_id: str,
242+
agent_id: str | None = None,
213243
system_prompt: str | None = None,
214244
) -> str:
215245
"""Get a response from OpenClaw, trying streaming first."""
@@ -219,6 +249,8 @@ async def _get_response(
219249
message=message,
220250
session_id=conversation_id,
221251
system_prompt=system_prompt,
252+
agent_id=agent_id,
253+
extra_headers=_VOICE_REQUEST_HEADERS,
222254
):
223255
full_response += chunk
224256

@@ -230,6 +262,8 @@ async def _get_response(
230262
message=message,
231263
session_id=conversation_id,
232264
system_prompt=system_prompt,
265+
agent_id=agent_id,
266+
extra_headers=_VOICE_REQUEST_HEADERS,
233267
)
234268
extracted = self._extract_text_recursive(response)
235269
return extracted or ""

custom_components/openclaw/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"iot_class": "local_polling",
99
"issue_tracker": "https://github.com/techartdev/OpenClawHomeAssistant/issues",
1010
"requirements": [],
11-
"version": "0.1.59",
11+
"version": "0.1.61",
1212
"dependencies": ["conversation"],
1313
"after_dependencies": ["hassio", "lovelace"]
1414
}

0 commit comments

Comments
 (0)