369 lines
8.9 KiB
Python
369 lines
8.9 KiB
Python
import asyncio
|
|
|
|
from app.tion.service import TionService
|
|
|
|
from datetime import (
|
|
date,
|
|
datetime,
|
|
timedelta,
|
|
)
|
|
|
|
from .models import (
|
|
ScheduleActionType,
|
|
ScheduleConfig,
|
|
ScheduleOccurrence,
|
|
ScheduleResolution,
|
|
ScheduledSettings,
|
|
)
|
|
|
|
|
|
WEEKDAYS = (
|
|
"mon",
|
|
"tue",
|
|
"wed",
|
|
"thu",
|
|
"fri",
|
|
"sat",
|
|
"sun",
|
|
)
|
|
|
|
|
|
class ScheduleService:
|
|
|
|
def __init__(
|
|
self,
|
|
config,
|
|
tion: TionService | None = None,
|
|
check_interval: float = 5.0,
|
|
):
|
|
self._config = config
|
|
self._tion = tion
|
|
self._check_interval = check_interval
|
|
|
|
self._running = False
|
|
self._task = None
|
|
self._last_applied_when = None
|
|
|
|
@property
|
|
def config(self) -> ScheduleConfig:
|
|
return self._config
|
|
|
|
def resolve(
|
|
self,
|
|
now: datetime | None = None,
|
|
) -> ScheduleResolution:
|
|
"""
|
|
Определить:
|
|
- текущую точку;
|
|
- следующую точку;
|
|
- накопленные SET-настройки;
|
|
- активен ли сейчас AUTO.
|
|
"""
|
|
|
|
if now is None:
|
|
now = datetime.now()
|
|
|
|
if not self._config.enabled:
|
|
return ScheduleResolution(
|
|
enabled=False,
|
|
current=None,
|
|
next=None,
|
|
scheduled_settings=ScheduledSettings(),
|
|
auto_active=False,
|
|
)
|
|
|
|
# Недели назад достаточно,
|
|
# поскольку расписание повторяется каждые 7 дней.
|
|
start_date = (
|
|
now.date()
|
|
- timedelta(days=7)
|
|
)
|
|
|
|
end_date = (
|
|
now.date()
|
|
+ timedelta(days=7)
|
|
)
|
|
|
|
occurrences = self._build_occurrences(
|
|
start_date,
|
|
end_date,
|
|
)
|
|
|
|
current = None
|
|
next_point = None
|
|
|
|
for occurrence in occurrences:
|
|
|
|
if occurrence.when <= now:
|
|
current = occurrence
|
|
continue
|
|
|
|
next_point = occurrence
|
|
break
|
|
|
|
if current is None:
|
|
raise RuntimeError(
|
|
"Could not determine current "
|
|
"schedule point"
|
|
)
|
|
|
|
scheduled_settings = (
|
|
self._calculate_settings(
|
|
occurrences,
|
|
current,
|
|
)
|
|
)
|
|
|
|
auto_active = (
|
|
current.point.action.type
|
|
== ScheduleActionType.AUTO
|
|
)
|
|
|
|
return ScheduleResolution(
|
|
enabled=True,
|
|
current=current,
|
|
next=next_point,
|
|
scheduled_settings=scheduled_settings,
|
|
auto_active=auto_active,
|
|
)
|
|
|
|
def _build_occurrences(
|
|
self,
|
|
start_date: date,
|
|
end_date: date,
|
|
) -> list[ScheduleOccurrence]:
|
|
|
|
result: list[ScheduleOccurrence] = []
|
|
|
|
current_date = start_date
|
|
|
|
while current_date <= end_date:
|
|
|
|
weekday = WEEKDAYS[
|
|
current_date.weekday()
|
|
]
|
|
|
|
template_name = (
|
|
self._config.days[weekday]
|
|
)
|
|
|
|
template = (
|
|
self._config.templates[
|
|
template_name
|
|
]
|
|
)
|
|
|
|
for point in template:
|
|
|
|
when = datetime.combine(
|
|
current_date,
|
|
point.at,
|
|
)
|
|
|
|
result.append(
|
|
ScheduleOccurrence(
|
|
when=when,
|
|
weekday=weekday,
|
|
template=template_name,
|
|
point=point,
|
|
)
|
|
)
|
|
|
|
current_date += timedelta(days=1)
|
|
|
|
result.sort(
|
|
key=lambda occurrence:
|
|
occurrence.when
|
|
)
|
|
|
|
return result
|
|
|
|
def _calculate_settings(
|
|
self,
|
|
occurrences: list[
|
|
ScheduleOccurrence
|
|
],
|
|
current: ScheduleOccurrence,
|
|
) -> ScheduledSettings:
|
|
"""
|
|
Восстановить последние SET-значения
|
|
каждого параметра расписания.
|
|
"""
|
|
|
|
settings = ScheduledSettings()
|
|
|
|
for occurrence in occurrences:
|
|
|
|
if occurrence.when > current.when:
|
|
break
|
|
|
|
action = occurrence.point.action
|
|
|
|
if (
|
|
action.type
|
|
!= ScheduleActionType.SET
|
|
):
|
|
continue
|
|
|
|
settings = settings.merged(
|
|
action.settings
|
|
)
|
|
|
|
return settings
|
|
|
|
async def start(self) -> None:
|
|
if self._running:
|
|
return
|
|
|
|
if self._tion is None:
|
|
raise RuntimeError(
|
|
"TionService is required to start schedule"
|
|
)
|
|
|
|
self._running = True
|
|
|
|
# При запуске сразу приводим Tion
|
|
# к текущему состоянию расписания.
|
|
await self._process(initial=True)
|
|
|
|
self._task = asyncio.create_task(
|
|
self._loop()
|
|
)
|
|
|
|
async def stop(self) -> None:
|
|
self._running = False
|
|
|
|
if self._task is not None:
|
|
self._task.cancel()
|
|
|
|
try:
|
|
await self._task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
self._task = None
|
|
|
|
async def _loop(self) -> None:
|
|
while self._running:
|
|
await asyncio.sleep(
|
|
self._check_interval
|
|
)
|
|
|
|
await self._process()
|
|
|
|
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
|
|
|
|
if initial:
|
|
# После запуска восстанавливаем всё
|
|
# накопленное состояние расписания.
|
|
settings = resolution.scheduled_settings
|
|
else:
|
|
# При обычном переходе применяем
|
|
# только параметры новой точки.
|
|
settings = current.point.action.settings
|
|
|
|
await self._apply_settings(settings)
|
|
|
|
self._last_applied_when = current.when
|
|
|
|
async def _apply_settings(
|
|
self,
|
|
settings: ScheduledSettings,
|
|
) -> None:
|
|
|
|
if self._tion is None:
|
|
return
|
|
|
|
# Если нужно включить Tion —
|
|
# сначала включаем.
|
|
if settings.power is True:
|
|
await self._tion.execute(
|
|
lambda tion: tion.power_on()
|
|
)
|
|
|
|
if settings.mode is not None:
|
|
await self._tion.execute(
|
|
lambda tion:
|
|
tion.set_air_mode(settings.mode)
|
|
)
|
|
|
|
if settings.target_temp is not None:
|
|
await self._tion.execute(
|
|
lambda tion:
|
|
tion.set_target_temperature(
|
|
settings.target_temp
|
|
)
|
|
)
|
|
|
|
if settings.heater is not None:
|
|
if settings.heater:
|
|
await self._tion.execute(
|
|
lambda tion: tion.heater_on()
|
|
)
|
|
else:
|
|
await self._tion.execute(
|
|
lambda tion: tion.heater_off()
|
|
)
|
|
|
|
if settings.speed is not None:
|
|
await self._tion.execute(
|
|
lambda tion:
|
|
tion.set_speed(settings.speed)
|
|
)
|
|
|
|
if settings.sound is not None:
|
|
if settings.sound:
|
|
await self._tion.execute(
|
|
lambda tion: tion.sound_on()
|
|
)
|
|
else:
|
|
await self._tion.execute(
|
|
lambda tion: tion.sound_off()
|
|
)
|
|
|
|
if settings.light is not None:
|
|
if settings.light:
|
|
await self._tion.execute(
|
|
lambda tion: tion.light_on()
|
|
)
|
|
else:
|
|
await self._tion.execute(
|
|
lambda tion: tion.light_off()
|
|
)
|
|
|
|
# Если нужно выключить —
|
|
# выключаем последним.
|
|
if settings.power is False:
|
|
await self._tion.execute(
|
|
lambda tion: tion.power_off()
|
|
) |