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="Нельзя удалить служебную задачу")