2026-05-29 12:26:34 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
import aiohttp
|
|
|
|
|
|
|
|
|
|
|
2026-05-29 13:41:55 +02:00
|
|
|
def make_session(token: str, timeout: float = 10.0) -> aiohttp.ClientSession:
|
|
|
|
|
"""Create a pre-configured ClientSession for use with HAClient."""
|
|
|
|
|
return aiohttp.ClientSession(
|
|
|
|
|
headers={
|
|
|
|
|
"Authorization": f"Bearer {token}",
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
timeout=aiohttp.ClientTimeout(total=timeout),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-05-29 12:26:34 +02:00
|
|
|
class HAClient:
|
2026-05-29 13:41:55 +02:00
|
|
|
"""Async Home Assistant REST API client.
|
2026-05-29 12:26:34 +02:00
|
|
|
|
2026-05-29 13:41:55 +02:00
|
|
|
Session lifecycle is managed externally — the caller creates the session
|
|
|
|
|
via make_session() at startup and closes it on shutdown. HAClient is a
|
|
|
|
|
session-borrower: it never opens or closes the session it receives.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, base_url: str, session: aiohttp.ClientSession) -> None:
|
2026-05-29 12:26:34 +02:00
|
|
|
self._base_url = base_url.rstrip("/")
|
2026-05-29 13:41:55 +02:00
|
|
|
self._session = session
|
2026-05-29 12:26:34 +02:00
|
|
|
|
|
|
|
|
async def get_api_status(self) -> dict[str, Any]:
|
|
|
|
|
"""GET /api/ — returns {"message": "API running."} when HA is up."""
|
2026-05-29 13:41:55 +02:00
|
|
|
async with self._session.get(f"{self._base_url}/api/") as resp:
|
2026-05-29 12:26:34 +02:00
|
|
|
resp.raise_for_status()
|
|
|
|
|
return await resp.json()
|
|
|
|
|
|
|
|
|
|
async def get_states(self) -> list[dict[str, Any]]:
|
|
|
|
|
"""GET /api/states — full entity state list."""
|
2026-05-29 13:41:55 +02:00
|
|
|
async with self._session.get(f"{self._base_url}/api/states") as resp:
|
2026-05-29 12:26:34 +02:00
|
|
|
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."""
|
2026-05-29 13:41:55 +02:00
|
|
|
async with self._session.get(f"{self._base_url}/api/system_health") as resp:
|
2026-05-29 12:26:34 +02:00
|
|
|
resp.raise_for_status()
|
|
|
|
|
return await resp.json()
|
|
|
|
|
|
|
|
|
|
async def get_config(self) -> dict[str, Any]:
|
|
|
|
|
"""GET /api/config — HA configuration including version."""
|
2026-05-29 13:41:55 +02:00
|
|
|
async with self._session.get(f"{self._base_url}/api/config") as resp:
|
|
|
|
|
resp.raise_for_status()
|
|
|
|
|
return await resp.json()
|
|
|
|
|
|
|
|
|
|
async def get_entity_registry(self) -> list[dict[str, Any]]:
|
|
|
|
|
"""GET /api/config/entity_registry — entity registry entries.
|
|
|
|
|
|
|
|
|
|
Each entry includes entity_id, platform (integration name), area_id,
|
|
|
|
|
config_entry_id, and other metadata.
|
|
|
|
|
"""
|
|
|
|
|
async with self._session.get(
|
|
|
|
|
f"{self._base_url}/api/config/entity_registry"
|
|
|
|
|
) as resp:
|
2026-05-29 12:26:34 +02:00
|
|
|
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}"
|
2026-05-29 13:41:55 +02:00
|
|
|
async with self._session.get(url) as resp:
|
2026-05-29 12:26:34 +02:00
|
|
|
resp.raise_for_status()
|
|
|
|
|
return await resp.json()
|
|
|
|
|
|
|
|
|
|
async def get_error_log(self) -> str:
|
|
|
|
|
"""GET /api/error_log — plaintext error log."""
|
2026-05-29 13:41:55 +02:00
|
|
|
async with self._session.get(f"{self._base_url}/api/error_log") as resp:
|
2026-05-29 12:26:34 +02:00
|
|
|
resp.raise_for_status()
|
|
|
|
|
return await resp.text()
|