import logging from contextlib import asynccontextmanager from typing import Literal from fastapi import FastAPI, HTTPException, Path from app.my_dataclasses import ( TION_MAC, MIN_FAN_SPEED, MAX_FAN_SPEED, MIN_TARGET_TEMP, MAX_TARGET_TEMP, AIR_MODE_OUTSIDE, AIR_MODE_RECIRCULATION, ) from app.tion import ( TionController, TionService, ) def configure_logging() -> None: noisy_loggers = ( "asyncio", "bleak", "bleak.backends", "bleak.backends.winrt", "bleak.backends.winrt.client", "tion_btle", "tion_btle.tion", "tion_btle.s4", "tion_btle.light_family", ) for logger_name in noisy_loggers: logging.getLogger(logger_name).setLevel(logging.WARNING) configure_logging() # ---------------------------------------------------------------------- # Tion # ---------------------------------------------------------------------- controller = TionController(TION_MAC) service = TionService( controller, poll_interval=5, ) # ---------------------------------------------------------------------- # Application lifecycle # ---------------------------------------------------------------------- @asynccontextmanager async def lifespan(app: FastAPI): await service.start() try: yield finally: await service.stop() # ---------------------------------------------------------------------- # FastAPI # ---------------------------------------------------------------------- app = FastAPI( title="Tion Controller", version="0.1.0", lifespan=lifespan, ) # ---------------------------------------------------------------------- # Helpers # ---------------------------------------------------------------------- def get_status() -> dict: """ Получить текущее состояние сервиса. Bluetooth-запрос здесь не выполняется. """ state = service.state return { "online": service.online, "running": service.running, "last_seen": ( service.last_seen.isoformat() if service.last_seen is not None else None ), "last_error": service.last_error, "tion": ( state.to_dict() if state is not None else None ), } async def execute_command(operation) -> dict: """ Выполнить команду Tion и вернуть обновлённый status. """ try: await service.execute(operation) except Exception as exc: raise HTTPException( status_code=503, detail={ "message": "Tion unavailable", "error": f"{type(exc).__name__}: {exc}", }, ) from exc return get_status() # ---------------------------------------------------------------------- # Status # ---------------------------------------------------------------------- @app.get("/api/status") async def status(): return get_status() # ---------------------------------------------------------------------- # Power # ---------------------------------------------------------------------- @app.post("/api/tion/power/{value}") async def set_power( value: Literal["on", "off"], ): if value == "on": return await execute_command( lambda tion: tion.power_on() ) return await execute_command( lambda tion: tion.power_off() ) # ---------------------------------------------------------------------- # Fan speed # ---------------------------------------------------------------------- @app.post("/api/tion/speed/{speed}") async def set_speed( speed: int = Path( ge=MIN_FAN_SPEED, le=MAX_FAN_SPEED, ), ): return await execute_command( lambda tion: tion.set_speed(speed) ) # ---------------------------------------------------------------------- # Heater # ---------------------------------------------------------------------- @app.post("/api/tion/heater/{value}") async def set_heater( value: Literal["on", "off"], ): if value == "on": return await execute_command( lambda tion: tion.heater_on() ) return await execute_command( lambda tion: tion.heater_off() ) # ---------------------------------------------------------------------- # Target temperature # ---------------------------------------------------------------------- @app.post("/api/tion/temperature/{temperature}") async def set_temperature( temperature: int = Path( ge=MIN_TARGET_TEMP, le=MAX_TARGET_TEMP, ), ): return await execute_command( lambda tion: tion.set_target_temperature( temperature ) ) # ---------------------------------------------------------------------- # Air mode # ---------------------------------------------------------------------- @app.post("/api/tion/mode/{mode}") async def set_air_mode( mode: Literal[ AIR_MODE_OUTSIDE, AIR_MODE_RECIRCULATION, ], ): return await execute_command( lambda tion: tion.set_air_mode(mode) ) # ---------------------------------------------------------------------- # Sound # ---------------------------------------------------------------------- @app.post("/api/tion/sound/{value}") async def set_sound( value: Literal["on", "off"], ): if value == "on": return await execute_command( lambda tion: tion.sound_on() ) return await execute_command( lambda tion: tion.sound_off() ) # ---------------------------------------------------------------------- # Light # ---------------------------------------------------------------------- @app.post("/api/tion/light/{value}") async def set_light( value: Literal["on", "off"], ): if value == "on": return await execute_command( lambda tion: tion.light_on() ) return await execute_command( lambda tion: tion.light_off() )