Files
ignis-core/app/api/routes/schedules.py
2026-05-15 23:12:28 +07:00

224 lines
6.9 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 typing import Any, Optional
from fastapi import APIRouter, Depends, HTTPException
from app.api.deps import require_admin
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"
def _build_action_params(
state: Optional[bool],
brightness: Optional[int],
scene: Optional[str],
temp: Optional[int],
r: Optional[int],
g: Optional[int],
b: Optional[int],
) -> dict[str, Any]:
params: dict[str, Any] = {}
if state is not None:
params["state"] = state
if brightness is not None:
params["dimming"] = brightness
if scene is not None:
if scene not in WizDriver.SCENES:
raise HTTPException(status_code=400, detail="Неизвестная сцена")
params["sceneId"] = WizDriver.SCENES[scene]
elif temp is not None:
params["temp"] = temp
elif any(channel is not None for channel in (r, g, b)):
params["r"] = r or 0
params["g"] = g or 0
params["b"] = b or 0
if not params:
raise HTTPException(status_code=400, detail="Никаких команд не передано")
return params
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")
async def schedule_once(
target_id: str,
state: Optional[bool] = None,
run_at: Optional[datetime] = None,
hours_from_now: Optional[int] = None,
is_group: bool = True,
brightness: Optional[int] = None,
scene: Optional[str] = None,
temp: Optional[int] = None,
r: Optional[int] = None,
g: Optional[int] = None,
b: Optional[int] = None,
):
if hours_from_now is not None:
if hours_from_now < 0:
raise HTTPException(
status_code=400, detail="hours_from_now не может быть отрицательным"
)
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.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(target_id, is_group)
action_params = _build_action_params(state, brightness, scene, temp, r, g, b)
try:
task = await create_schedule_task(
trigger_type="once",
target_id=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")
async def add_cron_task(
target_id: str,
hour: str,
minute: str,
day_of_week: str = "*",
is_group: bool = True,
state: Optional[bool] = None,
brightness: Optional[int] = None,
scene: Optional[str] = None,
temp: Optional[int] = None,
r: Optional[int] = None,
g: Optional[int] = None,
b: Optional[int] = None,
):
target_type = _validate_target(target_id, is_group)
action_params = _build_action_params(state, brightness, scene, temp, r, g, b)
trigger_args = {
"hour": hour,
"minute": minute,
"day_of_week": day_of_week,
}
try:
task = await create_schedule_task(
trigger_type="cron",
target_id=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")
async def get_all_tasks():
return {"tasks": await list_schedule_tasks()}
@router.delete("/{job_id}")
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="Нельзя удалить служебную задачу")