Route OpenClaw chat by HA user

This commit is contained in:
2026-05-06 08:08:18 +02:00
parent 90d525e842
commit 8221098ebc
12 changed files with 285 additions and 68 deletions
+86 -32
View File
@@ -47,9 +47,9 @@ from .const import (
ATTR_ACCOUNT_ID,
ATTR_SOURCE,
ATTR_TIMESTAMP,
ATTR_HA_USER_ID,
CONF_ADDON_CONFIG_PATH,
CONF_AGENT_ID,
CONF_VOICE_AGENT_ID,
CONF_GATEWAY_HOST,
CONF_GATEWAY_PORT,
CONF_GATEWAY_TOKEN,
@@ -67,7 +67,6 @@ from .const import (
CONF_THINKING_TIMEOUT,
CONTEXT_STRATEGY_TRUNCATE,
DEFAULT_AGENT_ID,
DEFAULT_VOICE_AGENT_ID,
DEFAULT_CONTEXT_MAX_CHARS,
DEFAULT_CONTEXT_STRATEGY,
DEFAULT_ENABLE_TOOL_CALLS,
@@ -90,6 +89,13 @@ from .const import (
from .coordinator import OpenClawCoordinator
from .exposure import apply_context_policy, build_exposed_entities_context
from .helpers import extract_text_recursive
from .routing import (
build_scoped_session_id,
normalize_ha_user_id,
normalize_optional_text,
resolve_agent_id,
resolve_model_override,
)
_LOGGER = logging.getLogger(__name__)
@@ -107,7 +113,7 @@ _CARD_PATH = Path(__file__).parent / "www" / _CARD_FILENAME
# URL at which the card JS is served (registered via register_static_path)
_CARD_STATIC_URL = f"/openclaw/{_CARD_FILENAME}"
# Versioned URL used for Lovelace resource registration to avoid stale browser cache
_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.62"
_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.63"
OpenClawConfigEntry = ConfigEntry
@@ -125,6 +131,7 @@ SEND_MESSAGE_SCHEMA = vol.Schema(
CLEAR_HISTORY_SCHEMA = vol.Schema(
{
vol.Optional(ATTR_SESSION_ID): cv.string,
vol.Optional(ATTR_AGENT_ID): cv.string,
}
)
@@ -399,18 +406,13 @@ async def _async_add_lovelace_resource(hass: HomeAssistant, url: str) -> bool:
def _async_register_services(hass: HomeAssistant) -> None:
"""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:
"""Handle the openclaw.send_message service call."""
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"
call_agent_id = _normalize_optional_text(call.data.get(ATTR_AGENT_ID))
raw_session_id: str = call.data.get(ATTR_SESSION_ID) or "default"
ha_user_id = normalize_ha_user_id(call.context.user_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)
@@ -421,12 +423,12 @@ def _async_register_services(hass: HomeAssistant) -> None:
client: OpenClawApiClient = entry_data["client"]
coordinator: OpenClawCoordinator = entry_data["coordinator"]
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 = resolve_agent_id(ha_user_id, call_agent_id)
scoped_session_id = build_scoped_session_id(
raw_session_id,
ha_user_id,
resolved_agent_id,
)
resolved_agent_id = call_agent_id
if resolved_agent_id is None and source == "voice":
resolved_agent_id = voice_agent_id
try:
include_context = options.get(
@@ -447,13 +449,13 @@ def _async_register_services(hass: HomeAssistant) -> None:
)
system_prompt = apply_context_policy(raw_context, max_chars, strategy)
active_model = _normalize_optional_text(options.get("active_model"))
active_model = resolve_model_override(options.get("active_model"))
_append_chat_history(hass, session_id, "user", message)
_append_chat_history(hass, scoped_session_id, "user", message)
response = await client.async_send_message(
message=message,
session_id=session_id,
session_id=scoped_session_id,
system_prompt=system_prompt,
agent_id=resolved_agent_id,
model=active_model,
@@ -469,7 +471,7 @@ def _async_register_services(hass: HomeAssistant) -> None:
+ "\n".join(f"- {line}" for line in tool_results)
+ "\nRespond to the user based on these results."
),
session_id=session_id,
session_id=scoped_session_id,
system_prompt=system_prompt,
agent_id=resolved_agent_id,
model=active_model,
@@ -486,12 +488,15 @@ def _async_register_services(hass: HomeAssistant) -> None:
list(response.keys()),
)
_append_chat_history(hass, session_id, "assistant", assistant_message)
_append_chat_history(hass, scoped_session_id, "assistant", assistant_message)
hass.bus.async_fire(
EVENT_MESSAGE_RECEIVED,
{
ATTR_MESSAGE: assistant_message,
ATTR_SESSION_ID: session_id,
ATTR_SESSION_ID: raw_session_id,
ATTR_SESSION_KEY: scoped_session_id,
ATTR_HA_USER_ID: ha_user_id,
ATTR_AGENT_ID: resolved_agent_id,
ATTR_MODEL: model_used,
ATTR_TIMESTAMP: datetime.now(timezone.utc).isoformat(),
},
@@ -500,12 +505,20 @@ def _async_register_services(hass: HomeAssistant) -> None:
except OpenClawApiError as err:
_LOGGER.error("Failed to send message to OpenClaw: %s", err)
_append_chat_history(hass, session_id, "assistant", f"OpenClaw error: {err}")
_append_chat_history(
hass,
scoped_session_id,
"assistant",
f"OpenClaw error: {err}",
)
hass.bus.async_fire(
EVENT_MESSAGE_RECEIVED,
{
ATTR_MESSAGE: f"OpenClaw error: {err}",
ATTR_SESSION_ID: session_id,
ATTR_SESSION_ID: raw_session_id,
ATTR_SESSION_KEY: scoped_session_id,
ATTR_HA_USER_ID: ha_user_id,
ATTR_AGENT_ID: resolved_agent_id,
ATTR_MODEL: "unknown",
ATTR_TIMESTAMP: datetime.now(timezone.utc).isoformat(),
},
@@ -513,13 +526,32 @@ def _async_register_services(hass: HomeAssistant) -> None:
async def handle_clear_history(call: ServiceCall) -> None:
"""Handle the openclaw.clear_history service call."""
session_id: str | None = call.data.get(ATTR_SESSION_ID)
_LOGGER.info("Clear history requested (session=%s)", session_id or "all")
raw_session_id: str | None = call.data.get(ATTR_SESSION_ID)
ha_user_id = normalize_ha_user_id(call.context.user_id)
requested_agent_id = normalize_optional_text(call.data.get(ATTR_AGENT_ID))
agent_id = resolve_agent_id(ha_user_id, requested_agent_id)
_LOGGER.info(
"Clear history requested (user=%s, agent=%s, session=%s)",
ha_user_id,
agent_id,
raw_session_id or "all",
)
store = _get_chat_history_store(hass)
if session_id:
store.pop(session_id, None)
if raw_session_id:
scoped_session_id = build_scoped_session_id(
raw_session_id,
ha_user_id,
agent_id,
)
store.pop(scoped_session_id, None)
else:
store.clear()
scoped_prefix = build_scoped_session_id("", ha_user_id, agent_id).rsplit(
"__session_",
1,
)[0] + "__session_"
for key in list(store):
if key.startswith(scoped_prefix):
store.pop(key, None)
async def handle_invoke_tool(call: ServiceCall) -> None:
"""Handle the openclaw.invoke_tool service call."""
@@ -793,9 +825,26 @@ def _async_register_websocket_api(hass: HomeAssistant) -> None:
msg: dict[str, Any],
) -> None:
"""Return chat history for a session."""
session_id = msg.get("session_id") or "default"
history = _get_chat_history_store(hass).get(session_id, [])
connection.send_result(msg["id"], {"session_id": session_id, "messages": history})
raw_session_id = msg.get("session_id") or "default"
connection_user = getattr(connection, "user", None)
ha_user_id = normalize_ha_user_id(getattr(connection_user, "id", None))
agent_id = resolve_agent_id(ha_user_id)
scoped_session_id = build_scoped_session_id(
raw_session_id,
ha_user_id,
agent_id,
)
history = _get_chat_history_store(hass).get(scoped_session_id, [])
connection.send_result(
msg["id"],
{
"session_id": raw_session_id,
ATTR_SESSION_KEY: scoped_session_id,
ATTR_HA_USER_ID: ha_user_id,
ATTR_AGENT_ID: agent_id,
"messages": history,
},
)
websocket_api.async_register_command(hass, websocket_get_history)
@@ -813,6 +862,9 @@ def _async_register_websocket_api(hass: HomeAssistant) -> None:
"""Return frontend-related integration settings."""
entry_data = _get_first_entry_data(hass)
options = _get_entry_options(hass, entry_data) if entry_data else {}
connection_user = getattr(connection, "user", None)
ha_user_id = normalize_ha_user_id(getattr(connection_user, "id", None))
agent_id = resolve_agent_id(ha_user_id)
connection.send_result(
msg["id"],
{
@@ -837,6 +889,8 @@ def _async_register_websocket_api(hass: HomeAssistant) -> None:
CONF_THINKING_TIMEOUT,
DEFAULT_THINKING_TIMEOUT,
),
ATTR_HA_USER_ID: ha_user_id,
ATTR_AGENT_ID: agent_id,
"language": hass.config.language,
},
)