Files
ignis-core/app/api/routes/schedules.py
Артём Кокос 62af4e46af Enable for 4 hours feature
2026-03-02 21:44:45 +07:00

153 lines
4.7 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
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="Задача не найдена")