1
0
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:
MarkParker5 2022-08-20 10:01:04 +02:00
parent c4abe6008f
commit 40560c25d9
No known key found for this signature in database
GPG Key ID: C87FA4BD47B5A169
86 changed files with 805 additions and 490 deletions

8
.gitignore vendored
View File

@ -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/

View File

@ -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

View File

@ -1,8 +0,0 @@
from . import (
ws,
house,
hub,
room,
device,
admin
)

View File

@ -1,2 +0,0 @@
from .DevicesManager import DevicesManager
from .router import router

View File

@ -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)

View File

@ -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] = []

View File

@ -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

View File

@ -1,2 +0,0 @@
from .HouseManager import HouseManager
from .router import router

View File

@ -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()

View File

@ -1,2 +0,0 @@
from .HubManager import HubManager
from .router import router

View File

@ -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)

View File

@ -1,2 +0,0 @@
from .RoomsManager import RoomsManager
from .router import router

View File

@ -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)

View File

@ -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))

View File

@ -1 +0,0 @@
from .router import router

View File

@ -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'
)

View File

@ -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
View 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()

View 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
View 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
View 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'

View File

@ -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

View 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

View File

@ -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()

View File

@ -1,2 +1,2 @@
from .Merlin import Merlin
from .Merlin import merlin
from .MerlinMessage import MerlinMessage

View File

@ -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

View File

@ -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()

View 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()

View File

@ -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]:

View 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())
)

View File

@ -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)

View File

@ -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()

View 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))

View 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

View 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] = []

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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()

View File

@ -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:

View 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

View File

@ -0,0 +1,6 @@
from . import admin
from . import device
from . import house
from . import hub
from . import room
from . import ws

View File

@ -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,

View 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)

View 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()

View 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)

View 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)

View File

@ -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(

View 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)

View 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'
# )
# }

View File

@ -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)

View File

@ -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')

View File

@ -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()

View File

@ -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():

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,2 +1,2 @@
# sudo
@reboot python3.10 ~/ArchieHub/SmartHome/Raspberry/WiFi.py
@reboot python3.10 ~/ArchieHub/SmartHome/hardware/WiFi.py

View File

@ -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'