130 lines
3.7 KiB
Python
130 lines
3.7 KiB
Python
import json
|
|
import asyncio
|
|
import socket
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class WizResponse:
|
|
ok: bool
|
|
ip: str
|
|
kind: str
|
|
payload: dict[str, Any] | None = None
|
|
message: str | None = None
|
|
|
|
@property
|
|
def result(self) -> dict[str, Any]:
|
|
if not self.payload:
|
|
return {}
|
|
return self.payload.get("result", {})
|
|
|
|
|
|
class WizDriver:
|
|
PORT = 38899
|
|
|
|
SCENES = {
|
|
# Динамические (меняют цвет/яркость во времени)
|
|
"ocean": 1,
|
|
"romance": 2,
|
|
"party": 3,
|
|
"fireplace": 5,
|
|
"cozy": 6,
|
|
"forest": 10,
|
|
"pastel_colors": 11,
|
|
"wake_up": 12,
|
|
"bedtime": 13,
|
|
"warm_white": 14,
|
|
"daylight": 15,
|
|
"cool_white": 16,
|
|
"night_light": 17,
|
|
"focus": 18,
|
|
"relax": 19,
|
|
"true_colors": 20,
|
|
"tv_time": 21,
|
|
"plant_growth": 22,
|
|
"spring": 23,
|
|
"summer": 24,
|
|
"fall": 25,
|
|
"deep_dive": 26,
|
|
"jungle": 27,
|
|
"mojito": 28,
|
|
"club": 29,
|
|
"christmas": 30,
|
|
"halloween": 31,
|
|
"candlelight": 32,
|
|
"golden_white": 33,
|
|
"pulse": 34,
|
|
"steampunk": 35,
|
|
}
|
|
|
|
async def send_udp(self, ip: str, payload: dict) -> WizResponse:
|
|
loop = asyncio.get_running_loop()
|
|
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
|
sock.settimeout(2.0)
|
|
data = json.dumps(payload).encode()
|
|
|
|
try:
|
|
await loop.run_in_executor(None, sock.sendto, data, (ip, self.PORT))
|
|
except OSError as exc:
|
|
return WizResponse(
|
|
ok=False,
|
|
ip=ip,
|
|
kind="network_error",
|
|
message=f"Ошибка отправки UDP: {exc}",
|
|
)
|
|
|
|
try:
|
|
resp, _ = await loop.run_in_executor(None, sock.recvfrom, 1024)
|
|
except socket.timeout:
|
|
return WizResponse(
|
|
ok=False,
|
|
ip=ip,
|
|
kind="timeout",
|
|
message="Таймаут ответа от лампы",
|
|
)
|
|
except OSError as exc:
|
|
return WizResponse(
|
|
ok=False,
|
|
ip=ip,
|
|
kind="network_error",
|
|
message=f"Ошибка чтения UDP: {exc}",
|
|
)
|
|
|
|
try:
|
|
decoded = json.loads(resp.decode())
|
|
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
return WizResponse(
|
|
ok=False,
|
|
ip=ip,
|
|
kind="protocol_error",
|
|
message=f"Невалидный ответ WiZ: {exc}",
|
|
)
|
|
|
|
if isinstance(decoded, dict) and "error" in decoded:
|
|
return WizResponse(
|
|
ok=False,
|
|
ip=ip,
|
|
kind="device_error",
|
|
payload=decoded,
|
|
message=str(decoded["error"]),
|
|
)
|
|
|
|
if not isinstance(decoded, dict):
|
|
return WizResponse(
|
|
ok=False,
|
|
ip=ip,
|
|
kind="protocol_error",
|
|
message="Ответ WiZ имеет неожиданный формат",
|
|
)
|
|
|
|
return WizResponse(ok=True, ip=ip, kind="ok", payload=decoded)
|
|
|
|
async def set_pilot(self, ip: str, params: dict) -> WizResponse:
|
|
payload = {"method": "setPilot", "params": params}
|
|
return await self.send_udp(ip, payload)
|
|
|
|
async def get_pilot(self, ip: str) -> WizResponse:
|
|
payload = {"method": "getPilot", "params": {}}
|
|
return await self.send_udp(ip, payload)
|