From faa962d8bc2e6b54dc20dc8045b61b48eb150cff Mon Sep 17 00:00:00 2001 From: techartdev Date: Fri, 20 Feb 2026 23:18:37 +0200 Subject: [PATCH] Update version to 0.1.40 and add OpenClaw Gateway tools endpoint integration with new service and event support --- CHANGELOG.md | 11 ++ README.md | 55 +++++++ custom_components/openclaw/__init__.py | 150 ++++++++++++++++-- .../__pycache__/__init__.cpython-312.pyc | Bin 28898 -> 33773 bytes .../openclaw/__pycache__/api.cpython-312.pyc | Bin 17463 -> 19956 bytes .../__pycache__/coordinator.cpython-312.pyc | Bin 8496 -> 10437 bytes .../__pycache__/sensor.cpython-312.pyc | Bin 5078 -> 6074 bytes custom_components/openclaw/api.py | 57 +++++++ custom_components/openclaw/const.py | 20 +++ custom_components/openclaw/coordinator.py | 66 ++++++++ custom_components/openclaw/manifest.json | 2 +- custom_components/openclaw/sensor.py | 39 +++++ custom_components/openclaw/services.yaml | 50 ++++++ custom_components/openclaw/strings.json | 46 ++++++ .../openclaw/translations/en.json | 46 ++++++ www/openclaw-chat-card.js | 2 +- 16 files changed, 526 insertions(+), 18 deletions(-) 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 69d0de4ce187ed350a85888fafef6dd4edb5890d..217e6120cc5856d5fe71bca9fac8f479cbd41c86 100644 GIT binary patch delta 8842 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 Ij+zJjKc*$QX#fBK delta 4947 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 diff --git a/custom_components/openclaw/__pycache__/api.cpython-312.pyc b/custom_components/openclaw/__pycache__/api.cpython-312.pyc index 8aff5c385a963c77aaeda3126ad93d0e9408466d..ab2066d55b5293231067683b6856b55c9f96afe1 100644 GIT binary patch delta 4441 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 T-l4tMF1wGJX#KwlA`kw5P2BNP delta 2568 zcmaJ@Yitx%6rQ^~+kJNX*awBSbmbwPwme#;0eP0bkrtHluoaf2Gqel);?As6Lo6l@ zf&rmkjV54>j~KOTOg0e{!ykVcjeiWkHkzn08WMgO<%c0L@qBl-FM|x>n|r@|?z!iD zbMD#Ry~b`_V8L(8%Y6zx?|t`1^4O;7U=6FAUbDZ2DGtSOom_^c#g7r3VKy^dK{no# zG4bfz;s&dY99?Qm@m4 z^sbt4X!V67u4X5Tii;kNtrZ2&0E-CbO@tn%a6V*w4ZTbhMT|#9-m9^0@s@W9+1&7& zOIH$4Uja6}35F<&7%V$|t>N8ZyaeZzpQ5!F)XRPz5f^+PRJ9Su*JE|oafe@HgW{CG zksQD3|B zVNnK!L-`JZdy$U3HBJ#HM-x^un~9xBBvXdYcVW%nIi9d$ba0l(cqg`Wi7%J$^g{w} zOC!n}8YcTOmxU_hm6ycM9SeH^1AsxmUVxM%Xv^VF8>X2UGdL=l>jcGebHcQYbUeqi z>72!nVIS3% z8NmJYIx2qG8d#5TH?#6OR|KiNgYA>IoD5@}VX zN&zvi0|YAiJNEU*hx!M)_Z}EpAPVZ;{+6CDqxx3F~K zldXrCc&+DF{c8B#L{M~%rn087_|Bg1UC+*o&wJxdjWBUi+u@oVB{P=nfBZ5WG=o!* zXR|5ZBDVLnJ$4l=mNa#NnPjKIIylD91@+{pH9}rnVgyG(i;w#1Bl~8z^C#v;+rxWUK5EpYCaxMB!1DWHTh7yg=yjMemql?a7W& z+qCm~9%OlEDoMS3D4900dCM+dbey8M`^>13LxY#UQ}!PtvSZ6AXG6CmFM777vwA*d mZ0FY%#jVioP|Pdh@IZsh$sB+9#0vv@#i)xl{;2?EHva-X{RamC diff --git a/custom_components/openclaw/__pycache__/coordinator.cpython-312.pyc b/custom_components/openclaw/__pycache__/coordinator.cpython-312.pyc index 984220034200287ba7b55a82b50942b7666ea138..6c9cae45a3bc1a13ec1bf61e408383d97c1e0794 100644 GIT binary patch literal 10437 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 diff --git a/custom_components/openclaw/__pycache__/sensor.cpython-312.pyc b/custom_components/openclaw/__pycache__/sensor.cpython-312.pyc index 85e9fc9341db073c1c4b6b01aa63a4a12abc517b..444b8d81c457259890588cce3651398d3e1381be 100644 GIT binary patch delta 1959 zcmZuyO>7%Q6rOQx$4z*p7c1J8o#2(x#yyv~_Ejq~xcK-J)(&tt`*BG4g9W- z2p^H6d=w+nMjYjs6zAg@nUH7~dkIIJ#C6ww$J}sXumRFRI>px4#&7%{Uk(v3*+sgE z@2>xj+t@4)ya z8{bD#VuI`$V~oA&NA{6{Yi2%aLN6oo+%-h@iXCFy&n!jTc3OP@;$Uh>eHTB#F12dv zUJhi^`Lr;Z&gF&t%*>>aIXj!qPs~gUQ#sw!q|46E&dlomCU#=_+{`Q4jF8UjT}`U# z^i)<4HIcK~+}X*zaC$a-ZX)Y@RrfaObNO`sY;NTplN*P7S@1Lh5M`h{hqSuCyF?KW)NDi#fflJDG(dDAj6Ns1zh|sdJ_#R!)=)3z12th~$}K$u5!7D458) zd&gW>3QApG^8f!pB-c=N!nxwSSiG{Ah{eKXT99<_jssGs%LS!Wtq795)bk8CnL0J} zOD=FE*F=$2B~cNnZjxl(v*W2$xmvv<5}}|h1)kw&qM=@jEA?7gypR(sa+PK)N=aEc zZyv(#Ae77|RusA@ln5PEZ(2t1kh*3`gcE~v#k#CiC81cAYSjuelIN>p;RO*)sMWs$fO8eEXZ6nd=p+C?D-Niyxs8FwzUayPr zO$dSRS3~wx`w)mD0K@7TyWe^k#1m@G9>jFzy1j&xhQ)rMPXc5Bz-Y_Ut{+Aqu@EQrj<3)P_d;oB9=`Mh70QS@&+6mwRZ~?dhx)2&b zw1bH$i%}?}Xw)cVg0VW5Wq7Y5uvq!tavpDekL~5dSpC-d>$1bO-j!JIe`)>b`PEK- zt)0K`58d|N^sUDZ-SdxVvBanSANQ{hjNgk*XyMrH`J40W$z%7z>4&Xmk8=Z=Ip>B0 zxjldVmO`OPEL)lDuX_ENF>A`!NI1lpM(3eDbQr+SK(|s+sXJ&zH&3$>OqnRJ{^N>c zUX8j7nG;}Pt5G(UR9U2xaOym8oY1)cMX_8K1U4KKj5UVi7&V6K0GI+u>NoDWlYJJ? zW(%@9w@gf;FfxX6nuT`|Ht=t<6BIoJ+jamR-~xkGiV7?99Wi{+=)_KUjNTa?GX~vg zFJ!CR^Al59_K;1TX4mecPVc}d_=1=|LyZt1Qr$KtRY|=p9-$Ssg;A3WSiRr5*YC!r zKhRU~5VntEYOw3_@=ujlF&ou4kBCN%e zTkTe6?lheAX}&>?OKIN3Pu`(5?~ukt4E4rEJUnJvMO~}j7dFu_Hf_`}hof3s_lA0_ aD;CIL>{<0DR+FP^-WR`^dxU^6a{CvqaP5@< delta 1072 zcmZ8g&rcIU6rNey(gg}FY+FhxEL5nA0yjY^Vu6B2K=~1j#JDz{kyOhTXSWhej2`#{ zG;`tN#puE4!K5)UMlW8yU^GS><3Z0vA`s)vdAowq+2q^zzJ2rFx8Kgb=>3pLyz%*} zkXTL4vE!he1_Qku2dkRLRiqzzrmtqSsj3h*58o9d_lob*n^|bOzlGoz+ ztuP6a5DAmUCy~c)XTMz(Y6 zhDaL?&{#xNQtw`eh4$5U?ZA!hS!$RCrc%SoSzMJqgypg_nwm@LdaK>VTB*JKo8ag5EW3aA@q7KOZ99%W16g)zjF=y|1A+@hdWi8_KaYE2)1~|B(|#O z2#fOvm3_E_f3EDrGFQDZcdsZI-8UVzpLM#AKCqQ%C5^OolmZ%E0jLrQQ z0%3Oo!sN|3{%$`oHJX+BjgN)Ls0f6mkOkqr^nePk-}Ft zzau^q>aMH8(oZB{*M<;Z4=vVj9f~HU4b-p^?B7OFENvzEN?2 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); }