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)