diff --git a/custom_components/openclaw/api.py b/custom_components/openclaw/api.py index 56c7ea7..3f81936 100644 --- a/custom_components/openclaw/api.py +++ b/custom_components/openclaw/api.py @@ -12,8 +12,6 @@ import aiohttp from .const import ( API_CHAT_COMPLETIONS, API_MODELS, - API_SESSIONS, - API_STATUS, ) _LOGGER = logging.getLogger(__name__) @@ -137,6 +135,14 @@ class OpenClawApiClient: raise OpenClawApiError( 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() except (aiohttp.ClientConnectorError, aiohttp.ClientOSError, asyncio.TimeoutError) as err: @@ -146,26 +152,6 @@ class OpenClawApiClient: # ─── 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]: """Get available models (OpenAI-compatible). @@ -303,6 +289,9 @@ class OpenClawApiClient: async def async_check_connection(self) -> bool: """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: True if connected and authenticated. @@ -311,7 +300,7 @@ class OpenClawApiClient: can distinguish bad tokens from unreachable gateways). """ try: - await self.async_get_status() + await self.async_get_models() return True except OpenClawAuthError: raise diff --git a/custom_components/openclaw/const.py b/custom_components/openclaw/const.py index 9e0b09c..f43b702 100644 --- a/custom_components/openclaw/const.py +++ b/custom_components/openclaw/const.py @@ -53,7 +53,8 @@ ATTR_MODEL = "model" ATTR_TIMESTAMP = "timestamp" # API endpoints -API_STATUS = "/api/status" -API_SESSIONS = "/api/sessions" +# The OpenClaw gateway exposes only the OpenAI-compatible endpoints. +# /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_CHAT_COMPLETIONS = "/v1/chat/completions" diff --git a/custom_components/openclaw/coordinator.py b/custom_components/openclaw/coordinator.py index d0edb88..3878bf4 100644 --- a/custom_components/openclaw/coordinator.py +++ b/custom_components/openclaw/coordinator.py @@ -60,7 +60,6 @@ class OpenClawCoordinator(DataUpdateCoordinator[dict[str, Any]]): self.client = client self._last_activity: datetime | None = None self._model_cache: dict[str, Any] = {} - self._model_poll_counter = 0 self._consecutive_failures = 0 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]: """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: Aggregated data dict for all entities. """ data = self._offline_data() - # ── Status ────────────────────────────────────────────────── try: - status_resp = await self.client.async_get_status() - data[DATA_STATUS] = status_resp.get("status", "online") + models_resp = await self.client.async_get_models() + models = models_resp.get("data", []) + + data[DATA_STATUS] = "online" data[DATA_CONNECTED] = True - data[DATA_GATEWAY_VERSION] = status_resp.get("version") - data[DATA_UPTIME] = status_resp.get("uptime") + data[DATA_GATEWAY_VERSION] = None # Not exposed by gateway API + 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 + 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() @@ -112,48 +128,9 @@ class OpenClawCoordinator(DataUpdateCoordinator[dict[str, Any]]): return data except OpenClawApiError as err: - _LOGGER.warning("Error fetching gateway status: %s", err) + _LOGGER.warning("Error fetching gateway models: %s", err) 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) return data diff --git a/custom_components/openclaw/manifest.json b/custom_components/openclaw/manifest.json index fb4ef19..cfa8327 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.0", + "version": "0.1.2", "dependencies": ["conversation"], "after_dependencies": ["hassio"] }