from __future__ import annotations from typing import Any import aiohttp class HAClient: """Async Home Assistant REST API client using long-lived token auth.""" def __init__(self, base_url: str, token: str, timeout: float = 10.0) -> None: self._base_url = base_url.rstrip("/") self._headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", } self._timeout = aiohttp.ClientTimeout(total=timeout) self._session: aiohttp.ClientSession | None = None async def __aenter__(self) -> "HAClient": self._session = aiohttp.ClientSession( headers=self._headers, timeout=self._timeout, ) return self async def __aexit__(self, *_: Any) -> None: if self._session: await self._session.close() self._session = None def _session_or_raise(self) -> aiohttp.ClientSession: if self._session is None: raise RuntimeError("HAClient must be used as an async context manager") return self._session async def get_api_status(self) -> dict[str, Any]: """GET /api/ — returns {"message": "API running."} when HA is up.""" async with self._session_or_raise().get(f"{self._base_url}/api/") as resp: resp.raise_for_status() return await resp.json() async def get_states(self) -> list[dict[str, Any]]: """GET /api/states — full entity state list.""" async with self._session_or_raise().get(f"{self._base_url}/api/states") as resp: resp.raise_for_status() return await resp.json() async def get_system_health(self) -> dict[str, Any]: """GET /api/system_health — per-integration health summary.""" async with self._session_or_raise().get( f"{self._base_url}/api/system_health" ) as resp: resp.raise_for_status() return await resp.json() async def get_config(self) -> dict[str, Any]: """GET /api/config — HA configuration including version.""" async with self._session_or_raise().get(f"{self._base_url}/api/config") as resp: resp.raise_for_status() return await resp.json() async def get_automation_traces(self, automation_id: str) -> list[dict[str, Any]]: """GET /api/trace/automation/ — last run traces for an automation.""" url = f"{self._base_url}/api/trace/automation/{automation_id}" async with self._session_or_raise().get(url) as resp: resp.raise_for_status() return await resp.json() async def get_error_log(self) -> str: """GET /api/error_log — plaintext error log.""" async with self._session_or_raise().get( f"{self._base_url}/api/error_log" ) as resp: resp.raise_for_status() return await resp.text()