nightui/src/nightserver.py

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