Vibecoded fixes & readme

This commit is contained in:
Artem Kokos
2026-03-28 20:11:23 +07:00
parent 62af4e46af
commit d024ba78ab
7 changed files with 631 additions and 259 deletions

View File

@@ -1,27 +1,43 @@
import asyncio
import logging
from datetime import datetime, timedelta
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
from app.core.scheduler import app_tz, scheduler, execute_lamp_command
from app.core.scheduler import app_tz, scheduler
from app.core.state import state_manager
from app.drivers.wiz import WizDriver
from app.api.deps import verify_token
logger = logging.getLogger(__name__)
router = APIRouter(dependencies=[Depends(verify_token)])
wiz = WizDriver()
async def run_delayed_command(ips: list[str], state: bool):
"""Вспомогательная функция для разовых задач"""
async def run_group_command(target_id: str, is_group: bool, params: dict):
"""
Универсальное выполнение команды по расписанию.
IP резолвится в момент выполнения, а не создания задачи --
корректно работает при смене IP (DHCP) и изменении состава группы.
"""
if is_group:
ips = state_manager.get_group_ips(target_id)
else:
dev = state_manager.devices.get(target_id)
ips = [dev.ip] if dev else []
if not ips:
logger.warning(f"Расписание: цель {target_id} не найдена (0 IP)")
return
local_wiz = WizDriver()
for ip in ips:
try:
await local_wiz.set_pilot(ip, {"state": state})
except Exception:
pass # Игнорим ошибки отдельных ламп
await local_wiz.set_pilot(ip, params)
logger.info(f"⏰ Расписание: {target_id} -> {ip}: {params}")
except Exception as e:
logger.error(f"⏰ Расписание: ошибка {ip}: {e}")
@router.post("/once")
@@ -43,23 +59,21 @@ async def schedule_once(
else:
raise HTTPException(status_code=400, detail="Нужно время или отступ в часах")
# 2. Получаем IP
# 2. Проверяем что цель существует (но IP резолвится при выполнении)
if is_group:
ips = state_manager.get_group_ips(target_id)
if target_id not in state_manager.groups:
raise HTTPException(status_code=404, detail="Группа не найдена")
else:
dev = state_manager.devices.get(target_id)
ips = [dev.ip] if dev else []
if not ips:
raise HTTPException(status_code=404, detail="Цель не найдена")
if target_id not in state_manager.devices:
raise HTTPException(status_code=404, detail="Устройство не найдено")
# 3. Регаем задачу
job_id = f"once_{target_id}_{int(exec_time.timestamp())}"
scheduler.add_job(
run_delayed_command,
run_group_command,
trigger=DateTrigger(run_date=exec_time, timezone=app_tz),
args=[ips, state],
args=[target_id, is_group, {"state": state}],
id=job_id,
name=f"Once: {target_id} | {state}",
replace_existing=True,
@@ -77,33 +91,31 @@ async def add_cron_task(
is_group: bool = True,
state: bool = True,
):
ips = state_manager.get_group_ips(target_id) if is_group else []
if not is_group:
dev = state_manager.devices.get(target_id)
if dev:
ips = [dev.ip]
# Проверяем что цель существует
if is_group:
if target_id not in state_manager.groups:
raise HTTPException(status_code=404, detail="Группа не найдена")
else:
if target_id not in state_manager.devices:
raise HTTPException(status_code=404, detail="Устройство не найдено")
if not ips:
raise HTTPException(status_code=404, detail="Цель не найдена")
# Используем таймзону приложения для крона
# Одна задача на всю группу -- IP резолвятся при каждом срабатывании
trigger = CronTrigger(
hour=hour, minute=minute, day_of_week=day_of_week, timezone=app_tz
)
job_ids = []
for ip in ips:
job = scheduler.add_job(
execute_lamp_command,
trigger,
args=[ip, {"state": state}],
id=f"cron_{target_id}_{ip}_{hour}_{minute}",
name=f"CRON: {target_id} | {hour}:{minute} | {state}",
replace_existing=True,
)
job_ids.append(job.id)
job_id = f"cron_{target_id}_{hour}_{minute}"
return {"status": "cron_scheduled", "jobs": job_ids}
scheduler.add_job(
run_group_command,
trigger,
args=[target_id, is_group, {"state": state}],
id=job_id,
name=f"CRON: {target_id} | {hour}:{minute} | {state}",
replace_existing=True,
)
return {"status": "cron_scheduled", "job_id": job_id}
@router.get("/tasks")
@@ -119,13 +131,13 @@ async def get_all_tasks():
next_run_str = None
if job.next_run_time:
# ПЕРЕВОДИМ ИЗ UTC В ЛОКАЛЬНУЮ ТАЙМЗОНУ ДЛЯ ВЫВОДА
# Переводим из UTC в локальную таймзону для вывода
local_time = job.next_run_time.astimezone(app_tz)
h = str(local_time.hour).zfill(2)
m = str(local_time.minute).zfill(2)
next_run_str = local_time.isoformat()
# Если это крон, подтягиваем значения из триггера (они там как строки)
# Если это крон, подтягиваем значения из триггера
if hasattr(job.trigger, "fields"):
h = str(job.trigger.fields[5])
m = str(job.trigger.fields[6])
@@ -148,5 +160,5 @@ async def cancel_task(job_id: str):
try:
scheduler.remove_job(job_id)
return {"status": "deleted"}
except:
except Exception:
raise HTTPException(status_code=404, detail="Задача не найдена")