548 lines
15 KiB
Python
548 lines
15 KiB
Python
from flask import Flask, render_template, jsonify, request
|
|
from flask_apscheduler import APScheduler
|
|
from copy import copy
|
|
import uuid
|
|
import time
|
|
from datetime import datetime
|
|
import requests
|
|
|
|
app = Flask(__name__)
|
|
|
|
# UUID used for internal calls.
|
|
internalUUID = str(uuid.uuid4())
|
|
|
|
|
|
# Vitals
|
|
vitalsHeartrate = None
|
|
vitalsOxygen = None
|
|
vitalsBodytemp = None
|
|
|
|
@app.route('/api/vitals/heartrate')
|
|
def getVitalsHeartrate():
|
|
returnArr = [ { 'heartrate': vitalsHeartrate } ]
|
|
return jsonify(returnArr)
|
|
|
|
@app.route('/api/vitals/heartrate', methods=['POST'])
|
|
def setVitalsHeartrate():
|
|
global vitalsHeartrate
|
|
json = request.get_json()
|
|
try:
|
|
if not authenticate(json['uuid'], '/api/vitals/heartrate'):
|
|
return 'Forbidden.', 403
|
|
|
|
vitalsHeartrate = json['heartrate']
|
|
except:
|
|
return 'Incorrect usage.\nUsage: { heartrate: INT, uuid: STRING }\n', 400
|
|
return '', 204
|
|
|
|
@app.route('/api/vitals/oxygen')
|
|
def getVitalsOxygen():
|
|
returnArr = [ { 'oxygen': vitalsOxygen } ]
|
|
return jsonify(returnArr)
|
|
|
|
@app.route('/api/vitals/oxygen', methods=['POST'])
|
|
def setVitalsOxygen():
|
|
global vitalsOxygen
|
|
json = request.get_json()
|
|
try:
|
|
if not authenticate(json['uuid'], '/api/vitals/oxygen'):
|
|
return 'Forbidden.', 403
|
|
|
|
vitalsOxygen = json['oxygen']
|
|
except:
|
|
return 'Incorrect usage.\nUsage: { oxygen: INT }\n', 400
|
|
return '', 204
|
|
|
|
@app.route('/api/vitals/bodytemp')
|
|
def getVitalsBodytemp():
|
|
returnArr = [ { 'bodytemp': vitalsBodytemp } ]
|
|
return jsonify(returnArr)
|
|
|
|
@app.route('/api/vitals/bodytemp', methods=['POST'])
|
|
def setVitalsBodytemp():
|
|
global vitalsBodytemp
|
|
json = request.get_json()
|
|
try:
|
|
if not authenticate(json['uuid'], '/api/vitals/bodytemp'):
|
|
return 'Forbidden.', 403
|
|
|
|
vitalsBodytemp = json['bodytemp']
|
|
except:
|
|
return 'Incorrect usage.\nUsage: { bodytemp: FLOAT }\n', 400
|
|
return '', 204
|
|
|
|
@app.route('/api/vitals')
|
|
def getVitals():
|
|
returnArr = [ { 'heartrate': vitalsHeartrate, 'oxygen': vitalsOxygen, 'bodytemp': vitalsBodytemp } ]
|
|
return jsonify(returnArr)
|
|
|
|
@app.route('/api/vitals', methods=['POST'])
|
|
def setVitals():
|
|
global vitalsHeartrate
|
|
global vitalsOxygen
|
|
global vitalsBodytemp
|
|
|
|
json = request.get_json()
|
|
try:
|
|
if not authenticate(json['uuid'], '/api/vitals'):
|
|
return 'Forbidden.', 403
|
|
# This is a bit ugly but its just how I'm checking that everything is there without setting variables if the json is incorrect
|
|
tempH = json['heartrate']
|
|
tempO = json['oxygen']
|
|
tempB = json['bodytemp']
|
|
vitalsHeartrate = tempH
|
|
vitalsOxygen = tempO
|
|
vitalsBodytemp = tempB
|
|
except:
|
|
return 'Incorrect usage.\nUsage: { heartrate: INT, oxygen: INT, bodytemp: FLOAT }\n', 400
|
|
return '', 204
|
|
|
|
# Fitness
|
|
fitnessSteps = None
|
|
|
|
@app.route('/api/fitness/steps')
|
|
def getSteps():
|
|
return getFitness() # This is the same for now.
|
|
|
|
@app.route('/api/fitness/steps', methods=['POST'])
|
|
def setFitnessSteps():
|
|
global fitnessSteps
|
|
json = request.get_json()
|
|
try:
|
|
if not authenticate(json['uuid'], '/api/fitness/steps'):
|
|
return 'Forbidden.', 403
|
|
|
|
vitalsBodytemp = json['steps']
|
|
except:
|
|
return 'Incorrect usage.\nUsage: { steps: INT }\n', 400
|
|
return '', 204
|
|
|
|
|
|
@app.route('/api/fitness')
|
|
def getFitness():
|
|
returnArr = [ { 'steps': fitnessSteps } ]
|
|
return jsonify(returnArr)
|
|
|
|
@app.route('/api/fitness', methods=['POST'])
|
|
def setFitness():
|
|
global fitnessSteps
|
|
json = request.get_json()
|
|
try:
|
|
if not authenticate(json['uuid'], '/api/fitness'):
|
|
return 'Forbidden.', 403
|
|
|
|
fitnessSteps = json['steps']
|
|
except:
|
|
return 'Incorrect usage.\nUsage: { steps: INT }\n', 400
|
|
return '', 204
|
|
|
|
|
|
# Cyberware management
|
|
cyberware = []
|
|
newCyberwareTemplate = { "uuid": None, "name": None, "lastContact": None, "hotpluggable": False, "lastMalfunction": None, "canSet": None, "battery": None, "messages": None }
|
|
newMessageTemplate = { "title": None, "message": None, "progress": None }
|
|
# Messages: { Title, Message, Progress }
|
|
# Title, Message, and Progress are all technically optional. It's up to the frontend to make heads or tails of what's happening.
|
|
# Typically, if Progress == None: No progress bar, will show up in the Top Message section on NightUI
|
|
# if Message == None: No message
|
|
# if Title == None: No title.
|
|
#
|
|
# Typical layout for such messages:
|
|
# _____________________________________________________
|
|
# | ICON This is a title!
|
|
# | ICON This is a message!
|
|
# | ICON [======= ]
|
|
|
|
# This makes the system aware of a new piece of hardware.
|
|
# While, for the most part, not required due to the design of this project,
|
|
# it's handy for error reporting and communication between hardware and
|
|
# the end user.
|
|
#
|
|
# Arguments: { name: STRING, hotpluggable: BOOL, canSet: ARRAY }
|
|
# Returns: { uuid: INT }
|
|
# name: A human-readable name
|
|
# hotpluggable argument: Used for removable modules known as shards.
|
|
#
|
|
# uuid: Returns a uuid for the hardware.
|
|
@app.route('/api/cyberware/add', methods=['POST'])
|
|
def addCyberware():
|
|
global cyberware
|
|
json = request.get_json()
|
|
|
|
try:
|
|
tempName = json['name']
|
|
tempHotpluggable = json['hotpluggable']
|
|
tempCanSet = json['canSet']
|
|
tempNewCyberware = copy(newCyberwareTemplate)
|
|
|
|
tempNewCyberware['uuid'] = str(uuid.uuid4())
|
|
tempNewCyberware['name'] = tempName
|
|
tempNewCyberware['lastContact'] = datetime.now()
|
|
tempNewCyberware['hotpluggable'] = tempHotpluggable
|
|
tempNewCyberware['canSet'] = tempCanSet
|
|
|
|
|
|
cyberware.append(tempNewCyberware)
|
|
except:
|
|
return 'Incorrect usage.\nUsage: { name: STRING, hotpluggable: BOOL, canSet: ARRAY }\n', 400
|
|
return jsonify([ { "uuid": tempNewCyberware['uuid'] } ]), 200
|
|
|
|
# Arguments: { uuid: INT }
|
|
@app.route('/api/cyberware/remove', methods=['POST'])
|
|
def removeCyberware():
|
|
global cyberware
|
|
json = request.get_json()
|
|
|
|
try:
|
|
desiredId = json['uuid']
|
|
i = 0
|
|
for c in cyberware:
|
|
if c['uuid'] == desiredId:
|
|
del cyberware[i]
|
|
return '', 204
|
|
i = i + 1
|
|
except:
|
|
return 'Incorrect usage.\nUsage: { uuid: STRING }\n', 400
|
|
return 'UUID Invalid\n', 400
|
|
|
|
# Returns: { name: STRING, hotpluggable: STRING lastMalfunction: STRING, battery: INT, messages: ARRAY }
|
|
# uuid: Unique identifier of the hardware
|
|
# name: Human-readable name
|
|
# hotpluggable: Hardware can be removed during runtime.
|
|
# lastMalfunction: A string with information on the last malfunction.
|
|
@app.route('/api/cyberware')
|
|
def getCyberware():
|
|
returnArr = []
|
|
|
|
for c in cyberware:
|
|
returnArr.append({ 'name': c['name'], 'hotpluggable': c['hotpluggable'], 'lastMalfunction': c['lastMalfunction'], 'battery': c['battery'] })
|
|
|
|
return jsonify(returnArr), 200
|
|
|
|
@app.route('/api/cyberware/malfunctions')
|
|
def getCyberwareMalfunctions():
|
|
returnArr = []
|
|
|
|
for c in cyberware:
|
|
if c['lastMalfunction'] != None:
|
|
returnArr.append({ 'name': c['name'], 'lastMalfunction': c['lastMalfunction'] })
|
|
|
|
resetMalfunctions()
|
|
|
|
return jsonify(returnArr), 200
|
|
|
|
# Arguments { uuid: STRING, malfunction: STRING }
|
|
@app.route('/api/cyberware/malfunctions', methods=['POST'])
|
|
def setCyberwareMalfunction():
|
|
json = request.get_json()
|
|
|
|
try:
|
|
desiredId = json['uuid']
|
|
malfunction = json['malfunction']
|
|
|
|
requestedCyberware = getCyberwareHelper(desiredId)
|
|
|
|
requestedCyberware['lastMalfunction'] = malfunction
|
|
except:
|
|
return 'Incorrect usage.\nUsage: { malfunction: STRING, uuid: STRING }\n', 400
|
|
|
|
@app.route('/api/cyberware/messages')
|
|
def getCyberwareMessages():
|
|
returnArr = []
|
|
|
|
for c in cyberware:
|
|
if c['messages'] != None:
|
|
returnArr.append({ 'name': c['name'], 'messages': c['messages'] })
|
|
resetMessages()
|
|
return jsonify(returnArr), 200
|
|
|
|
# Arguments { uuid: STRING, message:{ title: STRING, message: STRING, progress: INT } }
|
|
@app.route('/api/cyberware/messages', methods=['POST'])
|
|
def setCyberwareMessages():
|
|
global cyberware
|
|
json = request.get_json()
|
|
|
|
try:
|
|
desiredId = json['uuid']
|
|
message = json['message']
|
|
|
|
# Test message validity
|
|
testTitle = message["title"]
|
|
testMessage = message["message"]
|
|
testProgress = message["progress"]
|
|
|
|
requestedCyberware = getCyberwareHelper(desiredId)
|
|
|
|
if (requestedCyberware != None):
|
|
if (requestedCyberware["messages"] == None):
|
|
requestedCyberware["messages"] = []
|
|
requestedCyberware["messages"].append(message)
|
|
return '', 204
|
|
except:
|
|
return "Incorrect usage.\nUsage: { uuid: STRING, message:{ title: STRING, message: STRING, progress: INT } }\n", 400
|
|
return "UUID Invalid\n", 400
|
|
|
|
@app.route('/api/cyberware/get', methods=['POST'])
|
|
def getCyberwareSpecific():
|
|
json = request.get_json()
|
|
|
|
try:
|
|
desiredId = json['uuid']
|
|
|
|
requestedCyberware = getCyberwareHelper(desiredId)
|
|
|
|
if (requestedCyberware != None):
|
|
return jsonify(requestedCyberware), 200
|
|
except:
|
|
return 'Incorrect usage.\nUsage: { uuid: STRING }\n', 400
|
|
return 'UUID Invalid\n', 400
|
|
|
|
# Arguments { uuid: STRING }
|
|
@app.route('/api/cyberware/battery')
|
|
def getCyberwareBattery():
|
|
json = request.get_json()
|
|
|
|
try:
|
|
desiredId = json['uuid']
|
|
|
|
requestedCyberware = getCyberwareHelper(desiredId)
|
|
|
|
if (requestedCyberware != None):
|
|
return jsonify([ { "battery": requestedCyberware['battery'] } ]), 200
|
|
except:
|
|
return 'Incorrect usage.\nUsage: { uuid: STRING }\n', 400
|
|
return 'UUID Invalid\n', 400
|
|
|
|
# Arguments { uuid: STRING, battery: INT }
|
|
@app.route('/api/cyberware/battery', methods=['POST'])
|
|
def setCyberwareBattery():
|
|
global cyberware
|
|
json = request.get_json()
|
|
|
|
try:
|
|
desiredId = json['uuid']
|
|
battery = json['battery']
|
|
|
|
requestedCyberware = getCyberwareHelper(desiredId)
|
|
|
|
if (requestedCyberware != None):
|
|
requestedCyberware['battery'] = battery
|
|
return '', 204
|
|
except:
|
|
return 'Incorrect usage.\nUsage: { battery: INT, uuid: STRING }\n', 400
|
|
return 'UUID Invalid\n', 400
|
|
|
|
@app.route('/api/cyberware/reset_malfunction')
|
|
def resetAllCyberwareMalfunction():
|
|
resetMalfunctions()
|
|
|
|
# Arguments { uuid: STRING }
|
|
@app.route('/api/cyberware/reset_malfunction', methods=['POST'])
|
|
def resetCyberwareMalfunction():
|
|
global cyberware
|
|
json = request.get_json()
|
|
|
|
try:
|
|
desiredId = json['uuid']
|
|
|
|
requestedCyberware = getCyberwareHelper(desiredId)
|
|
|
|
if (requestedCyberware != None):
|
|
requestedCyberware['lastMalfunction'] = None
|
|
return '', 204
|
|
except:
|
|
return 'Incorrect usage.\nUsage: { uuid: STRING }\n', 400
|
|
return 'UUID Invalid\n', 400
|
|
|
|
def resetMalfunctions():
|
|
global cyberware
|
|
for c in cyberware:
|
|
c['lastMalfunction'] = None
|
|
|
|
def resetMessages():
|
|
global cyberware
|
|
for c in cyberware:
|
|
if c['messages'] != None:
|
|
newMessageList = []
|
|
for m in c['messages']:
|
|
if(m['progress'] != None): # Unless we're FULLY resetting messages, we want to keep messages with a progress bar attached.
|
|
newMessageList.append(m)
|
|
if newMessageList == []:
|
|
c['messages'] = None
|
|
else:
|
|
c['messages'] = newMessageList
|
|
|
|
def resetMessagesFull():
|
|
global cyberware
|
|
for c in cyberware:
|
|
c['messages'] = None
|
|
|
|
def getCyberwareHelper(desiredId):
|
|
i = 0
|
|
for c in cyberware:
|
|
if c['uuid'] == desiredId:
|
|
return c
|
|
break
|
|
return None
|
|
|
|
# Environment data
|
|
environmentTemperature = None
|
|
environmentHumidity = None
|
|
|
|
@app.route('/api/environment')
|
|
def getEnvironment():
|
|
returnArr = [ { 'temperature': environmentTemperature, 'humidity': environmentHumidity } ]
|
|
return jsonify(returnArr), 200
|
|
|
|
# Environment//Temperature
|
|
@app.route('/api/environment/temperature')
|
|
def getEnvironmentTemperature():
|
|
returnArr = [ { 'temperature': environmentTemperature } ]
|
|
return jsonify(returnArr), 200
|
|
|
|
@app.route('/api/environment/temperature', methods=['POST'])
|
|
def setEnvironmentTemperature():
|
|
global environmentTemperature
|
|
json = request.get_json()
|
|
|
|
try:
|
|
tempTemperature = json['temperature']
|
|
|
|
environmentTemperature = tempTemperature
|
|
except:
|
|
return 'Incorrect usage.\nUsage: { temperature: INT, uuid: STRING }\n', 400
|
|
return '', 204
|
|
|
|
# Environment//Humidity
|
|
@app.route('/api/environment/humidity')
|
|
def getEnvironmentHumidity():
|
|
returnArr = [ { 'humidity': environmentHumidity } ]
|
|
return jsonify(returnArr), 200
|
|
|
|
@app.route('/api/environment/humidity', methods=['POST'])
|
|
def setEnvironmentHumidity():
|
|
global environmentHumidity
|
|
json = request.get_json()
|
|
|
|
try:
|
|
tempHumidity = json['humidity']
|
|
|
|
environmentHumidity = tempHumidity
|
|
except:
|
|
return 'Incorrect usage.\nUsage: { humidity: INT, uuid: STRING }\n', 400
|
|
return '', 204
|
|
|
|
# Authentication method
|
|
# This authorizes the given UUID to determine whether the request is
|
|
# allowed to set the requested endpoint.
|
|
# This is ONLY used for PUSH requests currently.
|
|
def authenticate(uuid, endpoint):
|
|
# Check for internal UUID
|
|
if uuid == internalUUID:
|
|
return True
|
|
|
|
for c in cyberware: # UUID Match
|
|
if c['uuid'] == uuid:
|
|
requestedHardware = c
|
|
c['lastContact'] = datetime.now() # Update last contact
|
|
break
|
|
|
|
if requestedHardware['canSet'] == None:
|
|
return False
|
|
|
|
for e in requestedHardware['canSet']: # Endpoint match
|
|
if e == endpoint:
|
|
return True
|
|
return False
|
|
|
|
|
|
@app.route('/')
|
|
def uiindex():
|
|
return render_template('index.html')
|
|
|
|
|
|
# Maintenance functions
|
|
# The jank, my oh my
|
|
valuesToValidate = [ '/api/vitals/heartrate', '/api/vitals/oxygen', '/api/vitals/bodytemp',
|
|
'/api/fitness/steps',
|
|
'/api/environment/temperature', '/api/environment/humidity']
|
|
baseURL = "http://localhost:5000"
|
|
# Value invalidation. A value is deemed invalid when there's no hardware attached
|
|
# that can set it.
|
|
# This has the potential to become very slow. A better solution is needed.
|
|
def valueInvalidation():
|
|
print("Start value invalidation")
|
|
|
|
# Search for invalid values
|
|
invalidated = copy(valuesToValidate)
|
|
for c in cyberware:
|
|
for value in valuesToValidate:
|
|
if value in c["canSet"]:
|
|
invalidated.remove(value)
|
|
|
|
# Value invalidation begins
|
|
invalidStr = ""
|
|
for invalidValue in invalidated:
|
|
# Now this looks stupid, but since the key to post is always supposed to be the same
|
|
# as the last part of the path, this works.
|
|
# Does that make sense? I'm tired...
|
|
key = invalidValue.split('/')[-1]
|
|
|
|
endpointToReset = baseURL + invalidValue
|
|
|
|
requests.post(endpointToReset, json={ key: None, 'uuid': internalUUID })
|
|
invalidStr = invalidStr + invalidValue + ", "
|
|
|
|
print("Values invalidated: " + invalidStr)
|
|
|
|
# Device invalidation. A device is deemed invalid after no contact for 15 seconds.
|
|
timeToInvalid = 15.0
|
|
|
|
def deviceInvalidation():
|
|
cyberwareRemoveEndpoint = baseURL + "/api/cyberware/remove"
|
|
|
|
print("Start device invalidation")
|
|
now = datetime.now()
|
|
|
|
for c in cyberware:
|
|
timeSinceContact = (now-c['lastContact']).total_seconds()
|
|
|
|
if timeSinceContact > timeToInvalid:
|
|
# This hardware has not contacted the server in too long, and it is thus deemed invalid.
|
|
invalidCyberwareName = c['name']
|
|
invalidCyberwareUUID = c['uuid']
|
|
|
|
print("Cyberware is invalid and will be removed: " + invalidCyberwareName)
|
|
requests.post(cyberwareRemoveEndpoint, json={ 'uuid': invalidCyberwareUUID })
|
|
|
|
|
|
# APScheduler config
|
|
class Config:
|
|
JOBS = [
|
|
{
|
|
"id": "valueInvalidation",
|
|
"func": "nightserver:valueInvalidation",
|
|
"trigger": "interval",
|
|
"seconds": 10,
|
|
},
|
|
{
|
|
"id": "deviceInvalidation",
|
|
"func": "nightserver:deviceInvalidation",
|
|
"trigger": "interval",
|
|
"seconds": 15
|
|
}
|
|
]
|
|
|
|
SCHEDULER_API_ENABLED = True
|
|
|
|
# Config Flask and APScheduler
|
|
app.config.from_object(Config())
|
|
|
|
scheduler = APScheduler()
|
|
|
|
scheduler.init_app(app)
|
|
scheduler.start()
|
|
|
|
if __name__ == "__main__":
|
|
app.run()
|