Added my Controller as a samba share so I could edit profile files remotely, as OSX is very aggressive about shoving ._ files where you don't want them, I added a check for that and another check to make sure to only try and open files ending in .json
318 lines
9.4 KiB
Python
Executable File
318 lines
9.4 KiB
Python
Executable File
#!/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()
|