209 lines
5.2 KiB
Python
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,
|
|
}
|
|
|