@@ -1,27 +1,43 @@
import asyncio
import logging
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 . scheduler import app_tz , scheduler
from app . core . state import state_manager
from app . drivers . wiz import WizDriver
from app . api . deps import verify_token
logger = logging . getLogger ( __name__ )
router = APIRouter ( dependencies = [ Depends ( verify_token ) ] )
wiz = WizDriver ( )
async def run_delayed _command ( ips : li st[ str ] , state : bool ) :
""" Вспомогательная функция для разовых задач """
async def run_group _command ( target_id : str , is_group : bool , params : dict ) :
"""
Универсальное выполнение команды по расписанию.
IP резолвится в момент выполнения, а не создания задачи --
корректно работает при смене IP (DHCP) и изменении состава группы.
"""
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 ( )
for ip in ips :
try :
await local_wiz . set_pilot ( ip , { " state " : state } )
except Exception :
p ass # Игнорим ошибки отдельных ламп
await local_wiz . set_pilot ( ip , params )
logger . info ( f " ⏰ Расписание: { target_id } -> { ip } : { params } " )
except Exception as e :
logger . error ( f " ⏰ Расписание: ошибка { ip } : { e } " )
@router.post ( " /once " )
@@ -43,23 +59,21 @@ async def schedule_once(
else :
raise HTTPException ( status_code = 400 , detail = " Нужно время или отступ в часах " )
# 2. Получаем IP
# 2. Проверяем что цель существует (но IP резолвится при выполнении)
if is_group :
ips = state_manager . get_group_ips ( target_id )
if target_id not in state_manager . groups :
raise HTTPException ( status_code = 404 , detail = " Группа не найдена " )
else :
dev = state_manager . devices . get ( target_id )
ips = [ dev . ip ] if dev else [ ]
if not ips :
raise HTTPException ( status_code = 404 , detail = " Цель не найдена " )
if target_id not in state_manager . devices :
raise HTTPException ( status_co de = 404 , detail = " Устройство не найдено " )
# 3. Регаем задачу
job_id = f " once_ { target_id } _ { int ( exec_time . timestamp ( ) ) } "
scheduler . add_job (
run_delayed _command ,
run_group _command ,
trigger = DateTrigger ( run_date = exec_time , timezone = app_tz ) ,
args = [ ips , state ] ,
args = [ target_id , is_group , { " state " : state } ],
id = job_id ,
name = f " Once: { target_id } | { state } " ,
replace_existing = True ,
@@ -77,33 +91,31 @@ async def add_cron_task(
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 is_group :
if target_id not in state_manager . groups :
raise HTTPException ( status_code = 404 , detail = " Группа не найдена " )
else :
if target_id not in state_manager . devices :
raise HTTPException ( status_code = 404 , detail = " Устройство не найдено " )
if not ips :
raise HTTPException ( status_code = 404 , detail = " Цель не найдена " )
# Используем таймзону приложения для крона
# Одна задача на всю группу -- IP резолвятся при каждом срабатывании
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 )
job_id = f " cron_ { target_id } _ { hour } _ { minute } "
return { " status " : " cron_scheduled " , " jobs " : job_ids }
scheduler . add_job (
run_group_command ,
trigger ,
args = [ target_id , is_group , { " state " : state } ] ,
id = job_id ,
name = f " CRON: { target_id } | { hour } : { minute } | { state } " ,
replace_existing = True ,
)
return { " status " : " cron_scheduled " , " job_id " : job_id }
@router.get ( " /tasks " )
@@ -119,13 +131,13 @@ async def get_all_tasks():
next_run_str = None
if job . next_run_time :
# ПЕРЕВОДИМ ИЗ UTC В ЛОКАЛЬНУЮ ТАЙМЗОНУ ДЛЯ ВЫВОДА
# Переводим из 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 ] )
@@ -148,5 +160,5 @@ async def cancel_task(job_id: str):
try :
scheduler . remove_job ( job_id )
return { " status " : " deleted " }
except :
except Exception :
raise HTTPException ( status_code = 404 , detail = " Задача не найдена " )