- Sync and async HTTP clients for Ignis Core WiZ server - 23 endpoints: auth, devices, groups, control, schedules, stats, API keys - Pydantic models with client-side validation - 108 unit tests - README with role table and usage examples
185 lines
5.9 KiB
Python
185 lines
5.9 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
from .models import (
|
|
SCENES,
|
|
BlinkResponse,
|
|
CommandRequest,
|
|
DeleteStatusResponse,
|
|
DeviceControlResponse,
|
|
DeviceStatusResponse,
|
|
GroupControlResponse,
|
|
GroupStatusResponse,
|
|
IgnisError,
|
|
KeyActionRequest,
|
|
RescanResponse,
|
|
ScheduleCreateResponse,
|
|
ScheduleCronRequest,
|
|
ScheduleOnceRequest,
|
|
ScheduleTasksResponse,
|
|
ServerInfoResponse,
|
|
)
|
|
|
|
|
|
class IgnisClient:
|
|
def __init__(self, base_url: str, api_key: str, *, timeout: float = 10.0):
|
|
self._base = base_url.rstrip("/")
|
|
self._api_key = api_key
|
|
self._client = httpx.Client(
|
|
base_url=self._base,
|
|
headers={"X-API-Key": self._api_key},
|
|
timeout=httpx.Timeout(timeout),
|
|
follow_redirects=True,
|
|
)
|
|
|
|
def close(self):
|
|
self._client.close()
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *args):
|
|
self.close()
|
|
|
|
def _request(self, method: str, path: str, **kwargs) -> Any:
|
|
resp = self._client.request(method, path, **kwargs)
|
|
if resp.status_code >= 400:
|
|
try:
|
|
detail = resp.json().get("detail", resp.text)
|
|
except Exception:
|
|
detail = resp.text or f"HTTP {resp.status_code}"
|
|
raise IgnisError(resp.status_code, detail)
|
|
return resp.json()
|
|
|
|
def _get(self, path: str, **kwargs) -> Any:
|
|
return self._request("GET", path, **kwargs)
|
|
|
|
def _post(self, path: str, **kwargs) -> Any:
|
|
return self._request("POST", path, **kwargs)
|
|
|
|
def _delete(self, path: str, **kwargs) -> Any:
|
|
return self._request("DELETE", path, **kwargs)
|
|
|
|
# ------------------------------------------------------------------ Auth
|
|
|
|
def auth_me(self) -> dict[str, Any]:
|
|
return self._get("/auth/me")
|
|
|
|
# ------------------------------------------------------------------ System
|
|
|
|
def system_info(self) -> ServerInfoResponse:
|
|
data = self._get("/system/info")
|
|
return ServerInfoResponse(**data)
|
|
|
|
# ------------------------------------------------------------------ Devices
|
|
|
|
def list_devices(self) -> dict[str, dict[str, str]]:
|
|
return self._get("/devices")
|
|
|
|
def list_groups(self) -> dict[str, dict[str, Any]]:
|
|
return self._get("/devices/groups")
|
|
|
|
def list_scenes(self) -> dict[str, int]:
|
|
return dict(SCENES)
|
|
|
|
def create_group(self, group_id: str, name: str, macs: list[str]) -> dict[str, Any]:
|
|
return self._post(
|
|
"/devices/groups", json={"id": group_id, "name": name, "macs": macs}
|
|
)
|
|
|
|
def delete_group(self, group_id: str) -> dict[str, Any]:
|
|
return self._delete(f"/devices/groups/{group_id}")
|
|
|
|
def rescan(self) -> RescanResponse:
|
|
data = self._post("/devices/rescan")
|
|
return RescanResponse(**data)
|
|
|
|
# ------------------------------------------------------------------ Control
|
|
|
|
def control_device(
|
|
self, device_id: str, payload: CommandRequest
|
|
) -> DeviceControlResponse:
|
|
data = self._post(
|
|
f"/control/device/{device_id}",
|
|
json=payload.model_dump(exclude_none=True),
|
|
)
|
|
return DeviceControlResponse(**data)
|
|
|
|
def control_group(
|
|
self, group_id: str, payload: CommandRequest
|
|
) -> GroupControlResponse:
|
|
data = self._post(
|
|
f"/control/group/{group_id}",
|
|
json=payload.model_dump(exclude_none=True),
|
|
)
|
|
return GroupControlResponse(**data)
|
|
|
|
def blink_device(self, device_id: str) -> BlinkResponse:
|
|
data = self._post(f"/control/device/{device_id}/blink")
|
|
return BlinkResponse(**data)
|
|
|
|
def device_status(self, device_id: str) -> DeviceStatusResponse:
|
|
data = self._get(f"/control/device/{device_id}/status")
|
|
return DeviceStatusResponse(**data)
|
|
|
|
def group_status(self, group_id: str) -> GroupStatusResponse:
|
|
data = self._get(f"/control/group/{group_id}/status")
|
|
return GroupStatusResponse(**data)
|
|
|
|
# --------------------------------------------------------------- Schedules
|
|
|
|
def create_once_schedule(
|
|
self, payload: ScheduleOnceRequest
|
|
) -> ScheduleCreateResponse:
|
|
data = self._post(
|
|
"/schedules/once",
|
|
json=payload.model_dump(exclude_none=True),
|
|
)
|
|
return ScheduleCreateResponse(**data)
|
|
|
|
def create_cron_schedule(
|
|
self, payload: ScheduleCronRequest
|
|
) -> ScheduleCreateResponse:
|
|
data = self._post(
|
|
"/schedules/cron",
|
|
json=payload.model_dump(exclude_none=True),
|
|
)
|
|
return ScheduleCreateResponse(**data)
|
|
|
|
def list_schedules(self) -> ScheduleTasksResponse:
|
|
data = self._get("/schedules/tasks")
|
|
return ScheduleTasksResponse(**data)
|
|
|
|
def delete_schedule(self, job_id: str) -> DeleteStatusResponse:
|
|
data = self._delete(f"/schedules/{job_id}")
|
|
return DeleteStatusResponse(**data)
|
|
|
|
# ------------------------------------------------------------------- Stats
|
|
|
|
def stats_summary(self, days: int = 7) -> dict[str, Any]:
|
|
return self._get("/stats/summary", params={"days": days})
|
|
|
|
def stats_log(self, limit: int = 50) -> list[dict[str, Any]]:
|
|
return self._get("/stats/log", params={"limit": limit})
|
|
|
|
# ---------------------------------------------------------------- API Keys
|
|
|
|
def list_api_keys(self) -> list[dict[str, Any]]:
|
|
return self._get("/api-keys")
|
|
|
|
def create_api_key(self, name: str, *, is_admin: bool = False) -> dict[str, Any]:
|
|
return self._post("/api-keys", params={"name": name, "is_admin": is_admin})
|
|
|
|
def revoke_api_key(self, key_or_id: str) -> dict[str, Any]:
|
|
return self._post(
|
|
"/api-keys/revoke", json=KeyActionRequest(key=key_or_id).model_dump()
|
|
)
|
|
|
|
def activate_api_key(self, key_or_id: str) -> dict[str, Any]:
|
|
return self._post(
|
|
"/api-keys/activate", json=KeyActionRequest(key=key_or_id).model_dump()
|
|
)
|