Refactor API client to remove unsupported endpoints and improve error handling for unexpected response types

This commit is contained in:
techartdev
2026-02-20 13:35:23 +02:00
parent e9cd5e1fff
commit 65e6336f80
4 changed files with 39 additions and 72 deletions
+12 -23
View File
@@ -12,8 +12,6 @@ import aiohttp
from .const import ( from .const import (
API_CHAT_COMPLETIONS, API_CHAT_COMPLETIONS,
API_MODELS, API_MODELS,
API_SESSIONS,
API_STATUS,
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -137,6 +135,14 @@ class OpenClawApiClient:
raise OpenClawApiError( raise OpenClawApiError(
f"API error {resp.status}: {text[:200]}" f"API error {resp.status}: {text[:200]}"
) )
content_type = resp.content_type or ""
if "json" not in content_type:
text = await resp.text()
raise OpenClawApiError(
f"Unexpected response content type '{content_type}' (expected JSON). "
f"The host/port may be wrong or the gateway returned an error page. "
f"Response: {text[:200]}"
)
return await resp.json() return await resp.json()
except (aiohttp.ClientConnectorError, aiohttp.ClientOSError, asyncio.TimeoutError) as err: except (aiohttp.ClientConnectorError, aiohttp.ClientOSError, asyncio.TimeoutError) as err:
@@ -146,26 +152,6 @@ class OpenClawApiClient:
# ─── Public API methods ──────────────────────────────────────────── # ─── Public API methods ────────────────────────────────────────────
async def async_get_status(self) -> dict[str, Any]:
"""Get gateway status.
Returns:
Status dict with keys like 'status', 'version', 'uptime'.
Raises:
OpenClawConnectionError: If the gateway is unreachable.
OpenClawAuthError: If authentication fails.
"""
return await self._request("GET", API_STATUS)
async def async_get_sessions(self) -> dict[str, Any]:
"""Get active sessions list.
Returns:
Dict with 'sessions' list and 'count'.
"""
return await self._request("GET", API_SESSIONS)
async def async_get_models(self) -> dict[str, Any]: async def async_get_models(self) -> dict[str, Any]:
"""Get available models (OpenAI-compatible). """Get available models (OpenAI-compatible).
@@ -303,6 +289,9 @@ class OpenClawApiClient:
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 and authenticated.
Uses the /v1/models endpoint as a lightweight probe — the only
consistently available GET endpoint on the OpenClaw gateway.
Returns: Returns:
True if connected and authenticated. True if connected and authenticated.
@@ -311,7 +300,7 @@ class OpenClawApiClient:
can distinguish bad tokens from unreachable gateways). can distinguish bad tokens from unreachable gateways).
""" """
try: try:
await self.async_get_status() await self.async_get_models()
return True return True
except OpenClawAuthError: except OpenClawAuthError:
raise raise
+3 -2
View File
@@ -53,7 +53,8 @@ ATTR_MODEL = "model"
ATTR_TIMESTAMP = "timestamp" ATTR_TIMESTAMP = "timestamp"
# API endpoints # API endpoints
API_STATUS = "/api/status" # The OpenClaw gateway exposes only the OpenAI-compatible endpoints.
API_SESSIONS = "/api/sessions" # /api/status and /api/sessions do not exist — the gateway returns its SPA
# home page (text/html) for any unrecognised route.
API_MODELS = "/v1/models" API_MODELS = "/v1/models"
API_CHAT_COMPLETIONS = "/v1/chat/completions" API_CHAT_COMPLETIONS = "/v1/chat/completions"
+23 -46
View File
@@ -60,7 +60,6 @@ class OpenClawCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self.client = client self.client = client
self._last_activity: datetime | None = None self._last_activity: datetime | None = None
self._model_cache: dict[str, Any] = {} self._model_cache: dict[str, Any] = {}
self._model_poll_counter = 0
self._consecutive_failures = 0 self._consecutive_failures = 0
def _offline_data(self) -> dict[str, Any]: def _offline_data(self) -> dict[str, Any]:
@@ -81,20 +80,37 @@ 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.
/v1/models is used as both the connectivity probe and the model
data source; /api/status and /api/sessions do not exist on the
gateway — it returns its SPA home page for unknown routes.
Returns: Returns:
Aggregated data dict for all entities. Aggregated data dict for all entities.
""" """
data = self._offline_data() data = self._offline_data()
# ── Status ──────────────────────────────────────────────────
try: try:
status_resp = await self.client.async_get_status() models_resp = await self.client.async_get_models()
data[DATA_STATUS] = status_resp.get("status", "online") models = models_resp.get("data", [])
data[DATA_STATUS] = "online"
data[DATA_CONNECTED] = True data[DATA_CONNECTED] = True
data[DATA_GATEWAY_VERSION] = status_resp.get("version") data[DATA_GATEWAY_VERSION] = None # Not exposed by gateway API
data[DATA_UPTIME] = status_resp.get("uptime") data[DATA_UPTIME] = None # Not exposed by gateway API
data[DATA_SESSION_COUNT] = 0 # Sessions API does not exist
data[DATA_SESSIONS] = [] # Sessions API does not exist
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: except OpenClawAuthError as err:
_LOGGER.warning("Gateway auth failed during poll: %s", err) _LOGGER.warning("Gateway auth failed during poll: %s", err)
await self._try_refresh_token() await self._try_refresh_token()
@@ -112,48 +128,9 @@ class OpenClawCoordinator(DataUpdateCoordinator[dict[str, Any]]):
return data return data
except OpenClawApiError as err: except OpenClawApiError as err:
_LOGGER.warning("Error fetching gateway status: %s", err) _LOGGER.warning("Error fetching gateway models: %s", err)
return data return data
# ── Sessions ────────────────────────────────────────────────
try:
sessions_resp = await self.client.async_get_sessions()
sessions = sessions_resp.get("sessions", [])
data[DATA_SESSION_COUNT] = len(sessions)
data[DATA_SESSIONS] = sessions
if sessions:
latest = max(
(s.get("updated_at") or s.get("created_at", "") for s in sessions),
default=None,
)
if latest:
try:
self._last_activity = datetime.fromisoformat(latest)
except (ValueError, TypeError):
pass
data[DATA_LAST_ACTIVITY] = self._last_activity
except OpenClawApiError as err:
_LOGGER.debug("Error fetching sessions: %s", err)
# ── Models (polled every ~4 intervals ≈ 2 minutes) ──────────
self._model_poll_counter += 1
if not self._model_cache or self._model_poll_counter >= 4:
self._model_poll_counter = 0
try:
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 OpenClawApiError as err:
_LOGGER.debug("Error fetching models: %s", err)
data.update(self._model_cache) data.update(self._model_cache)
return data return data
+1 -1
View File
@@ -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.0", "version": "0.1.2",
"dependencies": ["conversation"], "dependencies": ["conversation"],
"after_dependencies": ["hassio"] "after_dependencies": ["hassio"]
} }