Compare commits

...

4 Commits
main ... master

Author SHA1 Message Date
Jason Bruce
2e756178dc
Merge pull request #168 from SamSkjord/master
profile filetype checks
2024-07-02 13:56:42 -04:00
Sam
96e5919464 profile filetype checks
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
2024-01-23 14:23:57 +00:00
jbruce
7c7a1b648e remove the loading message 2022-12-16 14:21:37 -05:00
jbruce
f0c97ed220 make hours the tick size on the live graph always 2022-12-16 09:49:19 -05:00
4 changed files with 105 additions and 85 deletions

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ thumbs.db
#storage/profiles #storage/profiles
#config.py #config.py
.idea/* .idea/*
.DS_Store

View File

@ -107,7 +107,7 @@ sim_R_ho_air = 0.05 # K/W " with internal air circulation
temp_scale = "f" # c = Celsius | f = Fahrenheit - Unit to display temp_scale = "f" # c = Celsius | f = Fahrenheit - Unit to display
time_scale_slope = "h" # s = Seconds | m = Minutes | h = Hours - Slope displayed in temp_scale per time_scale_slope time_scale_slope = "h" # s = Seconds | m = Minutes | h = Hours - Slope displayed in temp_scale per time_scale_slope
time_scale_profile = "h" # s = Seconds | m = Minutes | h = Hours - Enter and view target time in time_scale_profile time_scale_profile = "m" # s = Seconds | m = Minutes | h = Hours - Enter and view target time in time_scale_profile
# emergency shutoff the profile if this temp is reached or exceeded. # emergency shutoff the profile if this temp is reached or exceeded.
# This just shuts off the profile. If your SSR is working, your kiln will # This just shuts off the profile. If your SSR is working, your kiln will

View File

@ -8,7 +8,8 @@ import json
import bottle import bottle
import gevent import gevent
import geventwebsocket import geventwebsocket
#from bottle import post, get
# from bottle import post, get
from gevent.pywsgi import WSGIServer from gevent.pywsgi import WSGIServer
from geventwebsocket.handler import WebSocketHandler from geventwebsocket.handler import WebSocketHandler
from geventwebsocket import WebSocketError from geventwebsocket import WebSocketError
@ -16,10 +17,11 @@ from geventwebsocket import WebSocketError
try: try:
sys.dont_write_bytecode = True sys.dont_write_bytecode = True
import config import config
sys.dont_write_bytecode = False sys.dont_write_bytecode = False
except: except:
print ("Could not import config file.") print("Could not import config file.")
print ("Copy config.py.EXAMPLE to config.py and adapt it for your setup.") print("Copy config.py.EXAMPLE to config.py and adapt it for your setup.")
exit(1) exit(1)
logging.basicConfig(level=config.log_level, format=config.log_format) logging.basicConfig(level=config.log_level, format=config.log_format)
@ -27,7 +29,7 @@ log = logging.getLogger("kiln-controller")
log.info("Starting kiln controller") log.info("Starting kiln controller")
script_dir = os.path.dirname(os.path.realpath(__file__)) script_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, script_dir + '/lib/') sys.path.insert(0, script_dir + "/lib/")
profile_path = config.kiln_profiles_directory profile_path = config.kiln_profiles_directory
from oven import SimulatedOven, RealOven, Profile from oven import SimulatedOven, RealOven, Profile
@ -45,93 +47,99 @@ ovenWatcher = OvenWatcher(oven)
# this ovenwatcher is used in the oven class for restarts # this ovenwatcher is used in the oven class for restarts
oven.set_ovenwatcher(ovenWatcher) oven.set_ovenwatcher(ovenWatcher)
@app.route('/')
def index():
return bottle.redirect('/picoreflow/index.html')
@app.get('/api/stats') @app.route("/")
def index():
return bottle.redirect("/picoreflow/index.html")
@app.get("/api/stats")
def handle_api(): def handle_api():
log.info("/api/stats command received") log.info("/api/stats command received")
if hasattr(oven,'pid'): if hasattr(oven, "pid"):
if hasattr(oven.pid,'pidstats'): if hasattr(oven.pid, "pidstats"):
return json.dumps(oven.pid.pidstats) return json.dumps(oven.pid.pidstats)
@app.post('/api') @app.post("/api")
def handle_api(): def handle_api():
log.info("/api is alive") log.info("/api is alive")
# run a kiln schedule # run a kiln schedule
if bottle.request.json['cmd'] == 'run': if bottle.request.json["cmd"] == "run":
wanted = bottle.request.json['profile'] wanted = bottle.request.json["profile"]
log.info('api requested run of profile = %s' % wanted) log.info("api requested run of profile = %s" % wanted)
# start at a specific minute in the schedule # start at a specific minute in the schedule
# for restarting and skipping over early parts of a schedule # for restarting and skipping over early parts of a schedule
startat = 0; startat = 0
if 'startat' in bottle.request.json: if "startat" in bottle.request.json:
startat = bottle.request.json['startat'] startat = bottle.request.json["startat"]
# get the wanted profile/kiln schedule # get the wanted profile/kiln schedule
profile = find_profile(wanted) profile = find_profile(wanted)
if profile is None: if profile is None:
return { "success" : False, "error" : "profile %s not found" % wanted } return {"success": False, "error": "profile %s not found" % wanted}
# FIXME juggling of json should happen in the Profile class # FIXME juggling of json should happen in the Profile class
profile_json = json.dumps(profile) profile_json = json.dumps(profile)
profile = Profile(profile_json) profile = Profile(profile_json)
oven.run_profile(profile,startat=startat) oven.run_profile(profile, startat=startat)
ovenWatcher.record(profile) ovenWatcher.record(profile)
if bottle.request.json['cmd'] == 'stop': if bottle.request.json["cmd"] == "stop":
log.info("api stop command received") log.info("api stop command received")
oven.abort_run() oven.abort_run()
if bottle.request.json['cmd'] == 'memo': if bottle.request.json["cmd"] == "memo":
log.info("api memo command received") log.info("api memo command received")
memo = bottle.request.json['memo'] memo = bottle.request.json["memo"]
log.info("memo=%s" % (memo)) log.info("memo=%s" % (memo))
# get stats during a run # get stats during a run
if bottle.request.json['cmd'] == 'stats': if bottle.request.json["cmd"] == "stats":
log.info("api stats command received") log.info("api stats command received")
if hasattr(oven,'pid'): if hasattr(oven, "pid"):
if hasattr(oven.pid,'pidstats'): if hasattr(oven.pid, "pidstats"):
return json.dumps(oven.pid.pidstats) return json.dumps(oven.pid.pidstats)
return { "success" : True } return {"success": True}
def find_profile(wanted): def find_profile(wanted):
''' """
given a wanted profile name, find it and return the parsed given a wanted profile name, find it and return the parsed
json profile object or None. json profile object or None.
''' """
#load all profiles from disk # load all profiles from disk
profiles = get_profiles() profiles = get_profiles()
json_profiles = json.loads(profiles) json_profiles = json.loads(profiles)
# find the wanted profile # find the wanted profile
for profile in json_profiles: for profile in json_profiles:
if profile['name'] == wanted: if profile["name"] == wanted:
return profile return profile
return None return None
@app.route('/picoreflow/:filename#.*#')
@app.route("/picoreflow/:filename#.*#")
def send_static(filename): def send_static(filename):
log.debug("serving %s" % 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")) return bottle.static_file(
filename,
root=os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), "public"),
)
def get_websocket_from_request(): def get_websocket_from_request():
env = bottle.request.environ env = bottle.request.environ
wsock = env.get('wsgi.websocket') wsock = env.get("wsgi.websocket")
if not wsock: if not wsock:
abort(400, 'Expected WebSocket request.') abort(400, "Expected WebSocket request.")
return wsock return wsock
@app.route('/control') @app.route("/control")
def handle_control(): def handle_control():
wsock = get_websocket_from_request() wsock = get_websocket_from_request()
log.info("websocket (control) opened") log.info("websocket (control) opened")
@ -143,7 +151,7 @@ def handle_control():
msgdict = json.loads(message) msgdict = json.loads(message)
if msgdict.get("cmd") == "RUN": if msgdict.get("cmd") == "RUN":
log.info("RUN command received") log.info("RUN command received")
profile_obj = msgdict.get('profile') profile_obj = msgdict.get("profile")
if profile_obj: if profile_obj:
profile_json = json.dumps(profile_obj) profile_json = json.dumps(profile_obj)
profile = Profile(profile_json) profile = Profile(profile_json)
@ -151,15 +159,15 @@ def handle_control():
ovenWatcher.record(profile) ovenWatcher.record(profile)
elif msgdict.get("cmd") == "SIMULATE": elif msgdict.get("cmd") == "SIMULATE":
log.info("SIMULATE command received") log.info("SIMULATE command received")
#profile_obj = msgdict.get('profile') # profile_obj = msgdict.get('profile')
#if profile_obj: # if profile_obj:
# profile_json = json.dumps(profile_obj) # profile_json = json.dumps(profile_obj)
# profile = Profile(profile_json) # profile = Profile(profile_json)
#simulated_oven = Oven(simulate=True, time_step=0.05) # simulated_oven = Oven(simulate=True, time_step=0.05)
#simulation_watcher = OvenWatcher(simulated_oven) # simulation_watcher = OvenWatcher(simulated_oven)
#simulation_watcher.add_observer(wsock) # simulation_watcher.add_observer(wsock)
#simulated_oven.run_profile(profile) # simulated_oven.run_profile(profile)
#simulation_watcher.record(profile) # simulation_watcher.record(profile)
elif msgdict.get("cmd") == "STOP": elif msgdict.get("cmd") == "STOP":
log.info("Stop command received") log.info("Stop command received")
oven.abort_run() oven.abort_run()
@ -169,7 +177,7 @@ def handle_control():
log.info("websocket (control) closed") log.info("websocket (control) closed")
@app.route('/storage') @app.route("/storage")
def handle_storage(): def handle_storage():
wsock = get_websocket_from_request() wsock = get_websocket_from_request()
log.info("websocket (storage) opened") log.info("websocket (storage) opened")
@ -190,18 +198,18 @@ def handle_storage():
wsock.send(get_profiles()) wsock.send(get_profiles())
elif msgdict.get("cmd") == "DELETE": elif msgdict.get("cmd") == "DELETE":
log.info("DELETE command received") log.info("DELETE command received")
profile_obj = msgdict.get('profile') profile_obj = msgdict.get("profile")
if delete_profile(profile_obj): if delete_profile(profile_obj):
msgdict["resp"] = "OK" msgdict["resp"] = "OK"
wsock.send(json.dumps(msgdict)) wsock.send(json.dumps(msgdict))
#wsock.send(get_profiles()) # wsock.send(get_profiles())
elif msgdict.get("cmd") == "PUT": elif msgdict.get("cmd") == "PUT":
log.info("PUT command received") log.info("PUT command received")
profile_obj = msgdict.get('profile') profile_obj = msgdict.get("profile")
#force = msgdict.get('force', False) # force = msgdict.get('force', False)
force = True force = True
if profile_obj: if profile_obj:
#del msgdict["cmd"] # del msgdict["cmd"]
if save_profile(profile_obj, force): if save_profile(profile_obj, force):
msgdict["resp"] = "OK" msgdict["resp"] = "OK"
else: else:
@ -215,7 +223,7 @@ def handle_storage():
log.info("websocket (storage) closed") log.info("websocket (storage) closed")
@app.route('/config') @app.route("/config")
def handle_config(): def handle_config():
wsock = get_websocket_from_request() wsock = get_websocket_from_request()
log.info("websocket (config) opened") log.info("websocket (config) opened")
@ -228,7 +236,7 @@ def handle_config():
log.info("websocket (config) closed") log.info("websocket (config) closed")
@app.route('/status') @app.route("/status")
def handle_status(): def handle_status():
wsock = get_websocket_from_request() wsock = get_websocket_from_request()
ovenWatcher.add_observer(wsock) ovenWatcher.add_observer(wsock)
@ -245,31 +253,39 @@ def handle_status():
def get_profiles(): def get_profiles():
try: try:
profile_files = os.listdir(profile_path) profile_files = os.listdir(profile_path)
profile_files.sort()
except: except:
profile_files = [] profile_files = []
profiles = [] profiles = []
for filename in profile_files: for filename in profile_files:
with open(os.path.join(profile_path, filename), 'r') as f: if filename.startswith("._"):
profiles.append(json.load(f)) 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) return json.dumps(profiles)
def save_profile(profile, force=False): def save_profile(profile, force=False):
profile_json = json.dumps(profile) profile_json = json.dumps(profile)
filename = profile['name']+".json" filename = profile["name"] + ".json"
filepath = os.path.join(profile_path, filename) filepath = os.path.join(profile_path, filename)
if not force and os.path.exists(filepath): if not force and os.path.exists(filepath):
log.error("Could not write, %s already exists" % filepath) log.error("Could not write, %s already exists" % filepath)
return False return False
with open(filepath, 'w+') as f: with open(filepath, "w+") as f:
f.write(profile_json) f.write(profile_json)
f.close() f.close()
log.info("Wrote %s" % filepath) log.info("Wrote %s" % filepath)
return True return True
def delete_profile(profile): def delete_profile(profile):
profile_json = json.dumps(profile) profile_json = json.dumps(profile)
filename = profile['name']+".json" filename = profile["name"] + ".json"
filepath = os.path.join(profile_path, filename) filepath = os.path.join(profile_path, filename)
os.remove(filepath) os.remove(filepath)
log.info("Deleted %s" % filepath) log.info("Deleted %s" % filepath)
@ -277,11 +293,15 @@ def delete_profile(profile):
def get_config(): def get_config():
return json.dumps({"temp_scale": config.temp_scale, return json.dumps(
"time_scale_slope": config.time_scale_slope, {
"time_scale_profile": config.time_scale_profile, "temp_scale": config.temp_scale,
"kwh_rate": config.kwh_rate, "time_scale_slope": config.time_scale_slope,
"currency_type": config.currency_type}) "time_scale_profile": config.time_scale_profile,
"kwh_rate": config.kwh_rate,
"currency_type": config.currency_type,
}
)
def main(): def main():
@ -289,8 +309,7 @@ def main():
port = config.listening_port port = config.listening_port
log.info("listening on %s:%d" % (ip, port)) log.info("listening on %s:%d" % (ip, port))
server = WSGIServer((ip, port), app, server = WSGIServer((ip, port), app, handler_class=WebSocketHandler)
handler_class=WebSocketHandler)
server.serve_forever() server.serve_forever()

View File

@ -368,14 +368,14 @@ function saveProfile()
} }
function get_tick_size() { function get_tick_size() {
switch(time_scale_profile){ //switch(time_scale_profile){
case "s": // case "s":
return 1; // return 1;
case "m": // case "m":
return 60; // return 60;
case "h": // case "h":
return 3600; // return 3600;
} // }
return 3600; return 3600;
} }
@ -473,17 +473,17 @@ $(document).ready(function()
{ {
console.log("Status Socket has been opened"); console.log("Status Socket has been opened");
$.bootstrapGrowl("<span class=\"glyphicon glyphicon-exclamation-sign\"></span>Getting data from server", // $.bootstrapGrowl("<span class=\"glyphicon glyphicon-exclamation-sign\"></span>Getting data from server",
{ // {
ele: 'body', // which element to append to // ele: 'body', // which element to append to
type: 'success', // (null, 'info', 'error', 'success') // type: 'success', // (null, 'info', 'error', 'success')
offset: {from: 'top', amount: 250}, // 'top', or 'bottom' // offset: {from: 'top', amount: 250}, // 'top', or 'bottom'
align: 'center', // ('left', 'right', or 'center') // align: 'center', // ('left', 'right', or 'center')
width: 385, // (integer, or 'auto') // width: 385, // (integer, or 'auto')
delay: 2500, // delay: 2500,
allow_dismiss: true, // allow_dismiss: true,
stackup_spacing: 10 // spacing between consecutively stacked growls. // stackup_spacing: 10 // spacing between consecutively stacked growls.
}); // });
}; };
ws_status.onclose = function() ws_status.onclose = function()