Skip to content

Commit 0f5357e

Browse files
author
techartdev
committed
Add event and button entities, select entity for model switching, and update changelog to version 0.1.56
1 parent 11959e3 commit 0f5357e

12 files changed

Lines changed: 406 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
All notable changes to the OpenClaw Home Assistant Integration will be documented in this file.
44

5+
## [0.1.56] - 2026-02-25
6+
7+
### Added
8+
- **Event entities** (`event.openclaw_message_received`, `event.openclaw_tool_invoked`) — native HA EventEntity entities that fire on each assistant reply and tool invocation result. Selectable in the automation UI without YAML.
9+
- **Button entities** — dashboard-friendly buttons for common actions:
10+
- **Clear History** — clears in-memory conversation history
11+
- **Sync History** — triggers a backend coordinator refresh
12+
- **Run Diagnostics** — fires a connectivity check against the gateway
13+
- **Select entity** (`select.openclaw_active_model`) — exposes the list of available models from the gateway's `/v1/models` endpoint, allowing model switching from the HA dashboard. Selection is persisted in config entry options.
14+
- Coordinator now caches the full model list (not just the first model) and exposes it via `coordinator.available_models`.
15+
516
## [0.1.55] - 2026-02-23
617

718
### Added

custom_components/openclaw/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
# URL at which the card JS is served (registered via register_static_path)
9696
_CARD_STATIC_URL = f"/openclaw/{_CARD_FILENAME}"
9797
# Versioned URL used for Lovelace resource registration to avoid stale browser cache
98-
_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.55"
98+
_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.56"
9999

