77 lines
2.9 KiB
Python
77 lines
2.9 KiB
Python
|
|
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()
|