fulREST ready

This commit is contained in:
Артём Кокос
2026-02-12 22:21:38 +07:00
parent c7adc24b07
commit 738edd4db9
5 changed files with 132 additions and 69 deletions

17
app/core/database.py Normal file
View File

@@ -0,0 +1,17 @@
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase
DATABASE_URL = "sqlite+aiosqlite:///./ignis.db"
engine = create_async_engine(DATABASE_URL, echo=False)
async_session = async_sessionmaker(engine, expire_on_commit=False)
class Base(DeclarativeBase):
pass
async def init_db():
async with engine.begin() as conn:
# Создает таблицы, если их еще нет
await conn.run_sync(Base.metadata.create_all)

View File

@@ -1,25 +1,25 @@
import logging
from typing import Dict, List, Optional
from app.models.device import Device, Group
from app.models.device import DeviceSchema, GroupModel
logger = logging.getLogger(__name__)
class StateManager:
def __init__(self):
# Храним устройства по MAC (уникальный ID)
self.devices: Dict[str, Device] = {}
# Группы (люстры) по их ID
self.groups: Dict[str, Group] = {}
# Храним устройства как Pydantic объекты
self.devices: Dict[str, DeviceSchema] = {}
# Группы как модели SQLAlchemy
self.groups: Dict[str, GroupModel] = {}
def update_device(self, device_data: dict):
"""Обновляет или добавляет устройство в состояние."""
mac = device_data["mac"]
device = Device(
id=mac,
ip=device_data["ip"],
name=f"WiZ {mac[-4:]}", # Временное имя
room="Default",
# Используем DeviceSchema вместо Device
device = DeviceSchema(
id=mac, ip=device_data["ip"], name=f"WiZ {mac[-4:]}", room="Default"
)
self.devices[mac] = device
@@ -29,10 +29,10 @@ class StateManager:
if not group:
return []
# Извлекаем IP по MAC-адресам, которые хранятся в группе
return [
self.devices[d_id].ip for d_id in group.device_ids if d_id in self.devices
]
# Создаем синглтон для использования во всем приложении
state_manager = StateManager()

View File

@@ -1,15 +1,44 @@
from sqlalchemy import Column, String, JSON, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column
from app.core.database import Base
from pydantic import BaseModel
from typing import List, Optional
class Device(BaseModel):
id: str # MAC-адрес или UUID
# --- Таблицы в БД ---
class DeviceModel(Base):
__tablename__ = "devices"
id: Mapped[str] = mapped_column(String, primary_key=True) # MAC
ip: Mapped[str] = mapped_column(String)
name: Mapped[str] = mapped_column(String)
room: Mapped[str] = mapped_column(String, default="Default")
class GroupModel(Base):
__tablename__ = "groups"
id: Mapped[str] = mapped_column(String, primary_key=True)
name: Mapped[str] = mapped_column(String)
device_ids: Mapped[list] = mapped_column(JSON) # Храним список MAC-адресов как JSON
# --- Pydantic модели для API (оставляем для валидации) ---
class DeviceSchema(BaseModel):
id: str
ip: str
name: str
room: str
class Config:
from_attributes = True
class Group(BaseModel):
class GroupCreateSchema(BaseModel):
id: str
name: str
device_ids: List[str] # Список ID устройств, входящих в люстру
macs: List[str]

123
main.py
View File

@@ -4,10 +4,13 @@ from contextlib import asynccontextmanager
from typing import Optional, List
from fastapi import FastAPI, HTTPException
from sqlalchemy import select
from app.core.discovery import DiscoveryService
from app.core.state import state_manager
from app.drivers.wiz import WizDriver
from app.core.database import init_db, async_session
from app.models.device import GroupModel, DeviceModel, GroupCreateSchema
logging.basicConfig(
level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s"
@@ -20,11 +23,21 @@ wiz = WizDriver()
@asynccontextmanager
async def lifespan(app: FastAPI):
"""
Жизненный цикл приложения: выполняется при старте и остановке.
"""
"""Жизненный цикл приложения."""
# 1. Инициализация БД (создание таблиц)
await init_db()
logger.info("🗄️ База данных инициализирована")
# Задача для периодического сканирования сети
# 2. Загрузка групп из БД в память
async with async_session() as session:
result = await session.execute(select(GroupModel))
groups = result.scalars().all()
for g in groups:
# Превращаем модель БД в формат стейта (если нужно)
state_manager.groups[g.id] = g
logger.info(f"📂 Загружена группа: {g.name} (MACs: {g.device_ids})")
# 3. Задача для периодического сканирования сети
async def periodic_discovery():
logger.info("🚀 Запущена фоновая служба Discovery")
@@ -36,43 +49,72 @@ async def lifespan(app: FastAPI):
state_manager.update_device(dev_data)
logger.info(
f"📡 Сеть просканирована. Устройств в памяти: {len(state_manager.devices)}"
f"📡 Сеть просканирована. Устройств онлайн: {len(state_manager.devices)}"
)
except Exception as e:
logger.error(f"❌ Ошибка в цикле Discovery: {e}")
# Интервал сканирования (например, раз в 10 минут)
await asyncio.sleep(600)
discovery_task = asyncio.create_task(periodic_discovery())
yield
# Логика при завершении работы сервера
discovery_task.cancel()
logger.info("🛑 Фоновая служба Discovery остановлена")
app = FastAPI(
title="Ignis Core API",
description="Ядро системы умного дома для управления лампами WiZ/Gauss",
version="1.0.0",
lifespan=lifespan,
)
app = FastAPI(title="Ignis Core API", lifespan=lifespan)
@app.get("/health")
async def health():
"""Проверка работоспособности сервиса."""
return {"status": "online", "devices_discovered": len(state_manager.devices)}
# --- Эндпоинты Устройств ---
@app.get("/devices")
async def get_all_devices():
"""Возвращает список всех обнаруженных устройств из стейта."""
"""Список всех обнаруженных ламп."""
return state_manager.devices
@app.post("/control/device/{device_id}")
async def control_device(device_id: str, state: bool, brightness: Optional[int] = None):
"""Прямое управление лампой по MAC."""
device = state_manager.devices.get(device_id)
if not device:
raise HTTPException(status_code=404, detail="Лампа не в сети")
params = {"state": state}
if brightness:
params["dimming"] = brightness
result = await wiz.set_pilot(device.ip, params)
return {"device_id": device_id, "result": result}
# --- Эндпоинты Групп ---
@app.get("/groups")
async def get_groups():
"""Список всех созданных люстр/групп."""
return state_manager.groups
@app.post("/groups")
async def create_group(data: GroupCreateSchema):
async with async_session() as session:
existing = await session.get(GroupModel, data.id)
if existing:
raise HTTPException(status_code=400, detail="Группа с таким ID уже есть")
# Берем данные из схемы
new_group = GroupModel(id=data.id, name=data.name, device_ids=data.macs)
session.add(new_group)
await session.commit()
state_manager.groups[data.id] = new_group
return {"status": "created", "group": data.name}
@app.post("/control/group/{group_id}")
async def control_group(
group_id: str,
@@ -80,19 +122,14 @@ async def control_group(
brightness: Optional[int] = None,
scene: Optional[str] = None,
):
"""
Управление группой ламп (люстрой).
Параметры передаются в JSON или как query-params.
"""
# 1. Получаем IP-адреса всех ламп в группе из стейт-менеджера
ips = state_manager.get_group_ips(group_id)
"""Управление всей люстрой сразу."""
ips = state_manager.get_group_ips(group_id)
if not ips:
raise HTTPException(
status_code=404, detail=f"Группа '{group_id}' не найдена или пуста"
status_code=404, detail="Группа не найдена или лампы оффлайн"
)
# 2. Формируем параметры команды для WiZ
params = {}
if state is not None:
params["state"] = state
@@ -102,35 +139,13 @@ async def control_group(
params["sceneId"] = wiz.SCENES[scene]
if not params:
raise HTTPException(
status_code=400,
detail="Не указаны параметры управления (state, brightness или scene)",
)
raise HTTPException(status_code=400, detail="Никаких команд не передано")
# 3. Отправляем команды всем лампам группы параллельно
# Параллельная отправка всем лампам группы
tasks = [wiz.set_pilot(ip, params) for ip in ips]
results = await asyncio.gather(*tasks, return_exceptions=True)
await asyncio.gather(*tasks)
return {
"group_id": group_id,
"affected_ips": ips,
"status": "commands_sent",
"params": params,
}
@app.post("/control/device/{device_id}")
async def control_device(device_id: str, state: bool):
"""Прямое управление устройством по его ID (MAC-адресу)."""
device = state_manager.devices.get(device_id)
if not device:
raise HTTPException(
status_code=404, detail="Устройство не найдено в текущем стейте"
)
result = await wiz.set_pilot(device.ip, {"state": state})
return {"device_id": device_id, "ip": device.ip, "result": result}
return {"status": "ok", "group": group_id, "sent_to": ips}
if __name__ == "__main__":

View File

@@ -2,3 +2,5 @@ fastapi==0.109.0
uvicorn[standard]==0.27.0
pydantic==2.5.3
httpx==0.26.0
sqlalchemy==2.0.25
aiosqlite==0.19.0