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, }