100100
OpenClawConfigEntry = ConfigEntry
101101

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
"""Button entities for the OpenClaw integration.
2+
3+
Provides dashboard-friendly buttons for common actions:
4+
- Clear History — clears in-memory conversation history
5+
- Sync History — triggers backend history re-sync
6+
- Run Diagnostics — fires a connectivity check against the gateway
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import logging
12+
13+
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
14+
from homeassistant.config_entries import ConfigEntry
15+
from homeassistant.core import HomeAssistant
16+
from homeassistant.helpers.entity_platform import AddEntitiesCallback
17+
from homeassistant.helpers.update_coordinator import CoordinatorEntity
18+
19+
from .api import OpenClawApiClient
20+
from .const import DOMAIN
21+
from .coordinator import OpenClawCoordinator
22+
23+
_LOGGER = logging.getLogger(__name__)
24+
25+
BUTTON_DESCRIPTIONS: tuple[ButtonEntityDescription, ...] = (
26+
ButtonEntityDescription(
27+
key="clear_history",
28+
translation_key="clear_history",
29+
name="OpenClaw Clear History",
30+
icon="mdi:delete-sweep",
31+
),
32+
ButtonEntityDescription(
33+
key="sync_history",
34+
translation_key="sync_history",
35+
name="OpenClaw Sync History",
36+
icon="mdi:sync",
37+
),
38+
ButtonEntityDescription(
39+
key="run_diagnostics",
40+
translation_key="run_diagnostics",
41+
name="OpenClaw Run Diagnostics",
42+
icon="mdi:stethoscope",
43+
),
44+
)
45+
46+
47+
async def async_setup_entry(
48+
hass: HomeAssistant,
49+
entry: ConfigEntry,
50+
async_add_entities: AddEntitiesCallback,
51+
) -> None:
52+
"""Set up OpenClaw button entities from a config entry."""
53+
entry_data: dict = hass.data[DOMAIN][entry.entry_id]
54+
coordinator: OpenClawCoordinator = entry_data["coordinator"]
55+
client: OpenClawApiClient = entry_data["client"]
56+
57+
entities = [
58+
OpenClawButton(coordinator, client, description, entry, hass)
59+
for description in BUTTON_DESCRIPTIONS
60+
]
61+
async_add_entities(entities)
62+
63+
64+
class OpenClawButton(CoordinatorEntity[OpenClawCoordinator], ButtonEntity):
65+
"""Button entity for OpenClaw actions."""
66+
67+
_attr_has_entity_name = True
68+
69+
def __init__(
70+
self,
71+
coordinator: OpenClawCoordinator,
72+
client: OpenClawApiClient,
73+
description: ButtonEntityDescription,
74+
entry: ConfigEntry,
75+
hass: HomeAssistant,
76+
) -> None:
77+
"""Initialize the button."""
78+
super().__init__(coordinator)
79+
self.entity_description = description
80+
self._client = client
81+
self._entry = entry
82+
self._hass = hass
83+
self._attr_unique_id = f"{entry.entry_id}_{description.key}"
84+
self._attr_device_info = {
85+
"identifiers": {(DOMAIN, entry.entry_id)},
86+
"name": "OpenClaw Assistant",
87+
"manufacturer": "OpenClaw",
88+
"model": "OpenClaw Gateway",
89+
}
90+
91+
async def async_press(self) -> None:
92+
"""Handle button press."""
93+
key = self.entity_description.key
94+
95+
if key == "clear_history":
96+
store_key = f"{DOMAIN}_chat_history"
97+
store = self._hass.data.get(store_key)
98+
if isinstance(store, dict):
99+
store.clear()
100+
_LOGGER.info("OpenClaw chat history cleared via button")
101+
102+
elif key == "sync_history":
103+
await self.coordinator.async_request_refresh()
104+
_LOGGER.info("OpenClaw history sync triggered via button")
105+
106+
elif key == "run_diagnostics":
107+
try:
108+
alive = await self._client.async_check_alive()
109+
if alive:
110+
_LOGGER.info("OpenClaw diagnostics: gateway is reachable")
111+
else:
112+
_LOGGER.warning("OpenClaw diagnostics: gateway did not respond")
113+
except Exception as err: # noqa: BLE001
114+
_LOGGER.error("OpenClaw diagnostics failed: %s", err)
115+
await self.coordinator.async_request_refresh()

custom_components/openclaw/const.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@
106106
DATA_LAST_TOOL_RESULT_PREVIEW = "last_tool_result_preview"
107107

108108
# Platforms
109-
PLATFORMS = ["sensor", "binary_sensor", "conversation"]
109+
PLATFORMS = ["sensor", "binary_sensor", "conversation", "event", "button", "select"]
110110

111111
# Events
112112
EVENT_MESSAGE_RECEIVED = f"{DOMAIN}_message_received"

custom_components/openclaw/coordinator.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def __init__(
6666
self.client = client
6767
self._last_activity: datetime | None = None
6868
self._model_cache: dict[str, Any] = {}
69+
self._available_models: list[str] = []
6970
self._consecutive_failures = 0
7071
self._last_tool_state: dict[str, Any] = {
7172
DATA_LAST_TOOL_NAME: None,
@@ -147,6 +148,9 @@ async def _async_update_data(self) -> dict[str, Any]:
147148
DATA_PROVIDER: current.get("owned_by"),
148149
DATA_CONTEXT_WINDOW: current.get("context_window"),
149150
}
151+
self._available_models = [
152+
m.get("id") for m in models if m.get("id")
153+
]
150154
except OpenClawAuthError as err:
151155
_LOGGER.warning("Gateway auth failed during poll: %s", err)
152156
await self._try_refresh_token()
@@ -199,6 +203,11 @@ def update_last_activity(self) -> None:
199203
"""
200204
self._last_activity = datetime.now(timezone.utc)
201205

