This adds the possibility to use a datepicker in the modal after clicking the start button to schedule when the kiln should start running by itself automatically. The timer is implemented in the backend and the start is triggered there so closing or refreshing the browser does not stop it. In the state when it's "waiting to start", the frontend state changes so that the glowing timer icon is now shown instead of the previously unused door icon. The state is also displayed as SCHEDULED and above it the info states when it's due to start: "Start at: ..."
301 lines
9.2 KiB
Python
Executable File
301 lines
9.2 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
import os
|
|
import sys
|
|
import logging
|
|
import json
|
|
from datetime import datetime
|
|
|
|
import bottle
|
|
import gevent
|
|
import geventwebsocket
|
|
#from bottle import post, get
|
|
from gevent.pywsgi import WSGIServer
|
|
from geventwebsocket.handler import WebSocketHandler
|
|
from geventwebsocket import WebSocketError
|
|
|
|
|
|
try:
|
|
sys.dont_write_bytecode = True
|
|
import config
|
|
sys.dont_write_bytecode = False
|
|
except:
|
|
print ("Could not import config file.")
|
|
print ("Copy config.py.EXAMPLE to config.py and adapt it for your setup.")
|
|
exit(1)
|
|
|
|
logging.basicConfig(level=config.log_level, format=config.log_format)
|
|
log = logging.getLogger("kiln-controller")
|
|
log.info("Starting kiln controller")
|
|
|
|
script_dir = os.path.dirname(os.path.realpath(__file__))
|
|
sys.path.insert(0, script_dir + '/lib/')
|
|
profile_path = os.path.join(script_dir, "storage", "profiles")
|
|
|
|
from oven import SimulatedOven, RealOven, Profile
|
|
from ovenWatcher import OvenWatcher
|
|
|
|
app = bottle.Bottle()
|
|
|
|
if config.simulate == True:
|
|
log.info("this is a simulation")
|
|
oven = SimulatedOven()
|
|
else:
|
|
log.info("this is a real kiln")
|
|
oven = RealOven()
|
|
ovenWatcher = OvenWatcher(oven)
|
|
|
|
@app.route('/')
|
|
def index():
|
|
return bottle.redirect('/picoreflow/index.html')
|
|
|
|
@app.post('/api')
|
|
def handle_api():
|
|
log.info("/api is alive")
|
|
log.info(bottle.request.json)
|
|
|
|
# run a kiln schedule
|
|
if bottle.request.json['cmd'] == 'run':
|
|
wanted = bottle.request.json['profile']
|
|
log.info('api requested run of profile = %s' % wanted)
|
|
|
|
# start at a specific minute in the schedule
|
|
# for restarting and skipping over early parts of a schedule
|
|
startat = 0
|
|
if 'startat' in bottle.request.json:
|
|
startat = bottle.request.json['startat']
|
|
|
|
# get the wanted profile/kiln schedule
|
|
profile = find_profile(wanted)
|
|
if profile is None:
|
|
return { "success" : False, "error" : "profile %s not found" % wanted }
|
|
|
|
# FIXME juggling of json should happen in the Profile class
|
|
profile_json = json.dumps(profile)
|
|
profile = Profile(profile_json)
|
|
run_profile(profile,startat=startat)
|
|
|
|
if bottle.request.json['cmd'] == 'stop':
|
|
log.info("api stop command received")
|
|
oven.abort_run()
|
|
|
|
return { "success" : True }
|
|
|
|
def find_profile(wanted):
|
|
'''
|
|
given a wanted profile name, find it and return the parsed
|
|
json profile object or None.
|
|
'''
|
|
#load all profiles from disk
|
|
profiles = get_profiles()
|
|
json_profiles = json.loads(profiles)
|
|
|
|
# find the wanted profile
|
|
for profile in json_profiles:
|
|
if profile['name'] == wanted:
|
|
return profile
|
|
return None
|
|
|
|
def run_profile(profile, startat=0):
|
|
oven.run_profile(profile, startat)
|
|
ovenWatcher.record(profile)
|
|
|
|
|
|
@app.route('/picoreflow/:filename#.*#')
|
|
def send_static(filename):
|
|
log.debug("serving %s" % filename)
|
|
return bottle.static_file(filename, root=os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), "public"))
|
|
|
|
|
|
def get_websocket_from_request():
|
|
env = bottle.request.environ
|
|
wsock = env.get('wsgi.websocket')
|
|
if not wsock:
|
|
abort(400, 'Expected WebSocket request.')
|
|
return wsock
|
|
|
|
|
|
@app.route('/control')
|
|
def handle_control():
|
|
wsock = get_websocket_from_request()
|
|
log.info("websocket (control) opened")
|
|
while True:
|
|
try:
|
|
message = wsock.receive()
|
|
if message:
|
|
log.info("Received (control): %s" % message)
|
|
msgdict = json.loads(message)
|
|
if msgdict.get("cmd") == "RUN":
|
|
log.info("RUN command received")
|
|
profile_obj = msgdict.get('profile')
|
|
if profile_obj:
|
|
profile_json = json.dumps(profile_obj)
|
|
profile = Profile(profile_json)
|
|
|
|
run_profile(profile)
|
|
|
|
elif msgdict.get("cmd") == "SCHEDULED_RUN":
|
|
log.info("SCHEDULED_RUN command received")
|
|
scheduled_start_time = msgdict.get('scheduledStartTime')
|
|
profile_obj = msgdict.get('profile')
|
|
if profile_obj:
|
|
profile_json = json.dumps(profile_obj)
|
|
profile = Profile(profile_json)
|
|
|
|
start_datetime = datetime.fromisoformat(
|
|
scheduled_start_time,
|
|
)
|
|
oven.scheduled_run(
|
|
start_datetime,
|
|
profile,
|
|
lambda: ovenWatcher.record(profile),
|
|
)
|
|
|
|
elif msgdict.get("cmd") == "SIMULATE":
|
|
log.info("SIMULATE command received")
|
|
#profile_obj = msgdict.get('profile')
|
|
#if profile_obj:
|
|
# profile_json = json.dumps(profile_obj)
|
|
# profile = Profile(profile_json)
|
|
#simulated_oven = Oven(simulate=True, time_step=0.05)
|
|
#simulation_watcher = OvenWatcher(simulated_oven)
|
|
#simulation_watcher.add_observer(wsock)
|
|
#simulated_oven.run_profile(profile)
|
|
#simulation_watcher.record(profile)
|
|
elif msgdict.get("cmd") == "STOP":
|
|
log.info("Stop command received")
|
|
oven.abort_run()
|
|
except WebSocketError as e:
|
|
log.error(e)
|
|
break
|
|
log.info("websocket (control) closed")
|
|
|
|
|
|
@app.route('/storage')
|
|
def handle_storage():
|
|
wsock = get_websocket_from_request()
|
|
log.info("websocket (storage) opened")
|
|
while True:
|
|
try:
|
|
message = wsock.receive()
|
|
if not message:
|
|
break
|
|
log.debug("websocket (storage) received: %s" % message)
|
|
|
|
try:
|
|
msgdict = json.loads(message)
|
|
except:
|
|
msgdict = {}
|
|
|
|
if message == "GET":
|
|
log.info("GET command received")
|
|
wsock.send(get_profiles())
|
|
elif msgdict.get("cmd") == "DELETE":
|
|
log.info("DELETE command received")
|
|
profile_obj = msgdict.get('profile')
|
|
if delete_profile(profile_obj):
|
|
msgdict["resp"] = "OK"
|
|
wsock.send(json.dumps(msgdict))
|
|
#wsock.send(get_profiles())
|
|
elif msgdict.get("cmd") == "PUT":
|
|
log.info("PUT command received")
|
|
profile_obj = msgdict.get('profile')
|
|
#force = msgdict.get('force', False)
|
|
force = True
|
|
if profile_obj:
|
|
#del msgdict["cmd"]
|
|
if save_profile(profile_obj, force):
|
|
msgdict["resp"] = "OK"
|
|
else:
|
|
msgdict["resp"] = "FAIL"
|
|
log.debug("websocket (storage) sent: %s" % message)
|
|
|
|
wsock.send(json.dumps(msgdict))
|
|
wsock.send(get_profiles())
|
|
except WebSocketError:
|
|
break
|
|
log.info("websocket (storage) closed")
|
|
|
|
|
|
@app.route('/config')
|
|
def handle_config():
|
|
wsock = get_websocket_from_request()
|
|
log.info("websocket (config) opened")
|
|
while True:
|
|
try:
|
|
message = wsock.receive()
|
|
wsock.send(get_config())
|
|
except WebSocketError:
|
|
break
|
|
log.info("websocket (config) closed")
|
|
|
|
|
|
@app.route('/status')
|
|
def handle_status():
|
|
wsock = get_websocket_from_request()
|
|
ovenWatcher.add_observer(wsock)
|
|
log.info("websocket (status) opened")
|
|
while True:
|
|
try:
|
|
message = wsock.receive()
|
|
wsock.send("Your message was: %r" % message)
|
|
except WebSocketError:
|
|
break
|
|
log.info("websocket (status) closed")
|
|
|
|
|
|
def get_profiles():
|
|
try:
|
|
profile_files = os.listdir(profile_path)
|
|
except:
|
|
profile_files = []
|
|
profiles = []
|
|
for filename in profile_files:
|
|
with open(os.path.join(profile_path, filename), 'r') as f:
|
|
profiles.append(json.load(f))
|
|
return json.dumps(profiles)
|
|
|
|
|
|
def save_profile(profile, force=False):
|
|
profile_json = json.dumps(profile)
|
|
filename = profile['name']+".json"
|
|
filepath = os.path.join(profile_path, filename)
|
|
if not force and os.path.exists(filepath):
|
|
log.error("Could not write, %s already exists" % filepath)
|
|
return False
|
|
with open(filepath, 'w+') as f:
|
|
f.write(profile_json)
|
|
f.close()
|
|
log.info("Wrote %s" % filepath)
|
|
return True
|
|
|
|
def delete_profile(profile):
|
|
profile_json = json.dumps(profile)
|
|
filename = profile['name']+".json"
|
|
filepath = os.path.join(profile_path, filename)
|
|
os.remove(filepath)
|
|
log.info("Deleted %s" % filepath)
|
|
return True
|
|
|
|
|
|
def get_config():
|
|
return json.dumps({"temp_scale": config.temp_scale,
|
|
"time_scale_slope": config.time_scale_slope,
|
|
"time_scale_profile": config.time_scale_profile,
|
|
"kwh_rate": config.kwh_rate,
|
|
"currency_type": config.currency_type})
|
|
|
|
|
|
def main():
|
|
ip = config.listening_ip
|
|
port = config.listening_port
|
|
log.info("listening on %s:%d" % (ip, port))
|
|
|
|
server = WSGIServer((ip, port), app,
|
|
handler_class=WebSocketHandler)
|
|
server.serve_forever()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|