Files
ignis-core/app/api/routes/schedules.py
2026-05-16 10:29:54 +07:00

176 lines
5.6 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 logging
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException
from app.api.deps import require_admin
from app.api.schemas import (
DeleteStatusResponse,
ScheduleCreateResponse,
ScheduleCronRequest,
ScheduleOnceRequest,
ScheduleTasksResponse,
)
from app.core.scheduler import (
app_tz,
create_schedule_task,
delete_schedule_task,
is_internal_job_id,
list_schedule_tasks,
)
from app.core.state import state_manager
from app.drivers.wiz import WizDriver
logger = logging.getLogger(__name__)
router = APIRouter(dependencies=[Depends(require_admin)])
def _validate_target(target_id: str, is_group: bool):
if is_group:
if target_id not in state_manager.groups:
raise HTTPException(status_code=404, detail="Группа не найдена")
return "group"
if target_id not in state_manager.devices:
raise HTTPException(status_code=404, detail="Устройство не найдено")
return "device"
async def run_group_command(target_id: str, is_group: bool, params: dict):
"""
Универсальное выполнение команды по расписанию.
Сигнатура специально сохранена совместимой со старым persisted jobstore,
чтобы legacy APScheduler jobs можно было безопасно мигрировать.
"""
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()
success_count = 0
failure_count = 0
for ip in ips:
try:
result = await local_wiz.set_pilot(ip, params)
if result.ok:
success_count += 1
logger.info(f"Расписание: {target_id} -> {ip}: {params}")
else:
failure_count += 1
logger.error(
f"Расписание: ошибка {ip}: {result.message or result.kind}"
)
except Exception as e:
failure_count += 1
logger.error(f"Расписание: ошибка {ip}: {e}")
from app.api.routes.control import log_command_result_by_name
target_type = "group" if is_group else "device"
await log_command_result_by_name(
"scheduler",
target_type,
target_id,
params,
success_count=success_count,
failure_count=failure_count,
target_count=len(ips),
)
@router.post(
"/once",
response_model=ScheduleCreateResponse,
response_model_exclude_none=True,
)
async def schedule_once(payload: ScheduleOnceRequest):
if payload.hours_from_now is not None:
exec_time = datetime.now(app_tz) + timedelta(hours=payload.hours_from_now)
elif payload.run_at is not None:
if payload.run_at.tzinfo is None:
exec_time = app_tz.localize(payload.run_at)
else:
exec_time = payload.run_at.astimezone(app_tz)
else:
raise HTTPException(status_code=400, detail="Нужно время или отступ в часах")
if exec_time <= datetime.now(app_tz):
raise HTTPException(
status_code=400, detail="Время запуска должно быть в будущем"
)
target_type = _validate_target(payload.target_id, payload.is_group)
action_params = payload.to_wiz_params()
try:
task = await create_schedule_task(
trigger_type="once",
target_id=payload.target_id,
target_type=target_type,
trigger_args={"run_at": exec_time.isoformat()},
action_params=action_params,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))
return {
"status": "scheduled",
"job_id": task.job_id,
"run_at": exec_time.isoformat(),
}
@router.post(
"/cron",
response_model=ScheduleCreateResponse,
response_model_exclude_none=True,
)
async def add_cron_task(payload: ScheduleCronRequest):
target_type = _validate_target(payload.target_id, payload.is_group)
action_params = payload.to_wiz_params()
trigger_args = {
"hour": payload.hour,
"minute": payload.minute,
"day_of_week": payload.day_of_week,
}
try:
task = await create_schedule_task(
trigger_type="cron",
target_id=payload.target_id,
target_type=target_type,
trigger_args=trigger_args,
action_params=action_params,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))
return {"status": "cron_scheduled", "job_id": task.job_id}
@router.get("/tasks", response_model=ScheduleTasksResponse)
async def get_all_tasks():
return {"tasks": await list_schedule_tasks()}
@router.delete("/{job_id}", response_model=DeleteStatusResponse)
async def cancel_task(job_id: str):
if is_internal_job_id(job_id):
raise HTTPException(status_code=403, detail="Нельзя удалить служебную задачу")
try:
await delete_schedule_task(job_id)
return {"status": "deleted"}
except KeyError:
raise HTTPException(status_code=404, detail="Задача не найдена")
except ValueError:
raise HTTPException(status_code=403, detail="Нельзя удалить служебную задачу")