Refactor API client to remove unsupported endpoints and improve error handling for unexpected response types
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user