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 import logging
from typing import Dict, List, Optional from typing import Dict, List, Optional
from app.models.device import Device, Group from app.models.device import DeviceSchema, GroupModel
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class StateManager: class StateManager:
def __init__(self): def __init__(self):
# Храним устройства по MAC (уникальный ID) # Храним устройства как Pydantic объекты
self.devices: Dict[str, Device] = {} self.devices: Dict[str, DeviceSchema] = {}
# Группы (люстры) по их ID # Группы как модели SQLAlchemy
self.groups: Dict[str, Group] = {} self.groups: Dict[str, GroupModel] = {}
def update_device(self, device_data: dict): def update_device(self, device_data: dict):
"""Обновляет или добавляет устройство в состояние.""" """Обновляет или добавляет устройство в состояние."""
mac = device_data["mac"] mac = device_data["mac"]
device = Device(
id=mac, # Используем DeviceSchema вместо Device
ip=device_data["ip"], device = DeviceSchema(
name=f"WiZ {mac[-4:]}", # Временное имя id=mac, ip=device_data["ip"], name=f"WiZ {mac[-4:]}", room="Default"
room="Default",
) )
self.devices[mac] = device self.devices[mac] = device
@@ -29,10 +29,10 @@ class StateManager:
if not group: if not group:
return [] return []
# Извлекаем IP по MAC-адресам, которые хранятся в группе
return [ return [
self.devices[d_id].ip for d_id in group.device_ids if d_id in self.devices self.devices[d_id].ip for d_id in group.device_ids if d_id in self.devices
] ]
# Создаем синглтон для использования во всем приложении
state_manager = StateManager() 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 pydantic import BaseModel
from typing import List, Optional 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 ip: str
name: str name: str
room: str room: str
class Config:
from_attributes = True
class Group(BaseModel):
class GroupCreateSchema(BaseModel):
id: str id: str
name: 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 typing import Optional, List
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from sqlalchemy import select
from app.core.discovery import DiscoveryService from app.core.discovery import DiscoveryService
from app.core.state import state_manager from app.core.state import state_manager
from app.drivers.wiz import WizDriver 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( logging.basicConfig(
level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s" level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s"
@@ -20,11 +23,21 @@ wiz = WizDriver()
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): 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(): async def periodic_discovery():
logger.info("🚀 Запущена фоновая служба Discovery") logger.info("🚀 Запущена фоновая служба Discovery")
@@ -36,43 +49,72 @@ async def lifespan(app: FastAPI):
state_manager.update_device(dev_data) state_manager.update_device(dev_data)
logger.info( logger.info(
f"📡 Сеть просканирована. Устройств в памяти: {len(state_manager.devices)}" f"📡 Сеть просканирована. Устройств онлайн: {len(state_manager.devices)}"
) )
except Exception as e: except Exception as e:
logger.error(f"❌ Ошибка в цикле Discovery: {e}") logger.error(f"❌ Ошибка в цикле Discovery: {e}")
# Интервал сканирования (например, раз в 10 минут)
await asyncio.sleep(600) await asyncio.sleep(600)
discovery_task = asyncio.create_task(periodic_discovery()) discovery_task = asyncio.create_task(periodic_discovery())
yield yield
# Логика при завершении работы сервера
discovery_task.cancel() discovery_task.cancel()
logger.info("🛑 Фоновая служба Discovery остановлена") logger.info("🛑 Фоновая служба Discovery остановлена")
app = FastAPI( app = FastAPI(title="Ignis Core API", lifespan=lifespan)
title="Ignis Core API",
description="Ядро системы умного дома для управления лампами WiZ/Gauss",
version="1.0.0",
lifespan=lifespan,
)
# --- Эндпоинты Устройств ---
@app.get("/health")
async def health():
"""Проверка работоспособности сервиса."""
return {"status": "online", "devices_discovered": len(state_manager.devices)}
@app.get("/devices") @app.get("/devices")
async def get_all_devices(): async def get_all_devices():
"""Возвращает список всех обнаруженных устройств из стейта.""" """Список всех обнаруженных ламп."""
return state_manager.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}") @app.post("/control/group/{group_id}")
async def control_group( async def control_group(
group_id: str, group_id: str,
@@ -80,19 +122,14 @@ async def control_group(
brightness: Optional[int] = None, brightness: Optional[int] = None,
scene: Optional[str] = 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: if not ips:
raise HTTPException( raise HTTPException(
status_code=404, detail=f"Группа '{group_id}' не найдена или пуста" status_code=404, detail="Группа не найдена или лампы оффлайн"
) )
# 2. Формируем параметры команды для WiZ
params = {} params = {}
if state is not None: if state is not None:
params["state"] = state params["state"] = state
@@ -102,35 +139,13 @@ async def control_group(
params["sceneId"] = wiz.SCENES[scene] params["sceneId"] = wiz.SCENES[scene]
if not params: if not params:
raise HTTPException( raise HTTPException(status_code=400, detail="Никаких команд не передано")
status_code=400,
detail="Не указаны параметры управления (state, brightness или scene)",
)
# 3. Отправляем команды всем лампам группы параллельно # Параллельная отправка всем лампам группы
tasks = [wiz.set_pilot(ip, params) for ip in ips] tasks = [wiz.set_pilot(ip, params) for ip in ips]
results = await asyncio.gather(*tasks, return_exceptions=True) await asyncio.gather(*tasks)
return { return {"status": "ok", "group": group_id, "sent_to": ips}
"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}
if __name__ == "__main__": if __name__ == "__main__":

View File

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