import asyncio 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.state import state_manager from app.drivers.wiz import WizDriver from app.api.deps import verify_token router = APIRouter(dependencies=[Depends(verify_token)]) wiz = WizDriver() async def run_delayed_command(ips: list[str], state: bool): """Вспомогательная функция для разовых задач""" local_wiz = WizDriver() for ip in ips: try: await local_wiz.set_pilot(ip, {"state": state}) except Exception: pass # Игнорим ошибки отдельных ламп @router.post("/once") async def schedule_once( target_id: str, state: bool, run_at: Optional[datetime] = None, hours_from_now: Optional[int] = None, is_group: bool = True, ): # 1. Определяем время запуска в правильной таймзоне if hours_from_now is not None: exec_time = datetime.now(app_tz) + timedelta(hours=hours_from_now) elif run_at: if run_at.tzinfo is None: exec_time = app_tz.localize(run_at) else: exec_time = run_at else: raise HTTPException(status_code=400, detail="Нужно время или отступ в часах") # 2. Получаем IP 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: raise HTTPException(status_code=404, detail="Цель не найдена") # 3. Регаем задачу job_id = f"once_{target_id}_{int(exec_time.timestamp())}" scheduler.add_job( run_delayed_command, trigger=DateTrigger(run_date=exec_time, timezone=app_tz), args=[ips, state], id=job_id, name=f"Once: {target_id} | {state}", replace_existing=True, ) return {"status": "scheduled", "run_at": exec_time.isoformat()} @router.post("/cron") async def add_cron_task( target_id: str, hour: str, minute: str, day_of_week: str = "*", 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 not ips: raise HTTPException(status_code=404, detail="Цель не найдена") # Используем таймзону приложения для крона 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) return {"status": "cron_scheduled", "jobs": job_ids} @router.get("/tasks") async def get_all_tasks(): jobs = [] for job in scheduler.get_jobs(): # Парсим имя name_parts = job.name.split("|") target = name_parts[0].replace("CRON:", "").replace("Once:", "").strip() is_on = "True" in job.name or "true" in job.name.lower() h, m = None, None next_run_str = None if job.next_run_time: # ПЕРЕВОДИМ ИЗ 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]) jobs.append( { "id": job.id, "target_id": target, "state": is_on, "next_run": next_run_str, "hour": h, "minute": m, } ) return {"tasks": jobs} @router.delete("/{job_id}") async def cancel_task(job_id: str): try: scheduler.remove_job(job_id) return {"status": "deleted"} except: raise HTTPException(status_code=404, detail="Задача не найдена")