feat: add RGB/color temp control and enhance error handling for groups
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.db
|
||||||
|
.pytest_cache/
|
||||||
@@ -6,17 +6,39 @@ import socket
|
|||||||
class WizDriver:
|
class WizDriver:
|
||||||
PORT = 38899
|
PORT = 38899
|
||||||
|
|
||||||
# Стандартные ID сцен WiZ
|
|
||||||
SCENES = {
|
SCENES = {
|
||||||
|
# Динамические (меняют цвет/яркость во времени)
|
||||||
"ocean": 1,
|
"ocean": 1,
|
||||||
"romance": 2,
|
"romance": 2,
|
||||||
"party": 3,
|
"party": 3,
|
||||||
"fireplace": 5,
|
"fireplace": 5,
|
||||||
"cozy": 6,
|
"cozy": 6,
|
||||||
"forest": 10,
|
"forest": 10,
|
||||||
|
"pastel_colors": 11,
|
||||||
|
"wake_up": 12,
|
||||||
"bedtime": 13,
|
"bedtime": 13,
|
||||||
"warm_white": 33,
|
"warm_white": 14,
|
||||||
"daylight": 34,
|
"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):
|
async def send_udp(self, ip: str, payload: dict):
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ class GroupModel(Base):
|
|||||||
|
|
||||||
id: Mapped[str] = mapped_column(String, primary_key=True)
|
id: Mapped[str] = mapped_column(String, primary_key=True)
|
||||||
name: Mapped[str] = mapped_column(String)
|
name: Mapped[str] = mapped_column(String)
|
||||||
device_ids: Mapped[list] = mapped_column(JSON) # Храним список MAC-адресов как JSON
|
device_ids: Mapped[List[str]] = mapped_column(
|
||||||
|
JSON
|
||||||
|
) # Храним список MAC-адресов как JSON
|
||||||
|
|
||||||
|
|
||||||
# --- Pydantic модели для API (оставляем для валидации) ---
|
# --- Pydantic модели для API (оставляем для валидации) ---
|
||||||
|
|||||||
93
main.py
93
main.py
@@ -33,11 +33,9 @@ async def lifespan(app: FastAPI):
|
|||||||
result = await session.execute(select(GroupModel))
|
result = await session.execute(select(GroupModel))
|
||||||
groups = result.scalars().all()
|
groups = result.scalars().all()
|
||||||
for g in groups:
|
for g in groups:
|
||||||
# Превращаем модель БД в формат стейта (если нужно)
|
|
||||||
state_manager.groups[g.id] = g
|
state_manager.groups[g.id] = g
|
||||||
logger.info(f"📂 Загружена группа: {g.name} (MACs: {g.device_ids})")
|
logger.info(f"📂 Загружена группа: {g.name} (MACs: {g.device_ids})")
|
||||||
|
|
||||||
# 3. Задача для периодического сканирования сети
|
|
||||||
async def periodic_discovery():
|
async def periodic_discovery():
|
||||||
logger.info("🚀 Запущена фоновая служба Discovery")
|
logger.info("🚀 Запущена фоновая служба Discovery")
|
||||||
|
|
||||||
@@ -74,30 +72,17 @@ async def get_all_devices():
|
|||||||
return state_manager.devices
|
return state_manager.devices
|
||||||
|
|
||||||
|
|
||||||
@app.post("/control/device/{device_id}")
|
|
||||||
async def control_device(device_id: str, state: bool, brightness: Optional[int] = None):
|
|
||||||
"""Прямое управление лампой по MAC."""
|
|
||||||
device = state_manager.devices.get(device_id)
|
|
||||||
if not device:
|
|
||||||
raise HTTPException(status_code=404, detail="Лампа не в сети")
|
|
||||||
|
|
||||||
params = {"state": state}
|
|
||||||
if brightness:
|
|
||||||
params["dimming"] = brightness
|
|
||||||
|
|
||||||
result = await wiz.set_pilot(device.ip, params)
|
|
||||||
return {"device_id": device_id, "result": result}
|
|
||||||
|
|
||||||
|
|
||||||
# --- Эндпоинты Групп ---
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/groups")
|
@app.get("/groups")
|
||||||
async def get_groups():
|
async def get_groups():
|
||||||
"""Список всех созданных люстр/групп."""
|
"""Список всех созданных люстр/групп."""
|
||||||
return state_manager.groups
|
return state_manager.groups
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/scenes")
|
||||||
|
async def get_scenes():
|
||||||
|
return wiz.SCENES
|
||||||
|
|
||||||
|
|
||||||
@app.post("/groups")
|
@app.post("/groups")
|
||||||
async def create_group(data: GroupCreateSchema):
|
async def create_group(data: GroupCreateSchema):
|
||||||
async with async_session() as session:
|
async with async_session() as session:
|
||||||
@@ -115,37 +100,77 @@ async def create_group(data: GroupCreateSchema):
|
|||||||
return {"status": "created", "group": data.name}
|
return {"status": "created", "group": data.name}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/control/group/{group_id}")
|
@app.post("/control/device/{device_id}")
|
||||||
async def control_group(
|
async def control_device(
|
||||||
group_id: str,
|
device_id: str,
|
||||||
state: Optional[bool] = None,
|
state: Optional[bool] = None,
|
||||||
brightness: Optional[int] = None,
|
brightness: Optional[int] = None,
|
||||||
scene: Optional[str] = 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:
|
||||||
ips = state_manager.get_group_ips(group_id)
|
raise HTTPException(status_code=404, detail="Лампа не в сети")
|
||||||
if not ips:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=404, detail="Группа не найдена или лампы оффлайн"
|
|
||||||
)
|
|
||||||
|
|
||||||
params = {}
|
params = {}
|
||||||
if state is not None:
|
if state is not None:
|
||||||
params["state"] = state
|
params["state"] = state
|
||||||
if brightness is not None:
|
if brightness is not None:
|
||||||
params["dimming"] = brightness
|
params["dimming"] = brightness
|
||||||
|
|
||||||
if scene and scene in wiz.SCENES:
|
if scene and scene in wiz.SCENES:
|
||||||
params["sceneId"] = wiz.SCENES[scene]
|
params["sceneId"] = wiz.SCENES[scene]
|
||||||
|
elif temp is not None:
|
||||||
|
params["temp"] = temp
|
||||||
|
elif r is not None or g is not None or b is not None:
|
||||||
|
params["r"], params["g"], params["b"] = r or 0, g or 0, b or 0
|
||||||
|
|
||||||
if not params:
|
if not params:
|
||||||
raise HTTPException(status_code=400, detail="Никаких команд не передано")
|
raise HTTPException(status_code=400, detail="Никаких команд не передано")
|
||||||
|
|
||||||
# Параллельная отправка всем лампам группы
|
result = await wiz.set_pilot(device.ip, params)
|
||||||
tasks = [wiz.set_pilot(ip, params) for ip in ips]
|
return {"device_id": device_id, "applied": params, "result": result}
|
||||||
await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
return {"status": "ok", "group": group_id, "sent_to": ips}
|
|
||||||
|
@app.post("/control/group/{group_id}")
|
||||||
|
async def control_group(
|
||||||
|
group_id: str,
|
||||||
|
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 r is not None or g is not None or b is not None:
|
||||||
|
params["r"], params["g"], params["b"] = r or 0, g or 0, b or 0
|
||||||
|
|
||||||
|
if not params:
|
||||||
|
raise HTTPException(status_code=400, detail="Никаких команд не передано")
|
||||||
|
|
||||||
|
# Используем return_exceptions=True, чтобы ошибки одной лампы не ломали всё
|
||||||
|
tasks = [wiz.set_pilot(ip, params) for ip in ips]
|
||||||
|
await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
|
return {"status": "ok", "applied": params, "sent_to": ips}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user