Update version to 0.1.40 and add OpenClaw Gateway tools endpoint integration with new service and event support
This commit is contained in:
@@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
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.40] - 2026-02-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Added OpenClaw Gateway tools endpoint integration (`POST /tools/invoke`) in the API client.
|
||||||
|
- Added new Home Assistant service `openclaw.invoke_tool` with support for tool/action/args/session routing fields.
|
||||||
|
- Added new event `openclaw_tool_invoked` for automation hooks on tool execution results.
|
||||||
|
- Added tool telemetry sensors: last tool name, status, duration, and invocation timestamp.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Coordinator now performs a best-effort `sessions_list` tool invocation to populate session count/list when available.
|
||||||
|
|
||||||
## [0.1.39] - 2026-02-20
|
## [0.1.39] - 2026-02-20
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -23,8 +23,10 @@ OpenClaw is a Home Assistant custom integration that connects your HA instance t
|
|||||||
- **Services**
|
- **Services**
|
||||||
- `openclaw.send_message`
|
- `openclaw.send_message`
|
||||||
- `openclaw.clear_history`
|
- `openclaw.clear_history`
|
||||||
|
- `openclaw.invoke_tool`
|
||||||
- **Event**
|
- **Event**
|
||||||
- `openclaw_message_received`
|
- `openclaw_message_received`
|
||||||
|
- `openclaw_tool_invoked`
|
||||||
- **Sensors / status entities** for model and connection state
|
- **Sensors / status entities** for model and connection state
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -191,6 +193,31 @@ data:
|
|||||||
session_id: "living-room-session"
|
session_id: "living-room-session"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `openclaw.invoke_tool`
|
||||||
|
|
||||||
|
Invoke a single OpenClaw Gateway tool directly.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
|
||||||
|
- `tool` (required)
|
||||||
|
- `action` (optional)
|
||||||
|
- `args` (optional object)
|
||||||
|
- `session_key` (optional)
|
||||||
|
- `dry_run` (optional)
|
||||||
|
- `message_channel` (optional)
|
||||||
|
- `account_id` (optional)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
service: openclaw.invoke_tool
|
||||||
|
data:
|
||||||
|
tool: sessions_list
|
||||||
|
action: json
|
||||||
|
args: {}
|
||||||
|
session_key: main
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Event
|
## Event
|
||||||
@@ -217,6 +244,34 @@ action:
|
|||||||
message: "{{ trigger.event.data.message }}"
|
message: "{{ trigger.event.data.message }}"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `openclaw_tool_invoked`
|
||||||
|
|
||||||
|
Fired when `openclaw.invoke_tool` completes.
|
||||||
|
|
||||||
|
Event data includes:
|
||||||
|
|
||||||
|
- `tool`
|
||||||
|
- `ok`
|
||||||
|
- `result`
|
||||||
|
- `error`
|
||||||
|
- `duration_ms`
|
||||||
|
- `timestamp`
|
||||||
|
|
||||||
|
Automation example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
trigger:
|
||||||
|
- platform: event
|
||||||
|
event_type: openclaw_tool_invoked
|
||||||
|
condition:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ trigger.event.data.ok == false }}"
|
||||||
|
action:
|
||||||
|
- service: notify.mobile_app_phone
|
||||||
|
data:
|
||||||
|
message: "Tool {{ trigger.event.data.tool }} failed: {{ trigger.event.data.error }}"
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import json
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from time import perf_counter
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
@@ -32,7 +33,18 @@ from .const import (
|
|||||||
ATTR_ATTACHMENTS,
|
ATTR_ATTACHMENTS,
|
||||||
ATTR_MESSAGE,
|
ATTR_MESSAGE,
|
||||||
ATTR_MODEL,
|
ATTR_MODEL,
|
||||||
|
ATTR_OK,
|
||||||
|
ATTR_RESULT,
|
||||||
|
ATTR_ERROR,
|
||||||
|
ATTR_DURATION_MS,
|
||||||
ATTR_SESSION_ID,
|
ATTR_SESSION_ID,
|
||||||
|
ATTR_SESSION_KEY,
|
||||||
|
ATTR_TOOL,
|
||||||
|
ATTR_ACTION,
|
||||||
|
ATTR_ARGS,
|
||||||
|
ATTR_DRY_RUN,
|
||||||
|
ATTR_MESSAGE_CHANNEL,
|
||||||
|
ATTR_ACCOUNT_ID,
|
||||||
ATTR_TIMESTAMP,
|
ATTR_TIMESTAMP,
|
||||||
CONF_ADDON_CONFIG_PATH,
|
CONF_ADDON_CONFIG_PATH,
|
||||||
CONF_GATEWAY_HOST,
|
CONF_GATEWAY_HOST,
|
||||||
@@ -60,9 +72,11 @@ from .const import (
|
|||||||
DEFAULT_VOICE_PROVIDER,
|
DEFAULT_VOICE_PROVIDER,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
EVENT_MESSAGE_RECEIVED,
|
EVENT_MESSAGE_RECEIVED,
|
||||||
|
EVENT_TOOL_INVOKED,
|
||||||
OPENCLAW_CONFIG_REL_PATH,
|
OPENCLAW_CONFIG_REL_PATH,
|
||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
SERVICE_CLEAR_HISTORY,
|
SERVICE_CLEAR_HISTORY,
|
||||||
|
SERVICE_INVOKE_TOOL,
|
||||||
SERVICE_SEND_MESSAGE,
|
SERVICE_SEND_MESSAGE,
|
||||||
)
|
)
|
||||||
from .coordinator import OpenClawCoordinator
|
from .coordinator import OpenClawCoordinator
|
||||||
@@ -78,7 +92,7 @@ _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.39"
|
_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.40"
|
||||||
|
|
||||||
OpenClawConfigEntry = ConfigEntry
|
OpenClawConfigEntry = ConfigEntry
|
||||||
|
|
||||||
@@ -98,6 +112,18 @@ CLEAR_HISTORY_SCHEMA = vol.Schema(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
INVOKE_TOOL_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(ATTR_TOOL): cv.string,
|
||||||
|
vol.Optional(ATTR_ACTION): cv.string,
|
||||||
|
vol.Optional(ATTR_ARGS, default={}): dict,
|
||||||
|
vol.Optional(ATTR_SESSION_KEY): cv.string,
|
||||||
|
vol.Optional(ATTR_DRY_RUN, default=False): cv.boolean,
|
||||||
|
vol.Optional(ATTR_MESSAGE_CHANNEL): cv.string,
|
||||||
|
vol.Optional(ATTR_ACCOUNT_ID): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: OpenClawConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: OpenClawConfigEntry) -> bool:
|
||||||
"""Set up OpenClaw from a config entry.
|
"""Set up OpenClaw from a config entry.
|
||||||
@@ -348,9 +374,6 @@ 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."""
|
||||||
|
|
||||||
if hass.services.has_service(DOMAIN, SERVICE_SEND_MESSAGE):
|
|
||||||
return
|
|
||||||
|
|
||||||
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]
|
||||||
@@ -451,18 +474,94 @@ def _async_register_services(hass: HomeAssistant) -> None:
|
|||||||
else:
|
else:
|
||||||
store.clear()
|
store.clear()
|
||||||
|
|
||||||
|
async def handle_invoke_tool(call: ServiceCall) -> None:
|
||||||
|
"""Handle the openclaw.invoke_tool service call."""
|
||||||
|
tool_name: str = call.data[ATTR_TOOL]
|
||||||
|
action: str | None = call.data.get(ATTR_ACTION)
|
||||||
|
args: dict[str, Any] = call.data.get(ATTR_ARGS) or {}
|
||||||
|
session_key: str | None = call.data.get(ATTR_SESSION_KEY)
|
||||||
|
dry_run: bool = bool(call.data.get(ATTR_DRY_RUN, False))
|
||||||
|
message_channel: str | None = call.data.get(ATTR_MESSAGE_CHANNEL)
|
||||||
|
account_id: str | None = call.data.get(ATTR_ACCOUNT_ID)
|
||||||
|
|
||||||
|
entry_data = _get_first_entry_data(hass)
|
||||||
|
if not entry_data:
|
||||||
|
_LOGGER.error("No OpenClaw integration configured")
|
||||||
|
return
|
||||||
|
|
||||||
|
client: OpenClawApiClient = entry_data["client"]
|
||||||
|
coordinator: OpenClawCoordinator = entry_data["coordinator"]
|
||||||
|
|
||||||
|
started = perf_counter()
|
||||||
|
ok = False
|
||||||
|
result: Any = None
|
||||||
|
error_message: str | None = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await client.async_invoke_tool(
|
||||||
|
tool=tool_name,
|
||||||
|
action=action,
|
||||||
|
args=args,
|
||||||
|
session_key=session_key,
|
||||||
|
dry_run=dry_run,
|
||||||
|
message_channel=message_channel,
|
||||||
|
account_id=account_id,
|
||||||
|
)
|
||||||
|
ok = bool(response.get("ok", True)) if isinstance(response, dict) else True
|
||||||
|
result = response.get("result") if isinstance(response, dict) else response
|
||||||
|
if isinstance(response, dict) and response.get("error"):
|
||||||
|
error_message = str(response.get("error"))
|
||||||
|
except OpenClawApiError as err:
|
||||||
|
ok = False
|
||||||
|
error_message = str(err)
|
||||||
|
|
||||||
|
duration_ms = int((perf_counter() - started) * 1000)
|
||||||
|
result_preview = _summarize_tool_result(result)
|
||||||
|
coordinator.record_tool_invocation(
|
||||||
|
tool_name=tool_name,
|
||||||
|
ok=ok,
|
||||||
|
duration_ms=duration_ms,
|
||||||
|
error_message=error_message,
|
||||||
|
result_preview=result_preview,
|
||||||
|
)
|
||||||
|
|
||||||
|
event_payload = {
|
||||||
|
ATTR_TOOL: tool_name,
|
||||||
|
ATTR_ACTION: action,
|
||||||
|
ATTR_SESSION_KEY: session_key,
|
||||||
|
ATTR_DRY_RUN: dry_run,
|
||||||
|
ATTR_OK: ok,
|
||||||
|
ATTR_RESULT: result,
|
||||||
|
ATTR_ERROR: error_message,
|
||||||
|
ATTR_DURATION_MS: duration_ms,
|
||||||
|
ATTR_TIMESTAMP: datetime.now(timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
hass.bus.async_fire(EVENT_TOOL_INVOKED, event_payload)
|
||||||
|
|
||||||
|
if not ok:
|
||||||
|
raise OpenClawApiError(error_message or "Tool invocation failed")
|
||||||
|
|
||||||
|
if not hass.services.has_service(DOMAIN, SERVICE_SEND_MESSAGE):
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_SEND_MESSAGE,
|
SERVICE_SEND_MESSAGE,
|
||||||
handle_send_message,
|
handle_send_message,
|
||||||
schema=SEND_MESSAGE_SCHEMA,
|
schema=SEND_MESSAGE_SCHEMA,
|
||||||
)
|
)
|
||||||
|
if not hass.services.has_service(DOMAIN, SERVICE_CLEAR_HISTORY):
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_CLEAR_HISTORY,
|
SERVICE_CLEAR_HISTORY,
|
||||||
handle_clear_history,
|
handle_clear_history,
|
||||||
schema=CLEAR_HISTORY_SCHEMA,
|
schema=CLEAR_HISTORY_SCHEMA,
|
||||||
)
|
)
|
||||||
|
if not hass.services.has_service(DOMAIN, SERVICE_INVOKE_TOOL):
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_INVOKE_TOOL,
|
||||||
|
handle_invoke_tool,
|
||||||
|
schema=INVOKE_TOOL_SCHEMA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_first_entry_data(hass: HomeAssistant) -> dict[str, Any] | None:
|
def _get_first_entry_data(hass: HomeAssistant) -> dict[str, Any] | None:
|
||||||
@@ -521,6 +620,25 @@ def _extract_text_recursive(value: Any, depth: int = 0) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _summarize_tool_result(value: Any, max_len: int = 240) -> str | None:
|
||||||
|
"""Return compact string preview of tool result payload."""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, str):
|
||||||
|
text = value.strip()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
text = json.dumps(value, ensure_ascii=False)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
text = str(value)
|
||||||
|
text = text.strip()
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
if len(text) > max_len:
|
||||||
|
return f"{text[:max_len]}…"
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def _extract_assistant_message(response: dict[str, Any]) -> str | None:
|
def _extract_assistant_message(response: dict[str, Any]) -> str | None:
|
||||||
"""Extract assistant text from modern/legacy OpenAI-compatible responses."""
|
"""Extract assistant text from modern/legacy OpenAI-compatible responses."""
|
||||||
return _extract_text_recursive(response)
|
return _extract_text_recursive(response)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -12,6 +12,7 @@ import aiohttp
|
|||||||
from .const import (
|
from .const import (
|
||||||
API_CHAT_COMPLETIONS,
|
API_CHAT_COMPLETIONS,
|
||||||
API_MODELS,
|
API_MODELS,
|
||||||
|
API_TOOLS_INVOKE,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -387,6 +388,62 @@ class OpenClawApiClient:
|
|||||||
f"Cannot connect to OpenClaw gateway: {err}"
|
f"Cannot connect to OpenClaw gateway: {err}"
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
|
async def async_invoke_tool(
|
||||||
|
self,
|
||||||
|
tool: str,
|
||||||
|
action: str | None = None,
|
||||||
|
args: dict[str, Any] | None = None,
|
||||||
|
session_key: str | None = None,
|
||||||
|
dry_run: bool = False,
|
||||||
|
message_channel: str | None = None,
|
||||||
|
account_id: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Invoke a single OpenClaw tool via gateway HTTP endpoint."""
|
||||||
|
payload: dict[str, Any] = {
|
||||||
|
"tool": tool,
|
||||||
|
"args": args or {},
|
||||||
|
"dryRun": bool(dry_run),
|
||||||
|
}
|
||||||
|
if action:
|
||||||
|
payload["action"] = action
|
||||||
|
if session_key:
|
||||||
|
payload["sessionKey"] = session_key
|
||||||
|
|
||||||
|
headers = self._headers()
|
||||||
|
if message_channel:
|
||||||
|
headers["x-openclaw-message-channel"] = message_channel
|
||||||
|
if account_id:
|
||||||
|
headers["x-openclaw-account-id"] = account_id
|
||||||
|
|
||||||
|
session = await self._get_session()
|
||||||
|
url = f"{self._base_url}{API_TOOLS_INVOKE}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with session.post(
|
||||||
|
url,
|
||||||
|
headers=headers,
|
||||||
|
json=payload,
|
||||||
|
timeout=STREAM_TIMEOUT,
|
||||||
|
) as resp:
|
||||||
|
if resp.status == 401:
|
||||||
|
raise OpenClawAuthError("Authentication failed")
|
||||||
|
if resp.status >= 400:
|
||||||
|
text = await resp.text()
|
||||||
|
raise OpenClawApiError(f"Tool invoke error {resp.status}: {text[:300]}")
|
||||||
|
|
||||||
|
content_type = resp.content_type or ""
|
||||||
|
if "json" not in content_type:
|
||||||
|
text = await resp.text()
|
||||||
|
raise OpenClawApiError(
|
||||||
|
f"Unexpected tool response content type '{content_type}': {text[:300]}"
|
||||||
|
)
|
||||||
|
return await resp.json()
|
||||||
|
|
||||||
|
except (aiohttp.ClientConnectorError, aiohttp.ClientOSError, asyncio.TimeoutError) as err:
|
||||||
|
raise OpenClawConnectionError(
|
||||||
|
f"Cannot connect to OpenClaw gateway: {err}"
|
||||||
|
) from err
|
||||||
|
|
||||||
async def async_close(self) -> None:
|
async def async_close(self) -> None:
|
||||||
"""Close the HTTP session."""
|
"""Close the HTTP session."""
|
||||||
if self._session and not self._session.closed:
|
if self._session and not self._session.closed:
|
||||||
|
|||||||
@@ -58,16 +58,24 @@ DATA_GATEWAY_VERSION = "gateway_version"
|
|||||||
DATA_UPTIME = "uptime"
|
DATA_UPTIME = "uptime"
|
||||||
DATA_PROVIDER = "provider"
|
DATA_PROVIDER = "provider"
|
||||||
DATA_CONTEXT_WINDOW = "context_window"
|
DATA_CONTEXT_WINDOW = "context_window"
|
||||||
|
DATA_LAST_TOOL_NAME = "last_tool_name"
|
||||||
|
DATA_LAST_TOOL_STATUS = "last_tool_status"
|
||||||
|
DATA_LAST_TOOL_DURATION_MS = "last_tool_duration_ms"
|
||||||
|
DATA_LAST_TOOL_INVOKED_AT = "last_tool_invoked_at"
|
||||||
|
DATA_LAST_TOOL_ERROR = "last_tool_error"
|
||||||
|
DATA_LAST_TOOL_RESULT_PREVIEW = "last_tool_result_preview"
|
||||||
|
|
||||||
# Platforms
|
# Platforms
|
||||||
PLATFORMS = ["sensor", "binary_sensor", "conversation"]
|
PLATFORMS = ["sensor", "binary_sensor", "conversation"]
|
||||||
|
|
||||||
# Events
|
# Events
|
||||||
EVENT_MESSAGE_RECEIVED = f"{DOMAIN}_message_received"
|
EVENT_MESSAGE_RECEIVED = f"{DOMAIN}_message_received"
|
||||||
|
EVENT_TOOL_INVOKED = f"{DOMAIN}_tool_invoked"
|
||||||
|
|
||||||
# Services
|
# Services
|
||||||
SERVICE_SEND_MESSAGE = "send_message"
|
SERVICE_SEND_MESSAGE = "send_message"
|
||||||
SERVICE_CLEAR_HISTORY = "clear_history"
|
SERVICE_CLEAR_HISTORY = "clear_history"
|
||||||
|
SERVICE_INVOKE_TOOL = "invoke_tool"
|
||||||
|
|
||||||
# Attributes
|
# Attributes
|
||||||
ATTR_MESSAGE = "message"
|
ATTR_MESSAGE = "message"
|
||||||
@@ -75,6 +83,17 @@ ATTR_SESSION_ID = "session_id"
|
|||||||
ATTR_ATTACHMENTS = "attachments"
|
ATTR_ATTACHMENTS = "attachments"
|
||||||
ATTR_MODEL = "model"
|
ATTR_MODEL = "model"
|
||||||
ATTR_TIMESTAMP = "timestamp"
|
ATTR_TIMESTAMP = "timestamp"
|
||||||
|
ATTR_TOOL = "tool"
|
||||||
|
ATTR_ACTION = "action"
|
||||||
|
ATTR_ARGS = "args"
|
||||||
|
ATTR_SESSION_KEY = "session_key"
|
||||||
|
ATTR_DRY_RUN = "dry_run"
|
||||||
|
ATTR_MESSAGE_CHANNEL = "message_channel"
|
||||||
|
ATTR_ACCOUNT_ID = "account_id"
|
||||||
|
ATTR_OK = "ok"
|
||||||
|
ATTR_RESULT = "result"
|
||||||
|
ATTR_ERROR = "error"
|
||||||
|
ATTR_DURATION_MS = "duration_ms"
|
||||||
|
|
||||||
# API endpoints
|
# API endpoints
|
||||||
# The OpenClaw gateway exposes only the OpenAI-compatible endpoints.
|
# The OpenClaw gateway exposes only the OpenAI-compatible endpoints.
|
||||||
@@ -82,3 +101,4 @@ ATTR_TIMESTAMP = "timestamp"
|
|||||||
# home page (text/html) for any unrecognised route.
|
# home page (text/html) for any unrecognised route.
|
||||||
API_MODELS = "/v1/models"
|
API_MODELS = "/v1/models"
|
||||||
API_CHAT_COMPLETIONS = "/v1/chat/completions"
|
API_CHAT_COMPLETIONS = "/v1/chat/completions"
|
||||||
|
API_TOOLS_INVOKE = "/tools/invoke"
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ from .const import (
|
|||||||
DATA_CONNECTED,
|
DATA_CONNECTED,
|
||||||
DATA_CONTEXT_WINDOW,
|
DATA_CONTEXT_WINDOW,
|
||||||
DATA_GATEWAY_VERSION,
|
DATA_GATEWAY_VERSION,
|
||||||
|
DATA_LAST_TOOL_DURATION_MS,
|
||||||
|
DATA_LAST_TOOL_ERROR,
|
||||||
|
DATA_LAST_TOOL_INVOKED_AT,
|
||||||
|
DATA_LAST_TOOL_NAME,
|
||||||
|
DATA_LAST_TOOL_RESULT_PREVIEW,
|
||||||
|
DATA_LAST_TOOL_STATUS,
|
||||||
DATA_LAST_ACTIVITY,
|
DATA_LAST_ACTIVITY,
|
||||||
DATA_MODEL,
|
DATA_MODEL,
|
||||||
DATA_PROVIDER,
|
DATA_PROVIDER,
|
||||||
@@ -61,6 +67,14 @@ class OpenClawCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
self._last_activity: datetime | None = None
|
self._last_activity: datetime | None = None
|
||||||
self._model_cache: dict[str, Any] = {}
|
self._model_cache: dict[str, Any] = {}
|
||||||
self._consecutive_failures = 0
|
self._consecutive_failures = 0
|
||||||
|
self._last_tool_state: dict[str, Any] = {
|
||||||
|
DATA_LAST_TOOL_NAME: None,
|
||||||
|
DATA_LAST_TOOL_STATUS: None,
|
||||||
|
DATA_LAST_TOOL_DURATION_MS: None,
|
||||||
|
DATA_LAST_TOOL_INVOKED_AT: None,
|
||||||
|
DATA_LAST_TOOL_ERROR: None,
|
||||||
|
DATA_LAST_TOOL_RESULT_PREVIEW: None,
|
||||||
|
}
|
||||||
|
|
||||||
def _offline_data(self) -> dict[str, Any]:
|
def _offline_data(self) -> dict[str, Any]:
|
||||||
"""Return a data dict representing the offline state."""
|
"""Return a data dict representing the offline state."""
|
||||||
@@ -75,6 +89,12 @@ class OpenClawCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
DATA_UPTIME: None,
|
DATA_UPTIME: None,
|
||||||
DATA_PROVIDER: self._model_cache.get(DATA_PROVIDER),
|
DATA_PROVIDER: self._model_cache.get(DATA_PROVIDER),
|
||||||
DATA_CONTEXT_WINDOW: self._model_cache.get(DATA_CONTEXT_WINDOW),
|
DATA_CONTEXT_WINDOW: self._model_cache.get(DATA_CONTEXT_WINDOW),
|
||||||
|
DATA_LAST_TOOL_NAME: self._last_tool_state.get(DATA_LAST_TOOL_NAME),
|
||||||
|
DATA_LAST_TOOL_STATUS: self._last_tool_state.get(DATA_LAST_TOOL_STATUS),
|
||||||
|
DATA_LAST_TOOL_DURATION_MS: self._last_tool_state.get(DATA_LAST_TOOL_DURATION_MS),
|
||||||
|
DATA_LAST_TOOL_INVOKED_AT: self._last_tool_state.get(DATA_LAST_TOOL_INVOKED_AT),
|
||||||
|
DATA_LAST_TOOL_ERROR: self._last_tool_state.get(DATA_LAST_TOOL_ERROR),
|
||||||
|
DATA_LAST_TOOL_RESULT_PREVIEW: self._last_tool_state.get(DATA_LAST_TOOL_RESULT_PREVIEW),
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _async_update_data(self) -> dict[str, Any]:
|
async def _async_update_data(self) -> dict[str, Any]:
|
||||||
@@ -134,7 +154,31 @@ class OpenClawCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
# /v1/models not implemented — expected, not an error
|
# /v1/models not implemented — expected, not an error
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# ── Best-effort sessions_list via tools invoke ──────────────
|
||||||
|
try:
|
||||||
|
tool_resp = await self.client.async_invoke_tool(
|
||||||
|
tool="sessions_list",
|
||||||
|
action="json",
|
||||||
|
args={},
|
||||||
|
)
|
||||||
|
result = tool_resp.get("result") if isinstance(tool_resp, dict) else None
|
||||||
|
sessions: list[dict[str, Any]] = []
|
||||||
|
if isinstance(result, list):
|
||||||
|
sessions = [item for item in result if isinstance(item, dict)]
|
||||||
|
elif isinstance(result, dict):
|
||||||
|
candidates = result.get("sessions") or result.get("items") or result.get("data")
|
||||||
|
if isinstance(candidates, list):
|
||||||
|
sessions = [item for item in candidates if isinstance(item, dict)]
|
||||||
|
|
||||||
|
if sessions:
|
||||||
|
data[DATA_SESSIONS] = sessions
|
||||||
|
data[DATA_SESSION_COUNT] = len(sessions)
|
||||||
|
except OpenClawApiError:
|
||||||
|
# tools endpoint may be policy-limited; not fatal
|
||||||
|
pass
|
||||||
|
|
||||||
data.update(self._model_cache)
|
data.update(self._model_cache)
|
||||||
|
data.update(self._last_tool_state)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
async def _try_refresh_token(self) -> None:
|
async def _try_refresh_token(self) -> None:
|
||||||
@@ -154,3 +198,25 @@ class OpenClawCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
Called when a message is sent/received through the integration.
|
Called when a message is sent/received through the integration.
|
||||||
"""
|
"""
|
||||||
self._last_activity = datetime.now(timezone.utc)
|
self._last_activity = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
def record_tool_invocation(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
tool_name: str,
|
||||||
|
ok: bool,
|
||||||
|
duration_ms: int,
|
||||||
|
error_message: str | None = None,
|
||||||
|
result_preview: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Store latest tool invocation metadata and update entities immediately."""
|
||||||
|
self._last_tool_state = {
|
||||||
|
DATA_LAST_TOOL_NAME: tool_name,
|
||||||
|
DATA_LAST_TOOL_STATUS: "ok" if ok else "error",
|
||||||
|
DATA_LAST_TOOL_DURATION_MS: duration_ms,
|
||||||
|
DATA_LAST_TOOL_INVOKED_AT: datetime.now(timezone.utc),
|
||||||
|
DATA_LAST_TOOL_ERROR: error_message,
|
||||||
|
DATA_LAST_TOOL_RESULT_PREVIEW: result_preview,
|
||||||
|
}
|
||||||
|
current = dict(self.data or self._offline_data())
|
||||||
|
current.update(self._last_tool_state)
|
||||||
|
self.async_set_updated_data(current)
|
||||||
|
|||||||
@@ -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.39",
|
"version": "0.1.40",
|
||||||
"dependencies": ["conversation"],
|
"dependencies": ["conversation"],
|
||||||
"after_dependencies": ["hassio", "lovelace"]
|
"after_dependencies": ["hassio", "lovelace"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DATA_GATEWAY_VERSION,
|
DATA_GATEWAY_VERSION,
|
||||||
|
DATA_LAST_TOOL_DURATION_MS,
|
||||||
|
DATA_LAST_TOOL_ERROR,
|
||||||
|
DATA_LAST_TOOL_INVOKED_AT,
|
||||||
|
DATA_LAST_TOOL_NAME,
|
||||||
|
DATA_LAST_TOOL_RESULT_PREVIEW,
|
||||||
|
DATA_LAST_TOOL_STATUS,
|
||||||
DATA_LAST_ACTIVITY,
|
DATA_LAST_ACTIVITY,
|
||||||
DATA_MODEL,
|
DATA_MODEL,
|
||||||
DATA_PROVIDER,
|
DATA_PROVIDER,
|
||||||
@@ -55,6 +61,32 @@ SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
|||||||
name="OpenClaw Model",
|
name="OpenClaw Model",
|
||||||
icon="mdi:brain",
|
icon="mdi:brain",
|
||||||
),
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key=DATA_LAST_TOOL_NAME,
|
||||||
|
translation_key="last_tool_name",
|
||||||
|
name="OpenClaw Last Tool",
|
||||||
|
icon="mdi:tools",
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key=DATA_LAST_TOOL_STATUS,
|
||||||
|
translation_key="last_tool_status",
|
||||||
|
name="OpenClaw Last Tool Status",
|
||||||
|
icon="mdi:check-decagram",
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key=DATA_LAST_TOOL_DURATION_MS,
|
||||||
|
translation_key="last_tool_duration_ms",
|
||||||
|
name="OpenClaw Last Tool Duration",
|
||||||
|
icon="mdi:speedometer",
|
||||||
|
native_unit_of_measurement="ms",
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key=DATA_LAST_TOOL_INVOKED_AT,
|
||||||
|
translation_key="last_tool_invoked_at",
|
||||||
|
name="OpenClaw Last Tool Invoked",
|
||||||
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
|
icon="mdi:clock-outline",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -135,4 +167,11 @@ class OpenClawSensor(CoordinatorEntity[OpenClawCoordinator], SensorEntity):
|
|||||||
"last_message_preview": None, # TODO: populate from last message
|
"last_message_preview": None, # TODO: populate from last message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if key in {DATA_LAST_TOOL_NAME, DATA_LAST_TOOL_STATUS, DATA_LAST_TOOL_DURATION_MS}:
|
||||||
|
return {
|
||||||
|
"error": data.get(DATA_LAST_TOOL_ERROR),
|
||||||
|
"result_preview": data.get(DATA_LAST_TOOL_RESULT_PREVIEW),
|
||||||
|
"invoked_at": data.get(DATA_LAST_TOOL_INVOKED_AT),
|
||||||
|
}
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -35,3 +35,53 @@ clear_history:
|
|||||||
required: false
|
required: false
|
||||||
selector:
|
selector:
|
||||||
text:
|
text:
|
||||||
|
|
||||||
|
invoke_tool:
|
||||||
|
name: Invoke Tool
|
||||||
|
description: Invoke a single OpenClaw Gateway tool via /tools/invoke.
|
||||||
|
fields:
|
||||||
|
tool:
|
||||||
|
name: Tool
|
||||||
|
description: Tool name to invoke.
|
||||||
|
required: true
|
||||||
|
example: "sessions_list"
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
action:
|
||||||
|
name: Action
|
||||||
|
description: Optional action string passed to the tool endpoint.
|
||||||
|
required: false
|
||||||
|
example: "json"
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
args:
|
||||||
|
name: Args
|
||||||
|
description: Tool-specific args object.
|
||||||
|
required: false
|
||||||
|
selector:
|
||||||
|
object:
|
||||||
|
session_key:
|
||||||
|
name: Session Key
|
||||||
|
description: Optional target OpenClaw session key.
|
||||||
|
required: false
|
||||||
|
example: "main"
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
dry_run:
|
||||||
|
name: Dry Run
|
||||||
|
description: Optional dry-run flag (reserved by gateway).
|
||||||
|
required: false
|
||||||
|
selector:
|
||||||
|
boolean:
|
||||||
|
message_channel:
|
||||||
|
name: Message Channel
|
||||||
|
description: Optional channel hint header (e.g. slack, telegram).
|
||||||
|
required: false
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
account_id:
|
||||||
|
name: Account ID
|
||||||
|
description: Optional account hint header for multi-account routing.
|
||||||
|
required: false
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
|||||||
@@ -63,6 +63,18 @@
|
|||||||
},
|
},
|
||||||
"model": {
|
"model": {
|
||||||
"name": "Model"
|
"name": "Model"
|
||||||
|
},
|
||||||
|
"last_tool_name": {
|
||||||
|
"name": "Last Tool"
|
||||||
|
},
|
||||||
|
"last_tool_status": {
|
||||||
|
"name": "Last Tool Status"
|
||||||
|
},
|
||||||
|
"last_tool_duration_ms": {
|
||||||
|
"name": "Last Tool Duration"
|
||||||
|
},
|
||||||
|
"last_tool_invoked_at": {
|
||||||
|
"name": "Last Tool Invoked"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
@@ -99,6 +111,40 @@
|
|||||||
"description": "Session to clear."
|
"description": "Session to clear."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"invoke_tool": {
|
||||||
|
"name": "Invoke Tool",
|
||||||
|
"description": "Invoke a single OpenClaw gateway tool.",
|
||||||
|
"fields": {
|
||||||
|
"tool": {
|
||||||
|
"name": "Tool",
|
||||||
|
"description": "Tool name to invoke."
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"name": "Action",
|
||||||
|
"description": "Optional action string for the tool."
|
||||||
|
},
|
||||||
|
"args": {
|
||||||
|
"name": "Args",
|
||||||
|
"description": "Tool arguments object."
|
||||||
|
},
|
||||||
|
"session_key": {
|
||||||
|
"name": "Session Key",
|
||||||
|
"description": "Optional target session key."
|
||||||
|
},
|
||||||
|
"dry_run": {
|
||||||
|
"name": "Dry Run",
|
||||||
|
"description": "Optional dry-run flag."
|
||||||
|
},
|
||||||
|
"message_channel": {
|
||||||
|
"name": "Message Channel",
|
||||||
|
"description": "Optional channel context header."
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "Account ID",
|
||||||
|
"description": "Optional account context header."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,18 @@
|
|||||||
},
|
},
|
||||||
"model": {
|
"model": {
|
||||||
"name": "Model"
|
"name": "Model"
|
||||||
|
},
|
||||||
|
"last_tool_name": {
|
||||||
|
"name": "Last Tool"
|
||||||
|
},
|
||||||
|
"last_tool_status": {
|
||||||
|
"name": "Last Tool Status"
|
||||||
|
},
|
||||||
|
"last_tool_duration_ms": {
|
||||||
|
"name": "Last Tool Duration"
|
||||||
|
},
|
||||||
|
"last_tool_invoked_at": {
|
||||||
|
"name": "Last Tool Invoked"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
@@ -99,6 +111,40 @@
|
|||||||
"description": "Session to clear."
|
"description": "Session to clear."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"invoke_tool": {
|
||||||
|
"name": "Invoke Tool",
|
||||||
|
"description": "Invoke a single OpenClaw gateway tool.",
|
||||||
|
"fields": {
|
||||||
|
"tool": {
|
||||||
|
"name": "Tool",
|
||||||
|
"description": "Tool name to invoke."
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"name": "Action",
|
||||||
|
"description": "Optional action string for the tool."
|
||||||
|
},
|
||||||
|
"args": {
|
||||||
|
"name": "Args",
|
||||||
|
"description": "Tool arguments object."
|
||||||
|
},
|
||||||
|
"session_key": {
|
||||||
|
"name": "Session Key",
|
||||||
|
"description": "Optional target session key."
|
||||||
|
},
|
||||||
|
"dry_run": {
|
||||||
|
"name": "Dry Run",
|
||||||
|
"description": "Optional dry-run flag."
|
||||||
|
},
|
||||||
|
"message_channel": {
|
||||||
|
"name": "Message Channel",
|
||||||
|
"description": "Optional channel context header."
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "Account ID",
|
||||||
|
"description": "Optional account context header."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
if (!customElements.get("openclaw-chat-card")) {
|
if (!customElements.get("openclaw-chat-card")) {
|
||||||
const src = "/openclaw/openclaw-chat-card.js?v=0.1.39";
|
const src = "/openclaw/openclaw-chat-card.js?v=0.1.40";
|
||||||
console.info("OpenClaw loader importing", src);
|
console.info("OpenClaw loader importing", src);
|
||||||
await import(src);
|
await import(src);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user