diff --git a/CHANGELOG.md b/CHANGELOG.md index dfacc7a..0cdd6c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index a2fae6d..df9779f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/custom_components/openclaw/__init__.py b/custom_components/openclaw/__init__.py index 5b0a610..27169de 100644 --- a/custom_components/openclaw/__init__.py +++ b/custom_components/openclaw/__init__.py @@ -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() - hass.services.async_register( - DOMAIN, - SERVICE_SEND_MESSAGE, - handle_send_message, - schema=SEND_MESSAGE_SCHEMA, - ) - hass.services.async_register( - DOMAIN, - SERVICE_CLEAR_HISTORY, - handle_clear_history, - schema=CLEAR_HISTORY_SCHEMA, - ) + 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) diff --git a/custom_components/openclaw/__pycache__/__init__.cpython-312.pyc b/custom_components/openclaw/__pycache__/__init__.cpython-312.pyc index 69d0de4..217e612 100644 Binary files a/custom_components/openclaw/__pycache__/__init__.cpython-312.pyc and b/custom_components/openclaw/__pycache__/__init__.cpython-312.pyc differ diff --git a/custom_components/openclaw/__pycache__/api.cpython-312.pyc b/custom_components/openclaw/__pycache__/api.cpython-312.pyc index 8aff5c3..ab2066d 100644 Binary files a/custom_components/openclaw/__pycache__/api.cpython-312.pyc and b/custom_components/openclaw/__pycache__/api.cpython-312.pyc differ diff --git a/custom_components/openclaw/__pycache__/coordinator.cpython-312.pyc b/custom_components/openclaw/__pycache__/coordinator.cpython-312.pyc index 9842200..6c9cae4 100644 Binary files a/custom_components/openclaw/__pycache__/coordinator.cpython-312.pyc and b/custom_components/openclaw/__pycache__/coordinator.cpython-312.pyc differ diff --git a/custom_components/openclaw/__pycache__/sensor.cpython-312.pyc b/custom_components/openclaw/__pycache__/sensor.cpython-312.pyc index 85e9fc9..444b8d8 100644 Binary files a/custom_components/openclaw/__pycache__/sensor.cpython-312.pyc and b/custom_components/openclaw/__pycache__/sensor.cpython-312.pyc differ diff --git a/custom_components/openclaw/api.py b/custom_components/openclaw/api.py index 4007e88..d9198fb 100644 --- a/custom_components/openclaw/api.py +++ b/custom_components/openclaw/api.py @@ -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: diff --git a/custom_components/openclaw/const.py b/custom_components/openclaw/const.py index 81e4b98..f71d5a5 100644 --- a/custom_components/openclaw/const.py +++ b/custom_components/openclaw/const.py @@ -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" diff --git a/custom_components/openclaw/coordinator.py b/custom_components/openclaw/coordinator.py index 3db0fcd..c12afbe 100644 --- a/custom_components/openclaw/coordinator.py +++ b/custom_components/openclaw/coordinator.py @@ -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) diff --git a/custom_components/openclaw/manifest.json b/custom_components/openclaw/manifest.json index c18c93b..2257a49 100644 --- a/custom_components/openclaw/manifest.json +++ b/custom_components/openclaw/manifest.json @@ -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"] } diff --git a/custom_components/openclaw/sensor.py b/custom_components/openclaw/sensor.py index 22a1dcd..4cae0bf 100644 --- a/custom_components/openclaw/sensor.py +++ b/custom_components/openclaw/sensor.py @@ -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 diff --git a/custom_components/openclaw/services.yaml b/custom_components/openclaw/services.yaml index 4acc61e..7ac9f5a 100644 --- a/custom_components/openclaw/services.yaml +++ b/custom_components/openclaw/services.yaml @@ -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: diff --git a/custom_components/openclaw/strings.json b/custom_components/openclaw/strings.json index e2401ea..acc7940 100644 --- a/custom_components/openclaw/strings.json +++ b/custom_components/openclaw/strings.json @@ -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." + } + } } } } diff --git a/custom_components/openclaw/translations/en.json b/custom_components/openclaw/translations/en.json index e2401ea..acc7940 100644 --- a/custom_components/openclaw/translations/en.json +++ b/custom_components/openclaw/translations/en.json @@ -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." + } + } } } } diff --git a/www/openclaw-chat-card.js b/www/openclaw-chat-card.js index bbaa813..a81f4ab 100644 --- a/www/openclaw-chat-card.js +++ b/www/openclaw-chat-card.js @@ -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); }