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.
|
||||
|
||||
## [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
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -23,8 +23,10 @@ OpenClaw is a Home Assistant custom integration that connects your HA instance t
|
||||
- **Services**
|
||||
- `openclaw.send_message`
|
||||
- `openclaw.clear_history`
|
||||
- `openclaw.invoke_tool`
|
||||
- **Event**
|
||||
- `openclaw_message_received`
|
||||
- `openclaw_tool_invoked`
|
||||
- **Sensors / status entities** for model and connection state
|
||||
|
||||
---
|
||||
@@ -191,6 +193,31 @@ data:
|
||||
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
|
||||
@@ -217,6 +244,34 @@ action:
|
||||
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
|
||||
|
||||
@@ -10,6 +10,7 @@ import json
|
||||
from datetime import datetime, timezone
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from time import perf_counter
|
||||
from typing import Any
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
@@ -32,7 +33,18 @@ from .const import (
|
||||
ATTR_ATTACHMENTS,
|
||||
ATTR_MESSAGE,
|
||||
ATTR_MODEL,
|
||||
ATTR_OK,
|
||||
ATTR_RESULT,
|
||||
ATTR_ERROR,
|
||||
ATTR_DURATION_MS,
|
||||
ATTR_SESSION_ID,
|
||||
ATTR_SESSION_KEY,
|
||||
ATTR_TOOL,
|
||||
ATTR_ACTION,
|
||||
ATTR_ARGS,
|
||||
ATTR_DRY_RUN,
|
||||
ATTR_MESSAGE_CHANNEL,
|
||||
ATTR_ACCOUNT_ID,
|
||||
ATTR_TIMESTAMP,
|
||||
CONF_ADDON_CONFIG_PATH,
|
||||
CONF_GATEWAY_HOST,
|
||||
@@ -60,9 +72,11 @@ from .const import (
|
||||
DEFAULT_VOICE_PROVIDER,
|
||||
DOMAIN,
|
||||
EVENT_MESSAGE_RECEIVED,
|
||||
EVENT_TOOL_INVOKED,
|
||||
OPENCLAW_CONFIG_REL_PATH,
|
||||
PLATFORMS,
|
||||
SERVICE_CLEAR_HISTORY,
|
||||
SERVICE_INVOKE_TOOL,
|
||||
SERVICE_SEND_MESSAGE,
|
||||
)
|
||||
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)
|
||||
_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.39"
|
||||
_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.40"
|
||||
|
||||
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:
|
||||
"""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:
|
||||
"""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:
|
||||
"""Handle the openclaw.send_message service call."""
|
||||
message: str = call.data[ATTR_MESSAGE]
|
||||
@@ -451,18 +474,94 @@ def _async_register_services(hass: HomeAssistant) -> None:
|
||||
else:
|
||||
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(
|
||||
DOMAIN,
|
||||
SERVICE_SEND_MESSAGE,
|
||||
handle_send_message,
|
||||
schema=SEND_MESSAGE_SCHEMA,
|
||||
)
|
||||
if not hass.services.has_service(DOMAIN, SERVICE_CLEAR_HISTORY):
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_CLEAR_HISTORY,
|
||||
handle_clear_history,
|
||||
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:
|
||||
@@ -521,6 +620,25 @@ def _extract_text_recursive(value: Any, depth: int = 0) -> str | 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:
|
||||
"""Extract assistant text from modern/legacy OpenAI-compatible responses."""
|
||||
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 (
|
||||
API_CHAT_COMPLETIONS,
|
||||
API_MODELS,
|
||||
API_TOOLS_INVOKE,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -387,6 +388,62 @@ class OpenClawApiClient:
|
||||
f"Cannot connect to OpenClaw gateway: {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:
|
||||
"""Close the HTTP session."""
|
||||
if self._session and not self._session.closed:
|
||||
|
||||
@@ -58,16 +58,24 @@ DATA_GATEWAY_VERSION = "gateway_version"
|
||||
DATA_UPTIME = "uptime"
|
||||
DATA_PROVIDER = "provider"
|
||||
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 = ["sensor", "binary_sensor", "conversation"]
|
||||
|
||||
# Events
|
||||
EVENT_MESSAGE_RECEIVED = f"{DOMAIN}_message_received"
|
||||
EVENT_TOOL_INVOKED = f"{DOMAIN}_tool_invoked"
|
||||
|
||||
# Services
|
||||
SERVICE_SEND_MESSAGE = "send_message"
|
||||
SERVICE_CLEAR_HISTORY = "clear_history"
|
||||
SERVICE_INVOKE_TOOL = "invoke_tool"
|
||||
|
||||
# Attributes
|
||||
ATTR_MESSAGE = "message"
|
||||
@@ -75,6 +83,17 @@ ATTR_SESSION_ID = "session_id"
|
||||
ATTR_ATTACHMENTS = "attachments"
|
||||
ATTR_MODEL = "model"
|
||||
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
|
||||
# The OpenClaw gateway exposes only the OpenAI-compatible endpoints.
|
||||
@@ -82,3 +101,4 @@ ATTR_TIMESTAMP = "timestamp"
|
||||
# home page (text/html) for any unrecognised route.
|
||||
API_MODELS = "/v1/models"
|
||||
API_CHAT_COMPLETIONS = "/v1/chat/completions"
|
||||
API_TOOLS_INVOKE = "/tools/invoke"
|
||||
|
||||
@@ -19,6 +19,12 @@ from .const import (
|
||||
DATA_CONNECTED,
|
||||
DATA_CONTEXT_WINDOW,
|
||||
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_MODEL,
|
||||
DATA_PROVIDER,
|
||||
@@ -61,6 +67,14 @@ class OpenClawCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
self._last_activity: datetime | None = None
|
||||
self._model_cache: dict[str, Any] = {}
|
||||
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]:
|
||||
"""Return a data dict representing the offline state."""
|
||||
@@ -75,6 +89,12 @@ class OpenClawCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
DATA_UPTIME: None,
|
||||
DATA_PROVIDER: self._model_cache.get(DATA_PROVIDER),
|
||||
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]:
|
||||
@@ -134,7 +154,31 @@ class OpenClawCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
# /v1/models not implemented — expected, not an error
|
||||
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._last_tool_state)
|
||||
return data
|
||||
|
||||
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.
|
||||
"""
|
||||
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",
|
||||
"issue_tracker": "https://github.com/techartdev/OpenClawHomeAssistant/issues",
|
||||
"requirements": [],
|
||||
"version": "0.1.39",
|
||||
"version": "0.1.40",
|
||||
"dependencies": ["conversation"],
|
||||
"after_dependencies": ["hassio", "lovelace"]
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import (
|
||||
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_MODEL,
|
||||
DATA_PROVIDER,
|
||||
@@ -55,6 +61,32 @@ SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
||||
name="OpenClaw Model",
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -35,3 +35,53 @@ clear_history:
|
||||
required: false
|
||||
selector:
|
||||
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": {
|
||||
"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": {
|
||||
@@ -99,6 +111,40 @@
|
||||
"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": {
|
||||
"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": {
|
||||
@@ -99,6 +111,40 @@
|
||||
"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 () => {
|
||||
try {
|
||||
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);
|
||||
await import(src);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user