From 3fe2be551477acf97e43de8eeb2cf44195b847cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC=20=D0=9A=D0=BE=D0=BA=D0=BE?= =?UTF-8?q?=D1=81?= Date: Sat, 21 Feb 2026 11:45:15 +0700 Subject: [PATCH] Improve sched --- app/core/scheduler.py | 5 +- main.py | 105 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/app/core/scheduler.py b/app/core/scheduler.py index 1846f9b..87e6aa8 100644 --- a/app/core/scheduler.py +++ b/app/core/scheduler.py @@ -19,7 +19,10 @@ scheduler = AsyncIOScheduler(jobstores=jobstores, timezone=app_tz) async def execute_lamp_command(ip: str, params: dict): - """Выполнение команды по расписанию""" + """ + Универсальное выполнение команды. + params может содержать: state, dimming, temp, sceneId, r, g, b + """ driver = WizDriver() await driver.set_pilot(ip, params) logger.info(f"⏰ Сработало расписание для {ip}: {params}") diff --git a/main.py b/main.py index e847bf9..9d40871 100644 --- a/main.py +++ b/main.py @@ -16,6 +16,9 @@ from app.core.database import init_db, async_session from app.models.device import GroupModel, DeviceModel, GroupCreateSchema from app.models.schedule import ScheduleTask +from apscheduler.triggers.cron import CronTrigger +from apscheduler.triggers.date import DateTrigger + logging.basicConfig( level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s" ) @@ -297,6 +300,108 @@ async def cancel_task(job_id: str): raise HTTPException(status_code=404, detail="Задача не найдена") +@app.post("/schedules/at") +async def add_task_at_time( + target_id: str, + run_at: datetime, + is_group: bool = False, + state: Optional[bool] = None, + brightness: Optional[int] = None, + scene: Optional[str] = None, +): + """Запуск команды в конкретную дату и время (ISO формат)""" + # Собираем параметры команды + params = {} + if state is not None: + params["state"] = state + if brightness is not None: + params["dimming"] = brightness + if scene and scene in wiz.SCENES: + params["sceneId"] = wiz.SCENES[scene] + + # Определяем список IP + ips = [] + if is_group: + ips = state_manager.get_group_ips(target_id) + else: + dev = state_manager.devices.get(target_id) + if dev: + ips = [dev.ip] + + if not ips: + raise HTTPException(status_code=404, detail="Цель не найдена или оффлайн") + + job_ids = [] + for ip in ips: + job = scheduler.add_job( + execute_lamp_command, + DateTrigger(run_date=run_at, timezone=scheduler.timezone), + args=[ip, params], + name=f"{'Group' if is_group else 'Device'}: {target_id} | Action: {params}", + ) + job_ids.append(job.id) + + return {"status": "scheduled", "jobs": job_ids, "run_at": run_at} + + +@app.post("/schedules/cron") +async def add_cron_task( + target_id: str, + hour: str, + minute: str, + day_of_week: str = "*", + is_group: bool = True, + state: bool = True, +): + """ + Многоразовая задача (Cron). + Пример: hour="7", minute="30", day_of_week="mon-fri" + """ + 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="Цель не найдена") + + params = {"state": state} + trigger = CronTrigger( + hour=hour, minute=minute, day_of_week=day_of_week, timezone=scheduler.timezone + ) + + job_ids = [] + for ip in ips: + job = scheduler.add_job( + execute_lamp_command, + trigger, + args=[ip, params], + name=f"CRON: {target_id} | {hour}:{minute} | {day_of_week}", + ) + job_ids.append(job.id) + + return {"status": "cron_scheduled", "jobs": job_ids} + + +@app.get("/schedules/tasks") +async def get_all_tasks(): + """Тот самый расширенный список задач для бота или фронта""" + jobs = [] + for job in scheduler.get_jobs(): + jobs.append( + { + "id": job.id, + "name": job.name, # Мы сохранили описание в поле name выше + "next_run": ( + job.next_run_time.isoformat() if job.next_run_time else None + ), + "params": str(job.args[1]) if len(job.args) > 1 else None, + } + ) + return {"tasks": jobs} + + # --- МОНТИРОВАНИЕ СТАТИКИ (ДОЛЖНО БЫТЬ ПОСЛЕ ВСЕХ API МАРШРУТОВ) --- app.mount("/", StaticFiles(directory="static", html=True), name="static")