class SensorGroup(SensorPassive): sensordesc = "Select a sensor to be included in this group." sensor01 = Property.Sensor("Sensor 1", description=sensordesc) sensor02 = Property.Sensor("Sensor 2", description=sensordesc) sensor03 = Property.Sensor("Sensor 3", description=sensordesc) sensor04 = Property.Sensor("Sensor 4", description=sensordesc) sensor05 = Property.Sensor("Sensor 5", description=sensordesc) sensor06 = Property.Sensor("Sensor 6", description=sensordesc) sensor07 = Property.Sensor("Sensor 7", description=sensordesc) sensor08 = Property.Sensor("Sensor 8", description=sensordesc) value_type = Property.Select( "Value", options=["Average", "Minimum", "Maximum"], description="Select what data to return from the group.") def init(self): self.sensors = [] if isinstance(self.sensor01, unicode) and self.sensor01: self.sensors.append(int(self.sensor01)) if isinstance(self.sensor02, unicode) and self.sensor02: self.sensors.append(int(self.sensor02)) if isinstance(self.sensor03, unicode) and self.sensor03: self.sensors.append(int(self.sensor03)) if isinstance(self.sensor04, unicode) and self.sensor04: self.sensors.append(int(self.sensor04)) if isinstance(self.sensor05, unicode) and self.sensor05: self.sensors.append(int(self.sensor05)) if isinstance(self.sensor06, unicode) and self.sensor06: self.sensors.append(int(self.sensor06)) if isinstance(self.sensor07, unicode) and self.sensor07: self.sensors.append(int(self.sensor07)) if isinstance(self.sensor08, unicode) and self.sensor08: self.sensors.append(int(self.sensor08)) def stop(self): pass def read(self): values = [ float(cbpi.cache.get("sensors")[sensor].instance.last_value) for sensor in self.sensors ] if self.value_type == "Minimum": temp = min(values) elif self.value_type == "Maximum": temp = max(values) else: temp = sum(values) / len(values) self.data_received(round(temp, 2)) def get_unit(self): if len(self.sensors) > 0: return cbpi.cache.get("sensors")[ self.sensors[0]].instance.get_unit() else: return super(SensorBase, self).get_unit()
class TrailingAverageSensor(SensorPassive): sensor_prop = Property.Sensor("Sensor", description="Select a sensor to average readings of.") count_prop = Property.Number("Count", configurable=True, default_value=12, description="Number of readings to average.") decimals_prop = Property.Number("Decimals", configurable=True, default_value=1, description="How many decimals to round the average to.") #------------------------------------------------------------------------------- def init(self): self.values = list() self.sensor_id = int(self.sensor_prop) self.count = int(self.count_prop) self.weight = 1.0/self.count self.decimals = int(self.decimals_prop) #------------------------------------------------------------------------------- def read(self): self.values.append(float(cbpi.cache.get("sensors")[int(self.sensor_id)].instance.last_value)) while len(self.values) > self.count: self.values.pop(0) numerator = 0.0 denominator = 0.0 weight = 1.0 for value in reversed(self.values): numerator += value * weight denominator += weight weight = weight - self.weight self.data_received(round(numerator/denominator, self.decimals)) #------------------------------------------------------------------------------- def get_unit(self): return cbpi.cache.get("sensors")[int(self.sensor_id)].instance.get_unit() #------------------------------------------------------------------------------- def stop(self): pass
class SensorGroup(SensorPassive): sensordesc = "Select a sensor to be averaged with the other sensors in this group." sensor01 = Property.Sensor("Sensor 1", description=sensordesc) sensor02 = Property.Sensor("Sensor 2", description=sensordesc) sensor03 = Property.Sensor("Sensor 3", description=sensordesc) sensor04 = Property.Sensor("Sensor 4", description=sensordesc) sensor05 = Property.Sensor("Sensor 5", description=sensordesc) sensor06 = Property.Sensor("Sensor 6", description=sensordesc) sensor07 = Property.Sensor("Sensor 7", description=sensordesc) sensor08 = Property.Sensor("Sensor 8", description=sensordesc) def init(self): self.sensors = [] if isinstance(self.sensor01, unicode) and self.sensor01: self.sensors.append(int(self.sensor01)) if isinstance(self.sensor02, unicode) and self.sensor02: self.sensors.append(int(self.sensor02)) if isinstance(self.sensor03, unicode) and self.sensor03: self.sensors.append(int(self.sensor03)) if isinstance(self.sensor04, unicode) and self.sensor04: self.sensors.append(int(self.sensor04)) if isinstance(self.sensor05, unicode) and self.sensor05: self.sensors.append(int(self.sensor05)) if isinstance(self.sensor06, unicode) and self.sensor06: self.sensors.append(int(self.sensor06)) if isinstance(self.sensor07, unicode) and self.sensor07: self.sensors.append(int(self.sensor07)) if isinstance(self.sensor08, unicode) and self.sensor08: self.sensors.append(int(self.sensor08)) def stop(self): pass def read(self): tempsum = float(0) for sensor in self.sensors: tempsum += float( cbpi.cache.get("sensors")[sensor].instance.last_value) self.data_received(round(tempsum / len(self.sensors), 2)) def get_unit(self): if len(self.sensors) > 0: return cbpi.cache.get("sensors")[ self.sensors[0]].instance.get_unit() else: return super(SensorBase, self).get_unit()
class SimpleCascadeHysteresis(KettleController): """ This hysteresis controls MashTun temp. It creates hysteresis on HLT temp not allowing it to reach much higher values than desired mash tun temp (target). It allows to set offset to target MT temp and temp is held in these values so there is not so much overshooting HLT temp In other words target temp is set in mash tun but is regulated with hystersis in HLT There is also a "safety check" which is the temp of coil/tube in Herms/Rims breweries, which is often much higher than desired target temp. In this plugin, this temp is also switching off the heater with adjustable offset. """ pos_off_desc = "Positive value indicating possibility to go above target temp with actor still switched on. If target is 55 and offset is 1, heater will switch off when reaching 56." neg_off_desc = "Positive value indicating possibility to go below target temp with actor still switched off. If target is 55 and offset is 1, heater will switch back on when reaching 54." coil_sensor_desc = "Safety measurement for preventing overheating in Herms coil or rims tube. Leave blank if you don't have sensor after coil/tube." coil_off_desc = "Positive value indicating, when the heater will switch off if the temp at the end of coil/tube is above the target by this value or more. This helps to prevent rising the temp in HLT too much." a_hyst_sensor = Property.Sensor(label="HLT sensor") b_hysteresis_positive_offset = Property.Number( "Positive offset for hysteresis", True, 1, description=pos_off_desc) c_hysteresis_negative_offset = Property.Number( "Negative offset for hysteresis", True, 0, description=neg_off_desc) d_on_min = Property.Number("Hysteresis Minimum Time On (s)", True, 60) e_off_min = Property.Number("Hysteresis Minimum Time Off (s)", True, 60) f_coil_tube_sensor = Property.Sensor( label="Sensor after the HERMS coil or RIMS tube", description=coil_sensor_desc) g_coil_positive_offset = Property.Number("Positive offset for coil/tube", True, 1.5, description=coil_off_desc) def stop(self): self.heater_off() super(KettleController, self).stop() def run(self): on_min = abs(float(self.d_on_min)) off_min = abs(float(self.e_off_min)) hyst_pos_offset = abs(float(self.b_hysteresis_positive_offset)) hyst_neg_offset = abs(float(self.c_hysteresis_negative_offset)) coil_pos_offset = abs(float(self.g_coil_positive_offset)) hyst_sensor = int(self.a_hyst_sensor) if not self.f_coil_tube_sensor: coil_sensor = None coil_pos_offset = None else: coil_sensor = int(self.f_coil_tube_sensor) h = HysteresisWithTimeChecksAndSafetySwitch( True, hyst_pos_offset, hyst_neg_offset, on_min, off_min, safety_switch_offset=coil_pos_offset) heater_on = False while self.is_running(): waketime = time.time() + 3 target = self.get_target_temp() current = self.get_temp() # target reached in MT we can switch off no matter what if current >= target: self.heater_off() cbpi.app.logger.info("[%s] Target temp reached" % (waketime)) self.sleep(waketime - time.time()) continue # get control switch temp only if we have control switch control = None if coil_sensor is not None: control = float( cbpi.cache.get("sensors")[coil_sensor].instance.last_value) hyst_temp = float( cbpi.cache.get("sensors")[hyst_sensor].instance.last_value) # Update the hysteresis controller try: heater_on = h.run(hyst_temp, target, control) except TimeIntervalNotPassed as e: self.notify("Hysteresis warning", e.message, type="warning", timeout=1500) if heater_on: self.heater_on(100) cbpi.app.logger.info("[%s] Actor stays ON" % (waketime)) else: self.heater_off() cbpi.app.logger.info("[%s] Actor stays OFF" % (waketime)) # Sleep until update required again if waketime <= time.time() + 0.25: self.notify("Hysteresis Error", "Update interval is too short", type="warning") cbpi.app.logger.info( "Hysteresis - Update interval is too short") else: self.sleep(waketime - time.time())
class CascadePID(KettleController): a_inner_sensor = Property.Sensor(label="Inner loop sensor") b_inner_kp = Property.Number("Inner loop proportional term", True, 5.0, description=kp_description) c_inner_ki = Property.Number("Inner loop integral term", True, 0.25, description=ki_description) d_inner_kd = Property.Number("Inner loop derivative term", True, 0.0, description=kd_description) e_inner_integrator_max = Property.Number("Inner loop integrator max", True, 15.0, description=integrator_max_description) e_inner_integrator_initial = Property.Number("Inner loop integrator initial value", True, 0.0) f_outer_kp = Property.Number("Outer loop proportional term", True, 0.0, description=kp_description) g_outer_ki = Property.Number("Outer loop integral term", True, 2.0, description=ki_description) h_outer_kd = Property.Number("Outer loop derivative term", True, 1.0, description=kd_description) i_outer_integrator_max = Property.Number("Outer loop integrator max", True, 15.0, description=integrator_max_description) i_outer_integrator_initial = Property.Number("Outer loop integrator initial value", True, 0.0) j_update_interval = Property.Number("Update interval", True, 2.5, description=update_interval_description) k_notification_timeout = Property.Number("Notification duration", True, 5000, description=notification_timeout_description) def stop(self): self.heater_off() super(KettleController, self).stop() def run(self): if not isinstance(self.a_inner_sensor, unicode): self.notify("PID Error", "An inner sensor must be selected", timeout=None, type="danger") raise UserWarning("PID - An inner sensor must be selected") # Get inner sensor as an integer inner_sensor = int(self.a_inner_sensor) # Ensure all numerical properties are floats inner_kp = float(self.b_inner_kp) inner_ki = float(self.c_inner_ki) inner_kd = float(self.d_inner_kd) inner_integrator_max = float(self.e_inner_integrator_max) inner_integrator_initial = float(self.e_inner_integrator_initial) outer_kp = float(self.f_outer_kp) outer_ki = float(self.g_outer_ki) outer_kd = float(self.h_outer_kd) outer_integrator_max = float(self.i_outer_integrator_max) outer_integrator_initial = float(self.i_outer_integrator_initial) update_interval = float(self.j_update_interval) notification_timeout = float(self.k_notification_timeout) # Error check if update_interval <= 0.0: self.notify("PID Error", "Update interval must be positive", timeout=None, type="danger") raise ValueError("PID - Update interval must be positive") elif inner_integrator_max < 0.0: self.notify("PID Error", "Inner loop max integrator must be >= 0", timeout=None, type="danger") raise ValueError("PID - Inner loop max integrator must be >= 0") elif abs(inner_integrator_max) < abs(inner_integrator_initial): self.notify("PID Error", "Inner loop integrator initial value must be below the integrator max", timeout=None, type="danger") raise ValueError("PID - Inner loop integrator initial value must be below the integrator max") elif outer_integrator_max < 0.0: self.notify("PID Error", "Outer loop max integrator must be >= 0", timeout=None, type="danger") raise ValueError("PID - Outer loop max integrator must be >= 0") elif abs(outer_integrator_max) < abs(outer_integrator_initial): self.notify("PID Error", "Outer loop integrator initial value must be below the integrator max", timeout=None, type="danger") raise ValueError("PID - Outer loop integrator initial value must be below the integrator max") elif notification_timeout < 0.0: cbpi.notify("OneWire Error", "Notification timeout must be positive", timeout=None, type="danger") raise ValueError("OneWire - Notification timeout must be positive") else: self.heater_on(0.0) # Initialize PID cascade if cbpi.get_config_parameter("unit", "C") == "C": outer_pid = PID(outer_kp, outer_ki, outer_kd, 0.0, 100.0, outer_integrator_max, outer_integrator_initial) else: outer_pid = PID(outer_kp, outer_ki, outer_kd, 32, 212, outer_integrator_max, outer_integrator_initial) inner_pid = PID(inner_kp, inner_ki, inner_kd, 0.0, 100.0, inner_integrator_max, inner_integrator_initial) while self.is_running(): waketime = time.time() + update_interval # Get the target temperature outer_target_value = self.get_target_temp() # Calculate inner target value from outer PID outer_current_value = self.get_temp() inner_target_value = round(outer_pid.update(outer_current_value, outer_target_value),2) # Calculate inner output from inner PID inner_current_value = float(cbpi.cache.get("sensors")[inner_sensor].instance.last_value) inner_output = round(inner_pid.update(inner_current_value, inner_target_value), 2) # Update the heater power self.actor_power(inner_output) # Print loop details cbpi.app.logger.info("[%s] Outer loop PID target/actual/output/integrator: %s/%s/%s/%s" % (waketime, outer_target_value, outer_current_value, inner_target_value, round(outer_pid.integrator, 2))) cbpi.app.logger.info("[%s] Inner loop PID target/actual/output/integrator: %s/%s/%s/%s" % (waketime, inner_target_value, inner_current_value, inner_output, round(inner_pid.integrator, 2))) # Sleep until update required again if waketime <= time.time() + 0.25: self.notify("PID Error", "Update interval is too short", timeout=notification_timeout, type="warning") cbpi.app.logger.info("PID - Update interval is too short") else: self.sleep(waketime - time.time())
class FunctionActor(ActorBase): global function_actor_ids #Properties a_output_actor = Property.Actor( "Slave Actor", description="Select an Actor to be controlled") b_on_delay = Property.Number("On Delay", configurable=True, default_value=0, description="On wait time in seconds") c_off_delay = Property.Number("Off Delay", configurable=True, default_value=0, description="Off wait time in seconds") d_cycle_delay = Property.Number( "Cycle Delay", configurable=True, default_value=0, description="Minimum time before next turn on in seconds") h_control_word = Property.Text( "Control Func", configurable=True, default_value="", description= "Control function executed when actor switches on, see Readme for more info" ) trigger_sensor_a = Property.Sensor( "Trigger Sensor 1 (S1 or sensor)", description="Select a Sensor to be used as a trigger") trigger_sensor_b = Property.Sensor( "Trigger Sensor 2 (S2)", description="Select a Sensor to be used as a trigger") trigger_text = Property.Text( "Trigger Rule", configurable=True, default_value="True", description="Trigger eqation, use sensor as key word, eg sensor > 25") def init(self): try: cbpi.app.logger.info("Func Actor init") #guards and stored vals self.out = dict.fromkeys( [ "on", "req", "active", "im_on", "im_off", "no_force", "last_on" ], False ) #on: slave state, active: actor state trig: actor is triggered req: slave on is requested self.power = 100 #output timers time_now = datetime.utcnow() self.times = dict.fromkeys(["onoff", "cycle"], time_now) self.delay = {} self.delay["on"] = timedelta(seconds=tryfloat(self.b_on_delay)) self.delay["off"] = timedelta(seconds=tryfloat(self.c_off_delay)) self.delay["cycle"] = timedelta( seconds=tryfloat(self.d_cycle_delay)) self.pulse = { "on_list": [], "off_list": [], "next": [], "loop": False } #remove 'ordering' letter self.control_word = self.h_control_word self.output_actor = self.a_output_actor #check for sensor and trigger config if (((isinstance(self.trigger_sensor_a, unicode) and self.trigger_sensor_a) or (isinstance(self.trigger_sensor_b, unicode) and self.trigger_sensor_b)) and isinstance(self.trigger_text, unicode) and self.trigger_text): self.trig = {"s1": None, "s2": None, "text": self.trigger_text} if isinstance(self.trigger_sensor_a, unicode) and self.trigger_sensor_a: self.trig["s1"] = self.trigger_sensor_a if isinstance(self.trigger_sensor_b, unicode) and self.trigger_sensor_b: self.trig["s2"] = self.trigger_sensor_b self.trig.update( dict.fromkeys(["last", "im_on", "im_off", "type"], False)) else: self.trig = None #check for control word config if isinstance(self.control_word, unicode) and self.control_word: self.control_word = self.decode_control_word() else: self.control_word = None #add actor to list to execute if not int(self.id) in function_actor_ids: function_actor_ids.append(int(self.id)) self.api.switch_actor_off(int(self.output_actor)) self.display_power(0) except Exception as e: print "Function init fail" traceback.print_exc() e.throw def decode_control_word(self): try: #pulse vars word_temp = self.control_word.replace("_", "") word_temp = word_temp.replace("(", "[") word_temp = word_temp.replace(")", "]") words_temp = word_temp.split() for word in words_temp: if word[0] == "P": #Pulse command print "On Pulse command" self.pulse["on_list"] = tuple(eval(word[1:])) self.pulse["loop"] = False elif word[0] == "p": #Pulse command print "Off Pulse command" self.pulse["off_list"] = tuple(eval(word[1:])) elif word[0] == "L": #Pulse command print "Loop command" self.pulse["on_list"] = tuple(eval(word[1:])) self.pulse["loop"] = True elif word[0] == "R": #Ramp command print "On Ramp command not implemented" elif word[0] == "r": #Ramp command print "Off Ramp command not implemented" elif word == "trigSwitchActor": self.trig["type"] = "Sw" elif word == "trigToggleActor": self.trig["type"] = "Tog" elif word == "trigImOn": self.trig["im_on"] = True elif word == "trigImOff": self.trig["im_off"] = True elif word == "UiImOn": self.out["im_on"] = True elif word == "UiImOff": self.out["im_off"] = True elif word == "noForce": self.out["no_force"] = True else: print word raise ValueError("Control word not valid") except Exception as e: print e cbpi.app.logger.error("Control Word not valid") return False return True def trigger_eval(self): #print "trigger" if self.trig["s1"]: sensor = tryfloat(cbpi.get_sensor_value(tryint(self.trig["s1"]))) else: sensor = 0 s1 = sensor if self.trig["s2"]: s2 = tryfloat(cbpi.get_sensor_value(tryint(self.trig["s2"]))) else: s2 = 0 on = self.out["active"] state = self.out["on"] off = not self.out["active"] trig_sig = bool(eval(self.trig["text"])) #based on trigger and immediate settings, enable or disable actor if trig_sig == True: if self.trig["last"] is False: if self.trig["type"] == "Tog": pass #toggle actor self.update_self(self.power, "Tog") elif self.trig["type"] == "Sw": pass # switch actor on self.update_self(self.power, True) if not self.trig["im_on"]: self.times["onoff"] = datetime.utcnow() + self.delay["on"] self.trig["last"] = True return self.out["req"] else: if self.trig["last"] is True: if self.trig["type"] == "NTog": pass #toggle actor on negative switch elif self.trig["type"] == "Sw": pass # switch actor off self.update_self(self.power, False) if not self.trig["im_off"]: self.times["onoff"] = datetime.utcnow() + self.delay["off"] self.trig["last"] = False if self.trig["type"] is False: return False else: return self.out["req"] #should not get here print "Failure: should not be here" return False def execute_func(self): #evaluate trigger if setup was valid if self.trig is not None: trigger = self.trigger_eval() else: trigger = self.out["req"] #evalute active condition right_now = datetime.utcnow() if trigger != self.out["active"]: if (right_now >= self.times["onoff"]) and ( (right_now >= self.times["cycle"]) or not trigger): self.out["active"] = trigger self.out["on"] = trigger if trigger: self.pulse["next"] = list(self.pulse["on_list"]) else: self.pulse["next"] = list(self.pulse["off_list"]) if len(self.pulse["next"]) > 0: self.times["pulse"] = right_now + timedelta( seconds=tryfloat(self.pulse["next"][0])) #set output_actor and change func actor power displayed if ((cbpi.cache.get("actors").get(int(self.output_actor)).state != self.out["on"]) or (self.out["no_force"] == True)): if ((self.out["no_force"] == False) or (self.out["on"] != self.out["last_on"])): self.out["last_on"] = self.out["on"] if self.out["on"] == True: self.api.switch_actor_on(int(self.output_actor), power=self.power) self.display_power(self.power) else: self.api.switch_actor_off(int(self.output_actor)) self.display_power(0) # overwrite displayed power as Zero if slave actor is off if self.out["on"] == False and cbpi.cache.get("actors").get( int(self.id)).power != 0: self.display_power(0) #evalute output pulsing if len(self.pulse["next"]) > 0: if right_now > self.times["pulse"]: self.out["on"] = not self.out["on"] del self.pulse["next"][0] if len(self.pulse["next"]) == 0: if self.pulse["loop"] and self.out["active"]: self.pulse["next"] = list(self.pulse["on_list"]) if len(self.pulse["next"]) > 0: self.times["pulse"] = right_now + timedelta( seconds=tryfloat(self.pulse["next"][0])) else: if not self.out["active"]: self.out["on"] = False if self.out["last_on"]: self.times["cycle"] = right_now + self.delay["cycle"] def on(self, power=None): if power is not None: self.power = power if self.out["req"] is False: self.out["req"] = True if self.out["im_on"] is False: self.times["onoff"] = datetime.utcnow() + self.delay["on"] def off(self): if self.out["req"] == True: self.out["req"] = False if self.out["im_off"] is False: self.times["onoff"] = datetime.utcnow() + self.delay["off"] def set_power(self, power=None): if power is not None: self.power = power self.api.actor_power(int(self.output_actor), power=self.power) def update_self(self, pwr, state): actor = cbpi.cache.get("actors").get(int(self.id)) actor.power = pwr if state == "Tog": state = not actor.state if actor.state != state: actor.state = state self.out["req"] = state cbpi.emit("SWITCH_ACTOR", actor) def display_power(self, pwr): actor = cbpi.cache.get("actors").get(int(self.id)) actor.power = pwr cbpi.emit("SWITCH_ACTOR", actor)
class CascadeHysteresis(KettleController): aa_kp = Property.Number("Proportional term", True, 10.0, description=kp_description) ab_ki = Property.Number("Integral term", True, 2.0, description=ki_description) ac_kd = Property.Number("Derivative term", True, 1.0, description=kd_description) ad_integrator_initial = Property.Number("Integrator initial value", True, 0.0) if cbpi.get_config_parameter("unit", "C") == "C": ae_maxset = Property.Number("Max hysteresis target (°C)", True, 75, description=maxset_description) else: ae_maxset = Property.Number("Max hysteresis target (°F)", True, 168, description=maxset_description) ba_inner_sensor = Property.Sensor(label="Inner (hysteresis) sensor") bb_action = Property.Select(label="Hysteresis Action Type", options=["Positive", "Negative"], description=action_description) bc_on_min = Property.Number("Hysteresis Minimum Time On (s)", True, 45) bd_on_max = Property.Number("Hysteresis Maximum Time On (s)", True, 1800) be_off_min = Property.Number("Hysteresis Minimum Time Off (s)", True, 90) c_update_interval = Property.Number( "Update interval (s)", True, 2.5, description=update_interval_description) d_notification_timeout = Property.Number( "Notification duration (ms)", True, 5000, description=notification_timeout_description) def stop(self): self.heater_off() super(KettleController, self).stop() def run(self): # Get inner sensor as an integer inner_sensor = int(self.ba_inner_sensor) # Outer PID settings kp = float(self.aa_kp) ki = float(self.ab_ki) kd = float(self.ac_kd) integrator_initial = float(self.ad_integrator_initial) maxset = float(self.ae_maxset) # Inner hysteresis settings positive = self.bb_action == "Positive" on_min = float(self.bc_on_min) on_max = float(self.bd_on_max) off_min = float(self.be_off_min) # General settings update_interval = float(self.c_update_interval) notification_timeout = float(self.d_notification_timeout) # Error check if on_min <= 0.0: self.notify("Hysteresis Error", "Minimum 'on time' must be positive", timeout=None, type="danger") raise ValueError("Hysteresis - Minimum 'on time' must be positive") if on_max <= 0.0: self.notify("Hysteresis Error", "Maximum 'on time' must be positive", timeout=None, type="danger") raise ValueError("Hysteresis - Maximum 'on time' must be positive") if on_min >= on_max: self.notify( "Hysteresis Error", "Maximum 'on time' must be greater than the minimum 'on time'", timeout=None, type="danger") raise ValueError( "Hysteresis - Maximum 'on time' must be greater than the minimum 'on time'" ) if off_min <= 0.0: self.notify("Hysteresis Error", "Minimum 'off time' must be positive", timeout=None, type="danger") raise ValueError( "Hysteresis - Minimum 'off time' must be positive") if update_interval <= 0.0: self.notify("Hysteresis Error", "Update interval must be positive", timeout=None, type="danger") raise ValueError("Hysteresis - Update interval must be positive") elif notification_timeout <= 0.0: cbpi.notify("Hysteresis Error", "Notification timeout must be positive", timeout=None, type="danger") raise ValueError( "Hysteresis - Notification timeout must be positive") else: # Initialize outer PID if cbpi.get_config_parameter("unit", "C") == "C": outer_pid = PID(kp, ki, kd, 0.0, maxset, 1.0, integrator_initial) else: outer_pid = PID(kp, ki, kd, 32, maxset, 1.8, integrator_initial) # Initialize hysteresis inner_hysteresis = Hysteresis(positive, on_min, on_max, off_min) while self.is_running(): waketime = time.time() + update_interval # Get the target temperature outer_target_value = self.get_target_temp() # Calculate inner target value from outer PID outer_current_value = self.get_temp() inner_target_value = round( outer_pid.update(outer_current_value, outer_target_value), 2) inner_current_value = float( cbpi.cache.get("sensors") [inner_sensor].instance.last_value) # Update the hysteresis controller if inner_hysteresis.update(inner_current_value, inner_target_value): self.heater_on(100) cbpi.app.logger.info( "[%s] Inner hysteresis actor stays ON" % (waketime)) print( ("[%s] Innner hysteresis actor stays ON" % (waketime))) else: self.heater_off() cbpi.app.logger.info( "[%s] Inner hysteresis actor stays OFF" % (waketime)) print(("[%s] Innner hysteresis actor stays OFF" % (waketime))) # Print loop details cbpi.app.logger.info( "[%s] Outer loop PID target/actual/output/integrator: %s/%s/%s/%s" % (waketime, outer_target_value, outer_current_value, inner_target_value, round(outer_pid.integrator, 2))) print(( "[%s] Outer loop PID target/actual/output/integrator: %s/%s/%s/%s" % (waketime, outer_target_value, outer_current_value, inner_target_value, round(outer_pid.integrator, 2)))) # Sleep until update required again if waketime <= time.time() + 0.25: self.notify("Hysteresis Error", "Update interval is too short", timeout=notification_timeout, type="warning") cbpi.app.logger.info( "Hysteresis - Update interval is too short") print("Hysteresis - Update interval is too short") else: self.sleep(waketime - time.time())