kiln-controller/kiln-controller.py
Marko Burazin ebc4c89143 Add ability to schedule when the kiln starts
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: ..."
2021-12-18 13:11:29 +01:00

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