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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user