work: Протестировано расписание и ручная установка значений во время работающего расписания.

This commit is contained in:
Fedorov Dmitriy
2026-09-14 21:24:22 +03:00
parent 4c665d9108
commit dec14b1d12
5 changed files with 357 additions and 61 deletions
+202 -38
View File
@@ -42,12 +42,123 @@ class ScheduleService:
self._running = False
self._task = None
self._last_applied_when = None
# Последняя успешно применённая точка расписания.
self._last_applied_when: datetime | None = None
# Temporary override.
self._override_settings = ScheduledSettings()
self._override_until: datetime | None = None
# True означает:
# override записан, но применить его к Tion
# пока не удалось.
self._override_pending = False
# Ошибка именно ScheduleService.
self._last_error: str | None = None
@property
def running(self) -> bool:
return self._running
@property
def last_error(self) -> str | None:
return self._last_error
@property
def override_settings(self) -> ScheduledSettings:
return self._override_settings
@property
def override_until(self) -> datetime | None:
return self._override_until
@property
def override_active(self) -> bool:
if self._override_until is None:
return False
return datetime.now() < self._override_until
@property
def config(self) -> ScheduleConfig:
return self._config
async def apply_override(self, settings: ScheduledSettings) -> None:
if settings.empty:
raise ValueError(
"Override settings cannot be empty"
)
if self._tion is None:
raise RuntimeError(
"TionService is required"
)
now = datetime.now()
resolution = self.resolve(now)
if not resolution.enabled:
raise RuntimeError(
"Schedule is disabled"
)
if resolution.current is None:
raise RuntimeError(
"Current schedule point not found"
)
if resolution.next is None:
raise RuntimeError(
"Next schedule point not found"
)
# Если старый override уже закончился,
# перед созданием нового очищаем его.
if ( self._override_until is not None
and now >= self._override_until
):
self._clear_override()
# Добавляем новые ручные параметры
# к уже существующему override.
self._override_settings = ( self._override_settings.merged(settings) )
# Override действует строго до
# следующей точки расписания.
self._override_until = resolution.next.when
# Считаем его неприменённым,
# пока команда реально не прошла.
self._override_pending = True
try:
await self._apply_settings(settings)
except Exception as exc:
self._last_error = (
f"{type(exc).__name__}: {exc}"
)
# Override остаётся сохранённым.
# Фоновый цикл попробует ещё раз.
raise
else:
self._override_pending = False
self._last_error = None
def _clear_override(self) -> None:
self._override_settings = ScheduledSettings()
self._override_until = None
self._override_pending = False
def resolve(
self,
now: datetime | None = None,
@@ -216,21 +327,24 @@ class ScheduleService:
return
if self._tion is None:
raise RuntimeError(
"TionService is required to start schedule"
)
raise RuntimeError("TionService is required to start schedule")
self._running = True
# При запуске сразу приводим Tion
# к текущему состоянию расписания.
await self._process(initial=True)
try:
await self._process()
except Exception as exc:
self._last_error = f"{type(exc).__name__}: {exc}"
self._task = asyncio.create_task(self._loop())
self._task = asyncio.create_task(
self._loop()
)
async def stop(self) -> None:
if not self._running:
return
self._running = False
if self._task is not None:
@@ -243,21 +357,57 @@ class ScheduleService:
self._task = None
async def _loop(self) -> None:
while self._running:
await asyncio.sleep(
self._check_interval
)
await self._process()
try:
await self._process()
async def _process(
self,
*,
initial: bool = False,
) -> None:
except Exception as exc:
self._last_error = f"{type(exc).__name__}: {exc}"
resolution = self.resolve()
# async def _process(self, *, initial: bool = False) -> None:
#
# resolution = self.resolve()
#
# if not resolution.enabled:
# return
#
# current = resolution.current
#
# if current is None:
# return
#
# # Эта точка уже применена.
# if (
# not initial
# and self._last_applied_when == current.when
# ):
# return
#
# # AUTO пока только распознаём.
# # Сам AutoController подключим позже.
# if (
# current.point.action.type
# == ScheduleActionType.AUTO
# ):
# self._last_applied_when = current.when
# return
#
# settings = resolution.scheduled_settings
#
# await self._apply_settings(settings)
#
# self._last_applied_when = current.when
async def _process(self) -> None:
now = datetime.now()
resolution = self.resolve(now)
if not resolution.enabled:
return
@@ -267,39 +417,53 @@ class ScheduleService:
if current is None:
return
# Эта точка уже применена.
# --------------------------------------------------
# 1. Проверяем окончание temporary override
# --------------------------------------------------
if (
not initial
and self._last_applied_when == current.when
self._override_until is not None
and now >= self._override_until
):
self._clear_override()
# --------------------------------------------------
# 2. Пока override действует,
# расписание Tion не трогает
# --------------------------------------------------
if self.override_active:
# Если override ранее не удалось применить,
# пробуем снова.
if self._override_pending:
await self._apply_settings(self._override_settings)
self._override_pending = False
self._last_error = None
return
# AUTO пока только распознаём.
# Сам AutoController подключим позже.
if (
current.point.action.type
== ScheduleActionType.AUTO
):
self._last_applied_when = current.when
# --------------------------------------------------
# 3. Если текущая точка уже успешно применена,
# ничего не делаем
# --------------------------------------------------
if self._last_applied_when == current.when:
return
if initial:
# После запуска восстанавливаем всё
# накопленное состояние расписания.
settings = resolution.scheduled_settings
else:
# При обычном переходе применяем
# только параметры новой точки.
settings = current.point.action.settings
# --------------------------------------------------
# 4. Приводим Tion к ПОЛНОМУ актуальному
# состоянию расписания
# --------------------------------------------------
await self._apply_settings(settings)
await self._apply_settings(resolution.scheduled_settings)
# Ставим только ПОСЛЕ успешного применения.
self._last_applied_when = current.when
self._last_error = None
async def _apply_settings(
self,
settings: ScheduledSettings,
) -> None:
async def _apply_settings(self, settings: ScheduledSettings) -> None:
if self._tion is None:
return