From 4db8e30515987ed13916661ce76dd3091a88754b Mon Sep 17 00:00:00 2001 From: jbruce12000 Date: Sun, 19 Jun 2022 16:20:44 -0400 Subject: [PATCH] adding restart profile functionality --- config.py | 22 ++++++++++++---- kiln-controller.py | 2 ++ lib/oven.py | 66 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/config.py b/config.py index e49e716..c9b509a 100644 --- a/config.py +++ b/config.py @@ -13,7 +13,7 @@ log_format = '%(asctime)s %(levelname)s %(name)s: %(message)s' ### Server listening_ip = "0.0.0.0" -listening_port = 8081 +listening_port = 8082 ### Cost Estimate kwh_rate = 0.1319 # Rate in currency_type to calculate cost to run job @@ -65,7 +65,7 @@ sensor_time_wait = 2 # your specific kiln. Note that the integral pid_ki is # inverted so that a smaller number means more integral action. pid_kp = 25 # Proportional 25,200,200 -pid_ki = 20 # Integral +pid_ki = 10 # Integral pid_kd = 200 # Derivative @@ -82,10 +82,10 @@ stop_integral_windup = True # Simulation parameters simulate = True sim_t_env = 60.0 # deg C -sim_c_heat = 100.0 # J/K heat capacity of heat element +sim_c_heat = 500.0 # J/K heat capacity of heat element sim_c_oven = 5000.0 # J/K heat capacity of oven sim_p_heat = 5450.0 # W heating power of oven -sim_R_o_nocool = 0.1 # K/W thermal resistance oven -> environment +sim_R_o_nocool = 0.5 # K/W thermal resistance oven -> environment sim_R_o_cool = 0.05 # K/W " with cooling sim_R_ho_noair = 0.1 # K/W thermal resistance heat element -> oven sim_R_ho_air = 0.05 # K/W " with internal air circulation @@ -121,7 +121,7 @@ kiln_must_catch_up = True # or 100% off because the kiln is too hot. No integral builds up # outside the window. The bigger you make the window, the more # integral you will accumulate. -pid_control_window = 10 #degrees +pid_control_window = 5 #degrees # thermocouple offset # If you put your thermocouple in ice water and it reads 36F, you can @@ -151,3 +151,15 @@ ac_freq_50hz = False # - too many errors in a short period from thermocouple # and some people just want to ignore all of that and just log the emergencies but do not quit ignore_emergencies = False + +######################################################################## +# automatic restarts - if you have a power brown-out and the raspberry pi +# reboots, this restarts your kiln where it left off in the firing profile. +# This only happens if power comes back before automatic_restart_window +# is exceeded (in minutes). The kiln-controller.py process must start +# automatically on boot-up for this to work. +automatic_restarts = True +automatic_restart_window = 15 # max minutes since power outage +automatic_restart_state_file = "/tmp/kiln_controller_state.json" + + diff --git a/kiln-controller.py b/kiln-controller.py index 9fc5015..531aba2 100755 --- a/kiln-controller.py +++ b/kiln-controller.py @@ -42,6 +42,8 @@ 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(): diff --git a/lib/oven.py b/lib/oven.py index 28d09c5..8091585 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -5,6 +5,7 @@ import datetime import logging import json import config +import os log = logging.getLogger(__name__) @@ -290,9 +291,72 @@ class Oven(threading.Thread): } return state + def save_state(self): + with open(config.automatic_restart_state_file, 'w', encoding='utf-8') as f: + json.dump(self.get_state(), f, ensure_ascii=False, indent=4) + + def state_file_is_old(self): + '''returns True is state files is older than 15 mins default + False if younger + True if state file cannot be opened or does not exist + ''' + if os.path.isfile(config.automatic_restart_state_file): + state_age = os.path.getmtime(config.automatic_restart_state_file) + now = time.time() + if((now - state_age)/60 <= config.automatic_restart_window): + return False + return True + + + def automatic_restart(self): + '''takes one of two actions + if RUNNING, saves state every duty cycle + if IDLE, checks and then starts last known good profile where it died + ''' + # check if automatic_restart setting is True + if not config.automatic_restarts == True: + return + + if self.state == "RUNNING": + # save state every duty cycle (2s by default) + self.save_state() + return + + # check if within window (use file timestamp and setting) + if self.state_file_is_old(): + log.info("restart not possible. outside restart window") + return + + # restart the last known profile where it died + self.restart() + + def restart(self): + if os.path.isfile(config.automatic_restart_state_file): + with open(config.automatic_restart_state_file) as infile: d = json.load(infile) + else: + log.info("restart not possible. no state file found.") + return + # check if last profile finished + if d["totaltime"] - d["runtime"] > 60: + startat = d["runtime"]/60 + filename = "%s.json" % (d["profile"]) + profile_path = os.path.abspath(os.path.join(os.path.dirname( __file__ ), '..', 'storage','profiles',filename)) + + log.info("restarting profile = %s at minute = %d" % (profile_path,startat)) + with open(profile_path) as infile: + profile_json = json.dumps(json.load(infile)) + profile = Profile(profile_json) + self.run_profile(profile,startat=startat) + self.ovenwatcher.record(profile) + + + def set_ovenwatcher(self,watcher): + self.ovenwatcher = watcher + def run(self): while True: if self.state == "IDLE": + self.automatic_restart() time.sleep(1) continue if self.state == "RUNNING": @@ -302,7 +366,7 @@ class Oven(threading.Thread): self.heat_then_cool() self.reset_if_emergency() self.reset_if_schedule_ended() - + self.automatic_restart() class SimulatedOven(Oven):