#!/usr/bin/env python import os import sys import logging import json 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 = config.kiln_profiles_directory 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) # this ovenwatcher is used in the oven class for restarts oven.set_ovenwatcher(ovenWatcher) @app.route("/") def index(): return bottle.redirect("/picoreflow/index.html") @app.get("/api/stats") def handle_api(): log.info("/api/stats command received") if hasattr(oven, "pid"): if hasattr(oven.pid, "pidstats"): return json.dumps(oven.pid.pidstats) @app.post("/api") def handle_api(): log.info("/api is alive") # 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) oven.run_profile(profile, startat=startat) ovenWatcher.record(profile) if bottle.request.json["cmd"] == "stop": log.info("api stop command received") oven.abort_run() if bottle.request.json["cmd"] == "memo": log.info("api memo command received") memo = bottle.request.json["memo"] log.info("memo=%s" % (memo)) # get stats during a run if bottle.request.json["cmd"] == "stats": log.info("api stats command received") if hasattr(oven, "pid"): if hasattr(oven.pid, "pidstats"): return json.dumps(oven.pid.pidstats) 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 @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) oven.run_profile(profile) 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) profile_files.sort() except: profile_files = [] profiles = [] for filename in profile_files: if filename.startswith("._"): pass else: if filename.endswith(".json"): with open(os.path.join(profile_path, filename), "r") as f: profiles.append(json.load(f)) else: pass 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 = "0.0.0.0" 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()