Initial commit: Javis project
This commit is contained in:
commit
435c645f36
164 files changed
+35279
No files matched your search
@@ -0,0 +1,180 @@
|
||||
"""Async client for the Homey Pro local Web API + Socket.IO realtime events."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from collections import deque
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
try:
|
||||
import socketio # type: ignore
|
||||
except ImportError: # pragma: no cover
|
||||
socketio = None # type: ignore
|
||||
|
||||
from .config import Settings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HomeyClient:
|
||||
"""Thin async wrapper around Homey's local HTTP + Socket.IO APIs."""
|
||||
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
self.settings = settings
|
||||
self._http = httpx.AsyncClient(
|
||||
base_url=settings.base_url,
|
||||
headers={
|
||||
"Authorization": f"Bearer {settings.token}",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
timeout=settings.timeout,
|
||||
)
|
||||
self._events: deque[dict[str, Any]] = deque(maxlen=settings.event_buffer)
|
||||
self._sio: Any | None = None
|
||||
self._sio_connected: bool = False
|
||||
|
||||
# ---- lifecycle --------------------------------------------------------
|
||||
|
||||
async def aclose(self) -> None:
|
||||
try:
|
||||
if self._sio is not None and self._sio_connected:
|
||||
await self._sio.disconnect()
|
||||
except Exception: # pragma: no cover
|
||||
log.exception("Error disconnecting Socket.IO")
|
||||
await self._http.aclose()
|
||||
|
||||
# ---- helpers ----------------------------------------------------------
|
||||
|
||||
async def _get(self, path: str) -> Any:
|
||||
r = await self._http.get(path)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
async def _post(self, path: str, json: Any | None = None) -> Any:
|
||||
r = await self._http.post(path, json=json)
|
||||
r.raise_for_status()
|
||||
if r.content:
|
||||
try:
|
||||
return r.json()
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
|
||||
async def _put(self, path: str, json: Any | None = None) -> Any:
|
||||
r = await self._http.put(path, json=json)
|
||||
r.raise_for_status()
|
||||
if r.content:
|
||||
try:
|
||||
return r.json()
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
|
||||
# ---- REST: devices / zones / flows -----------------------------------
|
||||
|
||||
async def get_system_info(self) -> dict[str, Any]:
|
||||
return await self._get("/api/manager/system/info")
|
||||
|
||||
async def list_devices(self) -> dict[str, Any]:
|
||||
return await self._get("/api/manager/devices/device")
|
||||
|
||||
async def get_device(self, device_id: str) -> dict[str, Any]:
|
||||
return await self._get(f"/api/manager/devices/device/{device_id}")
|
||||
|
||||
async def list_zones(self) -> dict[str, Any]:
|
||||
return await self._get("/api/manager/zones/zone")
|
||||
|
||||
async def set_capability(
|
||||
self, device_id: str, capability_id: str, value: Any
|
||||
) -> Any:
|
||||
return await self._put(
|
||||
f"/api/manager/devices/device/{device_id}/capability/{capability_id}",
|
||||
json={"value": value},
|
||||
)
|
||||
|
||||
async def list_flows(self) -> dict[str, Any]:
|
||||
return await self._get("/api/manager/flow/flow")
|
||||
|
||||
async def trigger_flow(self, flow_id: str) -> Any:
|
||||
return await self._post(f"/api/manager/flow/flow/{flow_id}/trigger")
|
||||
|
||||
# ---- REST: logic variables -------------------------------------------
|
||||
|
||||
async def list_variables(self) -> dict[str, Any]:
|
||||
"""Return all Homey Logic Variables (type, current value, etc.)."""
|
||||
return await self._get("/api/manager/logic/variable")
|
||||
|
||||
async def set_variable(self, variable_id: str, value: Any) -> Any:
|
||||
"""Set a Homey Logic Variable to *value*."""
|
||||
return await self._put(
|
||||
f"/api/manager/logic/variable/{variable_id}",
|
||||
json={"value": value},
|
||||
)
|
||||
|
||||
# ---- REST: notifications ---------------------------------------------
|
||||
|
||||
async def send_notification(self, title: str, excerpt: str = "") -> Any:
|
||||
"""Push a notification to the Homey app on all registered phones."""
|
||||
return await self._post(
|
||||
"/api/manager/notifications/notification",
|
||||
json={
|
||||
"ownerUri": "homey:manager:notifications",
|
||||
"title": title,
|
||||
"excerpt": excerpt,
|
||||
},
|
||||
)
|
||||
|
||||
# ---- realtime events --------------------------------------------------
|
||||
|
||||
def recent_events(
|
||||
self, limit: int = 50, kind: str | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
items = list(self._events)
|
||||
if kind:
|
||||
items = [e for e in items if e.get("kind") == kind]
|
||||
return items[-limit:]
|
||||
|
||||
async def start_realtime(self) -> None:
|
||||
"""Best-effort Socket.IO connection. Failures are logged, not raised."""
|
||||
if not self.settings.realtime:
|
||||
return
|
||||
if socketio is None:
|
||||
log.warning("python-socketio not installed; realtime disabled")
|
||||
return
|
||||
if self._sio_connected:
|
||||
return
|
||||
|
||||
sio = socketio.AsyncClient(reconnection=True, logger=False, engineio_logger=False)
|
||||
self._sio = sio
|
||||
|
||||
def _record(kind: str) -> Any:
|
||||
async def _handler(data: Any = None) -> None:
|
||||
self._events.append(
|
||||
{"ts": time.time(), "kind": kind, "data": data}
|
||||
)
|
||||
|
||||
return _handler
|
||||
|
||||
for kind in (
|
||||
"device.create",
|
||||
"device.update",
|
||||
"device.delete",
|
||||
"capability",
|
||||
):
|
||||
sio.on(kind, _record(kind))
|
||||
|
||||
try:
|
||||
await sio.connect(
|
||||
self.settings.base_url,
|
||||
auth={"token": self.settings.token},
|
||||
transports=["websocket"],
|
||||
wait_timeout=self.settings.timeout,
|
||||
)
|
||||
self._sio_connected = True
|
||||
log.info("Socket.IO connected to %s", self.settings.base_url)
|
||||
except Exception as exc: # pragma: no cover
|
||||
log.warning("Socket.IO connection failed: %s", exc)
|
||||
self._sio_connected = False
|
||||
Reference in new issue
Block a user