mirror of
https://github.com/MarkParker5/STARK.git
synced 2025-02-17 11:55:35 +02:00
structure refactor
This commit is contained in:
parent
3576d793e4
commit
c089d1dd29
5
.gitignore
vendored
5
.gitignore
vendored
@ -10,12 +10,7 @@ __pycache__
|
||||
*.xls
|
||||
*.torrent
|
||||
|
||||
/config.py
|
||||
tts-gc-key.json
|
||||
jwt_access_token
|
||||
jwt_refresh_token
|
||||
jwt.key.pub
|
||||
jwt.key
|
||||
|
||||
audio/
|
||||
downloads/
|
||||
|
@ -2,14 +2,14 @@
|
||||
## Voice assistant, smart home hub, media center and smart tv
|
||||
|
||||
### Project structure:
|
||||
- #### ArchieCore - Core, base classes
|
||||
- #### VICore - Core, base classes
|
||||
- Command
|
||||
- CommandsManager
|
||||
- SearchResult
|
||||
- Response
|
||||
- ThreadData
|
||||
- Pattern
|
||||
- ACObject and subclasses
|
||||
- VIObject and subclasses
|
||||
- #### Controls - Responsible for user interaction
|
||||
- Control(ABC)
|
||||
- VoiceAssistant
|
||||
|
@ -1,55 +0,0 @@
|
||||
from typing import NamedTuple
|
||||
from enum import Enum, IntEnum, auto
|
||||
from uuid import UUID
|
||||
import time
|
||||
import qrcode
|
||||
from qrcode.image.pil import PilImage
|
||||
|
||||
|
||||
__all__ = ['UUID',]
|
||||
|
||||
class URDI(int):
|
||||
def __repr__(self) -> str:
|
||||
return f'URDI({super().__repr__()})'
|
||||
|
||||
class Model(IntEnum):
|
||||
zero = 0
|
||||
hub = 1
|
||||
relay = 2
|
||||
|
||||
class Version(tuple[int, int], Enum):
|
||||
zero = (0, 0)
|
||||
dev = (0, 1)
|
||||
test = (0, 2)
|
||||
prod = (1, 1)
|
||||
|
||||
class UUIDItems(NamedTuple):
|
||||
time: int
|
||||
time_low: int
|
||||
model: Model
|
||||
version: Version
|
||||
urdi: URDI
|
||||
|
||||
class AUID(UUID):
|
||||
|
||||
@classmethod
|
||||
def new(cls, model: Model = None, version: Version = None, urdi: int = None, now=time.time()) -> 'UUID':
|
||||
now = now or 0
|
||||
model = model or Model.zero
|
||||
version = version or Version.zero
|
||||
urdi = urdi or 0
|
||||
now = now or 0
|
||||
|
||||
time = int(now)
|
||||
time_low = int((now % 1) * 255 * 2)
|
||||
|
||||
return cls(fields=(time, time_low, model.value, *version.value, urdi))
|
||||
|
||||
@property
|
||||
def items(self) -> UUIDItems:
|
||||
time, time_low, model, *ver, urdi = self.fields
|
||||
return UUIDItems(time, time_low, Model(model), Version(tuple(ver)), URDI(urdi))
|
||||
|
||||
@property
|
||||
def qrcode(self) -> PilImage:
|
||||
return qrcode.make(str(self))
|
@ -1,37 +0,0 @@
|
||||
from typing import Type
|
||||
from aiohttp import ClientSession
|
||||
from pydantic import BaseModel
|
||||
from pydantic.error_wrappers import ValidationError
|
||||
from schemas.ws import RestRequest
|
||||
import config
|
||||
|
||||
|
||||
headers = {'Authorization': f'Bearer {config.access_token}'}
|
||||
client_session: ClientSession = None
|
||||
|
||||
async def start_client():
|
||||
global client_session
|
||||
if client_session:
|
||||
client_session.close()
|
||||
client_session = ClientSession()
|
||||
|
||||
async def get(url: str,
|
||||
model: Type[BaseModel]) -> BaseModel | None:
|
||||
try:
|
||||
async with client_session.get(f'{config.api_url}/{url}') as response:
|
||||
text = await response.text()
|
||||
return model.parse_raw(text)
|
||||
except ValidationError:
|
||||
print(f'\nAPI ValidationError Error: {url}\nModel: {model}\n{text}\n')
|
||||
return None
|
||||
|
||||
async def local_request(request: RestRequest) -> str:
|
||||
method = getattr(client_session, request.method)
|
||||
|
||||
url = f'{config.localhost}/{request.path}'
|
||||
json = request.body
|
||||
headers = {}
|
||||
|
||||
async with method(url, json = json, headers = headers) as r:
|
||||
if 200 <= response.status < 300:
|
||||
return await response.text()
|
@ -1,46 +0,0 @@
|
||||
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():
|
||||
await api.start_client()
|
||||
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)
|
@ -1,40 +0,0 @@
|
||||
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 = {'Authorization': f'Bearer {config.access_token}'}
|
||||
|
||||
def on_message(ws, msg):
|
||||
if response := ws_manager.handle_message(msg):
|
||||
ws.send(response)
|
||||
|
||||
def on_open(ws):
|
||||
pass
|
||||
|
||||
def on_close(ws, status_code, reason):
|
||||
start()
|
||||
|
||||
def on_error(ws, error):
|
||||
pass # TODO: log
|
||||
|
||||
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()
|
@ -1,43 +0,0 @@
|
||||
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 = 'https://home.parker-programs.com/api'
|
||||
ws_url = 'wss://home.parker-programs.com/ws/hub'
|
||||
localhost = 'http://localhost:8000'
|
||||
|
||||
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'
|
@ -1,27 +0,0 @@
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
import config
|
||||
|
||||
|
||||
# sync
|
||||
|
||||
engine = create_engine(
|
||||
config.db_url, connect_args={'check_same_thread': False}
|
||||
)
|
||||
|
||||
create_session = sessionmaker(
|
||||
autocommit=False, autoflush=False, bind=engine
|
||||
)
|
||||
|
||||
# async
|
||||
|
||||
async_engine = create_async_engine(
|
||||
config.db_async_url, connect_args={'check_same_thread': False}
|
||||
)
|
||||
|
||||
create_async_session = sessionmaker(
|
||||
async_engine, class_ = AsyncSession, expire_on_commit = False
|
||||
)
|
@ -1,19 +0,0 @@
|
||||
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
|
@ -1,77 +0,0 @@
|
||||
from typing import List
|
||||
from threading import Thread
|
||||
|
||||
try:
|
||||
import RPi.GPIO as GPIO
|
||||
import spidev
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
is_rpi = True
|
||||
except ModuleNotFoundError:
|
||||
is_rpi = False
|
||||
|
||||
from .MerlinMessage import MerlinMessage
|
||||
from .lib_nrf24 import NRF24
|
||||
|
||||
|
||||
class Merlin():
|
||||
radio: NRF24
|
||||
send_queue: List[MerlinMessage] = []
|
||||
my_urdi = [0xff, 0x00, 0x00, 0x00, 0x01]
|
||||
|
||||
def __init__(self):
|
||||
if not is_rpi: return
|
||||
|
||||
radio = NRF24(GPIO, spidev.SpiDev())
|
||||
radio.begin(0, 17)
|
||||
radio.setPALevel(NRF24.PA_HIGH)
|
||||
radio.setDataRate(NRF24.BR_250KBPS)
|
||||
radio.setChannel(0x60)
|
||||
radio.setPayloadSize(2)
|
||||
radio.setAutoAck(True)
|
||||
radio.setRetries(10, 20)
|
||||
#radio.setAddressWidth(4)
|
||||
radio.enableAckPayload()
|
||||
radio.openReadingPipe(1, self.my_urdi)
|
||||
|
||||
radio.startListening()
|
||||
radio.stopListening()
|
||||
|
||||
radio.startListening()
|
||||
|
||||
self.radio = radio
|
||||
|
||||
def send(self, message: MerlinMessage):
|
||||
self.send_queue.append(message)
|
||||
|
||||
def _send(self, message: MerlinMessage):
|
||||
if not is_rpi:
|
||||
# TODO: Log
|
||||
print(message.urdi, message.data)
|
||||
return
|
||||
self.radio.stopListening()
|
||||
pipe = [0xFF] + list(message.urdi.to_bytes(4, 'big'))
|
||||
self.radio.openWritingPipe(pipe)
|
||||
self.radio.write(message.data)
|
||||
self.radio.startListening()
|
||||
|
||||
def receiveAndTransmit(self):
|
||||
while True:
|
||||
# send messages from queue
|
||||
while self.send_queue and (message := self.send_queue.pop()):
|
||||
self._send(message)
|
||||
|
||||
if not is_rpi: return
|
||||
|
||||
# receiving messages
|
||||
if not self.radio.available():
|
||||
continue
|
||||
|
||||
rawData = []
|
||||
self.radio.read(rawData, self.radio.getPayloadSize())
|
||||
func, arg = rawData
|
||||
print(f'Received {func=} {arg=}') # TODO: Log
|
||||
|
||||
merlin = Merlin()
|
||||
|
||||
_receiveAndTransmitThread = Thread(target = Merlin().receiveAndTransmit)
|
||||
_receiveAndTransmitThread.start()
|
@ -1,15 +0,0 @@
|
||||
from typing import List
|
||||
|
||||
class MerlinMessage:
|
||||
urdi: int
|
||||
func: int # unsigned 1 byte int 0...255
|
||||
arg: int # unsigned 1 byte int 0...255
|
||||
|
||||
def __init__(self, urdi, func, arg):
|
||||
self.urdi = urdi
|
||||
self.func = func
|
||||
self.arg = arg
|
||||
|
||||
@property
|
||||
def data(self) -> List[int]:
|
||||
return [self.func, self.arg]
|
@ -1,2 +0,0 @@
|
||||
from .Merlin import merlin
|
||||
from .MerlinMessage import MerlinMessage
|
@ -1,788 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
|
||||
# This file lib_nrf24.py is a slightly tweaked version of Barraca's "pynrf24".
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
if __name__ == '__main__':
|
||||
print (sys.argv[0], 'is an importable module:')
|
||||
print ("... from", sys.argv[0], "import lib_nrf24")
|
||||
print ("")
|
||||
|
||||
exit()
|
||||
|
||||
def _BV(x):
|
||||
return 1 << x
|
||||
|
||||
|
||||
class NRF24:
|
||||
MAX_CHANNEL = 127
|
||||
MAX_PAYLOAD_SIZE = 32
|
||||
|
||||
# PA Levels
|
||||
PA_MIN = 0
|
||||
PA_LOW = 1
|
||||
PA_HIGH = 2
|
||||
PA_MAX = 3
|
||||
PA_ERROR = 4
|
||||
|
||||
# Bit rates
|
||||
BR_1MBPS = 0
|
||||
BR_2MBPS = 1
|
||||
BR_250KBPS = 2
|
||||
|
||||
# CRC
|
||||
CRC_DISABLED = 0
|
||||
CRC_8 = 1
|
||||
CRC_16 = 2
|
||||
CRC_ENABLED = 3
|
||||
|
||||
# Registers
|
||||
CONFIG = 0x00
|
||||
EN_AA = 0x01
|
||||
EN_RXADDR = 0x02
|
||||
SETUP_AW = 0x03
|
||||
SETUP_RETR = 0x04
|
||||
RF_CH = 0x05
|
||||
RF_SETUP = 0x06
|
||||
STATUS = 0x07
|
||||
OBSERVE_TX = 0x08
|
||||
CD = 0x09
|
||||
RX_ADDR_P0 = 0x0A
|
||||
RX_ADDR_P1 = 0x0B
|
||||
RX_ADDR_P2 = 0x0C
|
||||
RX_ADDR_P3 = 0x0D
|
||||
RX_ADDR_P4 = 0x0E
|
||||
RX_ADDR_P5 = 0x0F
|
||||
TX_ADDR = 0x10
|
||||
RX_PW_P0 = 0x11
|
||||
RX_PW_P1 = 0x12
|
||||
RX_PW_P2 = 0x13
|
||||
RX_PW_P3 = 0x14
|
||||
RX_PW_P4 = 0x15
|
||||
RX_PW_P5 = 0x16
|
||||
FIFO_STATUS = 0x17
|
||||
DYNPD = 0x1C
|
||||
FEATURE = 0x1D
|
||||
|
||||
|
||||
# Bit Mnemonics */
|
||||
MASK_RX_DR = 6
|
||||
MASK_TX_DS = 5
|
||||
MASK_MAX_RT = 4
|
||||
EN_CRC = 3
|
||||
CRCO = 2
|
||||
PWR_UP = 1
|
||||
PRIM_RX = 0
|
||||
ENAA_P5 = 5
|
||||
ENAA_P4 = 4
|
||||
ENAA_P3 = 3
|
||||
ENAA_P2 = 2
|
||||
ENAA_P1 = 1
|
||||
ENAA_P0 = 0
|
||||
ERX_P5 = 5
|
||||
ERX_P4 = 4
|
||||
ERX_P3 = 3
|
||||
ERX_P2 = 2
|
||||
ERX_P1 = 1
|
||||
ERX_P0 = 0
|
||||
AW = 0
|
||||
ARD = 4
|
||||
ARC = 0
|
||||
PLL_LOCK = 4
|
||||
RF_DR = 3
|
||||
RF_PWR = 6
|
||||
RX_DR = 6
|
||||
TX_DS = 5
|
||||
MAX_RT = 4
|
||||
RX_P_NO = 1
|
||||
TX_FULL = 0
|
||||
PLOS_CNT = 4
|
||||
ARC_CNT = 0
|
||||
TX_REUSE = 6
|
||||
FIFO_FULL = 5
|
||||
TX_EMPTY = 4
|
||||
RX_FULL = 1
|
||||
RX_EMPTY = 0
|
||||
DPL_P5 = 5
|
||||
DPL_P4 = 4
|
||||
DPL_P3 = 3
|
||||
DPL_P2 = 2
|
||||
DPL_P1 = 1
|
||||
DPL_P0 = 0
|
||||
EN_DPL = 2
|
||||
EN_ACK_PAY = 1
|
||||
EN_DYN_ACK = 0
|
||||
|
||||
# Instruction Mnemonics
|
||||
R_REGISTER = 0x00
|
||||
W_REGISTER = 0x20
|
||||
REGISTER_MASK = 0x1F
|
||||
ACTIVATE = 0x50
|
||||
R_RX_PL_WID = 0x60
|
||||
R_RX_PAYLOAD = 0x61
|
||||
W_TX_PAYLOAD = 0xA0
|
||||
W_ACK_PAYLOAD = 0xA8
|
||||
FLUSH_TX = 0xE1
|
||||
FLUSH_RX = 0xE2
|
||||
REUSE_TX_PL = 0xE3
|
||||
NOP = 0xFF
|
||||
|
||||
|
||||
# Non-P omissions
|
||||
LNA_HCURR = 0x00
|
||||
|
||||
# P model memory Map
|
||||
RPD = 0x09
|
||||
|
||||
# P model bit Mnemonics
|
||||
RF_DR_LOW = 5
|
||||
RF_DR_HIGH = 3
|
||||
RF_PWR_LOW = 1
|
||||
RF_PWR_HIGH = 2
|
||||
|
||||
# Signal Mnemonics
|
||||
LOW = 0
|
||||
HIGH = 1
|
||||
|
||||
datarate_e_str_P = ["1MBPS", "2MBPS", "250KBPS"]
|
||||
model_e_str_P = ["nRF24L01", "nRF24l01+"]
|
||||
crclength_e_str_P = ["Disabled", "8 bits", "16 bits"]
|
||||
pa_dbm_e_str_P = ["PA_MIN", "PA_LOW", "PA_MED", "PA_HIGH"]
|
||||
child_pipe = [RX_ADDR_P0, RX_ADDR_P1, RX_ADDR_P2, RX_ADDR_P3, RX_ADDR_P4, RX_ADDR_P5]
|
||||
|
||||
child_payload_size = [RX_PW_P0, RX_PW_P1, RX_PW_P2, RX_PW_P3, RX_PW_P4, RX_PW_P5]
|
||||
child_pipe_enable = [ERX_P0, ERX_P1, ERX_P2, ERX_P3, ERX_P4, ERX_P5]
|
||||
|
||||
GPIO = None
|
||||
spidev = None
|
||||
|
||||
def __init__(self, gpio, spidev):
|
||||
# It should be possible to instantiate multiple objects, with different GPIO / spidev
|
||||
# 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
|
||||
self.channel = 76
|
||||
self.data_rate = NRF24.BR_1MBPS
|
||||
self.wide_band = False # 2Mbs data rate in use?
|
||||
self.p_variant = False # False for RF24L01 and true for RF24L01P (nrf24l01+)
|
||||
self.payload_size = 5 #*< Fixed size of payloads
|
||||
self.ack_payload_available = False #*< Whether there is an ack payload waiting
|
||||
self.dynamic_payloads_enabled = False #*< Whether dynamic payloads are enabled.
|
||||
self.ack_payload_length = 5 #*< Dynamic size of pending ack payload.
|
||||
self.pipe0_reading_address = None #*< Last address set on pipe 0 for reading.
|
||||
|
||||
def ce(self, level):
|
||||
if self.ce_pin == 0:
|
||||
return
|
||||
# rf24-CE is optional. Tie to HIGH if not used. (Altho, left floating seems to read HIGH anyway??? - risky!)
|
||||
# Some RF24 modes may NEED control over CE.
|
||||
# non-powerdown, fixed PTX or RTX role, dynamic payload size & ack-payload: does NOT need CE.
|
||||
if level == NRF24.HIGH:
|
||||
self.GPIO.output(self.ce_pin, self.GPIO.HIGH)
|
||||
else:
|
||||
self.GPIO.output(self.ce_pin, self.GPIO.LOW)
|
||||
return
|
||||
|
||||
|
||||
|
||||
def read_register(self, reg, blen=1):
|
||||
buf = [NRF24.R_REGISTER | ( NRF24.REGISTER_MASK & reg )]
|
||||
for col in range(blen):
|
||||
buf.append(NRF24.NOP)
|
||||
|
||||
resp = self.spidev.xfer2(buf)
|
||||
if blen == 1:
|
||||
return resp[1]
|
||||
|
||||
return resp[1:blen + 1]
|
||||
|
||||
def write_register(self, reg, value, length=-1):
|
||||
buf = [NRF24.W_REGISTER | ( NRF24.REGISTER_MASK & reg )]
|
||||
###if isinstance(value, (int, long)): # ng for python3. but value should never be long anyway
|
||||
if isinstance(value, int):
|
||||
if length < 0:
|
||||
length = 1
|
||||
|
||||
length = min(4, length)
|
||||
for i in range(length):
|
||||
buf.insert(1, int(value & 0xff))
|
||||
value >>= 8
|
||||
|
||||
elif isinstance(value, list):
|
||||
if length < 0:
|
||||
length = len(value)
|
||||
|
||||
for i in range(min(len(value), length)):
|
||||
buf.append(int(value[len(value) - i - 1] & 0xff))
|
||||
else:
|
||||
raise Exception("Value must be int or list")
|
||||
|
||||
return self.spidev.xfer2(buf)[0]
|
||||
|
||||
|
||||
def write_payload(self, buf):
|
||||
data_len = min(self.payload_size, len(buf))
|
||||
blank_len = 0
|
||||
if not self.dynamic_payloads_enabled:
|
||||
blank_len = self.payload_size - data_len
|
||||
|
||||
txbuffer = [NRF24.W_TX_PAYLOAD]
|
||||
for n in buf:
|
||||
t = type(n)
|
||||
if t is str:
|
||||
txbuffer.append(ord(n))
|
||||
elif t is int:
|
||||
txbuffer.append(n)
|
||||
else:
|
||||
raise Exception("Only ints and chars are supported: Found " + str(t))
|
||||
|
||||
if blank_len != 0:
|
||||
blank = [0x00 for i in range(blank_len)]
|
||||
txbuffer.extend(blank)
|
||||
|
||||
return self.spidev.xfer2(txbuffer)
|
||||
|
||||
def read_payload(self, buf, buf_len=-1):
|
||||
if buf_len < 0:
|
||||
buf_len = self.payload_size
|
||||
data_len = min(self.payload_size, buf_len)
|
||||
blank_len = 0
|
||||
if not self.dynamic_payloads_enabled:
|
||||
blank_len = self.payload_size - data_len
|
||||
|
||||
txbuffer = [NRF24.NOP for i in range(0, blank_len + data_len + 1)]
|
||||
txbuffer[0] = NRF24.R_RX_PAYLOAD
|
||||
|
||||
payload = self.spidev.xfer2(txbuffer)
|
||||
del buf[:]
|
||||
buf.extend(payload[1:data_len + 1])
|
||||
return data_len
|
||||
|
||||
def flush_rx(self):
|
||||
return self.spidev.xfer2([NRF24.FLUSH_RX])[0]
|
||||
|
||||
def flush_tx(self):
|
||||
return self.spidev.xfer2([NRF24.FLUSH_TX])[0]
|
||||
|
||||
def get_status(self):
|
||||
return self.spidev.xfer2([NRF24.NOP])[0]
|
||||
|
||||
def print_status(self, status):
|
||||
status_str = "STATUS\t = 0x{0:02x} RX_DR={1:x} TX_DS={2:x} MAX_RT={3:x} RX_P_NO={4:x} TX_FULL={5:x}".format(
|
||||
status,
|
||||
1 if status & _BV(NRF24.RX_DR) else 0,
|
||||
1 if status & _BV(NRF24.TX_DS) else 0,
|
||||
1 if status & _BV(NRF24.MAX_RT) else 0,
|
||||
((status >> NRF24.RX_P_NO) & 7),
|
||||
1 if status & _BV(NRF24.TX_FULL) else 0)
|
||||
|
||||
print (status_str)
|
||||
|
||||
def print_observe_tx(self, value):
|
||||
print ("Observe Tx: %02x Lost Pkts: %d Retries: %d" % (value, value >> NRF24.PLOS_CNT, value & 15))
|
||||
|
||||
|
||||
def print_byte_register(self, name, reg, qty=1):
|
||||
extra_tab = '\t' if len(name) < 8 else 0
|
||||
print ("%s\t%c =" % (name, extra_tab)),
|
||||
while qty > 0:
|
||||
print ("0x%02x" % (self.read_register(reg))),
|
||||
qty -= 1
|
||||
reg += 1
|
||||
|
||||
print ("")
|
||||
|
||||
def print_address_register(self, name, reg, qty=1):
|
||||
extra_tab = '\t' if len(name) < 8 else 0
|
||||
print ("%s\t%c =" % (name, extra_tab)),
|
||||
|
||||
while qty > 0:
|
||||
qty -= 1
|
||||
buf = reversed(self.read_register(reg, 5))
|
||||
reg += 1
|
||||
sys.stdout.write(" 0x"),
|
||||
for i in buf:
|
||||
sys.stdout.write("%02x" % i)
|
||||
|
||||
print ("")
|
||||
|
||||
|
||||
def setChannel(self, channel):
|
||||
self.channel = min(max(0, channel), NRF24.MAX_CHANNEL)
|
||||
self.write_register(NRF24.RF_CH, self.channel)
|
||||
|
||||
def getChannel(self):
|
||||
return self.read_register(NRF24.RF_CH)
|
||||
|
||||
def setPayloadSize(self, size):
|
||||
self.payload_size = min(max(size, 1), NRF24.MAX_PAYLOAD_SIZE)
|
||||
|
||||
def getPayloadSize(self):
|
||||
return self.payload_size
|
||||
|
||||
def printDetails(self):
|
||||
self.print_status(self.get_status())
|
||||
self.print_address_register("RX_ADDR_P0-1", NRF24.RX_ADDR_P0, 2)
|
||||
self.print_byte_register("RX_ADDR_P2-5", NRF24.RX_ADDR_P2, 4)
|
||||
self.print_address_register("TX_ADDR", NRF24.TX_ADDR)
|
||||
|
||||
self.print_byte_register("RX_PW_P0-6", NRF24.RX_PW_P0, 6)
|
||||
self.print_byte_register("EN_AA", NRF24.EN_AA)
|
||||
self.print_byte_register("EN_RXADDR", NRF24.EN_RXADDR)
|
||||
self.print_byte_register("RF_CH", NRF24.RF_CH)
|
||||
self.print_byte_register("RF_SETUP", NRF24.RF_SETUP)
|
||||
self.print_byte_register("CONFIG", NRF24.CONFIG)
|
||||
self.print_byte_register("DYNPD/FEATURE", NRF24.DYNPD, 2)
|
||||
|
||||
#
|
||||
print ("Data Rate\t = %s" % NRF24.datarate_e_str_P[self.getDataRate()])
|
||||
print ("Model\t\t = %s" % NRF24.model_e_str_P[self.isPVariant()])
|
||||
print ("CRC Length\t = %s" % NRF24.crclength_e_str_P[self.getCRCLength()])
|
||||
print ("PA Power\t = %s" % NRF24.pa_dbm_e_str_P[self.getPALevel()])
|
||||
|
||||
def begin(self, csn_pin, ce_pin=0): # csn & ce are RF24 terminology. csn = SPI's CE!
|
||||
# Initialize SPI bus..
|
||||
# ce_pin is for the rx=listen or tx=trigger pin on RF24 (they call that ce !!!)
|
||||
# CE optional (at least in some circumstances, eg fixed PTX PRX roles, no powerdown)
|
||||
# CE seems to hold itself as (sufficiently) HIGH, but tie HIGH is safer!
|
||||
self.spidev.open(0, csn_pin)
|
||||
self.spidev.max_speed_hz = 4000000
|
||||
self.ce_pin = ce_pin
|
||||
|
||||
if ce_pin:
|
||||
self.GPIO.setup(self.ce_pin, self.GPIO.OUT)
|
||||
|
||||
time.sleep(5 / 1000000.0)
|
||||
|
||||
# Set 1500uS (minimum for 32B payload in ESB@250KBPS) timeouts, to make testing a little easier
|
||||
# WARNING: If this is ever lowered, either 250KBS mode with AA is broken or maximum packet
|
||||
# sizes must never be used. See documentation for a more complete explanation.
|
||||
self.write_register(NRF24.SETUP_RETR, (0b0100 << NRF24.ARD) | 0b1111)
|
||||
|
||||
# Restore our default PA level
|
||||
self.setPALevel(NRF24.PA_MAX)
|
||||
|
||||
# Determine if this is a p or non-p RF24 module and then
|
||||
# reset our data rate back to default value. This works
|
||||
# because a non-P variant won't allow the data rate to
|
||||
# be set to 250Kbps.
|
||||
if self.setDataRate(NRF24.BR_250KBPS):
|
||||
self.p_variant = True
|
||||
|
||||
# Then set the data rate to the slowest (and most reliable) speed supported by all
|
||||
# hardware.
|
||||
self.setDataRate(NRF24.BR_1MBPS)
|
||||
|
||||
# Initialize CRC and request 2-byte (16bit) CRC
|
||||
self.setCRCLength(NRF24.CRC_16)
|
||||
|
||||
# Disable dynamic payloads, to match dynamic_payloads_enabled setting
|
||||
self.write_register(NRF24.DYNPD, 0)
|
||||
|
||||
# Reset current status
|
||||
# Notice reset and flush is the last thing we do
|
||||
self.write_register(NRF24.STATUS, _BV(NRF24.RX_DR) | _BV(NRF24.TX_DS) | _BV(NRF24.MAX_RT))
|
||||
|
||||
# Set up default configuration. Callers can always change it later.
|
||||
# This channel should be universally safe and not bleed over into adjacent
|
||||
# spectrum.
|
||||
self.setChannel(self.channel)
|
||||
|
||||
# Flush buffers
|
||||
self.flush_rx()
|
||||
self.flush_tx()
|
||||
|
||||
def end(self):
|
||||
if self.spidev:
|
||||
self.spidev.close()
|
||||
self.spidev = None
|
||||
|
||||
def startListening(self):
|
||||
self.write_register(NRF24.CONFIG, self.read_register(NRF24.CONFIG) | _BV(NRF24.PWR_UP) | _BV(NRF24.PRIM_RX))
|
||||
self.write_register(NRF24.STATUS, _BV(NRF24.RX_DR) | _BV(NRF24.TX_DS) | _BV(NRF24.MAX_RT))
|
||||
|
||||
# Restore the pipe0 address, if exists
|
||||
if self.pipe0_reading_address:
|
||||
self.write_register(self.RX_ADDR_P0, self.pipe0_reading_address, 5)
|
||||
|
||||
# Go!
|
||||
self.ce(NRF24.HIGH)
|
||||
|
||||
# wait for the radio to come up (130us actually only needed)
|
||||
time.sleep(130 / 1000000.0)
|
||||
|
||||
def stopListening(self):
|
||||
self.ce(NRF24.LOW)
|
||||
self.flush_tx()
|
||||
self.flush_rx()
|
||||
|
||||
def powerDown(self):
|
||||
self.write_register(NRF24.CONFIG, self.read_register(NRF24.CONFIG) & ~_BV(NRF24.PWR_UP))
|
||||
|
||||
def powerUp(self):
|
||||
self.write_register(NRF24.CONFIG, self.read_register(NRF24.CONFIG) | _BV(NRF24.PWR_UP))
|
||||
time.sleep(150 / 1000000.0)
|
||||
|
||||
def write(self, buf):
|
||||
# Begin the write
|
||||
self.startWrite(buf)
|
||||
|
||||
timeout = self.getMaxTimeout() #s to wait for timeout
|
||||
sent_at = time.time()
|
||||
|
||||
while True:
|
||||
#status = self.read_register(NRF24.OBSERVE_TX, 1)
|
||||
status = self.get_status()
|
||||
if (status & (_BV(NRF24.TX_DS) | _BV(NRF24.MAX_RT))) or (time.time() - sent_at > timeout ):
|
||||
break
|
||||
time.sleep(10 / 1000000.0)
|
||||
#obs = self.read_register(NRF24.OBSERVE_TX)
|
||||
#self.print_observe_tx(obs)
|
||||
#self.print_status(status)
|
||||
# (for debugging)
|
||||
|
||||
what = self.whatHappened()
|
||||
|
||||
result = what['tx_ok']
|
||||
if what['tx_fail']:
|
||||
self.flush_tx(); # bl - dont jam up the fifo
|
||||
# Handle the ack packet
|
||||
if what['rx_ready']:
|
||||
self.ack_payload_length = self.getDynamicPayloadSize()
|
||||
self.ack_payload_available = True ## bl
|
||||
|
||||
return result
|
||||
|
||||
def startWrite(self, buf):
|
||||
# Transmitter power-up
|
||||
self.write_register(NRF24.CONFIG, (self.read_register(NRF24.CONFIG) | _BV(NRF24.PWR_UP) ) & ~_BV(NRF24.PRIM_RX))
|
||||
|
||||
# Send the payload
|
||||
self.write_payload(buf)
|
||||
|
||||
# Allons!
|
||||
if self.ce_pin:
|
||||
if self.GPIO.RPI_REVISION > 0:
|
||||
self.ce(self.GPIO.HIGH)
|
||||
time.sleep(10 / 1000000.0)
|
||||
self.ce(self.GPIO.LOW)
|
||||
else:
|
||||
# virtGPIO is slower. A 10 uSec pulse is better done with pulseOut():
|
||||
self.GPIO.pulseOut(self.ce_pin, self.GPIO.HIGH, 10)
|
||||
|
||||
|
||||
|
||||
def getDynamicPayloadSize(self):
|
||||
return self.spidev.xfer2([NRF24.R_RX_PL_WID, NRF24.NOP])[1]
|
||||
|
||||
def available(self, pipe_num=None):
|
||||
if not pipe_num:
|
||||
pipe_num = []
|
||||
|
||||
status = self.get_status()
|
||||
result = False
|
||||
|
||||
# Sometimes the radio specifies that there is data in one pipe but
|
||||
# doesn't set the RX flag...
|
||||
if status & _BV(NRF24.RX_DR) or (status & 0b00001110 != 0b00001110):
|
||||
result = True
|
||||
|
||||
if result:
|
||||
# If the caller wants the pipe number, include that
|
||||
if len(pipe_num) >= 1:
|
||||
pipe_num[0] = ( status >> NRF24.RX_P_NO ) & 0b00000111
|
||||
|
||||
# Clear the status bit
|
||||
|
||||
# ??? Should this REALLY be cleared now? Or wait until we
|
||||
# actually READ the payload?
|
||||
self.write_register(NRF24.STATUS, _BV(NRF24.RX_DR))
|
||||
|
||||
# Handle ack payload receipt
|
||||
if status & _BV(NRF24.TX_DS):
|
||||
self.write_register(NRF24.STATUS, _BV(NRF24.TX_DS))
|
||||
|
||||
return result
|
||||
|
||||
def read(self, buf, buf_len=-1):
|
||||
# Fetch the payload
|
||||
self.read_payload(buf, buf_len)
|
||||
|
||||
# was this the last of the data available?
|
||||
return self.read_register(NRF24.FIFO_STATUS) & _BV(NRF24.RX_EMPTY)
|
||||
|
||||
def whatHappened(self):
|
||||
# Read the status & reset the status in one easy call
|
||||
# Or is that such a good idea?
|
||||
status = self.write_register(NRF24.STATUS, _BV(NRF24.RX_DR) | _BV(NRF24.TX_DS) | _BV(NRF24.MAX_RT))
|
||||
|
||||
# Report to the user what happened
|
||||
tx_ok = status & _BV(NRF24.TX_DS)
|
||||
tx_fail = status & _BV(NRF24.MAX_RT)
|
||||
rx_ready = status & _BV(NRF24.RX_DR)
|
||||
return {'tx_ok': tx_ok, "tx_fail": tx_fail, "rx_ready": rx_ready}
|
||||
|
||||
def openWritingPipe(self, value):
|
||||
# Note that the NRF24L01(+)
|
||||
# expects it LSB first.
|
||||
|
||||
self.write_register(NRF24.RX_ADDR_P0, value, 5)
|
||||
self.write_register(NRF24.TX_ADDR, value, 5)
|
||||
|
||||
max_payload_size = 32
|
||||
self.write_register(NRF24.RX_PW_P0, min(self.payload_size, max_payload_size))
|
||||
|
||||
def openReadingPipe(self, child, address):
|
||||
# If this is pipe 0, cache the address. This is needed because
|
||||
# openWritingPipe() will overwrite the pipe 0 address, so
|
||||
# startListening() will have to restore it.
|
||||
if child == 0:
|
||||
self.pipe0_reading_address = address
|
||||
|
||||
if child <= 6:
|
||||
# For pipes 2-5, only write the LSB
|
||||
if child < 2:
|
||||
self.write_register(NRF24.child_pipe[child], address, 5)
|
||||
else:
|
||||
self.write_register(NRF24.child_pipe[child], address, 1)
|
||||
|
||||
self.write_register(NRF24.child_payload_size[child], self.payload_size)
|
||||
|
||||
# Note it would be more efficient to set all of the bits for all open
|
||||
# pipes at once. However, I thought it would make the calling code
|
||||
# more simple to do it this way.
|
||||
self.write_register(NRF24.EN_RXADDR,
|
||||
self.read_register(NRF24.EN_RXADDR) | _BV(NRF24.child_pipe_enable[child]))
|
||||
|
||||
|
||||
def closeReadingPipe(self, pipe):
|
||||
self.write_register(NRF24.EN_RXADDR,
|
||||
self.read_register(EN_RXADDR) & ~_BV(NRF24.child_pipe_enable[pipe]))
|
||||
|
||||
|
||||
def toggle_features(self):
|
||||
buf = [NRF24.ACTIVATE, 0x73]
|
||||
self.spidev.xfer2(buf)
|
||||
|
||||
def enableDynamicPayloads(self):
|
||||
# Enable dynamic payload throughout the system
|
||||
self.write_register(NRF24.FEATURE, self.read_register(NRF24.FEATURE) | _BV(NRF24.EN_DPL))
|
||||
|
||||
# If it didn't work, the features are not enabled
|
||||
if not self.read_register(NRF24.FEATURE):
|
||||
# So enable them and try again
|
||||
self.toggle_features()
|
||||
self.write_register(NRF24.FEATURE, self.read_register(NRF24.FEATURE) | _BV(NRF24.EN_DPL))
|
||||
|
||||
# Enable dynamic payload on all pipes
|
||||
|
||||
# Not sure the use case of only having dynamic payload on certain
|
||||
# pipes, so the library does not support it.
|
||||
self.write_register(NRF24.DYNPD, self.read_register(NRF24.DYNPD) | _BV(NRF24.DPL_P5) | _BV(NRF24.DPL_P4) | _BV(
|
||||
NRF24.DPL_P3) | _BV(NRF24.DPL_P2) | _BV(NRF24.DPL_P1) | _BV(NRF24.DPL_P0))
|
||||
|
||||
self.dynamic_payloads_enabled = True
|
||||
|
||||
|
||||
def enableAckPayload(self):
|
||||
# enable ack payload and dynamic payload features
|
||||
self.write_register(NRF24.FEATURE,
|
||||
self.read_register(NRF24.FEATURE) | _BV(NRF24.EN_ACK_PAY) | _BV(NRF24.EN_DPL))
|
||||
|
||||
# If it didn't work, the features are not enabled
|
||||
if not self.read_register(NRF24.FEATURE):
|
||||
# So enable them and try again
|
||||
self.toggle_features()
|
||||
self.write_register(NRF24.FEATURE,
|
||||
self.read_register(NRF24.FEATURE) | _BV(NRF24.EN_ACK_PAY) | _BV(NRF24.EN_DPL))
|
||||
|
||||
# Enable dynamic payload on pipes 0 & 1
|
||||
self.write_register(NRF24.DYNPD, self.read_register(NRF24.DYNPD) | _BV(NRF24.DPL_P1) | _BV(NRF24.DPL_P0))
|
||||
|
||||
def writeAckPayload(self, pipe, buf, buf_len):
|
||||
txbuffer = [NRF24.W_ACK_PAYLOAD | ( pipe & 0x7 )]
|
||||
|
||||
max_payload_size = 32
|
||||
data_len = min(buf_len, max_payload_size)
|
||||
txbuffer.extend(buf[0:data_len])
|
||||
|
||||
self.spidev.xfer2(txbuffer)
|
||||
|
||||
def isAckPayloadAvailable(self):
|
||||
result = self.ack_payload_available
|
||||
self.ack_payload_available = False
|
||||
return result
|
||||
|
||||
def isPVariant(self):
|
||||
return self.p_variant
|
||||
|
||||
def setAutoAck(self, enable):
|
||||
if enable:
|
||||
self.write_register(NRF24.EN_AA, 0b111111)
|
||||
else:
|
||||
self.write_register(NRF24.EN_AA, 0)
|
||||
|
||||
def setAutoAckPipe(self, pipe, enable):
|
||||
if pipe <= 6:
|
||||
en_aa = self.read_register(NRF24.EN_AA)
|
||||
if enable:
|
||||
en_aa |= _BV(pipe)
|
||||
else:
|
||||
en_aa &= ~_BV(pipe)
|
||||
|
||||
self.write_register(NRF24.EN_AA, en_aa)
|
||||
|
||||
def testCarrier(self):
|
||||
return self.read_register(NRF24.CD) & 1
|
||||
|
||||
def testRPD(self):
|
||||
return self.read_register(NRF24.RPD) & 1
|
||||
|
||||
def setPALevel(self, level):
|
||||
setup = self.read_register(NRF24.RF_SETUP)
|
||||
setup &= ~( _BV(NRF24.RF_PWR_LOW) | _BV(NRF24.RF_PWR_HIGH))
|
||||
# switch uses RAM (evil!)
|
||||
if level == NRF24.PA_MAX:
|
||||
setup |= (_BV(NRF24.RF_PWR_LOW) | _BV(NRF24.RF_PWR_HIGH))
|
||||
elif level == NRF24.PA_HIGH:
|
||||
setup |= _BV(NRF24.RF_PWR_HIGH)
|
||||
elif level == NRF24.PA_LOW:
|
||||
setup |= _BV(NRF24.RF_PWR_LOW)
|
||||
elif level == NRF24.PA_MIN:
|
||||
nop = 0
|
||||
elif level == NRF24.PA_ERROR:
|
||||
# On error, go to maximum PA
|
||||
setup |= (_BV(NRF24.RF_PWR_LOW) | _BV(NRF24.RF_PWR_HIGH))
|
||||
|
||||
self.write_register(NRF24.RF_SETUP, setup)
|
||||
|
||||
|
||||
def getPALevel(self):
|
||||
power = self.read_register(NRF24.RF_SETUP) & (_BV(NRF24.RF_PWR_LOW) | _BV(NRF24.RF_PWR_HIGH))
|
||||
|
||||
if power == (_BV(NRF24.RF_PWR_LOW) | _BV(NRF24.RF_PWR_HIGH)):
|
||||
return NRF24.PA_MAX
|
||||
elif power == _BV(NRF24.RF_PWR_HIGH):
|
||||
return NRF24.PA_HIGH
|
||||
elif power == _BV(NRF24.RF_PWR_LOW):
|
||||
return NRF24.PA_LOW
|
||||
else:
|
||||
return NRF24.PA_MIN
|
||||
|
||||
def setDataRate(self, speed):
|
||||
result = False
|
||||
setup = self.read_register(NRF24.RF_SETUP)
|
||||
|
||||
# HIGH and LOW '00' is 1Mbs - our default
|
||||
self.wide_band = False
|
||||
setup &= ~(_BV(NRF24.RF_DR_LOW) | _BV(NRF24.RF_DR_HIGH))
|
||||
|
||||
if speed == NRF24.BR_250KBPS:
|
||||
# Must set the RF_DR_LOW to 1 RF_DR_HIGH (used to be RF_DR) is already 0
|
||||
# Making it '10'.
|
||||
self.wide_band = False
|
||||
setup |= _BV(NRF24.RF_DR_LOW)
|
||||
else:
|
||||
# Set 2Mbs, RF_DR (RF_DR_HIGH) is set 1
|
||||
# Making it '01'
|
||||
if speed == NRF24.BR_2MBPS:
|
||||
self.wide_band = True
|
||||
setup |= _BV(NRF24.RF_DR_HIGH)
|
||||
else:
|
||||
# 1Mbs
|
||||
self.wide_band = False
|
||||
|
||||
self.write_register(NRF24.RF_SETUP, setup)
|
||||
|
||||
# Verify our result
|
||||
if self.read_register(NRF24.RF_SETUP) == setup:
|
||||
result = True
|
||||
else:
|
||||
self.wide_band = False
|
||||
return result
|
||||
|
||||
def getDataRate(self):
|
||||
dr = self.read_register(NRF24.RF_SETUP) & (_BV(NRF24.RF_DR_LOW) | _BV(NRF24.RF_DR_HIGH))
|
||||
# Order matters in our case below
|
||||
if dr == _BV(NRF24.RF_DR_LOW):
|
||||
# '10' = 250KBPS
|
||||
return NRF24.BR_250KBPS
|
||||
elif dr == _BV(NRF24.RF_DR_HIGH):
|
||||
# '01' = 2MBPS
|
||||
return NRF24.BR_2MBPS
|
||||
else:
|
||||
# '00' = 1MBPS
|
||||
return NRF24.BR_1MBPS
|
||||
|
||||
|
||||
def setCRCLength(self, length):
|
||||
config = self.read_register(NRF24.CONFIG) & ~( _BV(NRF24.CRC_16) | _BV(NRF24.CRC_ENABLED))
|
||||
|
||||
if length == NRF24.CRC_DISABLED:
|
||||
# Do nothing, we turned it off above.
|
||||
self.write_register(NRF24.CONFIG, config)
|
||||
return
|
||||
elif length == NRF24.CRC_8:
|
||||
config |= _BV(NRF24.CRC_ENABLED)
|
||||
config |= _BV(NRF24.CRC_8)
|
||||
else:
|
||||
config |= _BV(NRF24.CRC_ENABLED)
|
||||
config |= _BV(NRF24.CRC_16)
|
||||
|
||||
self.write_register(NRF24.CONFIG, config)
|
||||
|
||||
def getCRCLength(self):
|
||||
result = NRF24.CRC_DISABLED
|
||||
config = self.read_register(NRF24.CONFIG) & ( _BV(NRF24.CRCO) | _BV(NRF24.EN_CRC))
|
||||
|
||||
if config & _BV(NRF24.EN_CRC):
|
||||
if config & _BV(NRF24.CRCO):
|
||||
result = NRF24.CRC_16
|
||||
else:
|
||||
result = NRF24.CRC_8
|
||||
|
||||
return result
|
||||
|
||||
def disableCRC(self):
|
||||
disable = self.read_register(NRF24.CONFIG) & ~_BV(NRF24.EN_CRC)
|
||||
self.write_register(NRF24.CONFIG, disable)
|
||||
|
||||
def setRetries(self, delay, count):
|
||||
# see specs. Delay code below 5 can conflict with some ACK lengths
|
||||
# and count should be set = 0 for non-ACK modes
|
||||
self.write_register(NRF24.SETUP_RETR, (delay & 0xf) << NRF24.ARD | (count & 0xf))
|
||||
|
||||
def getRetries(self):
|
||||
return self.read_register(NRF24.SETUP_RETR)
|
||||
|
||||
def getMaxTimeout(self): # seconds
|
||||
retries = self.getRetries()
|
||||
tout = (((250+(250*((retries& 0xf0)>>4 ))) * (retries & 0x0f)) / 1000000.0 * 2) + 0.008
|
||||
# Fudged up to about double Barraca's calculation
|
||||
# Was too short & was timeing out wrongly. BL
|
||||
return tout
|
@ -1,171 +0,0 @@
|
||||
import os
|
||||
import subprocess
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
import time
|
||||
|
||||
|
||||
__all__ = [
|
||||
'networks',
|
||||
'start_hotspot_if_needed',
|
||||
'scan',
|
||||
'is_connected',
|
||||
'save',
|
||||
'start_wps',
|
||||
'start_hotspot',
|
||||
'stop_hotspot',
|
||||
'InterfaceBusyException'
|
||||
]
|
||||
|
||||
network_regex = re.compile(
|
||||
r'network=\{\s+ssid="(?P<ssid>.*)"\s+psk="(?P<psk>.*)"\s+id_str="(?P<id_str>.*)"\s+\}'
|
||||
)
|
||||
|
||||
cell_regex = re.compile(
|
||||
r'Cell(?:.|\n)*?Frequency:(?P<frequency>.+)(?:.|\n)*?Quality=(?P<quality>.+)(?:.|\n)*?Encryption key:(?P<encrypted>.+)(?:.|\n)*?ESSID:"(?P<ssid>.+)"'
|
||||
)
|
||||
|
||||
wpa_supplicant_path = r'/etc/wpa_supplicant/wpa_supplicant.conf'
|
||||
interfaces_path = r'/etc/network/interfaces'
|
||||
|
||||
wpa_supplicant_header = '''
|
||||
country=DE
|
||||
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
|
||||
ap_scan=1
|
||||
update_config=1
|
||||
'''
|
||||
|
||||
interfaces_header = '''
|
||||
source-directory /etc/network/interfaces.d
|
||||
|
||||
auto lo
|
||||
auto eth0
|
||||
auto wlan0
|
||||
auto ap0
|
||||
|
||||
iface eth0 inet dhcp
|
||||
iface lo inet loopback
|
||||
|
||||
allow-hotplug ap0
|
||||
iface ap0 inet static
|
||||
address 192.168.10.1
|
||||
netmask 255.255.255.0
|
||||
hostapd /etc/hostapd/hostapd.conf
|
||||
|
||||
allow-hotplug wlan0
|
||||
iface wlan0 inet manual
|
||||
wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf
|
||||
'''
|
||||
|
||||
class InterfaceBusyException(Exception):
|
||||
pass
|
||||
|
||||
@dataclass
|
||||
class Cell():
|
||||
ssid: str
|
||||
quality: float
|
||||
frequency: str
|
||||
encrypted: bool
|
||||
#encryption_type: str
|
||||
|
||||
@dataclass
|
||||
class Network:
|
||||
ssid: str
|
||||
psk: str
|
||||
id_str: str = ''
|
||||
|
||||
networks: Network = []
|
||||
is_hotspot_running = False
|
||||
|
||||
def read_saved_networks():
|
||||
with open(wpa_supplicant_path, 'r') as f:
|
||||
for match in network_regex.finditer(f.read()):
|
||||
networks.append(
|
||||
Network(
|
||||
**match.groupdict()
|
||||
)
|
||||
)
|
||||
|
||||
def write_networks():
|
||||
with open(wpa_supplicant_path, 'w') as f:
|
||||
f.write(wpa_supplicant_header)
|
||||
for i, network in enumerate(networks):
|
||||
f.write(f'''
|
||||
network={{
|
||||
ssid="{network.ssid}"
|
||||
psk="{network.psk}"
|
||||
id_str="AP{i}"
|
||||
}}''')
|
||||
with open(interfaces_path, 'w') as f:
|
||||
f.write(interfaces_header)
|
||||
for i in range(len(networks)):
|
||||
f.write(f'iface AP{i} inet dhcp')
|
||||
|
||||
def reconnect():
|
||||
os.system('sudo wpa_cli -i wlan0 reconfigure')
|
||||
|
||||
def is_connected() -> bool:
|
||||
try:
|
||||
return bool(subprocess.check_output(['iwgetid',]))
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
def start_hotspot():
|
||||
os.system('service hostapd stop')
|
||||
os.system('service dnsmasq stop')
|
||||
os.system('service dhcpcd stop')
|
||||
os.system('iw dev wlan0 interface add ap0 type __ap')
|
||||
os.system('service hostapd start')
|
||||
os.system('service dnsmasq start')
|
||||
os.system('service dhcpcd start')
|
||||
|
||||
def stop_hotspot():
|
||||
os.system('sudo service hostapd stop')
|
||||
|
||||
# Public
|
||||
|
||||
def start_hotspot_if_needed():
|
||||
global is_hotspot_running
|
||||
connected = is_connected()
|
||||
if not connected and not is_hotspot_running:
|
||||
start_hotspot()
|
||||
is_hotspot_running = True
|
||||
elif connected and is_hotspot_running:
|
||||
stop_hotspot()
|
||||
time.sleep(10)
|
||||
is_hotspot_running = False
|
||||
|
||||
def save(ssid: str, password: str):
|
||||
# read_saved_networks()
|
||||
networks.append(Network(ssid, password))
|
||||
write_networks()
|
||||
reconnect()
|
||||
|
||||
def start_wps():
|
||||
os.system('wpa_cli -i wlan0 wps_pbc')
|
||||
|
||||
def scan() -> list[Cell]:
|
||||
try:
|
||||
output = subprocess.check_output('sudo iwlist wlan0 scan'.split(' '), text = True)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
raise InterfaceBusyException from exc
|
||||
|
||||
for match in cell_regex.finditer(output):
|
||||
match = match.groupdict()
|
||||
|
||||
quality, max = map(float, match['quality'].split(' ')[0].split('/'))
|
||||
|
||||
ssid = match['ssid'].strip()
|
||||
quality = round(quality / max, 2)
|
||||
frequency = re.sub(r'\(Channel.*\)', '', match['frequency']).strip()
|
||||
encrypted = match['encrypted'] == 'on'
|
||||
|
||||
yield Cell(ssid, quality, frequency, encrypted)
|
||||
|
||||
# Main
|
||||
|
||||
if __name__ == '__main__':
|
||||
read_saved_networks()
|
||||
if not networks:
|
||||
start_hotspot()
|
||||
start_wps()
|
@ -1,22 +0,0 @@
|
||||
import os
|
||||
import time
|
||||
import RPi.GPIO as GPIO
|
||||
|
||||
pin = 3
|
||||
cooling = False
|
||||
|
||||
def current_temp():
|
||||
temp = os.popen("vcgencmd measure_temp").readline()
|
||||
return temp.replace("temp=","")
|
||||
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setup(pin, GPIO.OUT)
|
||||
while True:
|
||||
temp = float(current_temp()[:4])
|
||||
if temp > 55:
|
||||
cooling = True
|
||||
elif temp < 40:
|
||||
cooling = False
|
||||
GPIO.output(pin, cooling)
|
||||
print(f'temp={temp}, cooling={cooling}')
|
||||
time.sleep(1)
|
@ -1,21 +0,0 @@
|
||||
import uvicorn
|
||||
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__':
|
||||
run()
|
@ -1,33 +0,0 @@
|
||||
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()
|
@ -1,122 +0,0 @@
|
||||
from uuid import UUID
|
||||
from fastapi import Depends
|
||||
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
|
||||
import database
|
||||
from models import (
|
||||
User,
|
||||
Device,
|
||||
DeviceModel,
|
||||
DeviceParameterAssociation,
|
||||
DeviceParameterAssociation
|
||||
)
|
||||
from schemas.device import (
|
||||
DeviceParameter,
|
||||
Parameter,
|
||||
DeviceState,
|
||||
DevicePatch,
|
||||
DeviceCreate,
|
||||
Device as DeviceScheme
|
||||
)
|
||||
|
||||
|
||||
class DevicesManager:
|
||||
session: AsyncSession
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def get(self, id: UUID) -> Device:
|
||||
db: AsyncSession = self.session
|
||||
if device := await db.get(Device, id):
|
||||
return device
|
||||
else:
|
||||
raise InternalException(ExceptionCode.not_found, 'Device not found')
|
||||
|
||||
async def state(self, id: UUID) -> DeviceState | None:
|
||||
db: AsyncSession = self.session
|
||||
device = await self.get(id)
|
||||
|
||||
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)
|
||||
|
||||
device_state.parameters = list(map(DevicesManager._map_parameter, parameters))
|
||||
|
||||
return device_state
|
||||
|
||||
async def create(self, create_device: DeviceCreate) -> Device:
|
||||
db: AsyncSession = self.session
|
||||
|
||||
id = AUID(bytes=create_device.id.bytes)
|
||||
model_id = AUID.new(model=id.items.model, now=None)
|
||||
urdi = id.items.urdi
|
||||
|
||||
model = await db.get(DeviceModel, model_id)
|
||||
|
||||
if not model:
|
||||
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 InternalException(
|
||||
code = ExceptionCode.invalid_format,
|
||||
msg = 'Device with this id already exist'
|
||||
)
|
||||
|
||||
device = Device(
|
||||
id = create_device.id,
|
||||
name = create_device.name,
|
||||
urdi = urdi,
|
||||
room_id = create_device.room_id,
|
||||
model_id = model_id
|
||||
)
|
||||
|
||||
for model_parameter in model.parameters:
|
||||
device_parameter = DeviceParameterAssociation(
|
||||
device_id = device.id,
|
||||
parameter_id = model_parameter.parameter.id
|
||||
)
|
||||
db.add(device_parameter)
|
||||
|
||||
db.add(device)
|
||||
await db.commit()
|
||||
await db.refresh(device)
|
||||
|
||||
return device
|
||||
|
||||
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))
|
||||
await db.commit()
|
||||
|
||||
async def delete(self, device_id: UUID):
|
||||
db: AsyncSession = self.session
|
||||
device = await self.get(device_id)
|
||||
if device:
|
||||
await db.delete(device)
|
||||
await db.commit()
|
||||
else:
|
||||
raise InternalException(
|
||||
code = ExceptionCode.not_found,
|
||||
msg = 'Device not found'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _read_parameters(_, device: Device) -> list[DeviceParameter]:
|
||||
return device.parameters
|
||||
|
||||
@staticmethod
|
||||
def _map_parameter(association: DeviceParameterAssociation) -> DeviceParameter:
|
||||
parameter = Parameter.from_orm(association.parameter)
|
||||
device_parameter = DeviceParameter(**{**parameter.dict(), 'value': association.value})
|
||||
return device_parameter
|
@ -1,45 +0,0 @@
|
||||
from uuid import UUID
|
||||
from fastapi import Depends
|
||||
from sqlalchemy import select, delete, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.exc import NoResultFound
|
||||
|
||||
from exceptions.Internal import InternalException, ExceptionCode
|
||||
from models import House
|
||||
from server.dependencies import database
|
||||
from schemas.house import HousePatch
|
||||
|
||||
|
||||
class HouseManager:
|
||||
session: AsyncSession
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def get(self) -> House:
|
||||
db: AsyncSession = self.session
|
||||
try:
|
||||
result = await db.scalars(select(House))
|
||||
return result.one()
|
||||
except NoResultFound:
|
||||
raise InternalException(ExceptionCode.not_initialized)
|
||||
|
||||
async def create(self, house_id: UUID) -> House:
|
||||
db: AsyncSession = self.session
|
||||
|
||||
try:
|
||||
await db.delete(await self.get())
|
||||
except InternalException:
|
||||
pass
|
||||
|
||||
house = House(id = house_id, name = '')
|
||||
db.add(house)
|
||||
await db.commit()
|
||||
return house
|
||||
|
||||
async def patch(self, house: HousePatch):
|
||||
db: AsyncSession = self.session
|
||||
await db.execute(
|
||||
update(House)
|
||||
.values(**house.dict())
|
||||
)
|
@ -1,124 +0,0 @@
|
||||
from uuid import UUID
|
||||
from fastapi import Depends
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.exc import NoResultFound
|
||||
|
||||
import config
|
||||
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 | None
|
||||
user: User | None = None
|
||||
|
||||
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 InternalException(ExceptionCode.unauthorized)
|
||||
self.user = await db.get(User, self.token.user_id)
|
||||
|
||||
async def init(self, create_hub: schemas.HubInit, raw_token: str) -> Hub:
|
||||
db: AsyncSession = self.session
|
||||
|
||||
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 = 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)
|
||||
db.add(hub)
|
||||
|
||||
if not self.user:
|
||||
user = User(id = self.token.user_id, name = '')
|
||||
db.add(user)
|
||||
|
||||
await db.commit()
|
||||
return hub
|
||||
|
||||
async def patch(self, hub: schemas.HubPatch):
|
||||
db: AsyncSession = self.session
|
||||
values = {key: value for key, value in hub.dict().items() if value != None}
|
||||
|
||||
await db.execute(update(Hub).values(**values))
|
||||
await db.commit()
|
||||
|
||||
def wifi(self, ssid: str, password: str):
|
||||
WiFi.save(ssid, password)
|
||||
|
||||
def start_wps(self):
|
||||
WiFi.start_wps()
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
return WiFi.is_connected()
|
||||
|
||||
def stop_hotspot(self):
|
||||
WiFi.stop_hotspot()
|
||||
|
||||
def get_hotspots(self) -> list[schemas.Hotspot]:
|
||||
try:
|
||||
return list(WiFi.scan())
|
||||
except WiFi.InterfaceBusyException:
|
||||
return []
|
||||
|
||||
def save_credentials(self, credentials: schemas.HubAuthItems):
|
||||
self.save_tokens(credentials)
|
||||
|
||||
config.public_key = credentials.public_key
|
||||
with open(f'{config.src}/jwt.key.pub', 'w') as f:
|
||||
f.write(credentials.public_key)
|
||||
|
||||
def save_tokens(self, tokens_pair: schemas.HubAuthItems):
|
||||
config.access_token = tokens_pair.access_token
|
||||
with open(f'{config.src}/jwt_access_token', 'w') as f:
|
||||
f.write(tokens_pair.access_token)
|
||||
|
||||
config.refresh_token = tokens_pair.refresh_token
|
||||
with open(f'{config.src}/jwt_refresh_token', 'w') as f:
|
||||
f.write(tokens_pair.refresh_token)
|
||||
|
||||
async def check_access(self):
|
||||
if self.user:
|
||||
return
|
||||
|
||||
if not self.token:
|
||||
raise InternalException(ExceptionCode.access_denied)
|
||||
|
||||
db: AsyncSession = self.session
|
||||
self.user = await db.get(User, self.token.user_id)
|
||||
|
||||
if not self.user:
|
||||
raise InternalException(ExceptionCode.access_denied)
|
@ -1,49 +0,0 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy import select, update, delete
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
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
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def get(self, id: UUID) -> Room:
|
||||
db: AsyncSession = self.session
|
||||
result = await db.scalars(
|
||||
select(Room).where(Room.id == id).options(selectinload(Room.devices))
|
||||
)
|
||||
if room := result.first():
|
||||
return room
|
||||
else:
|
||||
raise InternalException(ExceptionCode.not_found, 'Room not found')
|
||||
|
||||
async def create(self, create_room: schemas.RoomCreate) -> Room:
|
||||
db: AsyncSession = self.session
|
||||
house = await endpoints.house.HouseManager(db).get()
|
||||
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.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))
|
||||
await db.commit()
|
||||
|
||||
async def delete(self, room_id: UUID):
|
||||
db: AsyncSession = self.session
|
||||
room = await self.get(room_id)
|
||||
await db.delete(room)
|
||||
await db.commit()
|
@ -1,71 +0,0 @@
|
||||
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,
|
||||
RestRequest
|
||||
)
|
||||
from client import api
|
||||
|
||||
|
||||
class WSManager:
|
||||
session: AsyncSession
|
||||
|
||||
def __inti__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def handle_message(self, msg: str) -> dict | None:
|
||||
socket = SocketData.parse_raw(msg)
|
||||
try:
|
||||
socket = SocketData.parse_raw(msg)
|
||||
except ValidationError:
|
||||
return
|
||||
|
||||
match socket.type:
|
||||
case SocketType.merlin:
|
||||
try:
|
||||
merlin_data = MerlinData(**socket.data)
|
||||
except ValidationError:
|
||||
return
|
||||
await self.merlin_send(merlin_data)
|
||||
|
||||
case SocketType.rest:
|
||||
try:
|
||||
rest_request = RestRequest(**socket.data)
|
||||
except ValidationError:
|
||||
return
|
||||
if local_response := await api.local_request(rest_request):
|
||||
# TODO: pydantic model
|
||||
return {'path': rest_request.path, 'data': local_response}
|
||||
|
||||
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))
|
@ -1,6 +0,0 @@
|
||||
from .DeviceModelManager import DeviceModelManager
|
||||
from .DevicesManager import DevicesManager
|
||||
from .HouseManager import HouseManager
|
||||
from .HubManager import HubManager
|
||||
from .RoomsManager import RoomsManager
|
||||
from .WSManager import WSManager
|
@ -1,13 +0,0 @@
|
||||
from sqlalchemy.ext.declarative import declarative_base, declared_attr
|
||||
|
||||
|
||||
class BaseClass(object):
|
||||
|
||||
@declared_attr
|
||||
def __tablename__(cls):
|
||||
return f'{cls.__name__.lower()}s'
|
||||
|
||||
def __str__(self):
|
||||
return f'{type(self).__name__}({self.id.hex[:8]})'
|
||||
|
||||
Base = declarative_base(cls = BaseClass)
|
@ -1,34 +0,0 @@
|
||||
from uuid import uuid1
|
||||
from sqlalchemy import Column, String, Integer, ForeignKey, UniqueConstraint
|
||||
from sqlalchemy_utils import UUIDType
|
||||
from sqlalchemy.orm import relationship
|
||||
from .Base import Base
|
||||
|
||||
|
||||
class DeviceParameterAssociation(Base): # TODO: remove
|
||||
id = Column(UUIDType, index = True, primary_key = True, default = uuid1)
|
||||
device_id = Column(ForeignKey('devices.id'), primary_key = True)
|
||||
parameter_id = Column(ForeignKey('parameters.id'), primary_key = True)
|
||||
value = Column(Integer, nullable = False, default = 0)
|
||||
parameter = relationship('Parameter', lazy = 'selectin')
|
||||
device = relationship('Device')
|
||||
__table_args__ = (UniqueConstraint('device_id', 'parameter_id'),)
|
||||
|
||||
def __str__(self):
|
||||
try:
|
||||
return self.parameter.name or super().__str__()
|
||||
except:
|
||||
return super().__str__()
|
||||
|
||||
class Device(Base):
|
||||
id = Column(UUIDType, index = True, primary_key = True, default = uuid1)
|
||||
name = Column(String)
|
||||
urdi = Column(Integer, unique = True, nullable = False)
|
||||
room_id = Column(UUIDType, ForeignKey('rooms.id'), nullable = False)
|
||||
model_id = Column(UUIDType, ForeignKey('devicemodels.id'), nullable = False)
|
||||
room = relationship('Room', back_populates = 'devices')
|
||||
model = relationship('DeviceModel', lazy = 'selectin')
|
||||
parameters = relationship('DeviceParameterAssociation', back_populates = 'device', cascade = 'all, delete-orphan')
|
||||
|
||||
def __str__(self):
|
||||
return self.name or super().__str__()
|
@ -1,26 +0,0 @@
|
||||
from uuid import uuid1
|
||||
from sqlalchemy import Table, Column, String, Integer, ForeignKey, UniqueConstraint
|
||||
from sqlalchemy_utils import UUIDType
|
||||
from sqlalchemy.orm import relationship
|
||||
from .Base import Base
|
||||
|
||||
|
||||
class DeviceModelParameter(Base):
|
||||
id = Column(UUIDType, index = True, primary_key = True, default = uuid1) # TODO: remove
|
||||
devicemodel_id = Column(ForeignKey('devicemodels.id'), primary_key = True)
|
||||
parameter_id = Column(ForeignKey('parameters.id'), primary_key = True)
|
||||
f = Column(Integer, nullable = False, default = 0)
|
||||
parameter = relationship('Parameter', lazy = 'selectin')
|
||||
devicemodel = relationship('DeviceModel')
|
||||
__table_args__ = (UniqueConstraint('devicemodel_id', 'parameter_id'),)
|
||||
|
||||
def __str__(self):
|
||||
return self.parameter.name or super().__str__()
|
||||
|
||||
class DeviceModel(Base):
|
||||
id = Column(UUIDType, index = True, primary_key = True, default = uuid1)
|
||||
name = Column(String)
|
||||
parameters = relationship('DeviceModelParameter', back_populates = 'devicemodel', cascade = 'all, delete-orphan', lazy = 'selectin')
|
||||
|
||||
def __str__(self):
|
||||
return self.name or super().__str__()
|
@ -1,15 +0,0 @@
|
||||
from uuid import uuid1
|
||||
from sqlalchemy import Column, String, ForeignKey
|
||||
from sqlalchemy_utils import UUIDType
|
||||
from sqlalchemy.orm import relationship
|
||||
from .Base import Base
|
||||
|
||||
|
||||
class House(Base):
|
||||
id = Column(UUIDType, index = True, primary_key = True, default = uuid1)
|
||||
name = Column(String)
|
||||
hubs = relationship('Hub', back_populates = 'house', cascade = 'all, delete-orphan', lazy = 'selectin')
|
||||
rooms = relationship('Room', back_populates = 'house', cascade = 'all, delete-orphan', lazy = 'selectin')
|
||||
|
||||
def __str__(self):
|
||||
return self.name or super().__str__()
|
@ -1,15 +0,0 @@
|
||||
from uuid import uuid1
|
||||
from sqlalchemy import Column, String, ForeignKey
|
||||
from sqlalchemy_utils import UUIDType
|
||||
from sqlalchemy.orm import relationship
|
||||
from .Base import Base
|
||||
|
||||
|
||||
class Hub(Base):
|
||||
id = Column(UUIDType, index = True, primary_key = True, default = uuid1)
|
||||
name = Column(String)
|
||||
house_id = Column(UUIDType, ForeignKey('houses.id'))
|
||||
house = relationship('House', back_populates = 'hubs')
|
||||
|
||||
def __str__(self):
|
||||
return self.name or super().__str__()
|
@ -1,16 +0,0 @@
|
||||
from uuid import uuid1
|
||||
from sqlalchemy import Column, String, Integer, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy_utils import UUIDType
|
||||
from .Base import Base
|
||||
|
||||
|
||||
class Parameter(Base): # TODO: remove
|
||||
id = Column(UUIDType, index = True, primary_key = True, default = uuid1)
|
||||
name = Column(String, nullable = False)
|
||||
value_type = Column(String, nullable = False)
|
||||
device_parameters = relationship('DeviceParameterAssociation', back_populates = 'parameter', cascade = 'all, delete-orphan')
|
||||
devicemodel_parameters = relationship('DeviceModelParameter', back_populates = 'parameter', cascade = 'all, delete-orphan')
|
||||
|
||||
def __str__(self):
|
||||
return self.name or super().__str__()
|
@ -1,16 +0,0 @@
|
||||
from uuid import uuid1
|
||||
from sqlalchemy import Column, String, ForeignKey
|
||||
from sqlalchemy_utils import UUIDType
|
||||
from sqlalchemy.orm import relationship
|
||||
from .Base import Base
|
||||
|
||||
|
||||
class Room(Base):
|
||||
id = Column(UUIDType, index = True, primary_key = True, default = uuid1)
|
||||
name = Column(String)
|
||||
house_id = Column(UUIDType, ForeignKey('houses.id'), nullable = False)
|
||||
house = relationship('House', back_populates = 'rooms')
|
||||
devices = relationship('Device', back_populates = 'room', cascade = 'all, delete-orphan')
|
||||
|
||||
def __str__(self):
|
||||
return self.name or super().__str__()
|
@ -1,9 +0,0 @@
|
||||
from uuid import uuid1
|
||||
from sqlalchemy import Column, String
|
||||
from sqlalchemy_utils import UUIDType
|
||||
from .Base import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
id = Column(UUIDType, index = True, primary_key = True, default = uuid1)
|
||||
name = Column(String)
|
@ -1,8 +0,0 @@
|
||||
from .Base import Base
|
||||
from .User import User
|
||||
from .House import House
|
||||
from .Hub import Hub
|
||||
from .Room import Room
|
||||
from .Device import Device, DeviceParameterAssociation
|
||||
from .DeviceModel import DeviceModel, DeviceModelParameter
|
||||
from .Parameter import Parameter
|
@ -1,32 +0,0 @@
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
from pydantic import BaseModel
|
||||
from AUID import AUID
|
||||
from .parameters import DeviceParameter, Parameter
|
||||
|
||||
|
||||
class DeviceModelInfo(BaseModel):
|
||||
id: UUID
|
||||
name: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
class DeviceModel(DeviceModelInfo):
|
||||
parameters: list[Parameter]
|
||||
|
||||
class DevicePatch(BaseModel):
|
||||
name: str
|
||||
room_id: UUID
|
||||
|
||||
class DeviceCreate(DevicePatch):
|
||||
id: AUID
|
||||
|
||||
class Device(DeviceCreate):
|
||||
model: DeviceModel
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
class DeviceState(Device):
|
||||
parameters: list[DeviceParameter] = []
|
@ -1,16 +0,0 @@
|
||||
from uuid import UUID
|
||||
from pydantic import BaseModel
|
||||
from .hub import Hub
|
||||
from .room import RoomInfo
|
||||
|
||||
|
||||
class HousePatch(BaseModel):
|
||||
id: UUID
|
||||
name: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
class House(HousePatch):
|
||||
hubs: list[Hub]
|
||||
rooms: list[RoomInfo]
|
@ -1,44 +0,0 @@
|
||||
from uuid import UUID
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
# Auth
|
||||
|
||||
class TokensPair(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
|
||||
class HubAuthItems(TokensPair):
|
||||
public_key: str
|
||||
|
||||
# Hub
|
||||
|
||||
class Hub(BaseModel):
|
||||
id: UUID
|
||||
name: str
|
||||
house_id: UUID
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
class HubPatch(BaseModel):
|
||||
name: str
|
||||
|
||||
class HubInit(Hub, HubAuthItems):
|
||||
...
|
||||
|
||||
# WiFi
|
||||
|
||||
class Hotspot(BaseModel):
|
||||
ssid: str
|
||||
quality: float
|
||||
frequency: str
|
||||
encrypted: bool
|
||||
#encryption_type: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
class WifiConnection(BaseModel):
|
||||
ssid: str
|
||||
password: str
|
@ -1,22 +0,0 @@
|
||||
from uuid import UUID
|
||||
from pydantic import BaseModel
|
||||
from models import DeviceModelParameter
|
||||
|
||||
|
||||
class Parameter(BaseModel):
|
||||
id: UUID
|
||||
name: str
|
||||
value_type: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
@classmethod
|
||||
def from_orm(cls, obj):
|
||||
if isinstance(obj, DeviceModelParameter):
|
||||
return super().from_orm(obj.parameter)
|
||||
else:
|
||||
return super().from_orm(obj)
|
||||
|
||||
class DeviceParameter(Parameter):
|
||||
value: int
|
@ -1,20 +0,0 @@
|
||||
from uuid import UUID
|
||||
from pydantic import BaseModel
|
||||
from .device import Device
|
||||
|
||||
|
||||
class RoomCreate(BaseModel):
|
||||
name: str
|
||||
|
||||
class RoomPatch(RoomCreate):
|
||||
pass
|
||||
|
||||
class RoomInfo(BaseModel):
|
||||
id: UUID
|
||||
name: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
class Room(RoomInfo):
|
||||
devices: list[Device]
|
@ -1,36 +0,0 @@
|
||||
from typing import Any
|
||||
from enum import Enum
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
# MARK: - Bridge
|
||||
|
||||
class HttpMethod(str, Enum):
|
||||
get = 'get'
|
||||
post = 'post'
|
||||
patch = 'patch'
|
||||
delete = 'delete'
|
||||
|
||||
BridgeRequestBody = dict[str, Any] | BaseModel
|
||||
|
||||
class RestRequest(BaseModel):
|
||||
path: str
|
||||
method: HttpMethod
|
||||
body: BridgeRequestBody
|
||||
|
||||
# MARK: - Merlin
|
||||
|
||||
class MerlinData(BaseModel):
|
||||
device_id: str
|
||||
parameter_id: str
|
||||
value: str
|
||||
|
||||
# MARK: - Socket
|
||||
|
||||
class SocketType(str, Enum):
|
||||
merlin = 'merlin'
|
||||
rest = 'rest'
|
||||
|
||||
class SocketData(BaseModel):
|
||||
type: SocketType
|
||||
data: dict[str, Any]
|
@ -1,39 +0,0 @@
|
||||
from fastapi import FastAPI, APIRouter
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
import models
|
||||
import database
|
||||
from server import exceptions_handlers, endpoints
|
||||
|
||||
models.Base.metadata.create_all(bind = database.engine)
|
||||
|
||||
description = '''
|
||||
[**Admin**](/admin)
|
||||
|
||||
[**GitHub**](https://github.com/MarkParker5/ArchieCloud)
|
||||
|
||||
[**FastAPI**](https://fastapi.tiangolo.com)
|
||||
'''
|
||||
|
||||
app = FastAPI(
|
||||
title = 'Archie Hub',
|
||||
description = description,
|
||||
version = '0.0.1',
|
||||
swagger_ui_parameters = {
|
||||
'syntaxHighlight.theme': 'obsidian',
|
||||
'docExpansion': 'none',
|
||||
}
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
endpoints.admin.setup(app)
|
@ -1,60 +0,0 @@
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from uuid import UUID
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import BaseModel
|
||||
from jose import jwt, JWTError
|
||||
from jose.exceptions import JWKError
|
||||
|
||||
import config
|
||||
from .exceptions import AuthException
|
||||
|
||||
|
||||
class TokenType(str, Enum):
|
||||
access = 'access'
|
||||
refresh = 'refresh'
|
||||
|
||||
class TokensPair(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
|
||||
class HubAuthItem(TokensPair):
|
||||
public_key: str
|
||||
|
||||
class BaseToken(BaseModel):
|
||||
type: TokenType
|
||||
exp: float
|
||||
|
||||
class BaseAuthManager(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def _get_parsed_token(self, payload: dict) -> BaseToken:
|
||||
pass
|
||||
|
||||
def validate_access(self, raw_token: str) -> BaseToken:
|
||||
token = self._parse_token(raw_token)
|
||||
|
||||
if not token:
|
||||
raise AuthException()
|
||||
|
||||
if token.type != TokenType.access:
|
||||
raise AuthException()
|
||||
|
||||
if not self._is_valid_token(token):
|
||||
raise AuthException()
|
||||
|
||||
return token
|
||||
|
||||
def _parse_token(self, raw_token: str) -> BaseToken:
|
||||
try:
|
||||
payload = jwt.decode(raw_token, config.public_key, algorithms = [config.algorithm])
|
||||
token = self._get_parsed_token(payload)
|
||||
except JWTError:
|
||||
raise AuthException()
|
||||
except JWKError:
|
||||
raise AuthException()
|
||||
return token
|
||||
|
||||
def _is_valid_token(self, token: BaseToken) -> bool:
|
||||
return time.time() <= token.exp
|
@ -1,21 +0,0 @@
|
||||
import time
|
||||
from uuid import UUID
|
||||
from datetime import timedelta
|
||||
from jose import jwt
|
||||
import config
|
||||
from .BaseAuth import BaseAuthManager, BaseToken, TokenType
|
||||
|
||||
|
||||
class UserToken(BaseToken):
|
||||
user_id: UUID
|
||||
is_admin: bool
|
||||
|
||||
class UserAuthManager(BaseAuthManager):
|
||||
|
||||
# abstract impl
|
||||
def _get_parsed_token(self, payload: dict) -> UserToken:
|
||||
return UserToken(**payload)
|
||||
|
||||
# override
|
||||
def _is_valid_token(self, token: UserToken) -> bool:
|
||||
return super()._is_valid_token(token) and token.user_id != None
|
@ -1,9 +0,0 @@
|
||||
from .BaseAuth import (
|
||||
TokenType,
|
||||
TokensPair
|
||||
)
|
||||
from .UserAuth import (
|
||||
UserToken,
|
||||
UserAuthManager
|
||||
)
|
||||
from .exceptions import AuthException
|
@ -1,2 +0,0 @@
|
||||
class AuthException(Exception):
|
||||
pass
|
@ -1,2 +0,0 @@
|
||||
from . import database
|
||||
from . import auth
|
@ -1,37 +0,0 @@
|
||||
from fastapi import Depends
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
|
||||
from exceptions.Internal import InternalException, ExceptionCode
|
||||
from models import User
|
||||
from ..auth import (
|
||||
UserToken,
|
||||
UserAuthManager,
|
||||
AuthException
|
||||
)
|
||||
from .database import get_async_session
|
||||
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl = '/api/user/login')
|
||||
optional_oauth2_scheme = OAuth2PasswordBearer(tokenUrl = '/api/user/login', auto_error=False)
|
||||
userAuthManager = UserAuthManager()
|
||||
|
||||
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:
|
||||
return userAuthManager.validate_access(token)
|
||||
|
||||
def optional_token(token: str = Depends(optional_oauth2_scheme)) -> UserToken | None:
|
||||
if not token:
|
||||
return None
|
||||
try:
|
||||
return userAuthManager.validate_access(token)
|
||||
except AuthException:
|
||||
return None
|
||||
|
||||
def raw_token(token: str = Depends(oauth2_scheme)) -> str:
|
||||
return token
|
@ -1,15 +0,0 @@
|
||||
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
|
@ -1,6 +0,0 @@
|
||||
from . import admin
|
||||
from . import device
|
||||
from . import house
|
||||
from . import hub
|
||||
from . import room
|
||||
from . import ws
|
@ -1,83 +0,0 @@
|
||||
from fastapi import FastAPI
|
||||
from sqladmin import Admin, ModelAdmin, SidebarLink
|
||||
import database
|
||||
from models import (
|
||||
Base,
|
||||
House,
|
||||
Hub,
|
||||
Room,
|
||||
DeviceModel,
|
||||
DeviceModelParameter,
|
||||
Device,
|
||||
Parameter,
|
||||
DeviceParameterAssociation
|
||||
)
|
||||
|
||||
|
||||
class HouseAdmin(ModelAdmin, model = House):
|
||||
icon = 'fa-solid fa-house'
|
||||
column_list = [House.name, House.rooms]
|
||||
column_details_exclude_list = [House.id]
|
||||
form_excluded_columns = [House.id]
|
||||
|
||||
class HubAdmin(ModelAdmin, model = Hub):
|
||||
icon = 'fa-solid fa-wifi'
|
||||
column_list = [Hub.name, Hub.house]
|
||||
column_details_exclude_list = [Hub.id, Hub.house_id]
|
||||
form_excluded_columns = [Hub.id, Hub.house_id]
|
||||
|
||||
class RoomAdmin(ModelAdmin, model = Room):
|
||||
icon = 'fa-solid fa-house-user'
|
||||
column_list = [Room.name, Room.house, Room.devices]
|
||||
column_details_list = [Room.name, Room.house, Room.devices]
|
||||
form_excluded_columns = [Room.house_id]
|
||||
|
||||
class DeviceModelAdmin(ModelAdmin, model = DeviceModel):
|
||||
icon = 'fa-solid fa-lightbulb'
|
||||
column_list = [DeviceModel.name, DeviceModel.parameters]
|
||||
column_details_list = [DeviceModel.name, DeviceModel.parameters]
|
||||
|
||||
class DeviceModelParameterAdmin(ModelAdmin, model = DeviceModelParameter):
|
||||
icon = 'fa-solid fa-arrow-down-1-9'
|
||||
column_list = [DeviceModelParameter.f, DeviceModelParameter.parameter, DeviceModelParameter.devicemodel]
|
||||
column_details_list = column_list
|
||||
form_columns = column_details_list
|
||||
|
||||
class DeviceAdmin(ModelAdmin, model = Device):
|
||||
icon = 'fa-solid fa-microchip'
|
||||
column_list = [Device.name, Device.urdi, Device.model, Device.room, Device.parameters]
|
||||
column_details_list = [Device.name, Device.urdi, Device.model, Device.room, Device.parameters]
|
||||
form_excluded_columns = [Device.room_id, Device.model_id, Device.parameters]
|
||||
|
||||
class ParameterAdmin(ModelAdmin, model = Parameter):
|
||||
icon = 'fa-solid fa-sliders'
|
||||
column_list = [Parameter.name, Parameter.value_type]
|
||||
column_details_list = [Parameter.name, Parameter.value_type]
|
||||
form_columns = [Parameter.name, Parameter.value_type]
|
||||
|
||||
class DeviceParameterAdmin(ModelAdmin, model = DeviceParameterAssociation):
|
||||
name = 'Device Parameter'
|
||||
name_plural = 'Device Parameters'
|
||||
icon = 'fa-solid fa-gears'
|
||||
column_list = [DeviceParameterAssociation.device, DeviceParameterAssociation.parameter, DeviceParameterAssociation.value]
|
||||
column_details_list = column_list
|
||||
form_columns = column_list
|
||||
|
||||
links = [
|
||||
SidebarLink('API', '/docs', True),
|
||||
]
|
||||
|
||||
def setup(app: FastAPI):
|
||||
admin = Admin(app,
|
||||
engine = database.engine,
|
||||
title = 'Archie Hub - Admin',
|
||||
links = links
|
||||
)
|
||||
admin.register_model(HouseAdmin)
|
||||
admin.register_model(RoomAdmin)
|
||||
admin.register_model(HubAdmin)
|
||||
admin.register_model(DeviceModelAdmin)
|
||||
admin.register_model(DeviceModelParameterAdmin)
|
||||
admin.register_model(ParameterAdmin)
|
||||
admin.register_model(DeviceParameterAdmin)
|
||||
admin.register_model(DeviceAdmin)
|
@ -1,43 +0,0 @@
|
||||
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)
|
@ -1,25 +0,0 @@
|
||||
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()
|
@ -1,62 +0,0 @@
|
||||
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)
|
@ -1,36 +0,0 @@
|
||||
from uuid import UUID
|
||||
from fastapi import APIRouter, Depends
|
||||
from managers import RoomsManager
|
||||
from schemas.room import Room, RoomCreate, RoomPatch
|
||||
from server.dependencies.database import get_async_session, AsyncSession
|
||||
from server.dependencies.auth import validate_user, raw_token
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix = '/room',
|
||||
tags = ['room'],
|
||||
)
|
||||
|
||||
# MARK: - dependencies
|
||||
|
||||
async def manager(session = Depends(get_async_session),
|
||||
user = Depends(validate_user)) -> RoomsManager:
|
||||
return RoomsManager(session)
|
||||
|
||||
# MARK: - endpoints
|
||||
|
||||
@router.post('', response_model = Room)
|
||||
async def create_room(room: RoomCreate, manager = Depends(manager)):
|
||||
return await manager.create(room)
|
||||
|
||||
@router.get('/{id}', response_model = Room)
|
||||
async def get_room(id: UUID, manager = Depends(manager)):
|
||||
return await manager.get(id)
|
||||
|
||||
@router.patch('/{id}')
|
||||
async def patch_room(id: UUID, room: RoomPatch, manager = Depends(manager)):
|
||||
await manager.patch(id, room)
|
||||
|
||||
@router.delete('/{id}')
|
||||
async def delete_room(id: UUID, manager = Depends(manager)):
|
||||
await manager.delete(id)
|
@ -1,37 +0,0 @@
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||
from managers import WSManager
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
active_connections: list[WebSocket]
|
||||
wsmanager: WSManager
|
||||
|
||||
def __init__(self):
|
||||
self.active_connections: list[WebSocket] = []
|
||||
self.wsmanager = WSManager()
|
||||
|
||||
async def connect(self, websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
self.active_connections.append(websocket)
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
self.active_connections.remove(websocket)
|
||||
|
||||
async def handle_socket(self, websocket: WebSocket, msg: str):
|
||||
await self.wsmanager.handle_message(msg)
|
||||
|
||||
connection = ConnectionManager()
|
||||
router = APIRouter(
|
||||
prefix = '/ws',
|
||||
tags = ['ws',]
|
||||
)
|
||||
|
||||
@router.websocket('/')
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
await connection.connect(websocket)
|
||||
try:
|
||||
while True:
|
||||
msg = await websocket.receive_text()
|
||||
await connection.handle_socket(websocket, msg)
|
||||
except WebSocketDisconnect:
|
||||
connection.disconnect(websocket)
|
@ -1,23 +0,0 @@
|
||||
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)
|
@ -1,44 +0,0 @@
|
||||
# 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'
|
||||
# )
|
||||
# }
|
@ -1,127 +0,0 @@
|
||||
body {
|
||||
background: #222D32;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
margin-top: 75px;
|
||||
height: auto;
|
||||
background: #1A2226;
|
||||
text-align: center;
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
|
||||
}
|
||||
|
||||
.login-key {
|
||||
height: 100px;
|
||||
font-size: 80px;
|
||||
line-height: 100px;
|
||||
background: -webkit-linear-gradient(#27EF9F, #0DB8DE);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
margin-top: 15px;
|
||||
text-align: center;
|
||||
font-size: 30px;
|
||||
letter-spacing: 2px;
|
||||
margin-top: 15px;
|
||||
font-weight: bold;
|
||||
color: #ECF0F5;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
margin-top: 25px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
background-color: #1A2226;
|
||||
border: none;
|
||||
border-bottom: 2px solid #0DB8DE;
|
||||
border-top: 0px;
|
||||
border-radius: 0px;
|
||||
font-weight: bold;
|
||||
outline: 0;
|
||||
margin-bottom: 20px;
|
||||
padding-left: 0px;
|
||||
color: #ECF0F5;
|
||||
}
|
||||
|
||||
input[type=password] {
|
||||
background-color: #1A2226;
|
||||
border: none;
|
||||
border-bottom: 2px solid #0DB8DE;
|
||||
border-top: 0px;
|
||||
border-radius: 0px;
|
||||
font-weight: bold;
|
||||
outline: 0;
|
||||
padding-left: 0px;
|
||||
margin-bottom: 20px;
|
||||
color: #ECF0F5;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 40px;
|
||||
outline: 0px;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: inherit;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
border-bottom: 2px solid #0DB8DE;
|
||||
outline: 0;
|
||||
background-color: #1A2226;
|
||||
color: #ECF0F5;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.form-control-label {
|
||||
font-size: 10px;
|
||||
color: #6C6C6C;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.btn-outline-primary {
|
||||
border-color: #0DB8DE;
|
||||
color: #0DB8DE;
|
||||
border-radius: 0px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
.btn-outline-primary:hover {
|
||||
background-color: #0DB8DE;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.login-btm {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
padding-right: 0px;
|
||||
text-align: right;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.login-text {
|
||||
text-align: left;
|
||||
padding-left: 0px;
|
||||
color: #A2A4A4;
|
||||
}
|
||||
|
||||
.loginbttm {
|
||||
padding: 0px;
|
||||
}
|
@ -1,835 +0,0 @@
|
||||
@import url('https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css');
|
||||
|
||||
button:focus {
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
a { color: #8c8cfa; }
|
||||
|
||||
::-webkit-scrollbar-track-piece { background-color: rgba(255, 255, 255, .2) !important; }
|
||||
|
||||
::-webkit-scrollbar-track { background-color: rgba(255, 255, 255, .3) !important; }
|
||||
|
||||
::-webkit-scrollbar-thumb { background-color: rgba(255, 255, 255, .5) !important; }
|
||||
|
||||
embed[type="application/pdf"] { filter: invert(90%); }
|
||||
|
||||
html {
|
||||
background: #1f1f1f !important;
|
||||
box-sizing: border-box;
|
||||
filter: contrast(100%) brightness(100%) saturate(100%);
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #1f1f1f;
|
||||
background-color: #1f1f1f;
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
button, input, select, textarea {
|
||||
background-color: #1f1f1f;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
|
||||
font, html { color: #bfbfbf; }
|
||||
|
||||
.swagger-ui, .swagger-ui section h3 { color: #b5bac9; }
|
||||
|
||||
.swagger-ui a { background-color: transparent; }
|
||||
|
||||
.swagger-ui mark {
|
||||
background-color: #664b00;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
|
||||
.swagger-ui legend { color: inherit; }
|
||||
|
||||
.swagger-ui .debug * { outline: #e6da99 solid 1px; }
|
||||
|
||||
.swagger-ui .debug-white * { outline: #fff solid 1px; }
|
||||
|
||||
.swagger-ui .debug-black * { outline: #bfbfbf solid 1px; }
|
||||
|
||||
.swagger-ui .debug-grid { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTRDOTY4N0U2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTRDOTY4N0Q2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3NjY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3NzY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PsBS+GMAAAAjSURBVHjaYvz//z8DLsD4gcGXiYEAGBIKGBne//fFpwAgwAB98AaF2pjlUQAAAABJRU5ErkJggg==) 0 0; }
|
||||
|
||||
.swagger-ui .debug-grid-16 { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODYyRjhERDU2N0YyMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODYyRjhERDQ2N0YyMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3QTY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3QjY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvCS01IAAABMSURBVHjaYmR4/5+BFPBfAMFm/MBgx8RAGWCn1AAmSg34Q6kBDKMGMDCwICeMIemF/5QawEipAWwUhwEjMDvbAWlWkvVBwu8vQIABAEwBCph8U6c0AAAAAElFTkSuQmCC) 0 0; }
|
||||
|
||||
.swagger-ui .debug-grid-8-solid { background: url(data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAAAAD/4QMxaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjYtYzExMSA3OS4xNTgzMjUsIDIwMTUvMDkvMTAtMDE6MTA6MjAgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE1IChNYWNpbnRvc2gpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkIxMjI0OTczNjdCMzExRTZCMkJDRTI0MDgxMDAyMTcxIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkIxMjI0OTc0NjdCMzExRTZCMkJDRTI0MDgxMDAyMTcxIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QjEyMjQ5NzE2N0IzMTFFNkIyQkNFMjQwODEwMDIxNzEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QjEyMjQ5NzI2N0IzMTFFNkIyQkNFMjQwODEwMDIxNzEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/7gAOQWRvYmUAZMAAAAAB/9sAhAAbGhopHSlBJiZBQi8vL0JHPz4+P0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHAR0pKTQmND8oKD9HPzU/R0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0f/wAARCAAIAAgDASIAAhEBAxEB/8QAWQABAQAAAAAAAAAAAAAAAAAAAAYBAQEAAAAAAAAAAAAAAAAAAAIEEAEBAAMBAAAAAAAAAAAAAAABADECA0ERAAEDBQAAAAAAAAAAAAAAAAARITFBUWESIv/aAAwDAQACEQMRAD8AoOnTV1QTD7JJshP3vSM3P//Z) 0 0 #1c1c21; }
|
||||
|
||||
.swagger-ui .debug-grid-16-solid { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NzY3MkJEN0U2N0M1MTFFNkIyQkNFMjQwODEwMDIxNzEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NzY3MkJEN0Y2N0M1MTFFNkIyQkNFMjQwODEwMDIxNzEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3QzY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3RDY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pve6J3kAAAAzSURBVHjaYvz//z8D0UDsMwMjSRoYP5Gq4SPNbRjVMEQ1fCRDg+in/6+J1AJUxsgAEGAA31BAJMS0GYEAAAAASUVORK5CYII=) 0 0 #1c1c21; }
|
||||
|
||||
.swagger-ui .b--black { border-color: #000; }
|
||||
|
||||
.swagger-ui .b--near-black { border-color: #121212; }
|
||||
|
||||
.swagger-ui .b--dark-gray { border-color: #333; }
|
||||
|
||||
.swagger-ui .b--mid-gray { border-color: #545454; }
|
||||
|
||||
.swagger-ui .b--gray { border-color: #787878; }
|
||||
|
||||
.swagger-ui .b--silver { border-color: #999; }
|
||||
|
||||
.swagger-ui .b--light-silver { border-color: #6e6e6e; }
|
||||
|
||||
.swagger-ui .b--moon-gray { border-color: #4d4d4d; }
|
||||
|
||||
.swagger-ui .b--light-gray { border-color: #2b2b2b; }
|
||||
|
||||
.swagger-ui .b--near-white { border-color: #242424; }
|
||||
|
||||
.swagger-ui .b--white { border-color: #1c1c21; }
|
||||
|
||||
.swagger-ui .b--white-90 { border-color: rgba(28, 28, 33, .9); }
|
||||
|
||||
.swagger-ui .b--white-80 { border-color: rgba(28, 28, 33, .8); }
|
||||
|
||||
.swagger-ui .b--white-70 { border-color: rgba(28, 28, 33, .7); }
|
||||
|
||||
.swagger-ui .b--white-60 { border-color: rgba(28, 28, 33, .6); }
|
||||
|
||||
.swagger-ui .b--white-50 { border-color: rgba(28, 28, 33, .5); }
|
||||
|
||||
.swagger-ui .b--white-40 { border-color: rgba(28, 28, 33, .4); }
|
||||
|
||||
.swagger-ui .b--white-30 { border-color: rgba(28, 28, 33, .3); }
|
||||
|
||||
.swagger-ui .b--white-20 { border-color: rgba(28, 28, 33, .2); }
|
||||
|
||||
.swagger-ui .b--white-10 { border-color: rgba(28, 28, 33, .1); }
|
||||
|
||||
.swagger-ui .b--white-05 { border-color: rgba(28, 28, 33, .05); }
|
||||
|
||||
.swagger-ui .b--white-025 { border-color: rgba(28, 28, 33, .024); }
|
||||
|
||||
.swagger-ui .b--white-0125 { border-color: rgba(28, 28, 33, .01); }
|
||||
|
||||
.swagger-ui .b--black-90 { border-color: rgba(0, 0, 0, .9); }
|
||||
|
||||
.swagger-ui .b--black-80 { border-color: rgba(0, 0, 0, .8); }
|
||||
|
||||
.swagger-ui .b--black-70 { border-color: rgba(0, 0, 0, .7); }
|
||||
|
||||
.swagger-ui .b--black-60 { border-color: rgba(0, 0, 0, .6); }
|
||||
|
||||
.swagger-ui .b--black-50 { border-color: rgba(0, 0, 0, .5); }
|
||||
|
||||
.swagger-ui .b--black-40 { border-color: rgba(0, 0, 0, .4); }
|
||||
|
||||
.swagger-ui .b--black-30 { border-color: rgba(0, 0, 0, .3); }
|
||||
|
||||
.swagger-ui .b--black-20 { border-color: rgba(0, 0, 0, .2); }
|
||||
|
||||
.swagger-ui .b--black-10 { border-color: rgba(0, 0, 0, .1); }
|
||||
|
||||
.swagger-ui .b--black-05 { border-color: rgba(0, 0, 0, .05); }
|
||||
|
||||
.swagger-ui .b--black-025 { border-color: rgba(0, 0, 0, .024); }
|
||||
|
||||
.swagger-ui .b--black-0125 { border-color: rgba(0, 0, 0, .01); }
|
||||
|
||||
.swagger-ui .b--dark-red { border-color: #bc2f36; }
|
||||
|
||||
.swagger-ui .b--red { border-color: #c83932; }
|
||||
|
||||
.swagger-ui .b--light-red { border-color: #ab3c2b; }
|
||||
|
||||
.swagger-ui .b--orange { border-color: #cc6e33; }
|
||||
|
||||
.swagger-ui .b--purple { border-color: #5e2ca5; }
|
||||
|
||||
.swagger-ui .b--light-purple { border-color: #672caf; }
|
||||
|
||||
.swagger-ui .b--dark-pink { border-color: #ab2b81; }
|
||||
|
||||
.swagger-ui .b--hot-pink { border-color: #c03086; }
|
||||
|
||||
.swagger-ui .b--pink { border-color: #8f2464; }
|
||||
|
||||
.swagger-ui .b--light-pink { border-color: #721d4d; }
|
||||
|
||||
.swagger-ui .b--dark-green { border-color: #1c6e50; }
|
||||
|
||||
.swagger-ui .b--green { border-color: #279b70; }
|
||||
|
||||
.swagger-ui .b--light-green { border-color: #228762; }
|
||||
|
||||
.swagger-ui .b--navy { border-color: #0d1d35; }
|
||||
|
||||
.swagger-ui .b--dark-blue { border-color: #20497e; }
|
||||
|
||||
.swagger-ui .b--blue { border-color: #4380d0; }
|
||||
|
||||
.swagger-ui .b--light-blue { border-color: #20517e; }
|
||||
|
||||
.swagger-ui .b--lightest-blue { border-color: #143a52; }
|
||||
|
||||
.swagger-ui .b--washed-blue { border-color: #0c312d; }
|
||||
|
||||
.swagger-ui .b--washed-green { border-color: #0f3d2c; }
|
||||
|
||||
.swagger-ui .b--washed-red { border-color: #411010; }
|
||||
|
||||
.swagger-ui .b--transparent { border-color: transparent; }
|
||||
|
||||
.swagger-ui .b--gold, .swagger-ui .b--light-yellow, .swagger-ui .b--washed-yellow, .swagger-ui .b--yellow { border-color: #664b00; }
|
||||
|
||||
.swagger-ui .shadow-1 { box-shadow: rgba(0, 0, 0, .2) 0 0 4px 2px; }
|
||||
|
||||
.swagger-ui .shadow-2 { box-shadow: rgba(0, 0, 0, .2) 0 0 8px 2px; }
|
||||
|
||||
.swagger-ui .shadow-3 { box-shadow: rgba(0, 0, 0, .2) 2px 2px 4px 2px; }
|
||||
|
||||
.swagger-ui .shadow-4 { box-shadow: rgba(0, 0, 0, .2) 2px 2px 8px 0; }
|
||||
|
||||
.swagger-ui .shadow-5 { box-shadow: rgba(0, 0, 0, .2) 4px 4px 8px 0; }
|
||||
|
||||
@media screen and (min-width: 30em) {
|
||||
.swagger-ui .shadow-1-ns { box-shadow: rgba(0, 0, 0, .2) 0 0 4px 2px; }
|
||||
|
||||
.swagger-ui .shadow-2-ns { box-shadow: rgba(0, 0, 0, .2) 0 0 8px 2px; }
|
||||
|
||||
.swagger-ui .shadow-3-ns { box-shadow: rgba(0, 0, 0, .2) 2px 2px 4px 2px; }
|
||||
|
||||
.swagger-ui .shadow-4-ns { box-shadow: rgba(0, 0, 0, .2) 2px 2px 8px 0; }
|
||||
|
||||
.swagger-ui .shadow-5-ns { box-shadow: rgba(0, 0, 0, .2) 4px 4px 8px 0; }
|
||||
}
|
||||
|
||||
@media screen and (max-width: 60em) and (min-width: 30em) {
|
||||
.swagger-ui .shadow-1-m { box-shadow: rgba(0, 0, 0, .2) 0 0 4px 2px; }
|
||||
|
||||
.swagger-ui .shadow-2-m { box-shadow: rgba(0, 0, 0, .2) 0 0 8px 2px; }
|
||||
|
||||
.swagger-ui .shadow-3-m { box-shadow: rgba(0, 0, 0, .2) 2px 2px 4px 2px; }
|
||||
|
||||
.swagger-ui .shadow-4-m { box-shadow: rgba(0, 0, 0, .2) 2px 2px 8px 0; }
|
||||
|
||||
.swagger-ui .shadow-5-m { box-shadow: rgba(0, 0, 0, .2) 4px 4px 8px 0; }
|
||||
}
|
||||
|
||||
@media screen and (min-width: 60em) {
|
||||
.swagger-ui .shadow-1-l { box-shadow: rgba(0, 0, 0, .2) 0 0 4px 2px; }
|
||||
|
||||
.swagger-ui .shadow-2-l { box-shadow: rgba(0, 0, 0, .2) 0 0 8px 2px; }
|
||||
|
||||
.swagger-ui .shadow-3-l { box-shadow: rgba(0, 0, 0, .2) 2px 2px 4px 2px; }
|
||||
|
||||
.swagger-ui .shadow-4-l { box-shadow: rgba(0, 0, 0, .2) 2px 2px 8px 0; }
|
||||
|
||||
.swagger-ui .shadow-5-l { box-shadow: rgba(0, 0, 0, .2) 4px 4px 8px 0; }
|
||||
}
|
||||
|
||||
.swagger-ui .black-05 { color: rgba(191, 191, 191, .05); }
|
||||
|
||||
.swagger-ui .bg-black-05 { background-color: rgba(0, 0, 0, .05); }
|
||||
|
||||
.swagger-ui .black-90, .swagger-ui .hover-black-90:focus, .swagger-ui .hover-black-90:hover { color: rgba(191, 191, 191, .9); }
|
||||
|
||||
.swagger-ui .black-80, .swagger-ui .hover-black-80:focus, .swagger-ui .hover-black-80:hover { color: rgba(191, 191, 191, .8); }
|
||||
|
||||
.swagger-ui .black-70, .swagger-ui .hover-black-70:focus, .swagger-ui .hover-black-70:hover { color: rgba(191, 191, 191, .7); }
|
||||
|
||||
.swagger-ui .black-60, .swagger-ui .hover-black-60:focus, .swagger-ui .hover-black-60:hover { color: rgba(191, 191, 191, .6); }
|
||||
|
||||
.swagger-ui .black-50, .swagger-ui .hover-black-50:focus, .swagger-ui .hover-black-50:hover { color: rgba(191, 191, 191, .5); }
|
||||
|
||||
.swagger-ui .black-40, .swagger-ui .hover-black-40:focus, .swagger-ui .hover-black-40:hover { color: rgba(191, 191, 191, .4); }
|
||||
|
||||
.swagger-ui .black-30, .swagger-ui .hover-black-30:focus, .swagger-ui .hover-black-30:hover { color: rgba(191, 191, 191, .3); }
|
||||
|
||||
.swagger-ui .black-20, .swagger-ui .hover-black-20:focus, .swagger-ui .hover-black-20:hover { color: rgba(191, 191, 191, .2); }
|
||||
|
||||
.swagger-ui .black-10, .swagger-ui .hover-black-10:focus, .swagger-ui .hover-black-10:hover { color: rgba(191, 191, 191, .1); }
|
||||
|
||||
.swagger-ui .hover-white-90:focus, .swagger-ui .hover-white-90:hover, .swagger-ui .white-90 { color: rgba(255, 255, 255, .9); }
|
||||
|
||||
.swagger-ui .hover-white-80:focus, .swagger-ui .hover-white-80:hover, .swagger-ui .white-80 { color: rgba(255, 255, 255, .8); }
|
||||
|
||||
.swagger-ui .hover-white-70:focus, .swagger-ui .hover-white-70:hover, .swagger-ui .white-70 { color: rgba(255, 255, 255, .7); }
|
||||
|
||||
.swagger-ui .hover-white-60:focus, .swagger-ui .hover-white-60:hover, .swagger-ui .white-60 { color: rgba(255, 255, 255, .6); }
|
||||
|
||||
.swagger-ui .hover-white-50:focus, .swagger-ui .hover-white-50:hover, .swagger-ui .white-50 { color: rgba(255, 255, 255, .5); }
|
||||
|
||||
.swagger-ui .hover-white-40:focus, .swagger-ui .hover-white-40:hover, .swagger-ui .white-40 { color: rgba(255, 255, 255, .4); }
|
||||
|
||||
.swagger-ui .hover-white-30:focus, .swagger-ui .hover-white-30:hover, .swagger-ui .white-30 { color: rgba(255, 255, 255, .3); }
|
||||
|
||||
.swagger-ui .hover-white-20:focus, .swagger-ui .hover-white-20:hover, .swagger-ui .white-20 { color: rgba(255, 255, 255, .2); }
|
||||
|
||||
.swagger-ui .hover-white-10:focus, .swagger-ui .hover-white-10:hover, .swagger-ui .white-10 { color: rgba(255, 255, 255, .1); }
|
||||
|
||||
.swagger-ui .hover-moon-gray:focus, .swagger-ui .hover-moon-gray:hover, .swagger-ui .moon-gray { color: #ccc; }
|
||||
|
||||
.swagger-ui .hover-light-gray:focus, .swagger-ui .hover-light-gray:hover, .swagger-ui .light-gray { color: #ededed; }
|
||||
|
||||
.swagger-ui .hover-near-white:focus, .swagger-ui .hover-near-white:hover, .swagger-ui .near-white { color: #f5f5f5; }
|
||||
|
||||
.swagger-ui .dark-red, .swagger-ui .hover-dark-red:focus, .swagger-ui .hover-dark-red:hover { color: #e6999d; }
|
||||
|
||||
.swagger-ui .hover-red:focus, .swagger-ui .hover-red:hover, .swagger-ui .red { color: #e69d99; }
|
||||
|
||||
.swagger-ui .hover-light-red:focus, .swagger-ui .hover-light-red:hover, .swagger-ui .light-red { color: #e6a399; }
|
||||
|
||||
.swagger-ui .hover-orange:focus, .swagger-ui .hover-orange:hover, .swagger-ui .orange { color: #e6b699; }
|
||||
|
||||
.swagger-ui .gold, .swagger-ui .hover-gold:focus, .swagger-ui .hover-gold:hover { color: #e6d099; }
|
||||
|
||||
.swagger-ui .hover-yellow:focus, .swagger-ui .hover-yellow:hover, .swagger-ui .yellow { color: #e6da99; }
|
||||
|
||||
.swagger-ui .hover-light-yellow:focus, .swagger-ui .hover-light-yellow:hover, .swagger-ui .light-yellow { color: #ede6b6; }
|
||||
|
||||
.swagger-ui .hover-purple:focus, .swagger-ui .hover-purple:hover, .swagger-ui .purple { color: #b99ae4; }
|
||||
|
||||
.swagger-ui .hover-light-purple:focus, .swagger-ui .hover-light-purple:hover, .swagger-ui .light-purple { color: #bb99e6; }
|
||||
|
||||
.swagger-ui .dark-pink, .swagger-ui .hover-dark-pink:focus, .swagger-ui .hover-dark-pink:hover { color: #e699cc; }
|
||||
|
||||
.swagger-ui .hot-pink, .swagger-ui .hover-hot-pink:focus, .swagger-ui .hover-hot-pink:hover, .swagger-ui .hover-pink:focus, .swagger-ui .hover-pink:hover, .swagger-ui .pink { color: #e699c7; }
|
||||
|
||||
.swagger-ui .hover-light-pink:focus, .swagger-ui .hover-light-pink:hover, .swagger-ui .light-pink { color: #edb6d5; }
|
||||
|
||||
.swagger-ui .dark-green, .swagger-ui .green, .swagger-ui .hover-dark-green:focus, .swagger-ui .hover-dark-green:hover, .swagger-ui .hover-green:focus, .swagger-ui .hover-green:hover { color: #99e6c9; }
|
||||
|
||||
.swagger-ui .hover-light-green:focus, .swagger-ui .hover-light-green:hover, .swagger-ui .light-green { color: #a1e8ce; }
|
||||
|
||||
.swagger-ui .hover-navy:focus, .swagger-ui .hover-navy:hover, .swagger-ui .navy { color: #99b8e6; }
|
||||
|
||||
.swagger-ui .blue, .swagger-ui .dark-blue, .swagger-ui .hover-blue:focus, .swagger-ui .hover-blue:hover, .swagger-ui .hover-dark-blue:focus, .swagger-ui .hover-dark-blue:hover { color: #99bae6; }
|
||||
|
||||
.swagger-ui .hover-light-blue:focus, .swagger-ui .hover-light-blue:hover, .swagger-ui .light-blue { color: #a9cbea; }
|
||||
|
||||
.swagger-ui .hover-lightest-blue:focus, .swagger-ui .hover-lightest-blue:hover, .swagger-ui .lightest-blue { color: #d6e9f5; }
|
||||
|
||||
.swagger-ui .hover-washed-blue:focus, .swagger-ui .hover-washed-blue:hover, .swagger-ui .washed-blue { color: #f7fdfc; }
|
||||
|
||||
.swagger-ui .hover-washed-green:focus, .swagger-ui .hover-washed-green:hover, .swagger-ui .washed-green { color: #ebfaf4; }
|
||||
|
||||
.swagger-ui .hover-washed-yellow:focus, .swagger-ui .hover-washed-yellow:hover, .swagger-ui .washed-yellow { color: #fbf9ef; }
|
||||
|
||||
.swagger-ui .hover-washed-red:focus, .swagger-ui .hover-washed-red:hover, .swagger-ui .washed-red { color: #f9e7e7; }
|
||||
|
||||
.swagger-ui .color-inherit, .swagger-ui .hover-inherit:focus, .swagger-ui .hover-inherit:hover { color: inherit; }
|
||||
|
||||
.swagger-ui .bg-black-90, .swagger-ui .hover-bg-black-90:focus, .swagger-ui .hover-bg-black-90:hover { background-color: rgba(0, 0, 0, .9); }
|
||||
|
||||
.swagger-ui .bg-black-80, .swagger-ui .hover-bg-black-80:focus, .swagger-ui .hover-bg-black-80:hover { background-color: rgba(0, 0, 0, .8); }
|
||||
|
||||
.swagger-ui .bg-black-70, .swagger-ui .hover-bg-black-70:focus, .swagger-ui .hover-bg-black-70:hover { background-color: rgba(0, 0, 0, .7); }
|
||||
|
||||
.swagger-ui .bg-black-60, .swagger-ui .hover-bg-black-60:focus, .swagger-ui .hover-bg-black-60:hover { background-color: rgba(0, 0, 0, .6); }
|
||||
|
||||
.swagger-ui .bg-black-50, .swagger-ui .hover-bg-black-50:focus, .swagger-ui .hover-bg-black-50:hover { background-color: rgba(0, 0, 0, .5); }
|
||||
|
||||
.swagger-ui .bg-black-40, .swagger-ui .hover-bg-black-40:focus, .swagger-ui .hover-bg-black-40:hover { background-color: rgba(0, 0, 0, .4); }
|
||||
|
||||
.swagger-ui .bg-black-30, .swagger-ui .hover-bg-black-30:focus, .swagger-ui .hover-bg-black-30:hover { background-color: rgba(0, 0, 0, .3); }
|
||||
|
||||
.swagger-ui .bg-black-20, .swagger-ui .hover-bg-black-20:focus, .swagger-ui .hover-bg-black-20:hover { background-color: rgba(0, 0, 0, .2); }
|
||||
|
||||
.swagger-ui .bg-white-90, .swagger-ui .hover-bg-white-90:focus, .swagger-ui .hover-bg-white-90:hover { background-color: rgba(28, 28, 33, .9); }
|
||||
|
||||
.swagger-ui .bg-white-80, .swagger-ui .hover-bg-white-80:focus, .swagger-ui .hover-bg-white-80:hover { background-color: rgba(28, 28, 33, .8); }
|
||||
|
||||
.swagger-ui .bg-white-70, .swagger-ui .hover-bg-white-70:focus, .swagger-ui .hover-bg-white-70:hover { background-color: rgba(28, 28, 33, .7); }
|
||||
|
||||
.swagger-ui .bg-white-60, .swagger-ui .hover-bg-white-60:focus, .swagger-ui .hover-bg-white-60:hover { background-color: rgba(28, 28, 33, .6); }
|
||||
|
||||
.swagger-ui .bg-white-50, .swagger-ui .hover-bg-white-50:focus, .swagger-ui .hover-bg-white-50:hover { background-color: rgba(28, 28, 33, .5); }
|
||||
|
||||
.swagger-ui .bg-white-40, .swagger-ui .hover-bg-white-40:focus, .swagger-ui .hover-bg-white-40:hover { background-color: rgba(28, 28, 33, .4); }
|
||||
|
||||
.swagger-ui .bg-white-30, .swagger-ui .hover-bg-white-30:focus, .swagger-ui .hover-bg-white-30:hover { background-color: rgba(28, 28, 33, .3); }
|
||||
|
||||
.swagger-ui .bg-white-20, .swagger-ui .hover-bg-white-20:focus, .swagger-ui .hover-bg-white-20:hover { background-color: rgba(28, 28, 33, .2); }
|
||||
|
||||
.swagger-ui .bg-black, .swagger-ui .hover-bg-black:focus, .swagger-ui .hover-bg-black:hover { background-color: #000; }
|
||||
|
||||
.swagger-ui .bg-near-black, .swagger-ui .hover-bg-near-black:focus, .swagger-ui .hover-bg-near-black:hover { background-color: #121212; }
|
||||
|
||||
.swagger-ui .bg-dark-gray, .swagger-ui .hover-bg-dark-gray:focus, .swagger-ui .hover-bg-dark-gray:hover { background-color: #333; }
|
||||
|
||||
.swagger-ui .bg-mid-gray, .swagger-ui .hover-bg-mid-gray:focus, .swagger-ui .hover-bg-mid-gray:hover { background-color: #545454; }
|
||||
|
||||
.swagger-ui .bg-gray, .swagger-ui .hover-bg-gray:focus, .swagger-ui .hover-bg-gray:hover { background-color: #787878; }
|
||||
|
||||
.swagger-ui .bg-silver, .swagger-ui .hover-bg-silver:focus, .swagger-ui .hover-bg-silver:hover { background-color: #999; }
|
||||
|
||||
.swagger-ui .bg-white, .swagger-ui .hover-bg-white:focus, .swagger-ui .hover-bg-white:hover { background-color: #1c1c21; }
|
||||
|
||||
.swagger-ui .bg-transparent, .swagger-ui .hover-bg-transparent:focus, .swagger-ui .hover-bg-transparent:hover { background-color: transparent; }
|
||||
|
||||
.swagger-ui .bg-dark-red, .swagger-ui .hover-bg-dark-red:focus, .swagger-ui .hover-bg-dark-red:hover { background-color: #bc2f36; }
|
||||
|
||||
.swagger-ui .bg-red, .swagger-ui .hover-bg-red:focus, .swagger-ui .hover-bg-red:hover { background-color: #c83932; }
|
||||
|
||||
.swagger-ui .bg-light-red, .swagger-ui .hover-bg-light-red:focus, .swagger-ui .hover-bg-light-red:hover { background-color: #ab3c2b; }
|
||||
|
||||
.swagger-ui .bg-orange, .swagger-ui .hover-bg-orange:focus, .swagger-ui .hover-bg-orange:hover { background-color: #cc6e33; }
|
||||
|
||||
.swagger-ui .bg-gold, .swagger-ui .bg-light-yellow, .swagger-ui .bg-washed-yellow, .swagger-ui .bg-yellow, .swagger-ui .hover-bg-gold:focus, .swagger-ui .hover-bg-gold:hover, .swagger-ui .hover-bg-light-yellow:focus, .swagger-ui .hover-bg-light-yellow:hover, .swagger-ui .hover-bg-washed-yellow:focus, .swagger-ui .hover-bg-washed-yellow:hover, .swagger-ui .hover-bg-yellow:focus, .swagger-ui .hover-bg-yellow:hover { background-color: #664b00; }
|
||||
|
||||
.swagger-ui .bg-purple, .swagger-ui .hover-bg-purple:focus, .swagger-ui .hover-bg-purple:hover { background-color: #5e2ca5; }
|
||||
|
||||
.swagger-ui .bg-light-purple, .swagger-ui .hover-bg-light-purple:focus, .swagger-ui .hover-bg-light-purple:hover { background-color: #672caf; }
|
||||
|
||||
.swagger-ui .bg-dark-pink, .swagger-ui .hover-bg-dark-pink:focus, .swagger-ui .hover-bg-dark-pink:hover { background-color: #ab2b81; }
|
||||
|
||||
.swagger-ui .bg-hot-pink, .swagger-ui .hover-bg-hot-pink:focus, .swagger-ui .hover-bg-hot-pink:hover { background-color: #c03086; }
|
||||
|
||||
.swagger-ui .bg-pink, .swagger-ui .hover-bg-pink:focus, .swagger-ui .hover-bg-pink:hover { background-color: #8f2464; }
|
||||
|
||||
.swagger-ui .bg-light-pink, .swagger-ui .hover-bg-light-pink:focus, .swagger-ui .hover-bg-light-pink:hover { background-color: #721d4d; }
|
||||
|
||||
.swagger-ui .bg-dark-green, .swagger-ui .hover-bg-dark-green:focus, .swagger-ui .hover-bg-dark-green:hover { background-color: #1c6e50; }
|
||||
|
||||
.swagger-ui .bg-green, .swagger-ui .hover-bg-green:focus, .swagger-ui .hover-bg-green:hover { background-color: #279b70; }
|
||||
|
||||
.swagger-ui .bg-light-green, .swagger-ui .hover-bg-light-green:focus, .swagger-ui .hover-bg-light-green:hover { background-color: #228762; }
|
||||
|
||||
.swagger-ui .bg-navy, .swagger-ui .hover-bg-navy:focus, .swagger-ui .hover-bg-navy:hover { background-color: #0d1d35; }
|
||||
|
||||
.swagger-ui .bg-dark-blue, .swagger-ui .hover-bg-dark-blue:focus, .swagger-ui .hover-bg-dark-blue:hover { background-color: #20497e; }
|
||||
|
||||
.swagger-ui .bg-blue, .swagger-ui .hover-bg-blue:focus, .swagger-ui .hover-bg-blue:hover { background-color: #4380d0; }
|
||||
|
||||
.swagger-ui .bg-light-blue, .swagger-ui .hover-bg-light-blue:focus, .swagger-ui .hover-bg-light-blue:hover { background-color: #20517e; }
|
||||
|
||||
.swagger-ui .bg-lightest-blue, .swagger-ui .hover-bg-lightest-blue:focus, .swagger-ui .hover-bg-lightest-blue:hover { background-color: #143a52; }
|
||||
|
||||
.swagger-ui .bg-washed-blue, .swagger-ui .hover-bg-washed-blue:focus, .swagger-ui .hover-bg-washed-blue:hover { background-color: #0c312d; }
|
||||
|
||||
.swagger-ui .bg-washed-green, .swagger-ui .hover-bg-washed-green:focus, .swagger-ui .hover-bg-washed-green:hover { background-color: #0f3d2c; }
|
||||
|
||||
.swagger-ui .bg-washed-red, .swagger-ui .hover-bg-washed-red:focus, .swagger-ui .hover-bg-washed-red:hover { background-color: #411010; }
|
||||
|
||||
.swagger-ui .bg-inherit, .swagger-ui .hover-bg-inherit:focus, .swagger-ui .hover-bg-inherit:hover { background-color: inherit; }
|
||||
|
||||
.swagger-ui .shadow-hover { transition: all .5s cubic-bezier(.165, .84, .44, 1) 0s; }
|
||||
|
||||
.swagger-ui .shadow-hover::after {
|
||||
border-radius: inherit;
|
||||
box-shadow: rgba(0, 0, 0, .2) 0 0 16px 2px;
|
||||
content: "";
|
||||
height: 100%;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transition: opacity .5s cubic-bezier(.165, .84, .44, 1) 0s;
|
||||
width: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.swagger-ui .bg-animate, .swagger-ui .bg-animate:focus, .swagger-ui .bg-animate:hover { transition: background-color .15s ease-in-out 0s; }
|
||||
|
||||
.swagger-ui .nested-links a {
|
||||
color: #99bae6;
|
||||
transition: color .15s ease-in 0s;
|
||||
}
|
||||
|
||||
.swagger-ui .nested-links a:focus, .swagger-ui .nested-links a:hover {
|
||||
color: #a9cbea;
|
||||
transition: color .15s ease-in 0s;
|
||||
}
|
||||
|
||||
.swagger-ui .opblock-tag {
|
||||
border-bottom: 1px solid rgba(58, 64, 80, .3);
|
||||
color: #b5bac9;
|
||||
transition: all .2s ease 0s;
|
||||
}
|
||||
|
||||
.swagger-ui .opblock-tag svg, .swagger-ui section.models h4 svg { transition: all .4s ease 0s; }
|
||||
|
||||
.swagger-ui .opblock {
|
||||
border: 1px solid #000;
|
||||
border-radius: 4px;
|
||||
box-shadow: rgba(0, 0, 0, .19) 0 0 3px;
|
||||
margin: 0 0 15px;
|
||||
}
|
||||
|
||||
.swagger-ui .opblock .tab-header .tab-item.active h4 span::after { background: gray; }
|
||||
|
||||
.swagger-ui .opblock.is-open .opblock-summary { border-bottom: 1px solid #000; }
|
||||
|
||||
.swagger-ui .opblock .opblock-section-header {
|
||||
background: rgba(28, 28, 33, .8);
|
||||
box-shadow: rgba(0, 0, 0, .1) 0 1px 2px;
|
||||
}
|
||||
|
||||
.swagger-ui .opblock .opblock-section-header > label > span { padding: 0 10px 0 0; }
|
||||
|
||||
.swagger-ui .opblock .opblock-summary-method {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
text-shadow: rgba(0, 0, 0, .1) 0 1px 0;
|
||||
}
|
||||
|
||||
.swagger-ui .opblock.opblock-post {
|
||||
background: rgba(72, 203, 144, .1);
|
||||
border-color: #48cb90;
|
||||
}
|
||||
|
||||
.swagger-ui .opblock.opblock-post .opblock-summary-method, .swagger-ui .opblock.opblock-post .tab-header .tab-item.active h4 span::after { background: #48cb90; }
|
||||
|
||||
.swagger-ui .opblock.opblock-post .opblock-summary { border-color: #48cb90; }
|
||||
|
||||
.swagger-ui .opblock.opblock-put {
|
||||
background: rgba(213, 157, 88, .1);
|
||||
border-color: #d59d58;
|
||||
}
|
||||
|
||||
.swagger-ui .opblock.opblock-put .opblock-summary-method, .swagger-ui .opblock.opblock-put .tab-header .tab-item.active h4 span::after { background: #d59d58; }
|
||||
|
||||
.swagger-ui .opblock.opblock-put .opblock-summary { border-color: #d59d58; }
|
||||
|
||||
.swagger-ui .opblock.opblock-delete {
|
||||
background: rgba(200, 50, 50, .1);
|
||||
border-color: #c83232;
|
||||
}
|
||||
|
||||
.swagger-ui .opblock.opblock-delete .opblock-summary-method, .swagger-ui .opblock.opblock-delete .tab-header .tab-item.active h4 span::after { background: #c83232; }
|
||||
|
||||
.swagger-ui .opblock.opblock-delete .opblock-summary { border-color: #c83232; }
|
||||
|
||||
.swagger-ui .opblock.opblock-get {
|
||||
background: rgba(42, 105, 167, .1);
|
||||
border-color: #2a69a7;
|
||||
}
|
||||
|
||||
.swagger-ui .opblock.opblock-get .opblock-summary-method, .swagger-ui .opblock.opblock-get .tab-header .tab-item.active h4 span::after { background: #2a69a7; }
|
||||
|
||||
.swagger-ui .opblock.opblock-get .opblock-summary { border-color: #2a69a7; }
|
||||
|
||||
.swagger-ui .opblock.opblock-patch {
|
||||
background: rgba(92, 214, 188, .1);
|
||||
border-color: #5cd6bc;
|
||||
}
|
||||
|
||||
.swagger-ui .opblock.opblock-patch .opblock-summary-method, .swagger-ui .opblock.opblock-patch .tab-header .tab-item.active h4 span::after { background: #5cd6bc; }
|
||||
|
||||
.swagger-ui .opblock.opblock-patch .opblock-summary { border-color: #5cd6bc; }
|
||||
|
||||
.swagger-ui .opblock.opblock-head {
|
||||
background: rgba(140, 63, 207, .1);
|
||||
border-color: #8c3fcf;
|
||||
}
|
||||
|
||||
.swagger-ui .opblock.opblock-head .opblock-summary-method, .swagger-ui .opblock.opblock-head .tab-header .tab-item.active h4 span::after { background: #8c3fcf; }
|
||||
|
||||
.swagger-ui .opblock.opblock-head .opblock-summary { border-color: #8c3fcf; }
|
||||
|
||||
.swagger-ui .opblock.opblock-options {
|
||||
background: rgba(36, 89, 143, .1);
|
||||
border-color: #24598f;
|
||||
}
|
||||
|
||||
.swagger-ui .opblock.opblock-options .opblock-summary-method, .swagger-ui .opblock.opblock-options .tab-header .tab-item.active h4 span::after { background: #24598f; }
|
||||
|
||||
.swagger-ui .opblock.opblock-options .opblock-summary { border-color: #24598f; }
|
||||
|
||||
.swagger-ui .opblock.opblock-deprecated {
|
||||
background: rgba(46, 46, 46, .1);
|
||||
border-color: #2e2e2e;
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
.swagger-ui .opblock.opblock-deprecated .opblock-summary-method, .swagger-ui .opblock.opblock-deprecated .tab-header .tab-item.active h4 span::after { background: #2e2e2e; }
|
||||
|
||||
.swagger-ui .opblock.opblock-deprecated .opblock-summary { border-color: #2e2e2e; }
|
||||
|
||||
.swagger-ui .filter .operation-filter-input { border: 2px solid #2b3446; }
|
||||
|
||||
.swagger-ui .tab li:first-of-type::after { background: rgba(0, 0, 0, .2); }
|
||||
|
||||
.swagger-ui .download-contents {
|
||||
background: #7c8192;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.swagger-ui .scheme-container {
|
||||
background: #1c1c21;
|
||||
box-shadow: rgba(0, 0, 0, .15) 0 1px 2px 0;
|
||||
}
|
||||
|
||||
.swagger-ui .loading-container .loading::before {
|
||||
animation: 1s linear 0s infinite normal none running rotation, .5s ease 0s 1 normal none running opacity;
|
||||
border-color: rgba(0, 0, 0, .6) rgba(84, 84, 84, .1) rgba(84, 84, 84, .1);
|
||||
}
|
||||
|
||||
.swagger-ui .response-control-media-type--accept-controller select { border-color: #196619; }
|
||||
|
||||
.swagger-ui .response-control-media-type__accept-message { color: #99e699; }
|
||||
|
||||
.swagger-ui .version-pragma__message code { background-color: #3b3b3b; }
|
||||
|
||||
.swagger-ui .btn {
|
||||
background: 0 0;
|
||||
border: 2px solid gray;
|
||||
box-shadow: rgba(0, 0, 0, .1) 0 1px 2px;
|
||||
color: #b5bac9;
|
||||
}
|
||||
|
||||
.swagger-ui .btn:hover { box-shadow: rgba(0, 0, 0, .3) 0 0 5px; }
|
||||
|
||||
.swagger-ui .btn.authorize, .swagger-ui .btn.cancel {
|
||||
background-color: transparent;
|
||||
border-color: #a72a2a;
|
||||
color: #e69999;
|
||||
}
|
||||
|
||||
.swagger-ui .btn.authorize {
|
||||
border-color: #48cb90;
|
||||
color: #9ce3c3;
|
||||
}
|
||||
|
||||
.swagger-ui .btn.authorize svg { fill: #9ce3c3; }
|
||||
|
||||
.swagger-ui .btn.execute {
|
||||
background-color: #5892d5;
|
||||
border-color: #5892d5;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.swagger-ui .copy-to-clipboard { background: #7c8192; }
|
||||
|
||||
.swagger-ui .copy-to-clipboard button { background: url("data:image/svg+xml;charset=utf-8,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path fill=\"%23fff\" fill-rule=\"evenodd\" d=\"M2 13h4v1H2v-1zm5-6H2v1h5V7zm2 3V8l-3 3 3 3v-2h5v-2H9zM4.5 9H2v1h2.5V9zM2 12h2.5v-1H2v1zm9 1h1v2c-.02.28-.11.52-.3.7-.19.18-.42.28-.7.3H1c-.55 0-1-.45-1-1V4c0-.55.45-1 1-1h3c0-1.11.89-2 2-2 1.11 0 2 .89 2 2h3c.55 0 1 .45 1 1v5h-1V6H1v9h10v-2zM2 5h8c0-.55-.45-1-1-1H8c-.55 0-1-.45-1-1s-.45-1-1-1-1 .45-1 1-.45 1-1 1H3c-.55 0-1 .45-1 1z\"/></svg>") 50% center no-repeat; }
|
||||
|
||||
.swagger-ui select {
|
||||
background: url("data:image/svg+xml;charset=utf-8,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\"><path d=\"M13.418 7.859a.695.695 0 01.978 0 .68.68 0 010 .969l-3.908 3.83a.697.697 0 01-.979 0l-3.908-3.83a.68.68 0 010-.969.695.695 0 01.978 0L10 11l3.418-3.141z\"/></svg>") right 10px center/20px no-repeat #212121;
|
||||
background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMS4wICg0MDM1YTRmYjQ5LCAyMDIwLTA1LTAxKSIKICAgc29kaXBvZGk6ZG9jbmFtZT0iZG93bmxvYWQuc3ZnIgogICBpZD0ic3ZnNCIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMjAgMjAiPgogIDxtZXRhZGF0YQogICAgIGlkPSJtZXRhZGF0YTEwIj4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZGVmcwogICAgIGlkPSJkZWZzOCIgLz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ic3ZnNCIKICAgICBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIxIgogICAgIGlua3NjYXBlOndpbmRvdy15PSItOSIKICAgICBpbmtzY2FwZTp3aW5kb3cteD0iLTkiCiAgICAgaW5rc2NhcGU6Y3k9IjEwIgogICAgIGlua3NjYXBlOmN4PSIxMCIKICAgICBpbmtzY2FwZTp6b29tPSI0MS41IgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpZD0ibmFtZWR2aWV3NiIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSIxMDAxIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTkyMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwIgogICAgIGd1aWRldG9sZXJhbmNlPSIxMCIKICAgICBncmlkdG9sZXJhbmNlPSIxMCIKICAgICBvYmplY3R0b2xlcmFuY2U9IjEwIgogICAgIGJvcmRlcm9wYWNpdHk9IjEiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIiAvPgogIDxwYXRoCiAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZiIKICAgICBpZD0icGF0aDIiCiAgICAgZD0iTTEzLjQxOCA3Ljg1OWEuNjk1LjY5NSAwIDAxLjk3OCAwIC42OC42OCAwIDAxMCAuOTY5bC0zLjkwOCAzLjgzYS42OTcuNjk3IDAgMDEtLjk3OSAwbC0zLjkwOC0zLjgzYS42OC42OCAwIDAxMC0uOTY5LjY5NS42OTUgMCAwMS45NzggMEwxMCAxMWwzLjQxOC0zLjE0MXoiIC8+Cjwvc3ZnPgo=) right 10px center/20px no-repeat #1c1c21;
|
||||
border: 2px solid #41444e;
|
||||
}
|
||||
|
||||
.swagger-ui select[multiple] { background: #212121; }
|
||||
|
||||
.swagger-ui button.invalid, .swagger-ui input[type=email].invalid, .swagger-ui input[type=file].invalid, .swagger-ui input[type=password].invalid, .swagger-ui input[type=search].invalid, .swagger-ui input[type=text].invalid, .swagger-ui select.invalid, .swagger-ui textarea.invalid {
|
||||
background: #390e0e;
|
||||
border-color: #c83232;
|
||||
}
|
||||
|
||||
.swagger-ui input[type=email], .swagger-ui input[type=file], .swagger-ui input[type=password], .swagger-ui input[type=search], .swagger-ui input[type=text], .swagger-ui textarea {
|
||||
background: #1c1c21;
|
||||
border: 1px solid #404040;
|
||||
}
|
||||
|
||||
.swagger-ui textarea {
|
||||
background: rgba(28, 28, 33, .8);
|
||||
color: #b5bac9;
|
||||
}
|
||||
|
||||
.swagger-ui input[disabled], .swagger-ui select[disabled] {
|
||||
background-color: #1f1f1f;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
|
||||
.swagger-ui textarea[disabled] {
|
||||
background-color: #41444e;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.swagger-ui select[disabled] { border-color: #878787; }
|
||||
|
||||
.swagger-ui textarea:focus { border: 2px solid #2a69a7; }
|
||||
|
||||
.swagger-ui .checkbox input[type=checkbox] + label > .item {
|
||||
background: #303030;
|
||||
box-shadow: #303030 0 0 0 2px;
|
||||
}
|
||||
|
||||
.swagger-ui .checkbox input[type=checkbox]:checked + label > .item { background: url("data:image/svg+xml;charset=utf-8,<svg width=\"10\" height=\"8\" viewBox=\"3 7 10 8\" xmlns=\"http://www.w3.org/2000/svg\"><path fill=\"%2341474E\" fill-rule=\"evenodd\" d=\"M6.333 15L3 11.667l1.333-1.334 2 2L11.667 7 13 8.333z\"/></svg>") 50% center no-repeat #303030; }
|
||||
|
||||
.swagger-ui .dialog-ux .backdrop-ux { background: rgba(0, 0, 0, .8); }
|
||||
|
||||
.swagger-ui .dialog-ux .modal-ux {
|
||||
background: #1c1c21;
|
||||
border: 1px solid #2e2e2e;
|
||||
box-shadow: rgba(0, 0, 0, .2) 0 10px 30px 0;
|
||||
}
|
||||
|
||||
.swagger-ui .dialog-ux .modal-ux-header .close-modal { background: 0 0; }
|
||||
|
||||
.swagger-ui .model .deprecated span, .swagger-ui .model .deprecated td { color: #bfbfbf !important; }
|
||||
|
||||
.swagger-ui .model-toggle::after { background: url("data:image/svg+xml;charset=utf-8,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\"><path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\"/></svg>") 50% center/100% no-repeat; }
|
||||
|
||||
.swagger-ui .model-hint {
|
||||
background: rgba(0, 0, 0, .7);
|
||||
color: #ebebeb;
|
||||
}
|
||||
|
||||
.swagger-ui section.models { border: 1px solid rgba(58, 64, 80, .3); }
|
||||
|
||||
.swagger-ui section.models.is-open h4 { border-bottom: 1px solid rgba(58, 64, 80, .3); }
|
||||
|
||||
.swagger-ui section.models .model-container { background: rgba(0, 0, 0, .05); }
|
||||
|
||||
.swagger-ui section.models .model-container:hover { background: rgba(0, 0, 0, .07); }
|
||||
|
||||
.swagger-ui .model-box { background: rgba(0, 0, 0, .1); }
|
||||
|
||||
.swagger-ui .prop-type { color: #aaaad4; }
|
||||
|
||||
.swagger-ui table thead tr td, .swagger-ui table thead tr th {
|
||||
border-bottom: 1px solid rgba(58, 64, 80, .2);
|
||||
color: #b5bac9;
|
||||
}
|
||||
|
||||
.swagger-ui .parameter__name.required::after { color: rgba(230, 153, 153, .6); }
|
||||
|
||||
.swagger-ui .topbar .download-url-wrapper .select-label { color: #f0f0f0; }
|
||||
|
||||
.swagger-ui .topbar .download-url-wrapper .download-url-button {
|
||||
background: #63a040;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.swagger-ui .info .title small { background: #7c8492; }
|
||||
|
||||
.swagger-ui .info .title small.version-stamp { background-color: #7a9b27; }
|
||||
|
||||
.swagger-ui .auth-container .errors {
|
||||
background-color: #350d0d;
|
||||
color: #b5bac9;
|
||||
}
|
||||
|
||||
.swagger-ui .errors-wrapper {
|
||||
background: rgba(200, 50, 50, .1);
|
||||
border: 2px solid #c83232;
|
||||
}
|
||||
|
||||
.swagger-ui .markdown code, .swagger-ui .renderedmarkdown code {
|
||||
background: rgba(0, 0, 0, .05);
|
||||
color: #c299e6;
|
||||
}
|
||||
|
||||
.swagger-ui .model-toggle:after { background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMS4wICg0MDM1YTRmYjQ5LCAyMDIwLTA1LTAxKSIKICAgc29kaXBvZGk6ZG9jbmFtZT0iZG93bmxvYWQyLnN2ZyIKICAgaWQ9InN2ZzQiCiAgIHZlcnNpb249IjEuMSIKICAgaGVpZ2h0PSIyNCIKICAgd2lkdGg9IjI0Ij4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGExMCI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGRlZnMKICAgICBpZD0iZGVmczgiIC8+CiAgPHNvZGlwb2RpOm5hbWVkdmlldwogICAgIGlua3NjYXBlOmN1cnJlbnQtbGF5ZXI9InN2ZzQiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTkiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9Ii05IgogICAgIGlua3NjYXBlOmN5PSIxMiIKICAgICBpbmtzY2FwZTpjeD0iMTIiCiAgICAgaW5rc2NhcGU6em9vbT0iMzQuNTgzMzMzIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpZD0ibmFtZWR2aWV3NiIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSIxMDAxIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTkyMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwIgogICAgIGd1aWRldG9sZXJhbmNlPSIxMCIKICAgICBncmlkdG9sZXJhbmNlPSIxMCIKICAgICBvYmplY3R0b2xlcmFuY2U9IjEwIgogICAgIGJvcmRlcm9wYWNpdHk9IjEiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIiAvPgogIDxwYXRoCiAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZiIKICAgICBpZD0icGF0aDIiCiAgICAgZD0iTTEwIDZMOC41OSA3LjQxIDEzLjE3IDEybC00LjU4IDQuNTlMMTAgMThsNi02eiIgLz4KPC9zdmc+Cg==) 50% no-repeat; }
|
||||
|
||||
.swagger-ui .expand-operation svg, .swagger-ui section.models h4 svg { fill: #fff; }
|
||||
|
||||
::-webkit-scrollbar-track { background-color: #646464 !important; }
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #242424 !important;
|
||||
border: 2px solid #3e4346 !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button:vertical:start:decrement {
|
||||
background: linear-gradient(130deg, #696969 40%, rgba(255, 0, 0, 0) 41%), linear-gradient(230deg, #696969 40%, transparent 41%), linear-gradient(0deg, #696969 40%, transparent 31%);
|
||||
background-color: #b6b6b6;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button:vertical:end:increment {
|
||||
background: linear-gradient(310deg, #696969 40%, transparent 41%), linear-gradient(50deg, #696969 40%, transparent 41%), linear-gradient(180deg, #696969 40%, transparent 31%);
|
||||
background-color: #b6b6b6;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button:horizontal:end:increment {
|
||||
background: linear-gradient(210deg, #696969 40%, transparent 41%), linear-gradient(330deg, #696969 40%, transparent 41%), linear-gradient(90deg, #696969 30%, transparent 31%);
|
||||
background-color: #b6b6b6;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button:horizontal:start:decrement {
|
||||
background: linear-gradient(30deg, #696969 40%, transparent 41%), linear-gradient(150deg, #696969 40%, transparent 41%), linear-gradient(270deg, #696969 30%, transparent 31%);
|
||||
background-color: #b6b6b6;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button, ::-webkit-scrollbar-track-piece { background-color: #3e4346 !important; }
|
||||
|
||||
.swagger-ui .black, .swagger-ui .checkbox, .swagger-ui .dark-gray, .swagger-ui .download-url-wrapper .loading, .swagger-ui .errors-wrapper .errors small, .swagger-ui .fallback, .swagger-ui .filter .loading, .swagger-ui .gray, .swagger-ui .hover-black:focus, .swagger-ui .hover-black:hover, .swagger-ui .hover-dark-gray:focus, .swagger-ui .hover-dark-gray:hover, .swagger-ui .hover-gray:focus, .swagger-ui .hover-gray:hover, .swagger-ui .hover-light-silver:focus, .swagger-ui .hover-light-silver:hover, .swagger-ui .hover-mid-gray:focus, .swagger-ui .hover-mid-gray:hover, .swagger-ui .hover-near-black:focus, .swagger-ui .hover-near-black:hover, .swagger-ui .hover-silver:focus, .swagger-ui .hover-silver:hover, .swagger-ui .light-silver, .swagger-ui .markdown pre, .swagger-ui .mid-gray, .swagger-ui .model .property, .swagger-ui .model .property.primitive, .swagger-ui .model-title, .swagger-ui .near-black, .swagger-ui .parameter__extension, .swagger-ui .parameter__in, .swagger-ui .prop-format, .swagger-ui .renderedmarkdown pre, .swagger-ui .response-col_links .response-undocumented, .swagger-ui .response-col_status .response-undocumented, .swagger-ui .silver, .swagger-ui section.models h4, .swagger-ui section.models h5, .swagger-ui span.token-not-formatted, .swagger-ui span.token-string, .swagger-ui table.headers .header-example, .swagger-ui table.model tr.description, .swagger-ui table.model tr.extension { color: #bfbfbf; }
|
||||
|
||||
.swagger-ui .hover-white:focus, .swagger-ui .hover-white:hover, .swagger-ui .info .title small pre, .swagger-ui .topbar a, .swagger-ui .white { color: #fff; }
|
||||
|
||||
.swagger-ui .bg-black-10, .swagger-ui .hover-bg-black-10:focus, .swagger-ui .hover-bg-black-10:hover, .swagger-ui .stripe-dark:nth-child(2n + 1) { background-color: rgba(0, 0, 0, .1); }
|
||||
|
||||
.swagger-ui .bg-white-10, .swagger-ui .hover-bg-white-10:focus, .swagger-ui .hover-bg-white-10:hover, .swagger-ui .stripe-light:nth-child(2n + 1) { background-color: rgba(28, 28, 33, .1); }
|
||||
|
||||
.swagger-ui .bg-light-silver, .swagger-ui .hover-bg-light-silver:focus, .swagger-ui .hover-bg-light-silver:hover, .swagger-ui .striped--light-silver:nth-child(2n + 1) { background-color: #6e6e6e; }
|
||||
|
||||
.swagger-ui .bg-moon-gray, .swagger-ui .hover-bg-moon-gray:focus, .swagger-ui .hover-bg-moon-gray:hover, .swagger-ui .striped--moon-gray:nth-child(2n + 1) { background-color: #4d4d4d; }
|
||||
|
||||
.swagger-ui .bg-light-gray, .swagger-ui .hover-bg-light-gray:focus, .swagger-ui .hover-bg-light-gray:hover, .swagger-ui .striped--light-gray:nth-child(2n + 1) { background-color: #2b2b2b; }
|
||||
|
||||
.swagger-ui .bg-near-white, .swagger-ui .hover-bg-near-white:focus, .swagger-ui .hover-bg-near-white:hover, .swagger-ui .striped--near-white:nth-child(2n + 1) { background-color: #242424; }
|
||||
|
||||
.swagger-ui .opblock-tag:hover, .swagger-ui section.models h4:hover { background: rgba(0, 0, 0, .02); }
|
||||
|
||||
.swagger-ui .checkbox p, .swagger-ui .dialog-ux .modal-ux-content h4, .swagger-ui .dialog-ux .modal-ux-content p, .swagger-ui .dialog-ux .modal-ux-header h3, .swagger-ui .errors-wrapper .errors h4, .swagger-ui .errors-wrapper hgroup h4, .swagger-ui .info .base-url, .swagger-ui .info .title, .swagger-ui .info h1, .swagger-ui .info h2, .swagger-ui .info h3, .swagger-ui .info h4, .swagger-ui .info h5, .swagger-ui .info li, .swagger-ui .info p, .swagger-ui .info table, .swagger-ui .loading-container .loading::after, .swagger-ui .model, .swagger-ui .opblock .opblock-section-header h4, .swagger-ui .opblock .opblock-section-header > label, .swagger-ui .opblock .opblock-summary-description, .swagger-ui .opblock .opblock-summary-operation-id, .swagger-ui .opblock .opblock-summary-path, .swagger-ui .opblock .opblock-summary-path__deprecated, .swagger-ui .opblock-description-wrapper, .swagger-ui .opblock-description-wrapper h4, .swagger-ui .opblock-description-wrapper p, .swagger-ui .opblock-external-docs-wrapper, .swagger-ui .opblock-external-docs-wrapper h4, .swagger-ui .opblock-external-docs-wrapper p, .swagger-ui .opblock-tag small, .swagger-ui .opblock-title_normal, .swagger-ui .opblock-title_normal h4, .swagger-ui .opblock-title_normal p, .swagger-ui .parameter__name, .swagger-ui .parameter__type, .swagger-ui .response-col_links, .swagger-ui .response-col_status, .swagger-ui .responses-inner h4, .swagger-ui .responses-inner h5, .swagger-ui .scheme-container .schemes > label, .swagger-ui .scopes h2, .swagger-ui .servers > label, .swagger-ui .tab li, .swagger-ui label, .swagger-ui select, .swagger-ui table.headers td { color: #b5bac9; }
|
||||
|
||||
.swagger-ui .download-url-wrapper .failed, .swagger-ui .filter .failed, .swagger-ui .model-deprecated-warning, .swagger-ui .parameter__deprecated, .swagger-ui .parameter__name.required span, .swagger-ui table.model tr.property-row .star { color: #e69999; }
|
||||
|
||||
.swagger-ui .opblock-body pre.microlight, .swagger-ui textarea.curl {
|
||||
background: #41444e;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.swagger-ui .expand-methods svg, .swagger-ui .expand-methods:hover svg { fill: #bfbfbf; }
|
||||
|
||||
.swagger-ui .auth-container, .swagger-ui .dialog-ux .modal-ux-header { border-bottom: 1px solid #2e2e2e; }
|
||||
|
||||
.swagger-ui .topbar .download-url-wrapper .select-label select, .swagger-ui .topbar .download-url-wrapper input[type=text] { border: 2px solid #63a040; }
|
||||
|
||||
.swagger-ui .info a, .swagger-ui .info a:hover, .swagger-ui .scopes h2 a { color: #99bde6; }
|
||||
|
||||
/* Dark Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button {
|
||||
background-color: #3e4346 !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: #646464 !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track-piece {
|
||||
background-color: #3e4346 !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
height: 50px;
|
||||
background-color: #242424 !important;
|
||||
border: 2px solid #3e4346 !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {}
|
||||
|
||||
::-webkit-resizer {}
|
||||
|
||||
::-webkit-scrollbar-button:vertical:start:decrement {
|
||||
background:
|
||||
linear-gradient(130deg, #696969 40%, rgba(255, 0, 0, 0) 41%),
|
||||
linear-gradient(230deg, #696969 40%, rgba(0, 0, 0, 0) 41%),
|
||||
linear-gradient(0deg, #696969 40%, rgba(0, 0, 0, 0) 31%);
|
||||
background-color: #b6b6b6;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button:vertical:end:increment {
|
||||
background:
|
||||
linear-gradient(310deg, #696969 40%, rgba(0, 0, 0, 0) 41%),
|
||||
linear-gradient(50deg, #696969 40%, rgba(0, 0, 0, 0) 41%),
|
||||
linear-gradient(180deg, #696969 40%, rgba(0, 0, 0, 0) 31%);
|
||||
background-color: #b6b6b6;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button:horizontal:end:increment {
|
||||
background:
|
||||
linear-gradient(210deg, #696969 40%, rgba(0, 0, 0, 0) 41%),
|
||||
linear-gradient(330deg, #696969 40%, rgba(0, 0, 0, 0) 41%),
|
||||
linear-gradient(90deg, #696969 30%, rgba(0, 0, 0, 0) 31%);
|
||||
background-color: #b6b6b6;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button:horizontal:start:decrement {
|
||||
background:
|
||||
linear-gradient(30deg, #696969 40%, rgba(0, 0, 0, 0) 41%),
|
||||
linear-gradient(150deg, #696969 40%, rgba(0, 0, 0, 0) 41%),
|
||||
linear-gradient(270deg, #696969 30%, rgba(0, 0, 0, 0) 31%);
|
||||
background-color: #b6b6b6;
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
|
||||
let error_div = document.getElementById('error-message')
|
||||
|
||||
function setCookie(name, value, minutes) {
|
||||
let expires = "";
|
||||
let date = new Date();
|
||||
date.setTime(date.getTime() + (minutes * 60 * 1000));
|
||||
expires = "; expires=" + date.toUTCString();
|
||||
document.cookie = name + "=" + (value || "") + expires + "; path=/";
|
||||
}
|
||||
|
||||
function processForm(e) {
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
|
||||
const data = Object.fromEntries(new FormData(e.target).entries());
|
||||
|
||||
let urlEncodedData = "";
|
||||
let urlEncodedDataPairs = [];
|
||||
let name;
|
||||
|
||||
for(name in data) {
|
||||
urlEncodedDataPairs.push(encodeURIComponent(name) + '=' + encodeURIComponent(data[name]));
|
||||
}
|
||||
|
||||
urlEncodedData = urlEncodedDataPairs.join('&').replace( /%20/g, '+');
|
||||
|
||||
fetch('/api/user/login', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: urlEncodedData
|
||||
}).then(response => {
|
||||
response.json().then(data => {
|
||||
if (data.access_token) {
|
||||
setCookie('access_token', data.access_token, 30)
|
||||
window.location.reload();
|
||||
} else {
|
||||
error_div.innerHTML = data.detail
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
let form = document.forms[0];
|
||||
if (form.attachEvent) {
|
||||
form.attachEvent('submit', processForm);
|
||||
} else {
|
||||
form.addEventListener('submit', processForm);
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" >
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Admin Panel</title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!-- Tell the browser to be responsive to screen width -->
|
||||
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
||||
<!-- Bootstrap 3.3.6 -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.5.0/css/font-awesome.min.css">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
|
||||
<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta/css/bootstrap.min.css'>
|
||||
<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.5.0/css/font-awesome.min.css'>
|
||||
<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Roboto'>
|
||||
<link rel="stylesheet" href="/static/css/login.css">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-md-2"></div>
|
||||
<div class="col-lg-6 col-md-8 login-box">
|
||||
<div class="col-lg-12 login-key">
|
||||
<i class="fa fa-key" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="col-lg-12 login-title">
|
||||
ADMIN PANEL
|
||||
</div>
|
||||
|
||||
<div class="col-lg-12 login-form">
|
||||
<div class="col-lg-12 login-form">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label class="form-control-label">USERNAME</label>
|
||||
<input type="text" class="form-control" name="username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-control-label">PASSWORD</label>
|
||||
<input type="password" class="form-control" name="password">
|
||||
</div>
|
||||
|
||||
<div class="col-lg-12 loginbttm d-flex justify-content-between">
|
||||
<div class="login-btm login-text" id="error-message">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div class="login-btm login-button">
|
||||
<button type="submit" class="btn btn-outline-primary">LOGIN</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="/static/js/login.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,136 +0,0 @@
|
||||
from uuid import UUID, uuid1
|
||||
from typing import Callable, Any
|
||||
import random
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
import models
|
||||
|
||||
|
||||
room_names = [ 'Atrium', 'Ballroom', 'Bathroom', 'Bedroom', 'Billiard room', 'Cabinet', 'Computer lab',
|
||||
'Conference hall', 'Control room', 'Corner office', 'Dining room', 'Drawing room', 'Office',
|
||||
'Roomsharing', 'Root cellar', 'Rotting room', 'Rotunda', 'Small office', 'Student lounge',
|
||||
'Studio', 'Sunroom', 'Throne room', 'Toolroom', 'Waiting room', 'Workshop']
|
||||
model_names = ['Lamp', 'Led', 'Light', 'Lock', 'Door', 'Window rollets', 'Cleaner', 'TV', 'Speaker', 'Spot']
|
||||
parameter_names = ['Is on', 'Color', 'Power', 'Brightness', 'Mode', 'Target', 'Timer', 'Volume', 'Channel']
|
||||
parameter_value_types = ['bool', 'decimal', 'enum']
|
||||
|
||||
class Faker:
|
||||
client: TestClient
|
||||
create_session: Callable[[], Session]
|
||||
|
||||
house_id = uuid1()
|
||||
|
||||
# inject
|
||||
user_access_token: str = ''
|
||||
hub_access_token: str = ''
|
||||
hub_refresh_token: str = ''
|
||||
public_key: str = ''
|
||||
|
||||
def __init__(self, client: TestClient, create_session: Callable[[], Session]):
|
||||
self.client = client
|
||||
self.create_session = create_session
|
||||
|
||||
def init_hub(self):
|
||||
hub_id = uuid1()
|
||||
default_name = 'Inited Hub'
|
||||
|
||||
response = self.client.post('/api/hub', json = {
|
||||
'id': str(hub_id),
|
||||
'name': default_name,
|
||||
'house_id': str(self.house_id),
|
||||
'access_token': self.hub_access_token,
|
||||
'refresh_token': self.hub_refresh_token,
|
||||
'public_key': self.public_key,
|
||||
}, headers = {
|
||||
'Authorization': f'Bearer {self.user_access_token}'
|
||||
})
|
||||
|
||||
return response.json()
|
||||
|
||||
def get_house(self) -> dict[str, Any]:
|
||||
response = self.client.get(f'/api/house/')
|
||||
assert response.status_code == 200
|
||||
return response.json()
|
||||
|
||||
def create_room(self):
|
||||
with self.create_session() as session:
|
||||
room = models.Room(house_id = self.house_id, name = random.choice(room_names))
|
||||
session.add(room)
|
||||
session.commit()
|
||||
session.refresh(room)
|
||||
return room
|
||||
|
||||
def create_device_model(self, id: UUID = uuid1()) -> models.DeviceModel:
|
||||
with self.create_session() as session:
|
||||
device_model = models.DeviceModel(id=id, name=random.choice(model_names))
|
||||
session.add(device_model)
|
||||
session.commit()
|
||||
session.refresh(device_model)
|
||||
return device_model
|
||||
|
||||
def create_parameter(self) -> models.Parameter:
|
||||
with self.create_session() as session:
|
||||
parameter = models.Parameter(name = random.choice(parameter_names),
|
||||
value_type = random.choice(parameter_value_types))
|
||||
session.add(parameter)
|
||||
session.commit()
|
||||
session.refresh(parameter)
|
||||
return parameter
|
||||
|
||||
def create_device(self, name: str, room_id: UUID, model_id: UUID) -> models.Device:
|
||||
with self.create_session() as session:
|
||||
device = models.Device(
|
||||
name = name,
|
||||
urdi = random.randint(0, 2**32),
|
||||
room_id = room_id,
|
||||
model_id = model_id,
|
||||
)
|
||||
session.add(device)
|
||||
session.commit()
|
||||
session.refresh(device)
|
||||
return device
|
||||
|
||||
def associate_devicemodel_parameter(self,
|
||||
devicemodel_id: UUID,
|
||||
parameter_id: UUID,
|
||||
f: int) -> models.DeviceModelParameter:
|
||||
|
||||
with self.create_session() as session:
|
||||
parameter = models.DeviceModelParameter(
|
||||
devicemodel_id = devicemodel_id,
|
||||
parameter_id = parameter_id,
|
||||
f = f
|
||||
)
|
||||
session.add(parameter)
|
||||
session.commit()
|
||||
session.refresh(parameter)
|
||||
return parameter
|
||||
|
||||
def fill_db(self) -> models.House:
|
||||
self.init_hub()
|
||||
|
||||
with self.create_session() as session:
|
||||
house = session.get(models.House, self.house_id)
|
||||
|
||||
device_models = [self.create_device_model() for _ in range(7)]
|
||||
parameters = [self.create_parameter() for _ in range(14)]
|
||||
|
||||
for model in device_models:
|
||||
random.shuffle(parameters)
|
||||
for i in range(random.randint(1, 9)):
|
||||
parameter = parameters[i]
|
||||
self.associate_devicemodel_parameter(
|
||||
model.id, parameter.id, i
|
||||
)
|
||||
|
||||
for r in range(5):
|
||||
room = self.create_room()
|
||||
house.rooms.append(room)
|
||||
for d in range(5):
|
||||
device_model = random.choice(device_models)
|
||||
name = f'{device_model.name} in {room.name.lower()}'
|
||||
device = self.create_device(name, room.id, device_model.id)
|
||||
|
||||
return house
|
@ -1,97 +0,0 @@
|
||||
from typing import Any
|
||||
from uuid import uuid1, UUID
|
||||
import os
|
||||
import time
|
||||
|
||||
from jose import jwt
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from sqlalchemy import create_engine, select
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
|
||||
from main import app
|
||||
from server.dependencies.database import get_session, get_async_session
|
||||
import models
|
||||
import config
|
||||
|
||||
from .faker import Faker
|
||||
|
||||
|
||||
# Settings
|
||||
|
||||
with open(f'{config.path}/tests/jwt.key', 'r') as f:
|
||||
secret_key = f.read()
|
||||
with open(f'{config.path}/tests/jwt.key.pub', 'r') as f:
|
||||
public_key = f.read()
|
||||
|
||||
user_id = UUID('f085ef73-f599-11ec-acda-58961df87e73')
|
||||
|
||||
access_token = ''
|
||||
hub_access_token = ''
|
||||
hub_refresh_token = ''
|
||||
|
||||
db_url = f'sqlite:///{config.src}/test_database.sqlite3'
|
||||
async_db_url = f'sqlite+aiosqlite:///{config.src}/test_database.sqlite3'
|
||||
|
||||
# Simulate Cloud Auth
|
||||
|
||||
access_token = jwt.encode({
|
||||
'type': 'access',
|
||||
'user_id': str(user_id),
|
||||
'is_admin': False,
|
||||
'exp': time.time() + 60
|
||||
}, secret_key, algorithm = 'RS256')
|
||||
|
||||
# Sync DB
|
||||
|
||||
engine = create_engine(
|
||||
db_url, connect_args = {'check_same_thread': False}
|
||||
)
|
||||
|
||||
create_session = sessionmaker(
|
||||
autocommit = False, autoflush = False, bind = engine
|
||||
)
|
||||
|
||||
def override_get_session() -> Session:
|
||||
with create_session() as session:
|
||||
yield session
|
||||
|
||||
# Async DB
|
||||
|
||||
async_engine = create_async_engine(
|
||||
async_db_url, connect_args = {'check_same_thread': False}
|
||||
)
|
||||
|
||||
create_async_session = sessionmaker(
|
||||
async_engine, class_ = AsyncSession, expire_on_commit = False
|
||||
)
|
||||
|
||||
async def override_get_async_session() -> AsyncSession:
|
||||
async with create_async_session() as session:
|
||||
yield session
|
||||
|
||||
# App
|
||||
|
||||
models.Base.metadata.drop_all(bind = engine)
|
||||
models.Base.metadata.create_all(bind = engine)
|
||||
|
||||
app.dependency_overrides[get_session] = override_get_session
|
||||
app.dependency_overrides[get_async_session] = override_get_async_session
|
||||
client = TestClient(app)
|
||||
|
||||
# Faker
|
||||
|
||||
faker = Faker(client, create_session)
|
||||
faker.hub_access_token = hub_access_token
|
||||
faker.hub_refresh_token = hub_refresh_token
|
||||
faker.public_key = public_key
|
||||
faker.user_access_token = access_token
|
||||
|
||||
#
|
||||
|
||||
auth_headers = {
|
||||
'Authorization': f'Bearer {access_token}'
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
from tests.setup import *
|
||||
|
||||
|
||||
faker.init_hub()
|
||||
id = '62ff3e2b-0130-0002-0001-000000000001'
|
||||
room_id = faker.create_room().id
|
||||
faker.create_device_model('00000000-0000-0002-0000-000000000000')
|
||||
device = {
|
||||
'id': id,
|
||||
'name': 'New Room',
|
||||
'room_id': str(room_id),
|
||||
}
|
||||
|
||||
def test_get_device_null():
|
||||
response = client.get(f'/api/device/', headers = auth_headers)
|
||||
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, 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, 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, response.text
|
||||
assert new_device.get('model')
|
||||
device['model'] = new_device.get('model')
|
||||
assert new_device == device
|
||||
|
||||
def test_get_device():
|
||||
response = client.get(f'/api/device/{id}', headers = auth_headers)
|
||||
device['parameters'] = []
|
||||
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, 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, response.text
|
||||
|
||||
def test_get_deleted_device():
|
||||
test_get_device_404()
|
@ -1,10 +0,0 @@
|
||||
from tests.setup import *
|
||||
|
||||
|
||||
def get_house() -> dict[str, Any]:
|
||||
response = client.get(f'/api/house/', headers = auth_headers)
|
||||
assert response.status_code == 200
|
||||
return response.json()
|
||||
|
||||
def test_get_house():
|
||||
get_house()
|
@ -1,52 +0,0 @@
|
||||
from tests.setup import *
|
||||
|
||||
|
||||
id = uuid1()
|
||||
default_name = 'Inited Hub'
|
||||
hub_init_json = {
|
||||
'id': str(id),
|
||||
'name': default_name,
|
||||
'house_id': str(faker.house_id),
|
||||
'access_token': hub_access_token,
|
||||
'refresh_token': hub_refresh_token,
|
||||
'public_key': public_key,
|
||||
}
|
||||
|
||||
def test_get_hub_401():
|
||||
response = client.get('/api/hub')
|
||||
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, response.text
|
||||
assert response.json() == {'detail': 'Not authenticated'}
|
||||
|
||||
def _test_get_hub_404():
|
||||
response = client.get('/api/hub', headers = auth_headers)
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {'detail': 'Not found'}
|
||||
|
||||
def test_init_hub():
|
||||
response = client.post('/api/hub', json = hub_init_json, headers = auth_headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_get_hub():
|
||||
response = client.get('/api/hub', headers = auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
'id': str(id),
|
||||
'name': default_name,
|
||||
'house_id': str(faker.house_id),
|
||||
}
|
||||
|
||||
def test_patch_hub():
|
||||
hub = client.get('/api/hub', headers = auth_headers).json()
|
||||
hub['name'] = 'Patched Hub'
|
||||
|
||||
response = client.patch('/api/hub', json = {
|
||||
'name': 'Patched Hub',
|
||||
}, headers = auth_headers)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert client.get('/api/hub', headers = auth_headers).json() == hub
|
@ -1,51 +0,0 @@
|
||||
from tests.setup import *
|
||||
|
||||
|
||||
id = uuid1()
|
||||
room = {
|
||||
'name': 'New Room',
|
||||
'devices': []
|
||||
}
|
||||
|
||||
def test_get_room_null():
|
||||
response = client.get(f'/api/room/', headers = auth_headers)
|
||||
assert response.status_code == 405
|
||||
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().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().get('detail') == 'Room not found'
|
||||
|
||||
def test_create_room():
|
||||
response = client.post(f'/api/room', json = room, headers = auth_headers)
|
||||
new_room = response.json()
|
||||
assert response.status_code == 200
|
||||
global id
|
||||
id = new_room.get('id')
|
||||
assert id
|
||||
room['id'] = id
|
||||
assert new_room == room
|
||||
|
||||
def test_get_room():
|
||||
response = client.get(f'/api/room/{id}', headers = auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == room
|
||||
|
||||
def test_patch_room():
|
||||
room['name'] = 'Patched Room'
|
||||
response = client.patch(f'/api/room/{id}', json = room, headers = auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert client.get(f'/api/room/{id}', headers = auth_headers).json() == room
|
||||
|
||||
def test_delete_room():
|
||||
response = client.delete(f'/api/room/{id}', json = room, headers = auth_headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_get_deleted_room():
|
||||
test_get_room_404()
|
@ -1,26 +0,0 @@
|
||||
from starlette.websockets import WebSocketDisconnect
|
||||
from tests.setup import *
|
||||
|
||||
|
||||
def get_device() -> models.Device:
|
||||
with create_session() as session:
|
||||
return session.scalars(
|
||||
select(models.Device)
|
||||
).first()
|
||||
|
||||
def _test_ws():
|
||||
faker.fill_db()
|
||||
device = get_device()
|
||||
parameter_id = device.model.parameters[0].parameter_id
|
||||
try:
|
||||
with client.websocket_connect('/ws/', headers = auth_headers) as ws:
|
||||
ws.send_json({
|
||||
'type': 'merlin',
|
||||
'data': {
|
||||
'device_id': str(device.id),
|
||||
'parameter_id': str(parameter_id),
|
||||
'value': 0
|
||||
}
|
||||
}, mode = 'text')
|
||||
except WebSocketDisconnect:
|
||||
pass
|
@ -1,47 +0,0 @@
|
||||
from telebot import TeleBot, util, apihelper
|
||||
|
||||
|
||||
# added callback for __threaded_polling
|
||||
class CallbackTeleBot(TeleBot):
|
||||
def polling(self, none_stop=False, interval=0, timeout=20, callback = lambda *args: None, args = () ):
|
||||
if self.threaded:
|
||||
self.__threaded_polling(none_stop, interval, timeout, callback, args)
|
||||
else:
|
||||
self._TeleBot__non_threaded_polling(none_stop, interval, timeout)
|
||||
|
||||
def __threaded_polling(self, none_stop=False, interval=0, timeout=20, callback = lambda *args: None, args = () ):
|
||||
self._TeleBot__stop_polling.clear()
|
||||
error_interval = 0.25
|
||||
|
||||
polling_thread = util.WorkerThread(name="PollingThread")
|
||||
or_event = util.OrEvent(
|
||||
polling_thread.done_event,
|
||||
polling_thread.exception_event,
|
||||
self.worker_pool.exception_event
|
||||
)
|
||||
|
||||
while not self._TeleBot__stop_polling.wait(interval):
|
||||
callback(*args) # added by Parker
|
||||
or_event.clear()
|
||||
try:
|
||||
polling_thread.put(self._TeleBot__retrieve_updates, timeout)
|
||||
|
||||
or_event.wait() # wait for polling thread finish, polling thread error or thread pool error
|
||||
|
||||
polling_thread.raise_exceptions()
|
||||
self.worker_pool.raise_exceptions()
|
||||
|
||||
error_interval = 0.25
|
||||
except apihelper.ApiException as e:
|
||||
if not none_stop:
|
||||
self._TeleBot__stop_polling.set()
|
||||
else:
|
||||
polling_thread.clear_exceptions()
|
||||
self.worker_pool.clear_exceptions()
|
||||
time.sleep(error_interval)
|
||||
error_interval *= 2
|
||||
except KeyboardInterrupt:
|
||||
self._TeleBot__stop_polling.set()
|
||||
break
|
||||
|
||||
polling_thread.stop()
|
@ -1,73 +0,0 @@
|
||||
import time
|
||||
import os
|
||||
|
||||
import config
|
||||
from ArchieCore import Command, CommandsContextManager, CommandsContextManagerDelegate
|
||||
from IO import Text2Speech
|
||||
from Features.Media import YoutubePlayer, TorrentPlayer
|
||||
from CallbackTeleBot import CallbackTeleBot
|
||||
|
||||
|
||||
class TelegramBot(CommandsContextManagerDelegate):
|
||||
|
||||
online = True
|
||||
voids = 0
|
||||
voice = Text2Speech.Engine()
|
||||
bot = CallbackTeleBot(config.telebot)
|
||||
commandsContext: CommandsContextManager
|
||||
|
||||
# Control
|
||||
|
||||
def __init__(self):
|
||||
self.commandsContext = CommandsContextManager(delegate = self)
|
||||
|
||||
def start(self):
|
||||
while True:
|
||||
try:
|
||||
print("Start polling...")
|
||||
self.bot.polling(callback = self.commandsContext.checkThreads)
|
||||
except Exception as e:
|
||||
print(e, "\nPolling failed")
|
||||
time.sleep(10)
|
||||
|
||||
def stop(self):
|
||||
raise NotImplementedError
|
||||
|
||||
# CommandsContextManagerDelegate
|
||||
|
||||
def commandsContextDidReceiveResponse(self, response):
|
||||
id = response.data.get('id')
|
||||
if not id: return
|
||||
|
||||
if response.text:
|
||||
self.bot.send_message(id, response.text)
|
||||
if response.voice:
|
||||
path = self.voice.generate(response.voice).path
|
||||
voiceFile = open(path, 'rb')
|
||||
try:
|
||||
self.bot.send_voice(id, voiceFile)
|
||||
finally:
|
||||
voiceFile.close()
|
||||
|
||||
# Telebot
|
||||
|
||||
@bot.message_handler(commands = ['vlc', 'queue', 'cmd'])
|
||||
def simple_commands(msg):
|
||||
command = msg.text.replace('/cmd', '').replace('/vlc', 'vlc')
|
||||
if '/queue' in msg.text: command = 'vlc ' + command.replace('/queue', '') + '--playlist-enqueue'
|
||||
os.system(f'lxterminal --command="{command}"')
|
||||
|
||||
@bot.message_handler(commands=['terminal'])
|
||||
def terminal(msg):
|
||||
command = msg.text.replace('/terminal', '')
|
||||
output = os.popen(command).read()
|
||||
bot.send_message(msg.chat.id, output)
|
||||
|
||||
@bot.message_handler(content_types = ['text'])
|
||||
def execute(msg):
|
||||
if 'youtu' in msg.text:
|
||||
YoutubePlayer(msg.text).play()
|
||||
elif '.torrent' in msg.text:
|
||||
TorrentPlayer.playUrl(msg.text)
|
||||
else:
|
||||
TelegramBot().commandsContext.processString(msg.text.lower(), data = {'id': msg.chat.id})
|
@ -1,11 +0,0 @@
|
||||
import os, sys
|
||||
|
||||
# root = os.path.dirname(os.path.dirname(__file__))
|
||||
# sys.path.append(root)
|
||||
# sys.path.append(root + '/VoiceAssistant')
|
||||
|
||||
from TelegramBot import TelegramBot
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
TelegramBot().start()
|
@ -1,31 +0,0 @@
|
||||
from __future__ import annotations
|
||||
from typing import Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from .ACObject import ACObject, Pattern, classproperty
|
||||
from .ACTimeInterval import ACTimeInterval
|
||||
|
||||
class ACTime(ACObject):
|
||||
value: datetime
|
||||
|
||||
def __init__(self, value: Optional[datetime] = None):
|
||||
self.value = value or datetime.now()
|
||||
|
||||
def addingInterval(self, timeinterval: Union[ACObject, Number]) -> ACTime:
|
||||
seconds = timeinterval.value if isinstance(timeinterval, ACTimeInterval) else timeinterval
|
||||
return ACTime(self.value + timedelta(seconds = seconds))
|
||||
|
||||
def addInterval(self, timeinterval: Union[ACObject, Number]):
|
||||
seconds = timeinterval.value if isinstance(timeinterval, ACTimeInterval) else timeinterval
|
||||
self.value += timedelta(seconds = seconds)
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fromString: str) -> ACObject:
|
||||
raise NotImplementedError
|
||||
|
||||
@classproperty
|
||||
def pattern() -> Pattern:
|
||||
raise NotImplementedError
|
||||
|
||||
def __sub__(self, other: ACTime) -> ACTimeInterval:
|
||||
return ACTimeInterval((self.value - other.value).total_seconds())
|
@ -1,75 +0,0 @@
|
||||
from __future__ import annotations
|
||||
from typing import Union
|
||||
from numbers import Number
|
||||
|
||||
from .ACObject import ACObject, Pattern, classproperty
|
||||
|
||||
class ACTimeInterval(ACObject):
|
||||
value: float # seconds
|
||||
|
||||
@classmethod
|
||||
def initWith(seconds: float = 0,
|
||||
minutes: float = 0,
|
||||
hours: float = 0,
|
||||
days: float = 0,
|
||||
weeks: float = 0) -> ACTimeInterval:
|
||||
|
||||
days += weeks * 7
|
||||
hours += days * 24
|
||||
minutes += hours * 60
|
||||
seconds += minutes * 60
|
||||
return ACTimeInterval(value = seconds)
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fromString: str) -> ACObject:
|
||||
raise NotImplementedError
|
||||
|
||||
@classproperty
|
||||
def pattern() -> Pattern:
|
||||
raise NotImplementedError
|
||||
|
||||
def __lt__(self, other: ACObject) -> bool:
|
||||
value = other.value if isinstance(other, ACTimeInterval) else other
|
||||
return self.value < value
|
||||
|
||||
def __le__(self, other: ACObject) -> bool:
|
||||
value = other.value if isinstance(other, ACTimeInterval) else other
|
||||
return self.value <= value
|
||||
|
||||
def __gt__(self, other: ACObject) -> bool:
|
||||
value = other.value if isinstance(other, ACTimeInterval) else other
|
||||
return self.value > value
|
||||
|
||||
def __ge__(self, other: ACObject) -> bool:
|
||||
value = other.value if isinstance(other, ACTimeInterval) else other
|
||||
return self.value >= value
|
||||
|
||||
def __eq__(self, other: ACObject) -> bool:
|
||||
value = other.value if isinstance(other, ACTimeInterval) else other
|
||||
return self.value == value
|
||||
|
||||
def __ne__(self, other: ACObject) -> bool:
|
||||
value = other.value if isinstance(other, ACTimeInterval) else other
|
||||
return self.value != value
|
||||
|
||||
def __add__(self, other: Union[ACObject, Number]) -> ACTimeInterval:
|
||||
value = other.value if isinstance(other, ACTimeInterval) else other
|
||||
return ACTimeInterval(self.value + value)
|
||||
|
||||
def __sub__(self, other: Union[ACObject, Number]) -> ACTimeInterval:
|
||||
value = other.value if isinstance(other, ACTimeInterval) else other
|
||||
return ACTimeInterval(self.value - value)
|
||||
|
||||
def __mul__(self, other: Union[ACObject, Number]) -> ACTimeInterval:
|
||||
value = other.value if isinstance(other, ACTimeInterval) else other
|
||||
return ACTimeInterval(self.value * value)
|
||||
|
||||
def __mod__(self, other: Union[ACObject, Number]) -> ACTimeInterval:
|
||||
value = other.value if isinstance(other, ACTimeInterval) else other
|
||||
return ACTimeInterval(self.value % value)
|
||||
|
||||
def __floordiv__(self, other: Union[ACObject, Number]) -> Union[ACObject, Number]:
|
||||
return self.value // other.value if isinstance(other, ACTimeInterval) else ACTimeInterval(self.value // other)
|
||||
|
||||
def __truediv__(self, other: Union[ACObject, Number]) -> Union[ACObject, Number]:
|
||||
return self.value / other.value if isinstance(other, ACTimeInterval) else ACTimeInterval(self.value / other)
|
@ -1,8 +0,0 @@
|
||||
from .ACObject import Pattern, classproperty
|
||||
from . import ACString
|
||||
|
||||
class ACWord(ACString):
|
||||
|
||||
@classproperty
|
||||
def pattern() -> Pattern:
|
||||
return Pattern('[:word:]')
|
@ -1,6 +0,0 @@
|
||||
from .ACObject import ACObject
|
||||
from .ACNumber import ACNumber
|
||||
from .ACString import ACString
|
||||
from .ACWord import ACWord
|
||||
from .ACTime import ACTime
|
||||
from .ACTimeInterval import ACTimeInterval
|
@ -1,101 +0,0 @@
|
||||
def is_number(x):
|
||||
if type(x) == str:
|
||||
x = x.replace(',', '')
|
||||
try:
|
||||
float(x)
|
||||
except:
|
||||
return False
|
||||
return True
|
||||
|
||||
def text2int (textnum, numwords={}):
|
||||
units = [
|
||||
'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight',
|
||||
'nine', 'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen',
|
||||
'sixteen', 'seventeen', 'eighteen', 'nineteen',
|
||||
]
|
||||
tens = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety']
|
||||
scales = ['hundred', 'thousand', 'million', 'billion', 'trillion']
|
||||
ordinal_words = {'first':1, 'second':2, 'third':3, 'fifth':5, 'eighth':8, 'ninth':9, 'twelfth':12}
|
||||
ordinal_endings = [('ieth', 'y'), ('th', '')]
|
||||
|
||||
if not numwords:
|
||||
numwords['and'] = (1, 0)
|
||||
for idx, word in enumerate(units): numwords[word] = (1, idx)
|
||||
for idx, word in enumerate(tens): numwords[word] = (1, idx * 10)
|
||||
for idx, word in enumerate(scales): numwords[word] = (10 ** (idx * 3 or 2), 0)
|
||||
|
||||
textnum = textnum.replace('-', ' ')
|
||||
|
||||
current = result = 0
|
||||
curstring = ''
|
||||
onnumber = False
|
||||
lastunit = False
|
||||
lastscale = False
|
||||
|
||||
def is_numword(x):
|
||||
if is_number(x):
|
||||
return True
|
||||
if word in numwords:
|
||||
return True
|
||||
return False
|
||||
|
||||
def from_numword(x):
|
||||
if is_number(x):
|
||||
scale = 0
|
||||
increment = int(x.replace(',', ''))
|
||||
return scale, increment
|
||||
return numwords[x]
|
||||
|
||||
for word in textnum.split():
|
||||
if word in ordinal_words:
|
||||
scale, increment = (1, ordinal_words[word])
|
||||
current = current * scale + increment
|
||||
if scale > 100:
|
||||
result += current
|
||||
current = 0
|
||||
onnumber = True
|
||||
lastunit = False
|
||||
lastscale = False
|
||||
else:
|
||||
for ending, replacement in ordinal_endings:
|
||||
if word.endswith(ending):
|
||||
word = "%s%s" % (word[:-len(ending)], replacement)
|
||||
|
||||
if (not is_numword(word)) or (word == 'and' and not lastscale):
|
||||
if onnumber:
|
||||
# Flush the current number we are building
|
||||
curstring += repr(result + current) + " "
|
||||
curstring += word + " "
|
||||
result = current = 0
|
||||
onnumber = False
|
||||
lastunit = False
|
||||
lastscale = False
|
||||
else:
|
||||
scale, increment = from_numword(word)
|
||||
onnumber = True
|
||||
|
||||
if lastunit and (word not in scales):
|
||||
# Assume this is part of a string of individual numbers to
|
||||
# be flushed, such as a zipcode "one two three four five"
|
||||
curstring += repr(result + current)
|
||||
result = current = 0
|
||||
|
||||
if scale > 1:
|
||||
current = max(1, current)
|
||||
|
||||
current = current * scale + increment
|
||||
if scale > 100:
|
||||
result += current
|
||||
current = 0
|
||||
|
||||
lastscale = False
|
||||
lastunit = False
|
||||
if word in scales:
|
||||
lastscale = True
|
||||
elif word in units:
|
||||
lastunit = True
|
||||
|
||||
if onnumber:
|
||||
curstring += repr(result + current)
|
||||
|
||||
return curstring
|
@ -1 +0,0 @@
|
||||
from .NumeralsParser import *
|
@ -1,37 +0,0 @@
|
||||
import pathlib
|
||||
path = str(pathlib.Path(__file__).parent.absolute())
|
||||
del pathlib
|
||||
|
||||
|
||||
src: str = path + '/resources'
|
||||
|
||||
# Api keys
|
||||
|
||||
telebot: str
|
||||
goole_tts_json_key: str = src + '/tts-gc-key.json'
|
||||
|
||||
# Speech Recognition
|
||||
|
||||
vosk_model: str = src + '/model-small-rus'
|
||||
|
||||
# TTS
|
||||
|
||||
language_code: str = 'ru-RU'
|
||||
voice_volume: float = 1
|
||||
|
||||
# Archie settings
|
||||
|
||||
double_clap_activation: bool = False
|
||||
|
||||
# Archie Core
|
||||
|
||||
names: list[str] = ['арчи', 'archie']
|
||||
|
||||
# DB
|
||||
|
||||
db_url: str = 'sqlite:///./sql_app.db'
|
||||
|
||||
# WiFi
|
||||
|
||||
wifi_ssid: str = 'Archie Hub'
|
||||
wifi_password: str = '12345678'
|
@ -2,14 +2,14 @@ import requests
|
||||
from bs4 import BeautifulSoup as BS
|
||||
import os
|
||||
from .TorrentPlayer import TorrentPlayer
|
||||
from ArchieCore import Command, Response
|
||||
from VICore import Command, Response
|
||||
|
||||
################################################################################
|
||||
@Command.new(['$name:ACString'])
|
||||
@Command.new(['$name:VIString'])
|
||||
def playfilm_cb(params):
|
||||
return playfilm(params)
|
||||
|
||||
@Command.new(['включ* фильм $name:ACString', 'включ* фильм*'])
|
||||
@Command.new(['включ* фильм $name:VIString', 'включ* фильм*'])
|
||||
def playfilm(params):
|
||||
name = params.get('name')
|
||||
|
@ -1,5 +1,5 @@
|
||||
from bs4 import BeautifulSoup as BS
|
||||
from ArchieCore import CommandsManager, Command, Response, ResponseAction
|
||||
from VICore import CommandsManager, Command, Response, ResponseAction
|
||||
import wikipedia as wiki
|
||||
import requests
|
||||
import random
|
@ -1,6 +1,6 @@
|
||||
from .Raspi import Raspi
|
||||
import os
|
||||
from ArchieCore import Command, CommandsManager, Response
|
||||
from VICore import Command, CommandsManager, Response
|
||||
import config
|
||||
|
||||
################################################################################
|
@ -1,5 +1,5 @@
|
||||
from .Raspi import *
|
||||
from ArchieCore import Command, Response
|
||||
from VICore import Command, Response
|
||||
|
||||
################################################################################
|
||||
|
@ -2,7 +2,7 @@ import datetime, time
|
||||
import requests
|
||||
from bs4 import BeautifulSoup as BS
|
||||
import math
|
||||
from ArchieCore import Command, Response
|
||||
from VICore import Command, Response
|
||||
|
||||
@Command.new([
|
||||
'который * час в $text',
|
@ -1,4 +1,4 @@
|
||||
from ArchieCore import Command, Response
|
||||
from VICore import Command, Response
|
||||
|
||||
@Command.new(['привет*',])
|
||||
def hello(params):
|
@ -1,4 +1,4 @@
|
||||
from ArchieCore import Command, Response
|
||||
from VICore import Command, Response
|
||||
from .Zieit import Zieit
|
||||
|
||||
def formatLesson(lesson):
|
@ -1,35 +1,35 @@
|
||||
from typing import Callable, Optional
|
||||
from time import sleep
|
||||
|
||||
from ArchieCore.ACObjects import ACTime, ACTimeInterval
|
||||
from VICore.VIObjects import VITime, VITimeInterval
|
||||
from .. import Singleton, UUID, threadingFunction
|
||||
from .DispatchQueueItem import DispatchQueueItem
|
||||
|
||||
class DispatchQueue(Singleton):
|
||||
_queue: [DispatchQueueItem] = []
|
||||
_nearestItemTime: Optional[ACTime] = None
|
||||
_nearestItemTime: Optional[VITime] = None
|
||||
|
||||
def asyncAt(self, time: ACTime, execute: Callable, *args, **kwargs) -> UUID:
|
||||
def asyncAt(self, time: VITime, execute: Callable, *args, **kwargs) -> UUID:
|
||||
item = DispatchQueueItem(execute = execute, time = time, args = args, kwargs = kwargs)
|
||||
self.insert(item)
|
||||
return item.id
|
||||
|
||||
def asyncAfter(self, timeinterval: ACTimeInterval, execute: Callable, *args, **kwargs) -> UUID:
|
||||
time = ACTime().addingInterval(timeinterval)
|
||||
def asyncAfter(self, timeinterval: VITimeInterval, execute: Callable, *args, **kwargs) -> UUID:
|
||||
time = VITime().addingInterval(timeinterval)
|
||||
item = DispatchQueueItem(execute = execute, time = time, timeinterval = timeinterval,
|
||||
args = args, kwargs = kwargs)
|
||||
self.insert(item)
|
||||
return item.id
|
||||
|
||||
def repeatEvery(self, timeinterval: ACTimeInterval, execute: Callable, *args, **kwargs) -> UUID:
|
||||
time = ACTime().addingInterval(timeinterval)
|
||||
def repeatEvery(self, timeinterval: VITimeInterval, execute: Callable, *args, **kwargs) -> UUID:
|
||||
time = VITime().addingInterval(timeinterval)
|
||||
item = DispatchQueueItem(execute = execute, time = time, timeinterval = timeinterval,
|
||||
repeat = True, args = args, kwargs = kwargs)
|
||||
self.insert(item)
|
||||
return item.id
|
||||
|
||||
def insert(self, item: DispatchQueueItem):
|
||||
now = ACTime()
|
||||
now = VITime()
|
||||
low = 0
|
||||
high = len(self._queue)
|
||||
|
||||
@ -51,7 +51,7 @@ class DispatchQueue(Singleton):
|
||||
def loop(self):
|
||||
while True:
|
||||
|
||||
if not self._queue or not self._nearestItemTime or self._nearestItemTime > ACTime():
|
||||
if not self._queue or not self._nearestItemTime or self._nearestItemTime > VITime():
|
||||
sleep(0.1)
|
||||
continue
|
||||
|
@ -1,16 +1,16 @@
|
||||
from typing import Callable, Any
|
||||
from ArchieCore import ACTime, ACTimeInterval
|
||||
from VICore import VITime, VITimeInterval
|
||||
from .. import Identifable, threadingFunction
|
||||
|
||||
class DispatchQueueItem(Identifable):
|
||||
time: ACTime
|
||||
timeinterval: ACTimeInterval
|
||||
time: VITime
|
||||
timeinterval: VITimeInterval
|
||||
worker: Callable
|
||||
args: list[Any]
|
||||
kwargs: dict[Any, Any]
|
||||
repeat: bool
|
||||
|
||||
def __init__(self, execute: Callable, time: ACTime = ACTime(), timeinterval: ACTimeInterval = ACTimeInterval(0),
|
||||
def __init__(self, execute: Callable, time: VITime = VITime(), timeinterval: VITimeInterval = VITimeInterval(0),
|
||||
repeat: bool = False, args: list[Any] = [], kwargs: dict[Any, Any] = []):
|
||||
self.time = time
|
||||
self.timeinterval = timeinterval
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user