Files
ignis-core/app/api/routes/control.py
2026-03-28 23:06:40 +07:00

180 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import asyncio
import json
import logging
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from app.core.state import state_manager
from app.core.database import async_session
from app.drivers.wiz import WizDriver
from app.api.deps import verify_token, AuthContext
from app.models.event_log import EventLog
logger = logging.getLogger(__name__)
router = APIRouter(dependencies=[Depends(verify_token)])
wiz = WizDriver()
async def _log_event(
auth: AuthContext, action: str, target_type: str, target_id: str, params: dict
):
"""Записать событие в лог."""
try:
async with async_session() as session:
event = EventLog(
key_name=auth.key_name,
action=action,
target_type=target_type,
target_id=target_id,
params=json.dumps(params, ensure_ascii=False) if params else None,
)
session.add(event)
await session.commit()
except Exception as e:
logger.error(f"Ошибка записи в лог: {e}")
def _classify_action(params: dict) -> str:
"""Определить тип действия по параметрам."""
if "state" in params and len(params) == 1:
return "toggle_on" if params["state"] else "toggle_off"
if "sceneId" in params or "scene" in params:
return "scene"
if "r" in params or "g" in params or "b" in params:
return "color"
if "temp" in params:
return "temperature"
if "dimming" in params:
return "brightness"
return "control"
@router.post("/device/{device_id}")
async def control_device(
device_id: str,
auth: AuthContext = Depends(verify_token),
state: Optional[bool] = None,
brightness: Optional[int] = None,
scene: Optional[str] = None,
temp: Optional[int] = None,
r: Optional[int] = None,
g: Optional[int] = None,
b: Optional[int] = None,
):
device = state_manager.devices.get(device_id)
if not device:
raise HTTPException(status_code=404, detail="Лампа не в сети")
params = {}
if state is not None:
params["state"] = state
if brightness is not None:
params["dimming"] = brightness
if scene and scene in wiz.SCENES:
params["sceneId"] = wiz.SCENES[scene]
elif temp is not None:
params["temp"] = temp
elif any(v is not None for v in [r, g, b]):
params["r"], params["g"], params["b"] = r or 0, g or 0, b or 0
if not params:
raise HTTPException(status_code=400, detail="Никаких команд не передано")
result = await wiz.set_pilot(device.ip, params)
# Логируем
await _log_event(auth, _classify_action(params), "device", device_id, params)
return {"device_id": device_id, "applied": params, "result": result}
@router.post("/group/{group_id}")
async def control_group(
group_id: str,
auth: AuthContext = Depends(verify_token),
state: Optional[bool] = None,
brightness: Optional[int] = None,
scene: Optional[str] = None,
temp: Optional[int] = None,
r: Optional[int] = None,
g: Optional[int] = None,
b: Optional[int] = None,
):
ips = state_manager.get_group_ips(group_id)
if not ips:
raise HTTPException(status_code=404, detail="Группа не найдена или оффлайн")
params = {}
if state is not None:
params["state"] = state
if brightness is not None:
params["dimming"] = brightness
if scene and scene in wiz.SCENES:
params["sceneId"] = wiz.SCENES[scene]
elif temp is not None:
params["temp"] = temp
elif any(v is not None for v in [r, g, b]):
params["r"], params["g"], params["b"] = r or 0, g or 0, b or 0
tasks = [wiz.set_pilot(ip, params) for ip in ips]
await asyncio.gather(*tasks, return_exceptions=True)
# Логируем
await _log_event(auth, _classify_action(params), "group", group_id, params)
return {"status": "ok", "applied": params, "sent_to": ips}
@router.post("/device/{device_id}/blink")
async def blink_device(device_id: str):
device = state_manager.devices.get(device_id)
if not device:
raise HTTPException(status_code=404, detail="Лампа оффлайн")
try:
current = await wiz.get_pilot(device.ip)
original_state = current.get("result", {}).get("state", False)
await wiz.set_pilot(device.ip, {"state": not original_state})
await asyncio.sleep(0.5)
await wiz.set_pilot(device.ip, {"state": original_state})
return {"status": "blink_done", "original": original_state}
except Exception as e:
logger.error(f"Blink error: {e}")
raise HTTPException(status_code=500, detail="Ошибка связи с лампой")
@router.get("/device/{device_id}/status")
async def get_device_status(device_id: str):
"""Опрос реального состояния конкретной лампы."""
device = state_manager.devices.get(device_id)
if not device:
raise HTTPException(status_code=404, detail="Лампа оффлайн или не найдена")
try:
status = await wiz.get_pilot(device.ip)
return {"device_id": device_id, "status": status.get("result", {})}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка опроса лампы: {e}")
@router.get("/group/{group_id}/status")
async def get_group_status(group_id: str):
"""Опрос состояния всей группы (возвращает список статусов)."""
ips = state_manager.get_group_ips(group_id)
if not ips:
raise HTTPException(status_code=404, detail="Группа не найдена или оффлайн")
tasks = [wiz.get_pilot(ip) for ip in ips]
results = await asyncio.gather(*tasks, return_exceptions=True)
status_report = []
for ip, res in zip(ips, results):
if isinstance(res, Exception):
status_report.append({"ip": ip, "error": str(res)})
else:
status_report.append({"ip": ip, "status": res.get("result", {})})
return {"group_id": group_id, "results": status_report}