mirror of
https://github.com/MarkParker5/STARK.git
synced 2024-11-24 08:12:13 +02:00
client and bridge in progress
This commit is contained in:
parent
c4abe6008f
commit
40560c25d9
8
.gitignore
vendored
8
.gitignore
vendored
@ -11,10 +11,10 @@ __pycache__
|
||||
*.torrent
|
||||
|
||||
/config.py
|
||||
resources/tts-gc-key.json
|
||||
resources/jwt_access_token
|
||||
resources/jwt_refresh_token
|
||||
resources/jwt-key.pub
|
||||
tts-gc-key.json
|
||||
jwt_access_token
|
||||
jwt_refresh_token
|
||||
jwt-key.pub
|
||||
|
||||
audio/
|
||||
downloads/
|
||||
|
@ -18,7 +18,7 @@
|
||||
- Django
|
||||
- #### Features - Possibilities, set of functions
|
||||
- #### General - For helper classes
|
||||
- #### Raspberry - Control system and hardware
|
||||
- #### hardware - Control system and hardware
|
||||
|
||||
### Root files:
|
||||
- **start.py** - entry point
|
||||
|
@ -1,8 +0,0 @@
|
||||
from . import (
|
||||
ws,
|
||||
house,
|
||||
hub,
|
||||
room,
|
||||
device,
|
||||
admin
|
||||
)
|
@ -1,2 +0,0 @@
|
||||
from .DevicesManager import DevicesManager
|
||||
from .router import router
|
@ -1,30 +0,0 @@
|
||||
from uuid import UUID
|
||||
from fastapi import APIRouter, Depends
|
||||
from API import exceptions
|
||||
from .DevicesManager import DevicesManager
|
||||
from .schemas import Device, DeviceState, CreateDevice, PatchDevice
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix = '/device',
|
||||
tags = ['device'],
|
||||
)
|
||||
|
||||
@router.post('', response_model = Device)
|
||||
async def create_device(device: CreateDevice, manager: DevicesManager = Depends()):
|
||||
return await manager.create(device)
|
||||
|
||||
@router.get('/{id}', response_model = DeviceState)
|
||||
async def get_device(id: UUID, manager: DevicesManager = Depends()):
|
||||
if device := await manager.state(id):
|
||||
return device
|
||||
else:
|
||||
raise exceptions.not_found
|
||||
|
||||
@router.patch('/{id}')
|
||||
async def patch_device(id: UUID, device: PatchDevice, manager: DevicesManager = Depends()):
|
||||
await manager.patch(id, device)
|
||||
|
||||
@router.delete('/{id}')
|
||||
async def delete_device(id: UUID, manager: DevicesManager = Depends()):
|
||||
await manager.delete(id)
|
@ -1,22 +0,0 @@
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
from pydantic import BaseModel
|
||||
from AUID import AUID
|
||||
from ..schemas import DeviceModel, DeviceParameter
|
||||
|
||||
|
||||
class PatchDevice(BaseModel):
|
||||
name: str
|
||||
room_id: UUID
|
||||
|
||||
class CreateDevice(PatchDevice):
|
||||
id: AUID
|
||||
|
||||
class Device(CreateDevice):
|
||||
model: DeviceModel
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
class DeviceState(Device):
|
||||
parameters: list[DeviceParameter] = []
|
@ -1,32 +0,0 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy import select, delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from API.models import House
|
||||
from API.dependencies import database
|
||||
from . import schemas
|
||||
|
||||
|
||||
class HouseManager:
|
||||
session: AsyncSession
|
||||
|
||||
def __init__(self, session = Depends(database.get_async_session)):
|
||||
self.session = session
|
||||
|
||||
async def get(self) -> House:
|
||||
db: AsyncSession = self.session
|
||||
result = await db.scalars(select(House))
|
||||
return result.first()
|
||||
|
||||
async def create(self, house_id: UUID) -> House: # TODO: remove
|
||||
db: AsyncSession = self.session
|
||||
|
||||
if house := await self.get():
|
||||
await db.delete(house)
|
||||
|
||||
house = House(id = house_id, name = '')
|
||||
db.add(house)
|
||||
await db.commit()
|
||||
return house
|
@ -1,2 +0,0 @@
|
||||
from .HouseManager import HouseManager
|
||||
from .router import router
|
@ -1,18 +0,0 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from API import exceptions
|
||||
from API.dependencies import auth
|
||||
from .HouseManager import HouseManager
|
||||
from .schemas import House
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix = '/house',
|
||||
tags = ['house'],
|
||||
)
|
||||
|
||||
@router.get('', response_model = House)
|
||||
async def get_house(manager: HouseManager = Depends(), user = Depends(auth.validate_user)):
|
||||
return await manager.get()
|
@ -1,2 +0,0 @@
|
||||
from .HubManager import HubManager
|
||||
from .router import router
|
@ -1,56 +0,0 @@
|
||||
from fastapi import APIRouter, Depends, BackgroundTasks
|
||||
from API import exceptions
|
||||
from API.dependencies import auth
|
||||
from .HubManager import HubManager
|
||||
from .schemas import HubInit, Hub, HubPatch, TokensPair, Hotspot, WifiConnection
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix = '/hub',
|
||||
tags = ['hub'],
|
||||
)
|
||||
|
||||
@router.post('', response_model = Hub)
|
||||
async def init_hub(hub_init: HubInit, manager: HubManager = Depends(), raw_token: str = Depends(auth.raw_token)):
|
||||
manager.save_credentials(hub_init)
|
||||
await manager.parse_token(raw_token)
|
||||
return await manager.init(hub_init)
|
||||
|
||||
@router.get('', response_model = Hub)
|
||||
async def get_hub(manager: HubManager = Depends()):
|
||||
await manager.check_access()
|
||||
hub = await manager.get()
|
||||
if hub:
|
||||
return hub
|
||||
raise exceptions.not_found
|
||||
|
||||
@router.patch('')
|
||||
async def patch_hub(hub: HubPatch, manager: HubManager = Depends()):
|
||||
await manager.check_access()
|
||||
await manager.patch(hub)
|
||||
|
||||
@router.post('/connect')
|
||||
async def connect_to_wifi(wifi: WifiConnection, manager: HubManager = Depends()):
|
||||
await manager.check_access()
|
||||
manager.wifi(wifi.ssid, wifi.password)
|
||||
|
||||
@router.post('/wps')
|
||||
async def start_wps(manager: HubManager = Depends()):
|
||||
await manager.check_access()
|
||||
manager.start_wps()
|
||||
|
||||
@router.get('/hotspots', response_model = list[Hotspot])
|
||||
def get_hub_hotspots(manager: HubManager = Depends()):
|
||||
return manager.get_hotspots()
|
||||
|
||||
@router.get('/is_connected', response_model=bool)
|
||||
def is_connected(bg_tasks: BackgroundTasks, manager: HubManager = Depends()):
|
||||
connected = manager.is_connected()
|
||||
if connected:
|
||||
bg_tasks.add_task(manager.stop_hotspot)
|
||||
return connected
|
||||
|
||||
@router.post('/set_tokens')
|
||||
async def set_tokens(tokens: TokensPair, manager: HubManager = Depends()):
|
||||
await manager.check_access()
|
||||
manager.save_tokens(tokens)
|
@ -1,2 +0,0 @@
|
||||
from .RoomsManager import RoomsManager
|
||||
from .router import router
|
@ -1,30 +0,0 @@
|
||||
from uuid import UUID
|
||||
from fastapi import APIRouter, Depends
|
||||
from API import exceptions
|
||||
from .RoomsManager import RoomsManager
|
||||
from .schemas import Room, CreateRoom, PatchRoom
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix = '/room',
|
||||
tags = ['room'],
|
||||
)
|
||||
|
||||
@router.post('', response_model = Room)
|
||||
async def create_room(room: CreateRoom, manager: RoomsManager = Depends()):
|
||||
return await manager.create(room)
|
||||
|
||||
@router.get('/{id}', response_model = Room)
|
||||
async def get_room(id: UUID, manager: RoomsManager = Depends()):
|
||||
room = await manager.get(id)
|
||||
if not room:
|
||||
raise exceptions.not_found
|
||||
return room
|
||||
|
||||
@router.patch('/{id}')
|
||||
async def patch_room(id: UUID, room: PatchRoom, manager: RoomsManager = Depends()):
|
||||
await manager.patch(id, room)
|
||||
|
||||
@router.delete('/{id}')
|
||||
async def delete_room(id: UUID, manager: RoomsManager = Depends()):
|
||||
await manager.delete(id)
|
@ -1,37 +0,0 @@
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from Merlin import Merlin, MerlinMessage
|
||||
from API.dependencies import database
|
||||
from API.models import Device, DeviceModelParameter
|
||||
from .schemas import MerlinData
|
||||
|
||||
|
||||
class WSManager:
|
||||
merlin = Merlin()
|
||||
|
||||
async def merlin_send(self, data: MerlinData):
|
||||
db: AsyncSession = database.create_async_session()
|
||||
try:
|
||||
if message := await self._get_message(db, data):
|
||||
self.merlin.send(message)
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
async def _get_message(self, db: AsyncSession, data: MerlinData) -> MerlinMessage | None:
|
||||
device = await db.get(Device, data.device_id)
|
||||
|
||||
if not device:
|
||||
return None
|
||||
|
||||
response = await db.execute(
|
||||
select(DeviceModelParameter)
|
||||
.where(
|
||||
DeviceModelParameter.devicemodel_id == device.model.id,
|
||||
DeviceModelParameter.parameter_id == data.parameter_id
|
||||
)
|
||||
)
|
||||
|
||||
model_parameter = response.scalar_one()
|
||||
|
||||
return MerlinMessage(device.urdi, model_parameter.f, int(data.value))
|
@ -1 +0,0 @@
|
||||
from .router import router
|
@ -1,39 +0,0 @@
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
|
||||
credentials = HTTPException(
|
||||
status_code = status.HTTP_401_UNAUTHORIZED,
|
||||
detail = 'Incorrect username or password',
|
||||
headers = {'WWW-Authenticate': 'Bearer'},
|
||||
)
|
||||
|
||||
invalid_token = HTTPException(
|
||||
status_code = status.HTTP_401_UNAUTHORIZED,
|
||||
detail = 'Token invalid or expired',
|
||||
headers = {'WWW-Authenticate': 'Bearer'},
|
||||
)
|
||||
|
||||
access_denied = HTTPException(
|
||||
status_code = status.HTTP_403_FORBIDDEN,
|
||||
detail = 'Access denied',
|
||||
)
|
||||
|
||||
not_found = HTTPException(
|
||||
status_code = status.HTTP_404_NOT_FOUND,
|
||||
detail = 'Not found',
|
||||
)
|
||||
|
||||
already_exist = HTTPException(
|
||||
status_code = 400,
|
||||
detail = 'Resource already exist'
|
||||
)
|
||||
|
||||
not_initialized = HTTPException(
|
||||
status_code = 400,
|
||||
detail = 'Hub not initialized'
|
||||
)
|
||||
|
||||
incorrect_format = HTTPException (
|
||||
status_code = status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail = 'Data in incorrect format'
|
||||
)
|
@ -1,5 +1,5 @@
|
||||
from typing import NamedTuple
|
||||
from enum import Enum, auto
|
||||
from enum import Enum, IntEnum, auto
|
||||
from uuid import UUID
|
||||
import time
|
||||
import qrcode
|
||||
@ -12,7 +12,7 @@ class URDI(int):
|
||||
def __repr__(self) -> str:
|
||||
return f'URDI({super().__repr__()})'
|
||||
|
||||
class Model(int, Enum):
|
||||
class Model(IntEnum):
|
||||
zero = 0
|
||||
hub = 1
|
||||
relay = 2
|
||||
|
24
SmartHome/client/api.py
Normal file
24
SmartHome/client/api.py
Normal file
@ -0,0 +1,24 @@
|
||||
from typing import Type
|
||||
from aiohttp import ClientSession
|
||||
from pydantic import BaseModel
|
||||
from pydantic.error_wrappers import ValidationError
|
||||
import config
|
||||
|
||||
|
||||
headers = {'Auth': f'Bearer {config.access_token}'}
|
||||
|
||||
async def get(url: str,
|
||||
model: Type[BaseModel],
|
||||
client_session: ClientSession = None) -> BaseModel | None:
|
||||
try:
|
||||
session = client_session or ClientSession()
|
||||
async with session.get(f'{config.api_url}/{url}') as response:
|
||||
text = await response.text()
|
||||
return model.parse_raw(text)
|
||||
except ValidationError:
|
||||
print(f'\nAPI Error: {url}\nModel: {model}\n{text}\n')
|
||||
return None
|
||||
finally:
|
||||
if not client_session:
|
||||
await session.close()
|
||||
response.close()
|
45
SmartHome/client/client.py
Normal file
45
SmartHome/client/client.py
Normal file
@ -0,0 +1,45 @@
|
||||
import asyncio
|
||||
from exceptions.Internal import InternalException
|
||||
from schemas.hub import HubPatch
|
||||
from schemas.house import HousePatch
|
||||
from schemas.device import DeviceModelInfo
|
||||
from managers import HubManager, HouseManager, DeviceModelManager
|
||||
from database import create_async_session, AsyncSession
|
||||
from . import api
|
||||
|
||||
|
||||
def fetch():
|
||||
asyncio.run(fetch_all())
|
||||
|
||||
async def fetch_all():
|
||||
async with create_async_session() as session:
|
||||
await asyncio.gather(
|
||||
fetch_house(session),
|
||||
fetch_hub(session),
|
||||
fetch_device_models(session)
|
||||
)
|
||||
|
||||
async def fetch_house(session: AsyncSession):
|
||||
manager = HouseManager(session)
|
||||
try:
|
||||
house = await manager.get()
|
||||
except InternalException:
|
||||
return
|
||||
if patch_house := await api.get(f'house/{house.id}', HousePatch):
|
||||
await manager.patch(patch_house)
|
||||
|
||||
async def fetch_hub(session: AsyncSession):
|
||||
manager = HubManager(session)
|
||||
try:
|
||||
hub = await manager.get()
|
||||
except InternalException:
|
||||
return
|
||||
if patch_hub := await api.get(f'hub/{hub.id}', HubPatch):
|
||||
await manager.patch(patch_hub)
|
||||
|
||||
async def fetch_device_models(session: AsyncSession):
|
||||
manager = DeviceModelManager(session)
|
||||
if devicemodels := await api.get(f'device_model', DeviceModelInfo):
|
||||
print(devicemodels)
|
||||
for devicemodel in devicemodels:
|
||||
manager.save(devicemodel)
|
42
SmartHome/client/ws.py
Normal file
42
SmartHome/client/ws.py
Normal file
@ -0,0 +1,42 @@
|
||||
from threading import Thread
|
||||
import websocket
|
||||
import config
|
||||
from server.endpoints.ws import WSManager
|
||||
|
||||
|
||||
ws: websocket.WebSocketApp = None
|
||||
ws_thread: Thread = None
|
||||
ws_manager = WSManager()
|
||||
|
||||
headers = {'Auth': f'Bearer {config.access_token}'}
|
||||
|
||||
def on_message(ws, msg):
|
||||
print('WS: ', msg)
|
||||
ws_manager.handle_message(msg)
|
||||
|
||||
def on_open(ws):
|
||||
print('WS Open')
|
||||
pass
|
||||
|
||||
def on_close(ws, status_code, reason):
|
||||
start_ws()
|
||||
|
||||
def on_error(ws, error):
|
||||
# start_ws()
|
||||
pass
|
||||
|
||||
def start():
|
||||
global ws, ws_thread
|
||||
|
||||
if ws: ws.close()
|
||||
|
||||
ws = websocket.WebSocketApp(config.ws_url,
|
||||
on_open=on_open,
|
||||
on_message=on_message,
|
||||
on_error=on_error,
|
||||
on_close=on_close,
|
||||
header=headers
|
||||
)
|
||||
|
||||
ws_thread = Thread(target=ws.run_forever)
|
||||
ws_thread.start()
|
42
SmartHome/config.py
Normal file
42
SmartHome/config.py
Normal file
@ -0,0 +1,42 @@
|
||||
import pathlib
|
||||
path = str(pathlib.Path(__file__).parent.absolute())
|
||||
del pathlib
|
||||
|
||||
|
||||
src: str = path + '/resources'
|
||||
|
||||
# DB
|
||||
|
||||
db_url: str = f'sqlite:///{src}/database.sqlite3'
|
||||
db_async_url: str = f'sqlite+aiosqlite:///{src}/database.sqlite3'
|
||||
|
||||
# WiFi
|
||||
|
||||
# TODO: hostapd.conf
|
||||
# wifi_ssid: str = 'Archie Hub'
|
||||
# wifi_password: str = '12345678'
|
||||
|
||||
# API
|
||||
|
||||
api_url = 'http://home.parker-programs.com/api'
|
||||
ws_url = 'ws://home.parker-programs.com/ws/hub'
|
||||
|
||||
try:
|
||||
with open(f'{src}/jwt-key.pub', 'r') as f:
|
||||
public_key = f.read()
|
||||
except FileNotFoundError:
|
||||
public_key = ''
|
||||
|
||||
try:
|
||||
with open(f'{src}/jwt_access_token', 'r') as f:
|
||||
access_token = f.read()
|
||||
except FileNotFoundError:
|
||||
access_token = ''
|
||||
|
||||
try:
|
||||
with open(f'{src}/jwt_refresh_token', 'r') as f:
|
||||
refresh_token = f.read()
|
||||
except FileNotFoundError:
|
||||
refresh_token = ''
|
||||
|
||||
algorithm = 'RS256'
|
@ -16,10 +16,6 @@ create_session = sessionmaker(
|
||||
autocommit=False, autoflush=False, bind=engine
|
||||
)
|
||||
|
||||
def get_session() -> Session:
|
||||
with create_session() as session:
|
||||
yield session
|
||||
|
||||
# async
|
||||
|
||||
async_engine = create_async_engine(
|
||||
@ -29,7 +25,3 @@ async_engine = create_async_engine(
|
||||
create_async_session = sessionmaker(
|
||||
async_engine, class_ = AsyncSession, expire_on_commit = False
|
||||
)
|
||||
|
||||
async def get_async_session() -> AsyncSession:
|
||||
async with create_async_session() as session:
|
||||
yield session
|
19
SmartHome/exceptions/Internal.py
Normal file
19
SmartHome/exceptions/Internal.py
Normal file
@ -0,0 +1,19 @@
|
||||
from enum import IntEnum
|
||||
|
||||
|
||||
class ExceptionCode(IntEnum):
|
||||
undefined = 1000
|
||||
unauthorized = 1001
|
||||
access_denied = 1003
|
||||
not_found = 1004
|
||||
already_exist = 1005
|
||||
not_initialized = 1006
|
||||
invalid_format = 1022
|
||||
|
||||
class InternalException(Exception):
|
||||
Code = ExceptionCode
|
||||
|
||||
def __init__(self, code: Code | int = Code.undefined, msg: str = '', debug: str = ''):
|
||||
self.code = code
|
||||
self.msg = msg
|
||||
self.debug = debug or msg
|
@ -71,5 +71,7 @@ class Merlin():
|
||||
func, arg = rawData
|
||||
print(f'Received {func=} {arg=}') # TODO: Log
|
||||
|
||||
receiveAndTransmitThread = Thread(target = Merlin().receiveAndTransmit)
|
||||
receiveAndTransmitThread.start()
|
||||
merlin = Merlin()
|
||||
|
||||
_receiveAndTransmitThread = Thread(target = Merlin().receiveAndTransmit)
|
||||
_receiveAndTransmitThread.start()
|
@ -1,2 +1,2 @@
|
||||
from .Merlin import Merlin
|
||||
from .Merlin import merlin
|
||||
from .MerlinMessage import MerlinMessage
|
@ -5,7 +5,7 @@
|
||||
|
||||
# This file lib_nrf24.py is a slightly tweaked version of Barraca's "pynrf24".
|
||||
|
||||
# So this is my tweak for Raspberry Pi and "Virtual GPIO" ...
|
||||
# So this is my tweak for hardware Pi and "Virtual GPIO" ...
|
||||
# ... of Barraca's port to BeagleBone python ... (Joao Paulo Barraca <jpbarraca@gmail.com>)
|
||||
# ... of maniacbug's NRF24L01 C++ library for Arduino.
|
||||
# Brian Lavery Oct 2014
|
||||
@ -182,7 +182,7 @@ class NRF24:
|
||||
|
||||
def __init__(self, gpio, spidev):
|
||||
# It should be possible to instantiate multiple objects, with different GPIO / spidev
|
||||
# EG on Raspberry, one could be RPI GPIO & spidev module, other could be virtual-GPIO
|
||||
# EG on hardware, one could be RPI GPIO & spidev module, other could be virtual-GPIO
|
||||
# On rpi, only bus 0 is supported here, not bus 1 of the model B plus
|
||||
self.GPIO = gpio # the GPIO module
|
||||
self.spidev = spidev # the spidev object/instance
|
@ -1,19 +1,21 @@
|
||||
import os, sys
|
||||
|
||||
root = os.path.dirname(os.path.dirname(__file__))
|
||||
sys.path.append(root)
|
||||
|
||||
import uvicorn
|
||||
from API.main import app
|
||||
from API.endpoints.hub import HubManager
|
||||
from Raspberry import WiFi
|
||||
from server import app
|
||||
from client import client, ws
|
||||
from hardware import WiFi
|
||||
|
||||
|
||||
def run():
|
||||
ws.start()
|
||||
client.fetch()
|
||||
|
||||
# try:
|
||||
# WiFi.connect_first()
|
||||
# except: # TODO: specify wifi exception
|
||||
# WiFi.start_hotspot()
|
||||
# finally:
|
||||
# print('complete')
|
||||
|
||||
uvicorn.run('main:app', host = '0.0.0.0', port = 8000, reload = False)
|
||||
|
||||
if __name__ == '__main__':
|
||||
# try:
|
||||
# hub = HubManager.default().get()
|
||||
# WiFi.connect_first()
|
||||
# except:
|
||||
# WiFi.start_hotspot()
|
||||
|
||||
uvicorn.run('main:app', host = '0.0.0.0', port = 8000, reload = False, reload_dirs=[root,])
|
||||
run()
|
||||
|
33
SmartHome/managers/DeviceModelManager.py
Normal file
33
SmartHome/managers/DeviceModelManager.py
Normal file
@ -0,0 +1,33 @@
|
||||
from uuid import UUID
|
||||
from sqlalchemy import select, update, delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
import database
|
||||
from models import DeviceModel, Parameter, DeviceModelParameter
|
||||
from schemas.device import DeviceModelInfo, DeviceModel as DeviceModelScheme
|
||||
|
||||
|
||||
class DeviceModelManager:
|
||||
session: AsyncSession
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def save(self, scheme: DeviceModelInfo | DeviceModelScheme):
|
||||
db: AsyncSession = self.session
|
||||
|
||||
device_model = DeviceModel(id = scheme.id, name = scheme.name)
|
||||
db.add(device_model)
|
||||
# if device_model := await db.get(DeviceModel, scheme.id):
|
||||
# device_model.name = scheme.name
|
||||
# else:
|
||||
|
||||
if isinstance(scheme, DeviceModelScheme):
|
||||
for parameter_scheme in scheme.parameters:
|
||||
# if parameter := await db.get(Parameter, parameter_scheme.id):
|
||||
# pass
|
||||
# else:
|
||||
db.add(Parameter(parameter_scheme.id, parameter_scheme.name, parameter_scheme.value_type))
|
||||
db.add(DeviceModelParameter(devicemodel_id = scheme.id, parameter_id = parameter_scheme.id))
|
||||
|
||||
await db.commit()
|
@ -4,40 +4,44 @@ from sqlalchemy import select, update, delete
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from exceptions.Internal import InternalException, ExceptionCode
|
||||
from AUID import AUID
|
||||
from API.models import (
|
||||
import database
|
||||
from models import (
|
||||
User,
|
||||
Device,
|
||||
DeviceModel,
|
||||
DeviceParameterAssociation,
|
||||
DeviceParameterAssociation
|
||||
)
|
||||
from API.dependencies import database, auth
|
||||
from API import exceptions
|
||||
from . import schemas
|
||||
from ..schemas import DeviceParameter, Parameter
|
||||
from schemas.device import (
|
||||
DeviceParameter,
|
||||
Parameter,
|
||||
DeviceState,
|
||||
DevicePatch,
|
||||
DeviceCreate,
|
||||
Device as DeviceScheme
|
||||
)
|
||||
|
||||
|
||||
class DevicesManager:
|
||||
session: AsyncSession
|
||||
user: User
|
||||
|
||||
def __init__(self, session = Depends(database.get_async_session), user = Depends(auth.validate_user)):
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
self.user = user
|
||||
|
||||
async def get(self, id: UUID) -> Device | None:
|
||||
async def get(self, id: UUID) -> Device:
|
||||
db: AsyncSession = self.session
|
||||
return await db.get(Device, id)
|
||||
if device := await db.get(Device, id):
|
||||
return device
|
||||
else:
|
||||
raise InternalException(ExceptionCode.not_found, 'Device not found')
|
||||
|
||||
async def state(self, id: UUID) -> schemas.DeviceState | None:
|
||||
async def state(self, id: UUID) -> DeviceState | None:
|
||||
db: AsyncSession = self.session
|
||||
device = await db.get(Device, id)
|
||||
device = await self.get(id)
|
||||
|
||||
if not device:
|
||||
return None
|
||||
|
||||
device_state = schemas.DeviceState(**schemas.Device.from_orm(device).dict())
|
||||
device_state = DeviceState(**DeviceScheme.from_orm(device).dict())
|
||||
|
||||
async with database.async_engine.begin() as conn:
|
||||
parameters = await conn.run_sync(DevicesManager._read_parameters, device)
|
||||
@ -46,7 +50,7 @@ class DevicesManager:
|
||||
|
||||
return device_state
|
||||
|
||||
async def create(self, create_device: schemas.CreateDevice) -> Device:
|
||||
async def create(self, create_device: DeviceCreate) -> Device:
|
||||
db: AsyncSession = self.session
|
||||
|
||||
id = AUID(bytes=create_device.id.bytes)
|
||||
@ -56,10 +60,17 @@ class DevicesManager:
|
||||
model = await db.get(DeviceModel, model_id)
|
||||
|
||||
if not model:
|
||||
raise exceptions.incorrect_format
|
||||
raise InternalException(
|
||||
code = ExceptionCode.invalid_format,
|
||||
msg = 'Unknown device id',
|
||||
debug = f'Unknown model in auid"{id}"'
|
||||
)
|
||||
|
||||
if (await db.scalars(select(Device).where(Device.urdi == urdi))).first():
|
||||
raise exceptions.already_exist
|
||||
raise InternalException(
|
||||
code = ExceptionCode.invalid_format,
|
||||
msg = 'Device with this id already exist'
|
||||
)
|
||||
|
||||
device = Device(
|
||||
id = create_device.id,
|
||||
@ -82,7 +93,7 @@ class DevicesManager:
|
||||
|
||||
return device
|
||||
|
||||
async def patch(self, id: UUID, device: schemas.PatchDevice):
|
||||
async def patch(self, id: UUID, device: DevicePatch):
|
||||
db: AsyncSession = self.session
|
||||
values = {key: value for key, value in device.dict().items() if key != 'id'}
|
||||
await db.execute(update(Device).values(**values).where(Device.id == id))
|
||||
@ -91,11 +102,14 @@ class DevicesManager:
|
||||
async def delete(self, device_id: UUID):
|
||||
db: AsyncSession = self.session
|
||||
device = await self.get(device_id)
|
||||
if device: # and device.house.owner_id == self.owner_id:
|
||||
if device:
|
||||
await db.delete(device)
|
||||
await db.commit()
|
||||
else:
|
||||
raise exceptions.not_found
|
||||
raise InternalException(
|
||||
code = ExceptionCode.not_found,
|
||||
msg = 'Device not found'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _read_parameters(_, device: Device) -> list[DeviceParameter]:
|
45
SmartHome/managers/HouseManager.py
Normal file
45
SmartHome/managers/HouseManager.py
Normal file
@ -0,0 +1,45 @@
|
||||
from uuid import UUID
|
||||
from fastapi import Depends
|
||||
from sqlalchemy import select, delete, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.exc import NoResultFound
|
||||
|
||||
from exceptions.Internal import InternalException, ExceptionCode
|
||||
from models import House
|
||||
from server.dependencies import database
|
||||
from schemas.house import HousePatch
|
||||
|
||||
|
||||
class HouseManager:
|
||||
session: AsyncSession
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def get(self) -> House:
|
||||
db: AsyncSession = self.session
|
||||
try:
|
||||
result = await db.scalars(select(House))
|
||||
return result.one()
|
||||
except NoResultFound:
|
||||
raise InternalException(ExceptionCode.not_initialized)
|
||||
|
||||
async def create(self, house_id: UUID) -> House:
|
||||
db: AsyncSession = self.session
|
||||
|
||||
try:
|
||||
await db.delete(await self.get())
|
||||
except InternalException:
|
||||
pass
|
||||
|
||||
house = House(id = house_id, name = '')
|
||||
db.add(house)
|
||||
await db.commit()
|
||||
return house
|
||||
|
||||
async def patch(self, house: HousePatch):
|
||||
db: AsyncSession = self.session
|
||||
await db.execute(
|
||||
update(House)
|
||||
.values(**house.dict())
|
||||
)
|
@ -1,46 +1,62 @@
|
||||
from __future__ import annotations
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.exc import NoResultFound
|
||||
|
||||
from Raspberry import WiFi
|
||||
import config
|
||||
from API import exceptions
|
||||
from API.models import User, Hub
|
||||
from API.dependencies import database, auth
|
||||
from API.auth import UserToken, UserAuthManager, AuthException
|
||||
from API import endpoints
|
||||
from . import schemas
|
||||
from exceptions.Internal import InternalException, ExceptionCode
|
||||
from hardware import WiFi
|
||||
from models import User, Hub
|
||||
from schemas import hub as schemas
|
||||
from managers import HouseManager
|
||||
from server.dependencies import database, auth
|
||||
from server.auth import UserToken, UserAuthManager, AuthException
|
||||
|
||||
|
||||
class HubManager:
|
||||
session: AsyncSession
|
||||
token: auth.UserToken
|
||||
token: auth.UserToken | None
|
||||
user: User | None = None
|
||||
|
||||
def __init__(self, session = Depends(database.get_async_session), token = Depends(auth.optional_token)):
|
||||
def __init__(self, session: AsyncSession, token: UserToken = None):
|
||||
self.session = session
|
||||
self.token = token
|
||||
|
||||
async def get(self) -> Hub | None:
|
||||
db: AsyncSession = self.session
|
||||
try:
|
||||
result = await db.scalars(select(Hub))
|
||||
return result.one()
|
||||
except NoResultFound:
|
||||
raise InternalException(ExceptionCode.not_initialized)
|
||||
|
||||
async def parse_token(self, raw_token: str):
|
||||
db: AsyncSession = self.session
|
||||
try:
|
||||
self.token = UserAuthManager().validate_access(raw_token)
|
||||
except AuthException:
|
||||
raise exceptions.access_denied
|
||||
raise InternalException(ExceptionCode.unauthorized)
|
||||
self.user = await db.get(User, self.token.user_id)
|
||||
|
||||
async def init(self, create_hub: schemas.HubInit) -> Hub:
|
||||
async def init(self, create_hub: schemas.HubInit, raw_token: str) -> Hub:
|
||||
db: AsyncSession = self.session
|
||||
|
||||
if hub := await self.get():
|
||||
try:
|
||||
hub = await self.get()
|
||||
except InternalException:
|
||||
hub = None
|
||||
|
||||
if hub:
|
||||
await self.parse_token(raw_token)
|
||||
await self.check_access()
|
||||
await db.delete(hub)
|
||||
self.save_tokens(create_hub)
|
||||
else:
|
||||
self.save_credentials(create_hub)
|
||||
await self.parse_token(raw_token)
|
||||
|
||||
house_manager = endpoints.house.HouseManager(db)
|
||||
house_manager = HouseManager(db)
|
||||
await house_manager.create(create_hub.house_id)
|
||||
|
||||
hub = Hub(id = create_hub.id, name = create_hub.name, house_id = create_hub.house_id)
|
||||
@ -53,12 +69,7 @@ class HubManager:
|
||||
await db.commit()
|
||||
return hub
|
||||
|
||||
async def get(self) -> Hub | None:
|
||||
db: AsyncSession = self.session
|
||||
result = await db.scalars(select(Hub))
|
||||
return result.first()
|
||||
|
||||
async def patch(self, hub: schemas.PatchHub):
|
||||
async def patch(self, hub: schemas.HubPatch):
|
||||
db: AsyncSession = self.session
|
||||
values = {key: value for key, value in hub.dict().items() if value != None}
|
||||
|
||||
@ -104,10 +115,10 @@ class HubManager:
|
||||
return
|
||||
|
||||
if not self.token:
|
||||
raise exceptions.access_denied
|
||||
raise InternalException(ExceptionCode.access_denied)
|
||||
|
||||
db: AsyncSession = self.session
|
||||
self.user = await db.get(User, self.token.user_id)
|
||||
|
||||
if not self.user:
|
||||
raise exceptions.access_denied
|
||||
raise InternalException(ExceptionCode.access_denied)
|
@ -5,42 +5,38 @@ from sqlalchemy import select, update, delete
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from API import exceptions
|
||||
from API import endpoints
|
||||
from API.models import User, Room
|
||||
from API.dependencies import database, auth
|
||||
from . import schemas
|
||||
from exceptions.Internal import InternalException, ExceptionCode
|
||||
from models import User, Room
|
||||
from schemas import room as schemas
|
||||
from server import endpoints
|
||||
|
||||
|
||||
class RoomsManager:
|
||||
session: AsyncSession
|
||||
user: User
|
||||
|
||||
def __init__(self, session = Depends(database.get_async_session), user = Depends(auth.validate_user)):
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
self.user = user
|
||||
|
||||
async def get(self, id: UUID) -> Room | None:
|
||||
async def get(self, id: UUID) -> Room:
|
||||
db: AsyncSession = self.session
|
||||
response = await db.scalars(
|
||||
result = await db.scalars(
|
||||
select(Room).where(Room.id == id).options(selectinload(Room.devices))
|
||||
)
|
||||
return response.first()
|
||||
if room := result.first():
|
||||
return room
|
||||
else:
|
||||
raise InternalException(ExceptionCode.not_found, 'Room not found')
|
||||
|
||||
async def create(self, create_room: schemas.CreateRoom) -> Room:
|
||||
async def create(self, create_room: schemas.RoomCreate) -> Room:
|
||||
db: AsyncSession = self.session
|
||||
house = await endpoints.house.HouseManager(db).get()
|
||||
|
||||
if not house:
|
||||
raise exceptions.not_initialized
|
||||
|
||||
room = Room(name = create_room.name, house_id = house.id, devices = [])
|
||||
db.add(room)
|
||||
await db.commit()
|
||||
|
||||
return room
|
||||
|
||||
async def patch(self, id: UUID, room: schemas.PatchRoom):
|
||||
async def patch(self, id: UUID, room: schemas.RoomPatch):
|
||||
db: AsyncSession = self.session
|
||||
values = {key: value for key, value in room.dict().items() if key != 'id'}
|
||||
await db.execute(update(Room).values(**values).where(Room.id == id))
|
||||
@ -49,8 +45,5 @@ class RoomsManager:
|
||||
async def delete(self, room_id: UUID):
|
||||
db: AsyncSession = self.session
|
||||
room = await self.get(room_id)
|
||||
if room:
|
||||
await db.delete(room)
|
||||
await db.commit()
|
||||
else:
|
||||
raise exceptions.not_found
|
||||
await db.delete(room)
|
||||
await db.commit()
|
57
SmartHome/managers/WSManager.py
Normal file
57
SmartHome/managers/WSManager.py
Normal file
@ -0,0 +1,57 @@
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from pydantic.error_wrappers import ValidationError
|
||||
|
||||
import database
|
||||
from hardware.Merlin import merlin, MerlinMessage
|
||||
from models import Device, DeviceModelParameter
|
||||
from schemas.ws import SocketType, SocketData, MerlinData
|
||||
|
||||
|
||||
class WSManager:
|
||||
session: AsyncSession
|
||||
|
||||
def __inti__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def handle_message(self, msg: str):
|
||||
socket = SocketData.parse_raw(msg)
|
||||
try:
|
||||
socket = SocketData.parse_raw(msg)
|
||||
except ValidationError:
|
||||
return
|
||||
|
||||
match socket.type:
|
||||
case SocketType.merlin:
|
||||
merlin_data = MerlinData(**socket.data)
|
||||
# try:
|
||||
# merlin_data = MerlinData(**socket.data)
|
||||
# except: # TODO: specify exception
|
||||
# return
|
||||
await self.merlin_send(merlin_data)
|
||||
|
||||
async def merlin_send(self, data: MerlinData):
|
||||
db: AsyncSession = self.session
|
||||
try:
|
||||
if message := await self._get_message(db, data):
|
||||
merlin.send(message)
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
async def _get_message(self, db: AsyncSession, data: MerlinData) -> MerlinMessage | None:
|
||||
device = await db.get(Device, data.device_id)
|
||||
|
||||
if not device:
|
||||
return None
|
||||
|
||||
result = await db.execute(
|
||||
select(DeviceModelParameter)
|
||||
.where(
|
||||
DeviceModelParameter.devicemodel_id == device.model.id,
|
||||
DeviceModelParameter.parameter_id == data.parameter_id
|
||||
)
|
||||
)
|
||||
|
||||
model_parameter = result.scalar_one()
|
||||
|
||||
return MerlinMessage(device.urdi, model_parameter.f, int(data.value))
|
6
SmartHome/managers/__init__.py
Normal file
6
SmartHome/managers/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
from .DeviceModelManager import DeviceModelManager
|
||||
from .DevicesManager import DevicesManager
|
||||
from .HouseManager import HouseManager
|
||||
from .HubManager import HubManager
|
||||
from .RoomsManager import RoomsManager
|
||||
from .WSManager import WSManager
|
32
SmartHome/schemas/device.py
Normal file
32
SmartHome/schemas/device.py
Normal file
@ -0,0 +1,32 @@
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
from pydantic import BaseModel
|
||||
from AUID import AUID
|
||||
from .parameters import DeviceParameter, Parameter
|
||||
|
||||
|
||||
class DeviceModelInfo(BaseModel):
|
||||
id: UUID
|
||||
name: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
class DeviceModel(DeviceModelInfo):
|
||||
parameters: list[Parameter]
|
||||
|
||||
class DevicePatch(BaseModel):
|
||||
name: str
|
||||
room_id: UUID
|
||||
|
||||
class DeviceCreate(DevicePatch):
|
||||
id: AUID
|
||||
|
||||
class Device(DeviceCreate):
|
||||
model: DeviceModel
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
class DeviceState(Device):
|
||||
parameters: list[DeviceParameter] = []
|
@ -1,14 +1,16 @@
|
||||
from uuid import UUID
|
||||
from pydantic import BaseModel
|
||||
from ..hub.schemas import Hub
|
||||
from ..room.schemas import RoomInfo
|
||||
from .hub import Hub
|
||||
from .room import RoomInfo
|
||||
|
||||
|
||||
class House(BaseModel):
|
||||
class HousePatch(BaseModel):
|
||||
id: UUID
|
||||
name: str
|
||||
hubs: list[Hub]
|
||||
rooms: list[RoomInfo]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
class House(HousePatch):
|
||||
hubs: list[Hub]
|
||||
rooms: list[RoomInfo]
|
@ -2,6 +2,8 @@ from uuid import UUID
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
# Auth
|
||||
|
||||
class TokensPair(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
@ -9,6 +11,8 @@ class TokensPair(BaseModel):
|
||||
class HubAuthItems(TokensPair):
|
||||
public_key: str
|
||||
|
||||
# Hub
|
||||
|
||||
class Hub(BaseModel):
|
||||
id: UUID
|
||||
name: str
|
||||
@ -23,6 +27,8 @@ class HubPatch(BaseModel):
|
||||
class HubInit(Hub, HubAuthItems):
|
||||
...
|
||||
|
||||
# WiFi
|
||||
|
||||
class Hotspot(BaseModel):
|
||||
ssid: str
|
||||
quality: float
|
@ -1,6 +1,6 @@
|
||||
from uuid import UUID
|
||||
from pydantic import BaseModel
|
||||
from API.models import DeviceModelParameter
|
||||
from models import DeviceModelParameter
|
||||
|
||||
|
||||
class Parameter(BaseModel):
|
||||
@ -20,11 +20,3 @@ class Parameter(BaseModel):
|
||||
|
||||
class DeviceParameter(Parameter):
|
||||
value: int
|
||||
|
||||
class DeviceModel(BaseModel):
|
||||
id: UUID
|
||||
name: str
|
||||
parameters: list[Parameter]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
@ -1,12 +1,12 @@
|
||||
from uuid import UUID
|
||||
from pydantic import BaseModel
|
||||
from ..device.schemas import Device
|
||||
from .device import Device
|
||||
|
||||
|
||||
class CreateRoom(BaseModel):
|
||||
class RoomCreate(BaseModel):
|
||||
name: str
|
||||
|
||||
class PatchRoom(CreateRoom):
|
||||
class RoomPatch(RoomCreate):
|
||||
pass
|
||||
|
||||
class RoomInfo(BaseModel):
|
@ -1,12 +1,11 @@
|
||||
from fastapi import FastAPI, APIRouter
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from . import models
|
||||
from . import endpoints
|
||||
from . import dependencies
|
||||
import models
|
||||
import database
|
||||
from server import exceptions_handlers, endpoints
|
||||
|
||||
|
||||
models.Base.metadata.create_all(bind = dependencies.database.engine)
|
||||
models.Base.metadata.create_all(bind = database.engine)
|
||||
|
||||
description = '''
|
||||
[**Admin**](/admin)
|
||||
@ -26,11 +25,14 @@ app = FastAPI(
|
||||
}
|
||||
)
|
||||
|
||||
exceptions_handlers.setup(app)
|
||||
|
||||
api = APIRouter(prefix = '/api')
|
||||
api.include_router(endpoints.house.router)
|
||||
api.include_router(endpoints.hub.router)
|
||||
api.include_router(endpoints.room.router)
|
||||
api.include_router(endpoints.device.router)
|
||||
|
||||
app.include_router(api)
|
||||
app.include_router(endpoints.ws.router)
|
||||
|
@ -32,8 +32,8 @@ class BaseAuthManager(ABC):
|
||||
def _get_parsed_token(self, payload: dict) -> BaseToken:
|
||||
pass
|
||||
|
||||
def validate_access(self, token: str) -> BaseToken:
|
||||
token = self._parse_token(token)
|
||||
def validate_access(self, raw_token: str) -> BaseToken:
|
||||
token = self._parse_token(raw_token)
|
||||
|
||||
if not token:
|
||||
raise AuthException()
|
||||
@ -46,9 +46,9 @@ class BaseAuthManager(ABC):
|
||||
|
||||
return token
|
||||
|
||||
def _parse_token(self, token: str) -> BaseToken:
|
||||
def _parse_token(self, raw_token: str) -> BaseToken:
|
||||
try:
|
||||
payload = jwt.decode(token, config.public_key, algorithms = [config.algorithm])
|
||||
payload = jwt.decode(raw_token, config.public_key, algorithms = [config.algorithm])
|
||||
token = self._get_parsed_token(payload)
|
||||
except JWTError:
|
||||
raise AuthException()
|
@ -1,8 +1,8 @@
|
||||
from fastapi import Depends
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
|
||||
from API import exceptions
|
||||
from API.models import User
|
||||
from exceptions.Internal import InternalException, ExceptionCode
|
||||
from models import User
|
||||
from ..auth import (
|
||||
UserToken,
|
||||
UserAuthManager,
|
||||
@ -15,21 +15,15 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl = '/api/user/login')
|
||||
optional_oauth2_scheme = OAuth2PasswordBearer(tokenUrl = '/api/user/login', auto_error=False)
|
||||
userAuthManager = UserAuthManager()
|
||||
|
||||
async def validate_user(token: str = Depends(oauth2_scheme), session = Depends(get_async_session)) -> User:
|
||||
try:
|
||||
token = userAuthManager.validate_access(token)
|
||||
user = await session.get(User, token.user_id)
|
||||
if not user:
|
||||
raise AuthException
|
||||
return user
|
||||
except AuthException:
|
||||
raise exceptions.access_denied
|
||||
async def validate_user(raw_token: str = Depends(oauth2_scheme), session = Depends(get_async_session)) -> User:
|
||||
token = userAuthManager.validate_access(raw_token)
|
||||
user = await session.get(User, token.user_id)
|
||||
if not user:
|
||||
raise AuthException()
|
||||
return user
|
||||
|
||||
def validate_token(token: str = Depends(oauth2_scheme)) -> UserToken:
|
||||
try:
|
||||
return userAuthManager.validate_access(token)
|
||||
except AuthException:
|
||||
raise exceptions.access_denied
|
||||
return userAuthManager.validate_access(token)
|
||||
|
||||
def optional_token(token: str = Depends(optional_oauth2_scheme)) -> UserToken | None:
|
||||
if not token:
|
15
SmartHome/server/dependencies/database.py
Normal file
15
SmartHome/server/dependencies/database.py
Normal file
@ -0,0 +1,15 @@
|
||||
from database import (
|
||||
create_session,
|
||||
create_async_session,
|
||||
Session,
|
||||
AsyncSession
|
||||
)
|
||||
|
||||
def get_session() -> Session:
|
||||
with create_session() as session:
|
||||
yield session
|
||||
|
||||
|
||||
async def get_async_session() -> AsyncSession:
|
||||
async with create_async_session() as session:
|
||||
yield session
|
6
SmartHome/server/endpoints/__init__.py
Normal file
6
SmartHome/server/endpoints/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
from . import admin
|
||||
from . import device
|
||||
from . import house
|
||||
from . import hub
|
||||
from . import room
|
||||
from . import ws
|
@ -1,7 +1,7 @@
|
||||
from fastapi import FastAPI
|
||||
from sqladmin import Admin, ModelAdmin, SidebarLink
|
||||
from API.dependencies import database
|
||||
from API.models import (
|
||||
import database
|
||||
from models import (
|
||||
Base,
|
||||
House,
|
||||
Hub,
|
43
SmartHome/server/endpoints/device.py
Normal file
43
SmartHome/server/endpoints/device.py
Normal file
@ -0,0 +1,43 @@
|
||||
from uuid import UUID
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from exceptions.Internal import InternalException, ExceptionCode
|
||||
from managers import DevicesManager
|
||||
from schemas.device import (
|
||||
Device,
|
||||
DeviceState,
|
||||
DeviceCreate,
|
||||
DevicePatch
|
||||
)
|
||||
from server.dependencies.database import get_async_session, AsyncSession
|
||||
from server.dependencies.auth import validate_user
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix = '/device',
|
||||
tags = ['device'],
|
||||
)
|
||||
|
||||
# MARK: - dependencies
|
||||
|
||||
async def manager(session = Depends(get_async_session),
|
||||
user = Depends(validate_user)) -> DevicesManager:
|
||||
return DevicesManager(session)
|
||||
|
||||
# MARK: - endpoints
|
||||
|
||||
@router.post('', response_model = Device)
|
||||
async def create_device(device: DeviceCreate, manager = Depends(manager)):
|
||||
return await manager.create(device)
|
||||
|
||||
@router.get('/{id}', response_model = DeviceState)
|
||||
async def get_device(id: UUID, manager = Depends(manager)):
|
||||
return await manager.state(id)
|
||||
|
||||
@router.patch('/{id}')
|
||||
async def patch_device(id: UUID, device: DevicePatch, manager = Depends(manager)):
|
||||
await manager.patch(id, device)
|
||||
|
||||
@router.delete('/{id}')
|
||||
async def delete_device(id: UUID, manager = Depends(manager)):
|
||||
await manager.delete(id)
|
25
SmartHome/server/endpoints/house.py
Normal file
25
SmartHome/server/endpoints/house.py
Normal file
@ -0,0 +1,25 @@
|
||||
from uuid import UUID
|
||||
from fastapi import APIRouter, Depends
|
||||
from managers import HouseManager
|
||||
from schemas.house import House
|
||||
from server.dependencies.database import get_async_session, AsyncSession
|
||||
from server.dependencies.auth import validate_user
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix = '/house',
|
||||
tags = ['house'],
|
||||
)
|
||||
|
||||
# MARK: - dependencies
|
||||
|
||||
async def session(session = Depends(get_async_session),
|
||||
user = Depends(validate_user)) -> AsyncSession:
|
||||
return session
|
||||
|
||||
# MARK: - endpoints
|
||||
|
||||
@router.get('', response_model = House)
|
||||
async def get_house(session = Depends(session)):
|
||||
manager = HouseManager(session)
|
||||
return await manager.get()
|
62
SmartHome/server/endpoints/hub.py
Normal file
62
SmartHome/server/endpoints/hub.py
Normal file
@ -0,0 +1,62 @@
|
||||
from fastapi import APIRouter, Depends, BackgroundTasks
|
||||
from managers import HubManager
|
||||
from schemas.hub import (
|
||||
HubInit,
|
||||
Hub,
|
||||
HubPatch,
|
||||
TokensPair,
|
||||
Hotspot,
|
||||
WifiConnection
|
||||
)
|
||||
from server.dependencies.database import get_async_session, AsyncSession
|
||||
from server.dependencies.auth import validate_user, raw_token
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix = '/hub',
|
||||
tags = ['hub'],
|
||||
)
|
||||
|
||||
# MARK: - dependencies
|
||||
|
||||
async def manager(session = Depends(get_async_session)) -> HubManager:
|
||||
return HubManager(session)
|
||||
|
||||
async def manager_auth(manager = Depends(manager),
|
||||
user = Depends(validate_user)) -> HubManager:
|
||||
return manager
|
||||
|
||||
# MARK: - endpoints
|
||||
|
||||
@router.post('', response_model = Hub)
|
||||
async def init_hub(hub_init: HubInit,
|
||||
manager = Depends(manager),
|
||||
raw_token: str = Depends(raw_token)):
|
||||
return await manager.init(hub_init, raw_token)
|
||||
|
||||
@router.get('', response_model = Hub)
|
||||
async def get_hub(manager = Depends(manager_auth)):
|
||||
return await manager.get()
|
||||
|
||||
@router.patch('')
|
||||
async def patch_hub(hub: HubPatch, manager = Depends(manager_auth)):
|
||||
await manager.patch(hub)
|
||||
|
||||
@router.post('/connect')
|
||||
async def connect_to_wifi(wifi: WifiConnection, manager = Depends(manager_auth)):
|
||||
manager.wifi(wifi.ssid, wifi.password)
|
||||
|
||||
@router.get('/hotspots', response_model = list[Hotspot])
|
||||
def get_hub_hotspots(manager = Depends(manager_auth)):
|
||||
return manager.get_hotspots()
|
||||
|
||||
@router.get('/is_connected', response_model=bool)
|
||||
def is_connected(bg_tasks: BackgroundTasks, manager = Depends(manager_auth)):
|
||||
connected = manager.is_connected()
|
||||
if connected:
|
||||
bg_tasks.add_task(manager.stop_hotspot)
|
||||
return connected
|
||||
|
||||
@router.post('/set_tokens')
|
||||
async def set_tokens(tokens: TokensPair, manager = Depends(manager_auth)):
|
||||
manager.save_tokens(tokens)
|
36
SmartHome/server/endpoints/room.py
Normal file
36
SmartHome/server/endpoints/room.py
Normal file
@ -0,0 +1,36 @@
|
||||
from uuid import UUID
|
||||
from fastapi import APIRouter, Depends
|
||||
from managers import RoomsManager
|
||||
from schemas.room import Room, RoomCreate, RoomPatch
|
||||
from server.dependencies.database import get_async_session, AsyncSession
|
||||
from server.dependencies.auth import validate_user, raw_token
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix = '/room',
|
||||
tags = ['room'],
|
||||
)
|
||||
|
||||
# MARK: - dependencies
|
||||
|
||||
async def manager(session = Depends(get_async_session),
|
||||
user = Depends(validate_user)) -> RoomsManager:
|
||||
return RoomsManager(session)
|
||||
|
||||
# MARK: - endpoints
|
||||
|
||||
@router.post('', response_model = Room)
|
||||
async def create_room(room: RoomCreate, manager = Depends(manager)):
|
||||
return await manager.create(room)
|
||||
|
||||
@router.get('/{id}', response_model = Room)
|
||||
async def get_room(id: UUID, manager = Depends(manager)):
|
||||
return await manager.get(id)
|
||||
|
||||
@router.patch('/{id}')
|
||||
async def patch_room(id: UUID, room: RoomPatch, manager = Depends(manager)):
|
||||
await manager.patch(id, room)
|
||||
|
||||
@router.delete('/{id}')
|
||||
async def delete_room(id: UUID, manager = Depends(manager)):
|
||||
await manager.delete(id)
|
@ -1,6 +1,5 @@
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||
from .schemas import SocketType, SocketData, MerlinData
|
||||
from .WSManager import WSManager
|
||||
from managers import WSManager
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
@ -19,20 +18,7 @@ class ConnectionManager:
|
||||
self.active_connections.remove(websocket)
|
||||
|
||||
async def handle_socket(self, websocket: WebSocket, msg: str):
|
||||
socket = SocketData.parse_raw(msg)
|
||||
# try:
|
||||
# socket = SocketData.parse_raw(msg)
|
||||
# except: # TODO: specify exception
|
||||
# return
|
||||
|
||||
match socket.type:
|
||||
case SocketType.merlin:
|
||||
merlin_data = MerlinData(**socket.data)
|
||||
# try:
|
||||
# merlin_data = MerlinData(**socket.data)
|
||||
# except: # TODO: specify exception
|
||||
# return
|
||||
await self.wsmanager.merlin_send(merlin_data)
|
||||
await self.wsmanager.handle_message(msg)
|
||||
|
||||
connection = ConnectionManager()
|
||||
router = APIRouter(
|
23
SmartHome/server/exceptions_handlers.py
Normal file
23
SmartHome/server/exceptions_handlers.py
Normal file
@ -0,0 +1,23 @@
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi.responses import PlainTextResponse, JSONResponse
|
||||
from exceptions.Internal import InternalException
|
||||
|
||||
|
||||
http_codes = {
|
||||
1000: 400,
|
||||
1001: 401,
|
||||
1003: 403,
|
||||
1004: 404,
|
||||
1005: 400,
|
||||
1006: 400,
|
||||
1022: 422,
|
||||
}
|
||||
|
||||
def internal(request: Request, exc: InternalException) -> Response:
|
||||
return JSONResponse({
|
||||
'msg': exc.msg,
|
||||
'detail': exc.debug
|
||||
}, status_code = http_codes.get(exc.code) or 400)
|
||||
|
||||
def setup(app: FastAPI):
|
||||
app.exception_handler(InternalException)(internal)
|
44
SmartHome/server/http_exceptions.py
Normal file
44
SmartHome/server/http_exceptions.py
Normal file
@ -0,0 +1,44 @@
|
||||
# from fastapi import status
|
||||
# from fastapi.responses import JSONResponse
|
||||
#
|
||||
#
|
||||
# class Response(JSONResponse):
|
||||
# def __init__(self, msg: str, dev: str, status_code: int)
|
||||
#
|
||||
# http_exceptions: dict[str, JSONResponse] = {
|
||||
# 1000: JSONResponse({
|
||||
# 'Undefined exception',
|
||||
# }, status_code = status.HTTP_400_BAD_REQUEST,
|
||||
# ),
|
||||
#
|
||||
# 1001: HTTPException(
|
||||
# status_code = status.HTTP_401_UNAUTHORIZED,
|
||||
# detail = 'Token invalid, expired, or belongs to unknown user',
|
||||
# headers = {'WWW-Authenticate': 'Bearer'},
|
||||
# ),
|
||||
#
|
||||
# 1003: HTTPException(
|
||||
# status_code = status.HTTP_403_FORBIDDEN,
|
||||
# detail = 'Access denied',
|
||||
# ),
|
||||
#
|
||||
# 1004: HTTPException(
|
||||
# status_code = status.HTTP_404_NOT_FOUND,
|
||||
# detail = 'Not found',
|
||||
# ),
|
||||
#
|
||||
# 1005: HTTPException(
|
||||
# status_code = 400,
|
||||
# detail = 'Resource already exist'
|
||||
# ),
|
||||
#
|
||||
# 1006: HTTPException(
|
||||
# status_code = 400,
|
||||
# detail = 'Hub not initialized'
|
||||
# ),
|
||||
#
|
||||
# 1022: HTTPException (
|
||||
# status_code = status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
# detail = 'Data in invalid format'
|
||||
# )
|
||||
# }
|
@ -5,7 +5,7 @@ import random
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from API import models
|
||||
import models
|
||||
|
||||
|
||||
room_names = [ 'Atrium', 'Ballroom', 'Bathroom', 'Bedroom', 'Billiard room', 'Cabinet', 'Computer lab',
|
||||
@ -46,7 +46,7 @@ class Faker:
|
||||
}, headers = {
|
||||
'Authorization': f'Bearer {self.user_access_token}'
|
||||
})
|
||||
|
||||
|
||||
return response.json()
|
||||
|
||||
def get_house(self) -> dict[str, Any]:
|
||||
@ -62,9 +62,9 @@ class Faker:
|
||||
session.refresh(room)
|
||||
return room
|
||||
|
||||
def create_device_model(self) -> models.DeviceModel:
|
||||
def create_device_model(self, id: UUID = uuid1()) -> models.DeviceModel:
|
||||
with self.create_session() as session:
|
||||
device_model = models.DeviceModel(name = random.choice(model_names))
|
||||
device_model = models.DeviceModel(id=id, name=random.choice(model_names))
|
||||
session.add(device_model)
|
||||
session.commit()
|
||||
session.refresh(device_model)
|
||||
|
@ -13,8 +13,8 @@ from sqlalchemy.orm import sessionmaker, Session
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
|
||||
from main import app
|
||||
from API.dependencies.database import get_session, get_async_session
|
||||
from API import models
|
||||
from server.dependencies.database import get_session, get_async_session
|
||||
import models
|
||||
import config
|
||||
|
||||
from .faker import Faker
|
||||
@ -22,9 +22,9 @@ from .faker import Faker
|
||||
|
||||
# Settings
|
||||
|
||||
with open(f'{config.path}/SmartHome/tests/jwt-key', 'r') as f:
|
||||
with open(f'{config.path}/tests/jwt-key', 'r') as f:
|
||||
secret_key = f.read()
|
||||
with open(f'{config.path}/SmartHome/tests/jwt-key.pub', 'r') as f:
|
||||
with open(f'{config.path}/tests/jwt-key.pub', 'r') as f:
|
||||
public_key = f.read()
|
||||
|
||||
user_id = UUID('f085ef73-f599-11ec-acda-58961df87e73')
|
||||
|
@ -2,35 +2,34 @@ from tests.setup import *
|
||||
|
||||
|
||||
faker.init_hub()
|
||||
id = uuid1()
|
||||
id = '62ff3e2b-0130-0002-0001-000000000001'
|
||||
room_id = faker.create_room().id
|
||||
model_id = faker.create_device_model().id
|
||||
faker.create_device_model('00000000-0000-0002-0000-000000000000')
|
||||
device = {
|
||||
'id': str(id),
|
||||
'id': id,
|
||||
'name': 'New Room',
|
||||
'room_id': str(room_id),
|
||||
'model_id': str(model_id),
|
||||
}
|
||||
|
||||
def test_get_device_null():
|
||||
response = client.get(f'/api/device/', headers = auth_headers)
|
||||
assert response.status_code == 405
|
||||
assert response.json() == {'detail': 'Method Not Allowed'}
|
||||
assert response.status_code == 405, response.text
|
||||
assert response.json().get('detail') == 'Method Not Allowed'
|
||||
|
||||
def test_get_device_404():
|
||||
response = client.get(f'/api/device/{id}', headers = auth_headers)
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {'detail': 'Not found'}
|
||||
assert response.status_code == 404, response.text
|
||||
assert response.json().get('detail') == 'Device not found'
|
||||
|
||||
def test_delete_device_404():
|
||||
response = client.delete(f'/api/device/{id}', headers = auth_headers)
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {'detail': 'Not found'}
|
||||
assert response.status_code == 404, response.text
|
||||
assert response.json().get('detail') == 'Device not found'
|
||||
|
||||
def test_create_device():
|
||||
response = client.post(f'/api/device', json = device, headers = auth_headers)
|
||||
new_device = response.json()
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 200, response.text
|
||||
assert new_device.get('model')
|
||||
device['model'] = new_device.get('model')
|
||||
assert new_device == device
|
||||
@ -38,18 +37,18 @@ def test_create_device():
|
||||
def test_get_device():
|
||||
response = client.get(f'/api/device/{id}', headers = auth_headers)
|
||||
device['parameters'] = []
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == device
|
||||
|
||||
def test_patch_device():
|
||||
device['name'] = 'Patched Device'
|
||||
response = client.patch(f'/api/device/{id}', json = device, headers = auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 200, response.text
|
||||
assert client.get(f'/api/device/{id}', headers = auth_headers).json() == device
|
||||
|
||||
def test_delete_device():
|
||||
response = client.delete(f'/api/device/{id}', json = device, headers = auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
def test_get_deleted_device():
|
||||
test_get_device_404()
|
||||
|
@ -14,12 +14,12 @@ hub_init_json = {
|
||||
|
||||
def test_get_hub_401():
|
||||
response = client.get('/api/hub')
|
||||
assert response.status_code == 401
|
||||
assert response.json() == {'detail': 'Not authenticated'}
|
||||
assert response.status_code in [401, 403], response.text
|
||||
assert response.json() in [{'detail': 'Not authenticated'}, {'detail': 'Access denied'}]
|
||||
|
||||
def test_init_hub_401():
|
||||
response = client.post('/api/hub', json = hub_init_json)
|
||||
assert response.status_code == 401
|
||||
assert response.status_code == 401, response.text
|
||||
assert response.json() == {'detail': 'Not authenticated'}
|
||||
|
||||
def _test_get_hub_404():
|
||||
|
@ -10,17 +10,17 @@ room = {
|
||||
def test_get_room_null():
|
||||
response = client.get(f'/api/room/', headers = auth_headers)
|
||||
assert response.status_code == 405
|
||||
assert response.json() == {'detail': 'Method Not Allowed'}
|
||||
assert response.json().get('detail') == 'Method Not Allowed'
|
||||
|
||||
def test_get_room_404():
|
||||
response = client.get(f'/api/room/{id}', headers = auth_headers)
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {'detail': 'Not found'}
|
||||
assert response.json().get('detail') == 'Room not found'
|
||||
|
||||
def test_delete_room_404():
|
||||
response = client.delete(f'/api/room/{id}', headers = auth_headers)
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {'detail': 'Not found'}
|
||||
assert response.json().get('detail') == 'Room not found'
|
||||
|
||||
def test_create_room():
|
||||
response = client.post(f'/api/room', json = room, headers = auth_headers)
|
||||
|
@ -1,8 +1,8 @@
|
||||
import os, sys
|
||||
|
||||
root = os.path.dirname(os.path.dirname(__file__))
|
||||
sys.path.append(root)
|
||||
sys.path.append(root + '/VoiceAssistant')
|
||||
# root = os.path.dirname(os.path.dirname(__file__))
|
||||
# sys.path.append(root)
|
||||
# sys.path.append(root + '/VoiceAssistant')
|
||||
|
||||
from TelegramBot import TelegramBot
|
||||
|
||||
|
@ -139,7 +139,7 @@ class YoutubeAPI:
|
||||
'Accept':'application/json'
|
||||
}
|
||||
async with ClientSession() as session:
|
||||
response = await session.get(url, params = params, headers = headers)
|
||||
result = await session.get(url, params = params, headers = headers)
|
||||
json = await response.json()
|
||||
return json
|
||||
return None
|
||||
|
@ -1,7 +1,7 @@
|
||||
import os, sys
|
||||
|
||||
root = os.path.dirname(os.path.dirname(__file__))
|
||||
sys.path.append(root)
|
||||
# root = os.path.dirname(os.path.dirname(__file__))
|
||||
# sys.path.append(root)
|
||||
|
||||
from VoiceAssistant import VoiceAssistant
|
||||
|
||||
|
@ -4,11 +4,11 @@
|
||||
# sudo apt-get install libssl-dev
|
||||
|
||||
|
||||
# general
|
||||
# General
|
||||
requests
|
||||
aiohttp
|
||||
|
||||
#Rpi
|
||||
# Rpi
|
||||
RPi.GPIO
|
||||
spidev
|
||||
|
||||
@ -22,24 +22,23 @@ https://github.com/alphacep/vosk-api/releases/download/v0.3.42/vosk-0.3.42-py3-n
|
||||
# google-cloud-texttospeech
|
||||
|
||||
# telegram
|
||||
PyTelegramBotApi
|
||||
PyTelegramBotApi # aiogram
|
||||
|
||||
# QA
|
||||
bs4
|
||||
wikipedia
|
||||
|
||||
# Zieit
|
||||
# xlrd
|
||||
# xlwt
|
||||
# xlutils
|
||||
#xlrd
|
||||
#xlwt
|
||||
#xlutils
|
||||
|
||||
|
||||
# Media
|
||||
aiohttp
|
||||
pafy
|
||||
screeninfo
|
||||
psutil
|
||||
yt_dlp #pip install youtube-dl
|
||||
#pafy
|
||||
#screeninfo
|
||||
#psutil
|
||||
#yt_dlp #pip install youtube-dl
|
||||
|
||||
|
||||
# API
|
||||
@ -54,7 +53,10 @@ python-jose
|
||||
bcrypt
|
||||
git+https://github.com/MarkParker5/sqladmin.git # sqladmin
|
||||
|
||||
# Client
|
||||
websocket
|
||||
|
||||
# qrcode
|
||||
|
||||
# QRcode
|
||||
qrcode
|
||||
pillow
|
@ -1,2 +1,2 @@
|
||||
# sudo
|
||||
@reboot python3.10 ~/ArchieHub/SmartHome/Raspberry/WiFi.py
|
||||
@reboot python3.10 ~/ArchieHub/SmartHome/hardware/WiFi.py
|
||||
|
@ -1,7 +1,7 @@
|
||||
import os
|
||||
import config
|
||||
import pathlib
|
||||
from SmartHome.Raspberry import WiFi
|
||||
from SmartHome.hardware import WiFi
|
||||
|
||||
path = str(pathlib.Path(__file__).parent.absolute())
|
||||
src: str = path + '/resources'
|
Loading…
Reference in New Issue
Block a user