Update changelog for version 0.1.13, add native Assist entity exposure support, enhance context handling in chat service, and introduce helper for entity exposure context.

This commit is contained in:
techartdev
2026-02-20 16:49:13 +02:00
parent 2331ec0d8d
commit c0f9e5c42e
7 changed files with 118 additions and 5 deletions
+9
View File
@@ -2,6 +2,15 @@
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.13] - 2026-02-20
### Added
- Added native Home Assistant Assist entity exposure support to OpenClaw requests.
- OpenClaw now includes context for entities exposed in **Settings → Voice assistants → Expose**.
### Changed
- Service chat (`openclaw.send_message`) and conversation agent (streaming + fallback) now pass exposed-entities context as a system prompt.
## [0.1.12] - 2026-02-20 ## [0.1.12] - 2026-02-20
### Fixed ### Fixed
+11
View File
@@ -33,6 +33,17 @@ A native Home Assistant integration for communicating with the
The **OpenClaw Assistant** addon must be installed and running. The **OpenClaw Assistant** addon must be installed and running.
The integration will auto-detect it — no manual configuration needed. The integration will auto-detect it — no manual configuration needed.
## Exposed entities (important)
OpenClaw uses Home Assistant's native **Assist entity exposure** controls.
Only entities exposed to Assist are included in the context sent to OpenClaw.
Configure this in Home Assistant:
1. Go to **Settings → Voice assistants**
2. Open the **Expose** tab
3. Select which entities are exposed to **Assist**
## Chat Card ## Chat Card
The chat card is registered automatically when the integration loads — no The chat card is registered automatically when the integration loads — no
+3
View File
@@ -44,6 +44,7 @@ from .const import (
SERVICE_SEND_MESSAGE, SERVICE_SEND_MESSAGE,
) )
from .coordinator import OpenClawCoordinator from .coordinator import OpenClawCoordinator
from .exposure import build_exposed_entities_context
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -313,9 +314,11 @@ def _async_register_services(hass: HomeAssistant) -> None:
coordinator: OpenClawCoordinator = entry_data["coordinator"] coordinator: OpenClawCoordinator = entry_data["coordinator"]
try: try:
system_prompt = build_exposed_entities_context(hass, assistant="assist")
response = await client.async_send_message( response = await client.async_send_message(
message=message, message=message,
session_id=session_id, session_id=session_id,
system_prompt=system_prompt,
) )
assistant_message = _extract_assistant_message(response) assistant_message = _extract_assistant_message(response)
+14 -2
View File
@@ -165,6 +165,7 @@ class OpenClawApiClient:
message: str, message: str,
session_id: str | None = None, session_id: str | None = None,
model: str | None = None, model: str | None = None,
system_prompt: str | None = None,
stream: bool = False, stream: bool = False,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Send a chat message (non-streaming). """Send a chat message (non-streaming).
@@ -181,8 +182,13 @@ class OpenClawApiClient:
if stream: if stream:
raise ValueError("Use async_stream_message() for streaming") raise ValueError("Use async_stream_message() for streaming")
messages: list[dict[str, str]] = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": message})
payload: dict[str, Any] = { payload: dict[str, Any] = {
"messages": [{"role": "user", "content": message}], "messages": messages,
"stream": False, "stream": False,
} }
if model: if model:
@@ -220,6 +226,7 @@ class OpenClawApiClient:
message: str, message: str,
session_id: str | None = None, session_id: str | None = None,
model: str | None = None, model: str | None = None,
system_prompt: 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.
@@ -233,8 +240,13 @@ class OpenClawApiClient:
Yields: Yields:
Content delta strings from the streaming response. Content delta strings from the streaming response.
""" """
messages: list[dict[str, str]] = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": message})
payload: dict[str, Any] = { payload: dict[str, Any] = {
"messages": [{"role": "user", "content": message}], "messages": messages,
"stream": True, "stream": True,
} }
if model: if model:
+23 -2
View File
@@ -26,6 +26,7 @@ from .const import (
EVENT_MESSAGE_RECEIVED, EVENT_MESSAGE_RECEIVED,
) )
from .coordinator import OpenClawCoordinator from .coordinator import OpenClawCoordinator
from .exposure import build_exposed_entities_context
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -100,9 +101,23 @@ class OpenClawConversationAgent(conversation.AbstractConversationAgent):
message = user_input.text message = user_input.text
conversation_id = user_input.conversation_id or "default" conversation_id = user_input.conversation_id or "default"
assistant_id = getattr(user_input, "assistant", None) or "assist"
exposed_context = build_exposed_entities_context(
self.hass,
assistant=assistant_id,
)
extra_system_prompt = getattr(user_input, "extra_system_prompt", None)
system_prompt = "\n\n".join(
part for part in (exposed_context, extra_system_prompt) if part
) or None
try: try:
full_response = await self._get_response(client, message, conversation_id) full_response = await self._get_response(
client,
message,
conversation_id,
system_prompt,
)
except OpenClawApiError as err: except OpenClawApiError as err:
_LOGGER.error("OpenClaw conversation error: %s", err) _LOGGER.error("OpenClaw conversation error: %s", err)
@@ -113,7 +128,10 @@ class OpenClawConversationAgent(conversation.AbstractConversationAgent):
if refreshed: if refreshed:
try: try:
full_response = await self._get_response( full_response = await self._get_response(
client, message, conversation_id client,
message,
conversation_id,
system_prompt,
) )
except OpenClawApiError as retry_err: except OpenClawApiError as retry_err:
return self._error_result( return self._error_result(
@@ -156,6 +174,7 @@ class OpenClawConversationAgent(conversation.AbstractConversationAgent):
client: OpenClawApiClient, client: OpenClawApiClient,
message: str, message: str,
conversation_id: str, conversation_id: str,
system_prompt: str | None = None,
) -> str: ) -> str:
"""Get a response from OpenClaw, trying streaming first.""" """Get a response from OpenClaw, trying streaming first."""
# Try streaming (lower TTFB for voice pipeline) # Try streaming (lower TTFB for voice pipeline)
@@ -163,6 +182,7 @@ class OpenClawConversationAgent(conversation.AbstractConversationAgent):
async for chunk in client.async_stream_message( async for chunk in client.async_stream_message(
message=message, message=message,
session_id=conversation_id, session_id=conversation_id,
system_prompt=system_prompt,
): ):
full_response += chunk full_response += chunk
@@ -173,6 +193,7 @@ class OpenClawConversationAgent(conversation.AbstractConversationAgent):
response = await client.async_send_message( response = await client.async_send_message(
message=message, message=message,
session_id=conversation_id, session_id=conversation_id,
system_prompt=system_prompt,
) )
extracted = self._extract_text_recursive(response) extracted = self._extract_text_recursive(response)
return extracted or "" return extracted or ""
+57
View File
@@ -0,0 +1,57 @@
"""Helpers for Assist entity exposure context."""
from __future__ import annotations
from collections import Counter
from homeassistant.components.homeassistant import async_should_expose
from homeassistant.core import HomeAssistant
def build_exposed_entities_context(
hass: HomeAssistant,
assistant: str | None = "assist",
max_entities: int = 250,
) -> str | None:
"""Build a compact prompt block of entities exposed to an assistant.
Uses Home Assistant's built-in expose rules (Settings -> Voice assistants -> Expose).
"""
assistant_id = assistant or "assist"
exposed_states = [
state
for state in hass.states.async_all()
if async_should_expose(hass, assistant_id, state.entity_id)
]
if not exposed_states:
return None
exposed_states.sort(key=lambda state: state.entity_id)
domain_counts = Counter(state.domain for state in exposed_states)
lines: list[str] = [
"Home Assistant live context (entities exposed to this assistant):",
f"- total_exposed_entities: {len(exposed_states)}",
"- domain_counts:",
]
lines.extend(f" - {domain}: {count}" for domain, count in sorted(domain_counts.items()))
lines.append("- entities:")
for state in exposed_states[:max_entities]:
friendly_name = state.name or state.entity_id
lines.append(
f" - id: {state.entity_id}; name: {friendly_name}; state: {state.state}"
)
if len(exposed_states) > max_entities:
lines.append(
f" - ... {len(exposed_states) - max_entities} additional exposed entities omitted"
)
lines.append(
"Use only these exposed entities when answering or controlling Home Assistant."
)
return "\n".join(lines)
+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.12", "version": "0.1.13",
"dependencies": ["conversation"], "dependencies": ["conversation"],
"after_dependencies": ["hassio", "lovelace"] "after_dependencies": ["hassio", "lovelace"]
} }