work: Реализован API

This commit is contained in:
Fedorov Dmitriy
2026-09-13 22:05:31 +03:00
parent fdae9dec72
commit 419b8efe33
2 changed files with 261 additions and 1 deletions
+258
View File
@@ -0,0 +1,258 @@
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()
)