Update changelog, enhance connection checks, and add lightweight gateway status check
This commit is contained in:
@@ -2,6 +2,26 @@
|
|||||||
|
|
||||||
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.4] - 2026-02-20
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Connection probe no longer uses `/v1/models`** — the OpenClaw gateway does
|
||||||
|
not implement that endpoint (only `/v1/chat/completions` is registered when
|
||||||
|
`enable_openai_api` is enabled). Unrecognised routes fall through to the SPA
|
||||||
|
catch-all and return HTML, which caused every connection check to fail with
|
||||||
|
`openai_api_disabled` even when the API was actually enabled.
|
||||||
|
- `async_check_connection()` now POSTs to `/v1/chat/completions` with an empty
|
||||||
|
messages body. The gateway's auth middleware validates the token first, then
|
||||||
|
the endpoint returns a JSON error for the invalid body — proving server is
|
||||||
|
reachable, API is enabled, and the token is accepted. No LLM call is made.
|
||||||
|
- Coordinator polling now uses `async_check_alive()` (lightweight base-URL GET)
|
||||||
|
for connectivity, with `async_get_models()` as a best-effort call that is
|
||||||
|
silently ignored if the endpoint doesn't exist.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- New `async_check_alive()` method — simple HTTP GET to the gateway base URL to
|
||||||
|
confirm the gateway process is running (does not verify auth or API status).
|
||||||
|
|
||||||
## [0.1.3] - 2026-02-20
|
## [0.1.3] - 2026-02-20
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -287,27 +287,85 @@ class OpenClawApiClient:
|
|||||||
) from err
|
) from err
|
||||||
|
|
||||||
async def async_check_connection(self) -> bool:
|
async def async_check_connection(self) -> bool:
|
||||||
"""Check if the gateway is reachable and authenticated.
|
"""Check if the gateway is reachable, API is enabled, and auth works.
|
||||||
|
|
||||||
Uses the /v1/models endpoint as a lightweight probe — the only
|
The OpenClaw gateway only implements /v1/chat/completions (not
|
||||||
consistently available GET endpoint on the OpenClaw gateway.
|
/v1/models). We send a POST with an empty messages list — the gateway
|
||||||
|
auth middleware validates the token first, then the endpoint returns a
|
||||||
|
400 (or similar) for the invalid body. This proves:
|
||||||
|
- Server is reachable.
|
||||||
|
- The OpenAI-compatible API layer is enabled (enable_openai_api).
|
||||||
|
- The auth token is accepted.
|
||||||
|
|
||||||
The /v1/models endpoint requires 'enable_openai_api' to be enabled in
|
If the route is not registered (API disabled) the SPA catch-all
|
||||||
the addon configuration. If the gateway returns HTML instead of JSON,
|
returns 200 text/html — detected via content-type check.
|
||||||
the OpenAI-compatible API is likely disabled and an OpenClawApiError
|
|
||||||
is raised so the caller can surface a meaningful error.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if connected and authenticated.
|
True if connected and authenticated.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
OpenClawAuthError: If authentication fails.
|
OpenClawAuthError: If authentication fails.
|
||||||
OpenClawApiError: If the gateway returns an unexpected response
|
OpenClawApiError: If the gateway returns HTML (API not enabled).
|
||||||
(e.g. HTML when enable_openai_api is disabled).
|
|
||||||
OpenClawConnectionError: If the gateway is unreachable.
|
OpenClawConnectionError: If the gateway is unreachable.
|
||||||
"""
|
"""
|
||||||
await self.async_get_models()
|
session = await self._get_session()
|
||||||
return True
|
url = f"{self._base_url}{API_CHAT_COMPLETIONS}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with session.post(
|
||||||
|
url,
|
||||||
|
headers=self._headers(),
|
||||||
|
json={"messages": [], "stream": False},
|
||||||
|
timeout=API_TIMEOUT,
|
||||||
|
) as resp:
|
||||||
|
if resp.status in (401, 403):
|
||||||
|
raise OpenClawAuthError(
|
||||||
|
"Authentication failed — check gateway token"
|
||||||
|
)
|
||||||
|
|
||||||
|
content_type = resp.content_type or ""
|
||||||
|
if "json" not in content_type:
|
||||||
|
text = await resp.text()
|
||||||
|
raise OpenClawApiError(
|
||||||
|
f"Gateway returned '{content_type}' instead of JSON. "
|
||||||
|
"The OpenAI-compatible API is likely not enabled. "
|
||||||
|
"Enable 'enable_openai_api' in the addon settings "
|
||||||
|
f"and restart. Response: {text[:200]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Any JSON response (200, 400, 422, etc.) means the
|
||||||
|
# endpoint exists, auth passed, and the API layer is active.
|
||||||
|
return True
|
||||||
|
|
||||||
|
except (aiohttp.ClientConnectorError, aiohttp.ClientOSError, asyncio.TimeoutError) as err:
|
||||||
|
raise OpenClawConnectionError(
|
||||||
|
f"Cannot connect to OpenClaw gateway at {url}: {err}"
|
||||||
|
) from err
|
||||||
|
|
||||||
|
async def async_check_alive(self) -> bool:
|
||||||
|
"""Lightweight connectivity check — is the gateway process running?
|
||||||
|
|
||||||
|
Sends a GET to the base URL. The SPA catch-all returns 200 HTML for
|
||||||
|
any route, so any non-error HTTP response means the server is alive.
|
||||||
|
Auth is NOT verified here (the SPA ignores tokens).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the gateway HTTP server is responding.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
OpenClawConnectionError: If the gateway is unreachable.
|
||||||
|
"""
|
||||||
|
session = await self._get_session()
|
||||||
|
try:
|
||||||
|
async with session.get(
|
||||||
|
self._base_url,
|
||||||
|
timeout=API_TIMEOUT,
|
||||||
|
) as resp:
|
||||||
|
return resp.status < 500
|
||||||
|
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."""
|
||||||
|
|||||||
@@ -80,42 +80,31 @@ class OpenClawCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
async def _async_update_data(self) -> dict[str, Any]:
|
async def _async_update_data(self) -> dict[str, Any]:
|
||||||
"""Fetch data from the OpenClaw gateway.
|
"""Fetch data from the OpenClaw gateway.
|
||||||
|
|
||||||
The OpenClaw gateway only exposes OpenAI-compatible endpoints.
|
The OpenClaw gateway does not implement /v1/models — only
|
||||||
/v1/models is used as both the connectivity probe and the model
|
/v1/chat/completions is guaranteed. We use a lightweight base-URL
|
||||||
data source; /api/status and /api/sessions do not exist on the
|
ping (``async_check_alive``) to confirm the gateway process is
|
||||||
gateway — it returns its SPA home page for unknown routes.
|
running and then attempt ``async_get_models`` as a best-effort call.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Aggregated data dict for all entities.
|
Aggregated data dict for all entities.
|
||||||
"""
|
"""
|
||||||
data = self._offline_data()
|
data = self._offline_data()
|
||||||
|
|
||||||
|
# ── Connectivity check (base URL ping) ─────────────────────
|
||||||
try:
|
try:
|
||||||
models_resp = await self.client.async_get_models()
|
alive = await self.client.async_check_alive()
|
||||||
models = models_resp.get("data", [])
|
if not alive:
|
||||||
|
return data
|
||||||
|
|
||||||
data[DATA_STATUS] = "online"
|
data[DATA_STATUS] = "online"
|
||||||
data[DATA_CONNECTED] = True
|
data[DATA_CONNECTED] = True
|
||||||
data[DATA_GATEWAY_VERSION] = None # Not exposed by gateway API
|
data[DATA_GATEWAY_VERSION] = None
|
||||||
data[DATA_UPTIME] = None # Not exposed by gateway API
|
data[DATA_UPTIME] = None
|
||||||
data[DATA_SESSION_COUNT] = 0 # Sessions API does not exist
|
data[DATA_SESSION_COUNT] = 0
|
||||||
data[DATA_SESSIONS] = [] # Sessions API does not exist
|
data[DATA_SESSIONS] = []
|
||||||
data[DATA_LAST_ACTIVITY] = self._last_activity
|
data[DATA_LAST_ACTIVITY] = self._last_activity
|
||||||
self._consecutive_failures = 0
|
self._consecutive_failures = 0
|
||||||
|
|
||||||
if models:
|
|
||||||
current = models[0]
|
|
||||||
self._model_cache = {
|
|
||||||
DATA_MODEL: current.get("id", "unknown"),
|
|
||||||
DATA_PROVIDER: current.get("owned_by"),
|
|
||||||
DATA_CONTEXT_WINDOW: current.get("context_window"),
|
|
||||||
}
|
|
||||||
|
|
||||||
except OpenClawAuthError as err:
|
|
||||||
_LOGGER.warning("Gateway auth failed during poll: %s", err)
|
|
||||||
await self._try_refresh_token()
|
|
||||||
return data
|
|
||||||
|
|
||||||
except OpenClawConnectionError:
|
except OpenClawConnectionError:
|
||||||
self._consecutive_failures += 1
|
self._consecutive_failures += 1
|
||||||
if self._consecutive_failures <= 3:
|
if self._consecutive_failures <= 3:
|
||||||
@@ -127,9 +116,23 @@ class OpenClawCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
)
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
except OpenClawApiError as err:
|
# ── Best-effort model info (/v1/models may not exist) ──────
|
||||||
_LOGGER.warning("Error fetching gateway models: %s", err)
|
try:
|
||||||
return data
|
models_resp = await self.client.async_get_models()
|
||||||
|
models = models_resp.get("data", [])
|
||||||
|
if models:
|
||||||
|
current = models[0]
|
||||||
|
self._model_cache = {
|
||||||
|
DATA_MODEL: current.get("id", "unknown"),
|
||||||
|
DATA_PROVIDER: current.get("owned_by"),
|
||||||
|
DATA_CONTEXT_WINDOW: current.get("context_window"),
|
||||||
|
}
|
||||||
|
except OpenClawAuthError as err:
|
||||||
|
_LOGGER.warning("Gateway auth failed during poll: %s", err)
|
||||||
|
await self._try_refresh_token()
|
||||||
|
except OpenClawApiError:
|
||||||
|
# /v1/models not implemented — expected, not an error
|
||||||
|
pass
|
||||||
|
|
||||||
data.update(self._model_cache)
|
data.update(self._model_cache)
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -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.3",
|
"version": "0.1.4",
|
||||||
"dependencies": ["conversation"],
|
"dependencies": ["conversation"],
|
||||||
"after_dependencies": ["hassio"]
|
"after_dependencies": ["hassio"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user