Files
ClimatController/app/auto/controller.py
T

209 lines
5.2 KiB
Python

import asyncio
from datetime import datetime
from app.auto.co2_policy import Co2SpeedPolicy
class AutoController:
def __init__(
self,
schedule_service,
qingping_service,
tion_service,
policy: Co2SpeedPolicy | None = None,
interval: float = 5.0,
):
self._schedule = schedule_service
self._qingping = qingping_service
self._tion = tion_service
self._policy = policy
self._interval = interval
self._task: asyncio.Task | None = None
self._state = "inactive"
self._reason: str | None = None
self._target_speed: int | None = None
self._last_error: str | None = None
self._auto_speed: int | None = None
async def start(self) -> None:
if self._task is not None:
return
self._task = asyncio.create_task( self._loop() )
async def stop(self) -> None:
if self._task is None:
return
self._task.cancel()
try:
await self._task
except asyncio.CancelledError:
pass
finally:
self._task = None
async def _loop(self) -> None:
while True:
try:
await self._process()
except asyncio.CancelledError:
raise
except Exception as exc:
self._last_error = str(exc)
await asyncio.sleep( self._interval )
async def _process(self) -> None:
now = datetime.now()
resolution = self._schedule.resolve(now)
# ----------------------------------------------
# Активен ручной override.
#
# Пока пользователь вручную управляет Tion,
# AUTO вообще не вмешивается.
# ----------------------------------------------
if self._schedule.override_active:
self._state = "suspended"
self._reason = "manual_override"
self._target_speed = None
self._last_error = None
self._auto_speed = None
return
# ----------------------------------------------
# AUTO сейчас не активен
# ----------------------------------------------
if (
not resolution.enabled
or not resolution.auto_active
):
self._state = "inactive"
self._reason = None
self._target_speed = None
self._last_error = None
self._auto_speed = None
return
fallback_speed = resolution.auto_fallback_speed
if fallback_speed is None:
raise RuntimeError(
"AUTO is active but "
"fallback speed is missing"
)
# ----------------------------------------------
# CO2 policy недоступна.
#
# Например, auto.yaml не загрузился.
# AUTO работает в аварийном fallback-only режиме.
# ----------------------------------------------
if self._policy is None:
self._state = "fallback"
self._reason = "auto_policy_unavailable"
self._auto_speed = None
await self._set_speed(
fallback_speed
)
self._last_error = None
return
qingping_state = self._qingping.state
# ----------------------------------------------
# Qingping исправен
# ----------------------------------------------
if (
self._qingping.online
and qingping_state.co2 is not None
):
target_speed = (
self._policy.select_speed(
co2=qingping_state.co2,
current_speed=self._auto_speed,
)
)
self._state = "active"
self._reason = None
await self._set_speed(
target_speed
)
self._auto_speed = target_speed
self._last_error = None
return
# ----------------------------------------------
# AUTO не может работать.
# Переходим на fallback.
# ----------------------------------------------
if not self._qingping.online:
reason = "qingping_offline"
else:
reason = "co2_missing"
self._state = "fallback"
self._reason = reason
self._auto_speed = None
await self._set_speed(fallback_speed)
self._last_error = None
async def _set_speed(self, speed: int) -> None:
if self._target_speed == speed:
return
await self._tion.execute(
lambda controller:
controller.set_speed(speed)
)
self._target_speed = speed
def status(self) -> dict:
return {
"state": self._state,
"reason": self._reason,
"target_speed": self._target_speed,
"auto_speed": self._auto_speed,
"last_error": self._last_error,
}