kopia lustrzana https://github.com/jbruce12000/kiln-controller
				
				
				
			
		
			
				
	
	
		
			299 wiersze
		
	
	
		
			9.1 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
			
		
		
	
	
			299 wiersze
		
	
	
		
			9.1 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)
 | |
|     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 = "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()
 |