diff --git a/.gitignore b/.gitignore index 304d9f5..b43f5ed 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/README.md b/README.md index 689f249..c52040b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/SmartHome/API/__init__.py b/SmartHome/API/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/SmartHome/API/endpoints/__init__.py b/SmartHome/API/endpoints/__init__.py deleted file mode 100644 index 2d361b2..0000000 --- a/SmartHome/API/endpoints/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from . import ( - ws, - house, - hub, - room, - device, - admin -) diff --git a/SmartHome/API/endpoints/device/__init__.py b/SmartHome/API/endpoints/device/__init__.py deleted file mode 100644 index b09b0b6..0000000 --- a/SmartHome/API/endpoints/device/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .DevicesManager import DevicesManager -from .router import router diff --git a/SmartHome/API/endpoints/device/router.py b/SmartHome/API/endpoints/device/router.py deleted file mode 100644 index b3b5d82..0000000 --- a/SmartHome/API/endpoints/device/router.py +++ /dev/null @@ -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) diff --git a/SmartHome/API/endpoints/device/schemas.py b/SmartHome/API/endpoints/device/schemas.py deleted file mode 100644 index cedbe29..0000000 --- a/SmartHome/API/endpoints/device/schemas.py +++ /dev/null @@ -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] = [] diff --git a/SmartHome/API/endpoints/house/HouseManager.py b/SmartHome/API/endpoints/house/HouseManager.py deleted file mode 100644 index c41bff7..0000000 --- a/SmartHome/API/endpoints/house/HouseManager.py +++ /dev/null @@ -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 diff --git a/SmartHome/API/endpoints/house/__init__.py b/SmartHome/API/endpoints/house/__init__.py deleted file mode 100644 index 85e5de3..0000000 --- a/SmartHome/API/endpoints/house/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .HouseManager import HouseManager -from .router import router diff --git a/SmartHome/API/endpoints/house/router.py b/SmartHome/API/endpoints/house/router.py deleted file mode 100644 index 699abda..0000000 --- a/SmartHome/API/endpoints/house/router.py +++ /dev/null @@ -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() diff --git a/SmartHome/API/endpoints/hub/__init__.py b/SmartHome/API/endpoints/hub/__init__.py deleted file mode 100644 index 8d7bf83..0000000 --- a/SmartHome/API/endpoints/hub/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .HubManager import HubManager -from .router import router diff --git a/SmartHome/API/endpoints/hub/router.py b/SmartHome/API/endpoints/hub/router.py deleted file mode 100644 index 8ccc52e..0000000 --- a/SmartHome/API/endpoints/hub/router.py +++ /dev/null @@ -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) diff --git a/SmartHome/API/endpoints/room/__init__.py b/SmartHome/API/endpoints/room/__init__.py deleted file mode 100644 index 96ddeb5..0000000 --- a/SmartHome/API/endpoints/room/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .RoomsManager import RoomsManager -from .router import router diff --git a/SmartHome/API/endpoints/room/router.py b/SmartHome/API/endpoints/room/router.py deleted file mode 100644 index a6fe6c8..0000000 --- a/SmartHome/API/endpoints/room/router.py +++ /dev/null @@ -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) diff --git a/SmartHome/API/endpoints/ws/WSManager.py b/SmartHome/API/endpoints/ws/WSManager.py deleted file mode 100644 index 846f655..0000000 --- a/SmartHome/API/endpoints/ws/WSManager.py +++ /dev/null @@ -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)) diff --git a/SmartHome/API/endpoints/ws/__init__.py b/SmartHome/API/endpoints/ws/__init__.py deleted file mode 100644 index 2378043..0000000 --- a/SmartHome/API/endpoints/ws/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .router import router diff --git a/SmartHome/API/exceptions.py b/SmartHome/API/exceptions.py deleted file mode 100644 index 91a6b99..0000000 --- a/SmartHome/API/exceptions.py +++ /dev/null @@ -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' -) diff --git a/SmartHome/AUID.py b/SmartHome/AUID.py index ca725ab..acfaadf 100644 --- a/SmartHome/AUID.py +++ b/SmartHome/AUID.py @@ -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 diff --git a/SmartHome/client/api.py b/SmartHome/client/api.py new file mode 100644 index 0000000..dee8daa --- /dev/null +++ b/SmartHome/client/api.py @@ -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() diff --git a/SmartHome/client/client.py b/SmartHome/client/client.py new file mode 100644 index 0000000..8ebd4a6 --- /dev/null +++ b/SmartHome/client/client.py @@ -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) diff --git a/SmartHome/client/ws.py b/SmartHome/client/ws.py new file mode 100644 index 0000000..8a87050 --- /dev/null +++ b/SmartHome/client/ws.py @@ -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() diff --git a/SmartHome/config.py b/SmartHome/config.py new file mode 100644 index 0000000..6b96794 --- /dev/null +++ b/SmartHome/config.py @@ -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' diff --git a/SmartHome/API/dependencies/database.py b/SmartHome/database.py similarity index 74% rename from SmartHome/API/dependencies/database.py rename to SmartHome/database.py index 492b5ae..0c731b5 100644 --- a/SmartHome/API/dependencies/database.py +++ b/SmartHome/database.py @@ -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 diff --git a/SmartHome/exceptions/Internal.py b/SmartHome/exceptions/Internal.py new file mode 100644 index 0000000..39ce8bd --- /dev/null +++ b/SmartHome/exceptions/Internal.py @@ -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 diff --git a/SmartHome/Merlin/Merlin.py b/SmartHome/hardware/Merlin/Merlin.py similarity index 93% rename from SmartHome/Merlin/Merlin.py rename to SmartHome/hardware/Merlin/Merlin.py index d24ae25..f555ee9 100644 --- a/SmartHome/Merlin/Merlin.py +++ b/SmartHome/hardware/Merlin/Merlin.py @@ -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() diff --git a/SmartHome/Merlin/MerlinMessage.py b/SmartHome/hardware/Merlin/MerlinMessage.py similarity index 100% rename from SmartHome/Merlin/MerlinMessage.py rename to SmartHome/hardware/Merlin/MerlinMessage.py diff --git a/SmartHome/Merlin/__init__.py b/SmartHome/hardware/Merlin/__init__.py similarity index 60% rename from SmartHome/Merlin/__init__.py rename to SmartHome/hardware/Merlin/__init__.py index 3ca042c..dbb64d2 100644 --- a/SmartHome/Merlin/__init__.py +++ b/SmartHome/hardware/Merlin/__init__.py @@ -1,2 +1,2 @@ -from .Merlin import Merlin +from .Merlin import merlin from .MerlinMessage import MerlinMessage diff --git a/SmartHome/Merlin/lib_nrf24.py b/SmartHome/hardware/Merlin/lib_nrf24.py similarity index 99% rename from SmartHome/Merlin/lib_nrf24.py rename to SmartHome/hardware/Merlin/lib_nrf24.py index 60bd6fc..fafad43 100644 --- a/SmartHome/Merlin/lib_nrf24.py +++ b/SmartHome/hardware/Merlin/lib_nrf24.py @@ -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 ) # ... 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 diff --git a/SmartHome/Raspberry/WiFi.py b/SmartHome/hardware/WiFi.py similarity index 100% rename from SmartHome/Raspberry/WiFi.py rename to SmartHome/hardware/WiFi.py diff --git a/SmartHome/Raspberry/pi_temp.py b/SmartHome/hardware/pi_temp.py similarity index 100% rename from SmartHome/Raspberry/pi_temp.py rename to SmartHome/hardware/pi_temp.py diff --git a/SmartHome/main.py b/SmartHome/main.py index 1b0ba76..b78d8f5 100644 --- a/SmartHome/main.py +++ b/SmartHome/main.py @@ -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() diff --git a/SmartHome/managers/DeviceModelManager.py b/SmartHome/managers/DeviceModelManager.py new file mode 100644 index 0000000..550c09d --- /dev/null +++ b/SmartHome/managers/DeviceModelManager.py @@ -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() diff --git a/SmartHome/API/endpoints/device/DevicesManager.py b/SmartHome/managers/DevicesManager.py similarity index 65% rename from SmartHome/API/endpoints/device/DevicesManager.py rename to SmartHome/managers/DevicesManager.py index 41c1931..fcee435 100644 --- a/SmartHome/API/endpoints/device/DevicesManager.py +++ b/SmartHome/managers/DevicesManager.py @@ -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]: diff --git a/SmartHome/managers/HouseManager.py b/SmartHome/managers/HouseManager.py new file mode 100644 index 0000000..e9bb625 --- /dev/null +++ b/SmartHome/managers/HouseManager.py @@ -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()) + ) diff --git a/SmartHome/API/endpoints/hub/HubManager.py b/SmartHome/managers/HubManager.py similarity index 67% rename from SmartHome/API/endpoints/hub/HubManager.py rename to SmartHome/managers/HubManager.py index a1958da..fd2fe09 100644 --- a/SmartHome/API/endpoints/hub/HubManager.py +++ b/SmartHome/managers/HubManager.py @@ -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) diff --git a/SmartHome/API/endpoints/room/RoomsManager.py b/SmartHome/managers/RoomsManager.py similarity index 58% rename from SmartHome/API/endpoints/room/RoomsManager.py rename to SmartHome/managers/RoomsManager.py index 1c7ebb9..88ec3ca 100644 --- a/SmartHome/API/endpoints/room/RoomsManager.py +++ b/SmartHome/managers/RoomsManager.py @@ -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() diff --git a/SmartHome/managers/WSManager.py b/SmartHome/managers/WSManager.py new file mode 100644 index 0000000..729f351 --- /dev/null +++ b/SmartHome/managers/WSManager.py @@ -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)) diff --git a/SmartHome/managers/__init__.py b/SmartHome/managers/__init__.py new file mode 100644 index 0000000..e2a484d --- /dev/null +++ b/SmartHome/managers/__init__.py @@ -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 diff --git a/SmartHome/API/models/Base.py b/SmartHome/models/Base.py similarity index 100% rename from SmartHome/API/models/Base.py rename to SmartHome/models/Base.py diff --git a/SmartHome/API/models/Device.py b/SmartHome/models/Device.py similarity index 100% rename from SmartHome/API/models/Device.py rename to SmartHome/models/Device.py diff --git a/SmartHome/API/models/DeviceModel.py b/SmartHome/models/DeviceModel.py similarity index 100% rename from SmartHome/API/models/DeviceModel.py rename to SmartHome/models/DeviceModel.py diff --git a/SmartHome/API/models/House.py b/SmartHome/models/House.py similarity index 100% rename from SmartHome/API/models/House.py rename to SmartHome/models/House.py diff --git a/SmartHome/API/models/Hub.py b/SmartHome/models/Hub.py similarity index 100% rename from SmartHome/API/models/Hub.py rename to SmartHome/models/Hub.py diff --git a/SmartHome/API/models/Parameter.py b/SmartHome/models/Parameter.py similarity index 100% rename from SmartHome/API/models/Parameter.py rename to SmartHome/models/Parameter.py diff --git a/SmartHome/API/models/Room.py b/SmartHome/models/Room.py similarity index 100% rename from SmartHome/API/models/Room.py rename to SmartHome/models/Room.py diff --git a/SmartHome/API/models/User.py b/SmartHome/models/User.py similarity index 100% rename from SmartHome/API/models/User.py rename to SmartHome/models/User.py diff --git a/SmartHome/API/models/__init__.py b/SmartHome/models/__init__.py similarity index 100% rename from SmartHome/API/models/__init__.py rename to SmartHome/models/__init__.py diff --git a/SmartHome/schemas/device.py b/SmartHome/schemas/device.py new file mode 100644 index 0000000..166b23a --- /dev/null +++ b/SmartHome/schemas/device.py @@ -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] = [] diff --git a/SmartHome/API/endpoints/house/schemas.py b/SmartHome/schemas/house.py similarity index 62% rename from SmartHome/API/endpoints/house/schemas.py rename to SmartHome/schemas/house.py index 7199841..b1dc0c4 100644 --- a/SmartHome/API/endpoints/house/schemas.py +++ b/SmartHome/schemas/house.py @@ -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] diff --git a/SmartHome/API/endpoints/hub/schemas.py b/SmartHome/schemas/hub.py similarity index 96% rename from SmartHome/API/endpoints/hub/schemas.py rename to SmartHome/schemas/hub.py index 79b04a7..7c91bb8 100644 --- a/SmartHome/API/endpoints/hub/schemas.py +++ b/SmartHome/schemas/hub.py @@ -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 diff --git a/SmartHome/API/endpoints/schemas.py b/SmartHome/schemas/parameters.py similarity index 70% rename from SmartHome/API/endpoints/schemas.py rename to SmartHome/schemas/parameters.py index 59d0bef..4a80bea 100644 --- a/SmartHome/API/endpoints/schemas.py +++ b/SmartHome/schemas/parameters.py @@ -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 diff --git a/SmartHome/API/endpoints/room/schemas.py b/SmartHome/schemas/room.py similarity index 70% rename from SmartHome/API/endpoints/room/schemas.py rename to SmartHome/schemas/room.py index dc66b5c..09d7c09 100644 --- a/SmartHome/API/endpoints/room/schemas.py +++ b/SmartHome/schemas/room.py @@ -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): diff --git a/SmartHome/API/endpoints/ws/schemas.py b/SmartHome/schemas/ws.py similarity index 100% rename from SmartHome/API/endpoints/ws/schemas.py rename to SmartHome/schemas/ws.py diff --git a/SmartHome/API/main.py b/SmartHome/server/__init__.py similarity index 81% rename from SmartHome/API/main.py rename to SmartHome/server/__init__.py index 870b0cf..8da23d2 100644 --- a/SmartHome/API/main.py +++ b/SmartHome/server/__init__.py @@ -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) diff --git a/SmartHome/API/auth/BaseAuth.py b/SmartHome/server/auth/BaseAuth.py similarity index 82% rename from SmartHome/API/auth/BaseAuth.py rename to SmartHome/server/auth/BaseAuth.py index 923ee09..6ca49c4 100644 --- a/SmartHome/API/auth/BaseAuth.py +++ b/SmartHome/server/auth/BaseAuth.py @@ -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() diff --git a/SmartHome/API/auth/UserAuth.py b/SmartHome/server/auth/UserAuth.py similarity index 100% rename from SmartHome/API/auth/UserAuth.py rename to SmartHome/server/auth/UserAuth.py diff --git a/SmartHome/API/auth/__init__.py b/SmartHome/server/auth/__init__.py similarity index 100% rename from SmartHome/API/auth/__init__.py rename to SmartHome/server/auth/__init__.py diff --git a/SmartHome/API/auth/exceptions.py b/SmartHome/server/auth/exceptions.py similarity index 100% rename from SmartHome/API/auth/exceptions.py rename to SmartHome/server/auth/exceptions.py diff --git a/SmartHome/API/dependencies/__init__.py b/SmartHome/server/dependencies/__init__.py similarity index 100% rename from SmartHome/API/dependencies/__init__.py rename to SmartHome/server/dependencies/__init__.py diff --git a/SmartHome/API/dependencies/auth.py b/SmartHome/server/dependencies/auth.py similarity index 58% rename from SmartHome/API/dependencies/auth.py rename to SmartHome/server/dependencies/auth.py index 28d1d09..484bb7f 100644 --- a/SmartHome/API/dependencies/auth.py +++ b/SmartHome/server/dependencies/auth.py @@ -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: diff --git a/SmartHome/server/dependencies/database.py b/SmartHome/server/dependencies/database.py new file mode 100644 index 0000000..053b754 --- /dev/null +++ b/SmartHome/server/dependencies/database.py @@ -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 diff --git a/SmartHome/server/endpoints/__init__.py b/SmartHome/server/endpoints/__init__.py new file mode 100644 index 0000000..cbb7c27 --- /dev/null +++ b/SmartHome/server/endpoints/__init__.py @@ -0,0 +1,6 @@ +from . import admin +from . import device +from . import house +from . import hub +from . import room +from . import ws diff --git a/SmartHome/API/endpoints/admin.py b/SmartHome/server/endpoints/admin.py similarity index 97% rename from SmartHome/API/endpoints/admin.py rename to SmartHome/server/endpoints/admin.py index cb400cf..baaf667 100644 --- a/SmartHome/API/endpoints/admin.py +++ b/SmartHome/server/endpoints/admin.py @@ -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, diff --git a/SmartHome/server/endpoints/device.py b/SmartHome/server/endpoints/device.py new file mode 100644 index 0000000..db8dc1c --- /dev/null +++ b/SmartHome/server/endpoints/device.py @@ -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) diff --git a/SmartHome/server/endpoints/house.py b/SmartHome/server/endpoints/house.py new file mode 100644 index 0000000..177afc2 --- /dev/null +++ b/SmartHome/server/endpoints/house.py @@ -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() diff --git a/SmartHome/server/endpoints/hub.py b/SmartHome/server/endpoints/hub.py new file mode 100644 index 0000000..a13ce87 --- /dev/null +++ b/SmartHome/server/endpoints/hub.py @@ -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) diff --git a/SmartHome/server/endpoints/room.py b/SmartHome/server/endpoints/room.py new file mode 100644 index 0000000..b32dfdc --- /dev/null +++ b/SmartHome/server/endpoints/room.py @@ -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) diff --git a/SmartHome/API/endpoints/ws/router.py b/SmartHome/server/endpoints/ws.py similarity index 61% rename from SmartHome/API/endpoints/ws/router.py rename to SmartHome/server/endpoints/ws.py index e62c3b5..82d63a9 100644 --- a/SmartHome/API/endpoints/ws/router.py +++ b/SmartHome/server/endpoints/ws.py @@ -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( diff --git a/SmartHome/server/exceptions_handlers.py b/SmartHome/server/exceptions_handlers.py new file mode 100644 index 0000000..e25f092 --- /dev/null +++ b/SmartHome/server/exceptions_handlers.py @@ -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) diff --git a/SmartHome/server/http_exceptions.py b/SmartHome/server/http_exceptions.py new file mode 100644 index 0000000..b6ae509 --- /dev/null +++ b/SmartHome/server/http_exceptions.py @@ -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' +# ) +# } diff --git a/SmartHome/API/view/static/css/login.css b/SmartHome/server/view/static/css/login.css similarity index 100% rename from SmartHome/API/view/static/css/login.css rename to SmartHome/server/view/static/css/login.css diff --git a/SmartHome/API/view/static/css/swagger-ui-dark.css b/SmartHome/server/view/static/css/swagger-ui-dark.css similarity index 100% rename from SmartHome/API/view/static/css/swagger-ui-dark.css rename to SmartHome/server/view/static/css/swagger-ui-dark.css diff --git a/SmartHome/API/view/static/js/login.js b/SmartHome/server/view/static/js/login.js similarity index 100% rename from SmartHome/API/view/static/js/login.js rename to SmartHome/server/view/static/js/login.js diff --git a/SmartHome/API/view/templates/login.jinja b/SmartHome/server/view/templates/login.jinja similarity index 100% rename from SmartHome/API/view/templates/login.jinja rename to SmartHome/server/view/templates/login.jinja diff --git a/SmartHome/tests/faker.py b/SmartHome/tests/faker.py index cf45d48..4238a8e 100644 --- a/SmartHome/tests/faker.py +++ b/SmartHome/tests/faker.py @@ -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) diff --git a/SmartHome/tests/setup.py b/SmartHome/tests/setup.py index c0cef97..4261bb6 100644 --- a/SmartHome/tests/setup.py +++ b/SmartHome/tests/setup.py @@ -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') diff --git a/SmartHome/tests/test_endpoints/test_device.py b/SmartHome/tests/test_endpoints/test_device.py index 79f1ce3..2fb462c 100644 --- a/SmartHome/tests/test_endpoints/test_device.py +++ b/SmartHome/tests/test_endpoints/test_device.py @@ -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() diff --git a/SmartHome/tests/test_endpoints/test_hub.py b/SmartHome/tests/test_endpoints/test_hub.py index 3407f97..fa48d60 100644 --- a/SmartHome/tests/test_endpoints/test_hub.py +++ b/SmartHome/tests/test_endpoints/test_hub.py @@ -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(): diff --git a/SmartHome/tests/test_endpoints/test_room.py b/SmartHome/tests/test_endpoints/test_room.py index f43866a..3d705e9 100644 --- a/SmartHome/tests/test_endpoints/test_room.py +++ b/SmartHome/tests/test_endpoints/test_room.py @@ -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) diff --git a/TelegramBot/main.py b/TelegramBot/main.py index 5f7533e..ce4a949 100644 --- a/TelegramBot/main.py +++ b/TelegramBot/main.py @@ -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 diff --git a/VoiceAssistant/Features/Media/YoutubePlayer.py b/VoiceAssistant/Features/Media/YoutubePlayer.py index a15ce55..0a92685 100644 --- a/VoiceAssistant/Features/Media/YoutubePlayer.py +++ b/VoiceAssistant/Features/Media/YoutubePlayer.py @@ -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 diff --git a/VoiceAssistant/main.py b/VoiceAssistant/main.py index ab43160..37880ea 100644 --- a/VoiceAssistant/main.py +++ b/VoiceAssistant/main.py @@ -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 diff --git a/resources/requirments.txt b/requirments.txt similarity index 78% rename from resources/requirments.txt rename to requirments.txt index 8244ca6..51cb5f2 100644 --- a/resources/requirments.txt +++ b/requirments.txt @@ -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 diff --git a/resources/crontab b/resources/crontab index 547acec..c615003 100644 --- a/resources/crontab +++ b/resources/crontab @@ -1,2 +1,2 @@ # sudo -@reboot python3.10 ~/ArchieHub/SmartHome/Raspberry/WiFi.py +@reboot python3.10 ~/ArchieHub/SmartHome/hardware/WiFi.py diff --git a/reset.py b/resources/reset.py similarity index 92% rename from reset.py rename to resources/reset.py index af6a3ee..87a78d2 100644 --- a/reset.py +++ b/resources/reset.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' diff --git a/resources/smarthome.service b/smarthome.service similarity index 100% rename from resources/smarthome.service rename to smarthome.service