homelab-codex-ws/services/ha-diag-agent/src/ha_diag/ha_client.py

77 lines
2.9 KiB
Python
Raw Normal View History

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/<id> — 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()