206+
@property
207+
def available_models(self) -> list[str]:
208+
"""Return the list of model IDs from the last successful poll."""
209+
return list(self._available_models)
210+
202211
def record_tool_invocation(
203212
self,
204213
*,
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
"""Event entities for the OpenClaw integration.
2+
3+
Provides native HA EventEntity entities for:
4+
- openclaw_message_received — fires on each assistant reply
5+
- openclaw_tool_invoked — fires on each tool invocation result
6+
7+
These complement the raw HA bus events with proper entity-registry entries
8+
that are selectable in the automation UI (no YAML needed).
9+
"""
10+
11+
from __future__ import annotations
12+
13+
from typing import Any
14+
15+
from homeassistant.components.event import EventEntity, EventEntityDescription
16+
from homeassistant.config_entries import ConfigEntry
17+
from homeassistant.core import HomeAssistant, callback
18+
from homeassistant.helpers.entity_platform import AddEntitiesCallback
19+
20+
from .const import (
21+
DOMAIN,
22+
EVENT_MESSAGE_RECEIVED,
23+
EVENT_TOOL_INVOKED,
24+
)
25+
26+
EVENT_DESCRIPTIONS: tuple[EventEntityDescription, ...] = (
27+
EventEntityDescription(
28+
key="message_received",
29+
translation_key="message_received",
30+
name="OpenClaw Message Received",
31+
icon="mdi:message-text",
32+
event_types=["message_received"],
33+
),
34+
EventEntityDescription(
35+
key="tool_invoked",
36+
translation_key="tool_invoked",
37+
name="OpenClaw Tool Invoked",
38+
icon="mdi:tools",
39+
event_types=["tool_invoked_ok", "tool_invoked_error"],
40+
),
41+
)
42+
43+
44+
async def async_setup_entry(
45+
hass: HomeAssistant,
46+
entry: ConfigEntry,
47+
async_add_entities: AddEntitiesCallback,
48+
) -> None:
49+
"""Set up OpenClaw event entities from a config entry."""
50+
entities = [
51+
OpenClawEventEntity(entry, description)
52+
for description in EVENT_DESCRIPTIONS
53+
]
54+
async_add_entities(entities)
55+
56+
# Wire HA bus events → entity triggers
57+
for entity in entities:
58+
entity.async_start_listening(hass)
59+
60+
61+
class OpenClawEventEntity(EventEntity):
62+
"""Event entity that mirrors HA bus events into the entity registry."""
63+
64+
_attr_has_entity_name = True
65+
66+
def __init__(
67+
self,
68+
entry: ConfigEntry,
69+
description: EventEntityDescription,
70+
) -> None:
71+
"""Initialize the event entity."""
72+
self.entity_description = description
73+
self._attr_unique_id = f"{entry.entry_id}_{description.key}"
74+
self._attr_device_info = {
75+
"identifiers": {(DOMAIN, entry.entry_id)},
76+
"name": "OpenClaw Assistant",
77+
"manufacturer": "OpenClaw",
78+
"model": "OpenClaw Gateway",
79+
}
80+
self._entry_id = entry.entry_id
81+
self._unsub: callback | None = None
82+
83+
@callback
84+
def async_start_listening(self, hass: HomeAssistant) -> None:
85+
"""Subscribe to the matching HA bus event."""
86+
key = self.entity_description.key
87+
88+
if key == "message_received":
89+
bus_event = EVENT_MESSAGE_RECEIVED
90+
elif key == "tool_invoked":
91+
bus_event = EVENT_TOOL_INVOKED
92+
else:
93+
return
94+
95+
@callback
96+
def _handle_event(event) -> None:
97+
data: dict[str, Any] = dict(event.data or {})
98+
if key == "message_received":
99+
self._trigger_event("message_received", data)
100+
elif key == "tool_invoked":
101+
ok = data.get("ok", False)
102+
event_type = "tool_invoked_ok" if ok else "tool_invoked_error"
103+
self._trigger_event(event_type, data)
104+
self.async_write_ha_state()
105+
106+
self._unsub = hass.bus.async_listen(bus_event, _handle_event)
107+
108+
async def async_will_remove_from_hass(self) -> None:
109+
"""Unsubscribe when entity is removed."""
110+
if self._unsub:
111+
self._unsub()
112+
self._unsub = None
21.4 KB
Loading
21.4 KB
Loading

custom_components/openclaw/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"iot_class": "local_polling",
99
"issue_tracker": "https://github.com/techartdev/OpenClawHomeAssistant/issues",
1010
"requirements": [],
11-
"version": "0.1.55",
11+
"version": "0.1.56",
1212
"dependencies": ["conversation"],
1313
"after_dependencies": ["hassio", "lovelace"]
1414
}

0 commit comments

Comments
 (0)