def __init__(self, ready, logger, timer_id): threading.Thread.__init__(self) self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.logger = logger self.timer_id = timer_id self.control = DaemonControl() with session_scope(MYCODO_DB_PATH) as new_session: timer = new_session.query(Timer).filter( Timer.id == self.timer_id).first() self.name = timer.name self.relay_id = timer.relay_id self.state = timer.state self.time = timer.time_on self.duration_on = timer.duration_on self.duration_off = timer.duration_off # Time of day split into hour and minute if self.time: time_split = self.time.split(":") self.hour = time_split[0] self.minute = time_split[1] else: self.hour = None self.minute = None self.duration_timer = time.time() self.date_timer_not_executed = True self.running = False
def __init__(self, ready, pid_id): threading.Thread.__init__(self) self.logger = logging.getLogger("mycodo.pid_{id}".format(id=pid_id)) self.running = False self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.pid_id = pid_id self.control = DaemonControl() self.initialize_values() self.control_variable = 0 self.Derivator = 0 self.Integrator = 0 self.error = 0.0 self.P_value = None self.I_value = None self.D_value = None self.set_point = 0 self.lower_seconds_on = 0 self.raise_seconds_on = 0 self.last_measurement_success = False self.timer = t.time() + self.measure_interval # Check if a method is set for this PID if self.method_id: method = db_retrieve_table(MYCODO_DB_PATH, Method) method = method.filter(Method.method_id == self.method_id) method = method.filter(Method.method_order == 0).first() self.method_type = method.method_type self.method_start_time = method.start_time if self.method_type == 'Duration': if self.method_start_time == 'Ended': # Method has ended and hasn't been instructed to begin again pass elif self.method_start_time == 'Ready' or self.method_start_time is None: # Method has been instructed to begin with session_scope(MYCODO_DB_PATH) as db_session: mod_method = db_session.query(Method) mod_method = mod_method.filter( Method.method_id == self.method_id) mod_method = mod_method.filter( Method.method_order == 0).first() mod_method.start_time = datetime.datetime.now() self.method_start_time = mod_method.start_time db_session.commit() else: # Method neither instructed to begin or not to # Likely there was a daemon restart ot power failure # Resume method with saved start_time self.method_start_time = datetime.datetime.strptime( str(self.method_start_time), '%Y-%m-%d %H:%M:%S.%f') self.logger.warning( "Resuming method {id} started at {time}".format( id=self.method_id, time=self.method_start_time))
def __init__(self): threading.Thread.__init__(self) self.logger = logging.getLogger("mycodo.output") self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.control = DaemonControl() self.output_id = {} self.output_unique_id = {} self.output_type = {} self.output_name = {} self.output_pin = {} self.output_amps = {} self.output_trigger = {} self.output_on_at_start = {} self.output_on_until = {} self.output_last_duration = {} self.output_on_duration = {} # wireless self.output_protocol = {} self.output_pulse_length = {} self.output_bit_length = {} self.output_on_command = {} self.output_off_command = {} self.wireless_pi_switch = {} # PWM self.pwm_hertz = {} self.pwm_library = {} self.pwm_output = {} self.pwm_state = {} self.pwm_time_turned_on = {} self.output_time_turned_on = {} self.logger.debug("Initializing Outputs") try: smtp = db_retrieve_table_daemon(SMTP, entry='first') self.smtp_max_count = smtp.hourly_max self.smtp_wait_time = time.time() + 3600 self.smtp_timer = time.time() self.email_count = 0 self.allowed_to_send_notice = True outputs = db_retrieve_table_daemon(Output, entry='all') self.all_outputs_initialize(outputs) # Turn all outputs off self.all_outputs_off() # Turn outputs on that are set to be on at start self.all_outputs_on() self.logger.debug("Outputs Initialized") except Exception as except_msg: self.logger.exception( "Problem initializing outputs: {err}".format(err=except_msg)) self.running = False
def __init__(self, ready, timer_id): threading.Thread.__init__(self) self.logger = logging.getLogger( "mycodo.timer_{id}".format(id=timer_id)) self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.timer_id = timer_id self.control = DaemonControl() timer = db_retrieve_table_daemon(Timer, device_id=self.timer_id) self.timer_type = timer.timer_type self.name = timer.name self.relay_unique_id = timer.relay_id self.state = timer.state self.time_start = timer.time_start self.time_end = timer.time_end self.duration_on = timer.duration_on self.duration_off = timer.duration_off self.relay_id = db_retrieve_table_daemon( Relay, unique_id=self.relay_unique_id).id # Time of day split into hour and minute if self.time_start: time_split = self.time_start.split(":") self.start_hour = time_split[0] self.start_minute = time_split[1] else: self.start_hour = None self.start_minute = None if self.time_end: time_split = self.time_end.split(":") self.end_hour = time_split[0] self.end_minute = time_split[1] else: self.end_hour = None self.end_minute = None self.duration_timer = time.time() self.date_timer_not_executed = True self.running = False
def __init__(self, logger): threading.Thread.__init__(self) self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.logger = logger self.control = DaemonControl() self.relay_id = {} self.relay_name = {} self.relay_pin = {} self.relay_amps = {} self.relay_trigger = {} self.relay_start_state = {} self.relay_on_until = {} self.relay_last_duration = {} self.relay_on_duration = {} self.logger.debug("[Relay] Initializing Relays") try: # Setup GPIO (BCM numbering) and initialize all relays in database GPIO.setmode(GPIO.BCM) GPIO.setwarnings(False) with session_scope(MYCODO_DB_PATH) as new_session: smtp = new_session.query(SMTP).first() self.smtp_max_count = smtp.hourly_max self.smtp_wait_time = time.time() + 3600 self.smtp_timer = time.time() self.email_count = 0 self.allowed_to_send_notice = True relays = new_session.query(Relay).all() self.all_relays_initialize(relays) # Turn all relays off self.all_relays_off() # Turn relays on that are set to be on at start self.all_relays_on() self.logger.info("[Relay] Relays Initialized") except Exception as except_msg: self.logger.exception("[Relay] Problem initializing " "relays: {}", except_msg) self.running = False
def __init__(self): threading.Thread.__init__(self) self.logger = logging.getLogger("mycodo.relay") self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.control = DaemonControl() self.relay_id = {} self.relay_unique_id = {} self.relay_name = {} self.relay_pin = {} self.relay_amps = {} self.relay_trigger = {} self.relay_on_at_start = {} self.relay_on_until = {} self.relay_last_duration = {} self.relay_on_duration = {} self.relay_time_turned_on = {} self.logger.debug("Initializing Relays") try: smtp = db_retrieve_table_daemon(SMTP, entry='first') self.smtp_max_count = smtp.hourly_max self.smtp_wait_time = time.time() + 3600 self.smtp_timer = time.time() self.email_count = 0 self.allowed_to_send_notice = True relays = db_retrieve_table_daemon(Relay, entry='all') self.all_relays_initialize(relays) # Turn all relays off self.all_relays_off() # Turn relays on that are set to be on at start self.all_relays_on() self.logger.debug("Relays Initialized") except Exception as except_msg: self.logger.exception( "Problem initializing relays: {err}", err=except_msg) self.running = False
def __init__(self, ready, logger, pid_id): threading.Thread.__init__(self) self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.logger = logger self.pid_id = pid_id self.control = DaemonControl() with session_scope(MYCODO_DB_PATH) as new_session: pid = new_session.query(PID).filter(PID.id == self.pid_id).first() self.sensor_id = pid.sensor_id self.measure_type = pid.measure_type self.direction = pid.direction self.raise_relay_id = pid.raise_relay_id self.raise_min_duration = pid.raise_min_duration self.raise_max_duration = pid.raise_max_duration self.lower_relay_id = pid.lower_relay_id self.lower_min_duration = pid.lower_min_duration self.lower_max_duration = pid.lower_max_duration self.Kp = pid.p self.Ki = pid.i self.Kd = pid.d self.measure_interval = pid.period self.default_set_point = pid.setpoint self.set_point = pid.setpoint with session_scope(MYCODO_DB_PATH) as new_session: self.pidsetpoints = new_session.query(PIDSetpoints) self.pidsetpoints = self.pidsetpoints.filter(PIDSetpoints.pid_id == self.pid_id) self.pidsetpoints = self.pidsetpoints.order_by(PIDSetpoints.start_time.asc()) new_session.expunge_all() new_session.close() self.Derivator = 0 self.Integrator = 0 self.Integrator_max = 500 self.Integrator_min = -500 self.error = 0.0 self.P_value = None self.I_value = None self.D_value = None self.raise_seconds_on = 0 self.timer = t.time() + self.measure_interval
class PIDController(threading.Thread): """ Class to operate discrete PID controller """ def __init__(self, ready, logger, pid_id): threading.Thread.__init__(self) self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.logger = logger self.pid_id = pid_id self.control = DaemonControl() with session_scope(MYCODO_DB_PATH) as new_session: pid = new_session.query(PID).filter(PID.id == self.pid_id).first() self.sensor_id = pid.sensor_id self.measure_type = pid.measure_type self.direction = pid.direction self.raise_relay_id = pid.raise_relay_id self.raise_min_duration = pid.raise_min_duration self.raise_max_duration = pid.raise_max_duration self.lower_relay_id = pid.lower_relay_id self.lower_min_duration = pid.lower_min_duration self.lower_max_duration = pid.lower_max_duration self.Kp = pid.p self.Ki = pid.i self.Kd = pid.d self.measure_interval = pid.period self.default_set_point = pid.setpoint self.set_point = pid.setpoint with session_scope(MYCODO_DB_PATH) as new_session: self.pidsetpoints = new_session.query(PIDSetpoints) self.pidsetpoints = self.pidsetpoints.filter(PIDSetpoints.pid_id == self.pid_id) self.pidsetpoints = self.pidsetpoints.order_by(PIDSetpoints.start_time.asc()) new_session.expunge_all() new_session.close() self.Derivator = 0 self.Integrator = 0 self.Integrator_max = 500 self.Integrator_min = -500 self.error = 0.0 self.P_value = None self.I_value = None self.D_value = None self.raise_seconds_on = 0 self.timer = t.time() + self.measure_interval def run(self): try: self.running = True self.logger.info("[PID {}] Activated in {}ms".format( self.pid_id, (timeit.default_timer()-self.thread_startup_timer)*1000)) self.ready.set() while (self.running): if t.time() > self.timer: self.timer = t.time() + self.measure_interval self.get_last_measurement() self.manipulate_relays() t.sleep(0.1) if self.raise_relay_id: self.control.relay_off(self.raise_relay_id) if self.lower_relay_id: self.control.relay_off(self.lower_relay_id) self.running = False self.logger.info("[PID {}] Deactivated in {}ms".format( self.pid_id, (timeit.default_timer()-self.thread_shutdown_timer)*1000)) except Exception as except_msg: self.logger.exception("[PID {}] Error: {}".format(self.pid_id, except_msg)) def update(self, current_value): """ Calculate PID output value from reference input and feedback :return: Manipulated, or control, variable. This is the PID output. :rtype: float :param current_value: The input, or process, variable (the actual measured condition by the sensor) :type current_value: float """ self.error = self.set_point - current_value # Calculate P-value self.P_value = self.Kp * self.error # Calculate I-value self.Integrator += self.error # Old method for managing Integrator # if self.Integrator > self.Integrator_max: # self.Integrator = self.Integrator_max # elif self.Integrator < self.Integrator_min: # self.Integrator = self.Integrator_min # New method for regulating Integrator if self.measure_interval is not None: if self.Integrator * self.Ki > self.measure_interval: self.Integrator = self.measure_interval / self.Ki elif self.Integrator * self.Ki < -self.measure_interval: self.Integrator = -self.measure_interval / self.Ki self.I_value = self.Integrator * self.Ki # Calculate D-value self.D_value = self.Kd * (self.error - self.Derivator) self.Derivator = self.error # Produce output form P, I, and D values PID = self.P_value + self.I_value + self.D_value return PID def get_last_measurement(self): """ Retrieve the latest sensor measurement from InfluxDB :rtype: None """ self.last_measurement_success = False # Get latest measurement (from within the past minute) from influxdb try: self.last_measurement = read_last_influxdb( INFLUXDB_HOST, INFLUXDB_PORT, INFLUXDB_USER, INFLUXDB_PASSWORD, INFLUXDB_DATABASE, self.sensor_id, self.measure_type) if self.last_measurement: measurement_list = list(self.last_measurement.get_points( measurement=self.measure_type)) self.last_time = measurement_list[0]['time'] self.last_measurement = measurement_list[0]['value'] utc_dt = datetime.strptime(self.last_time.split(".")[0], '%Y-%m-%dT%H:%M:%S') utc_timestamp = calendar.timegm(utc_dt.timetuple()) local_timestamp = str(datetime.fromtimestamp(utc_timestamp)) self.logger.debug("[PID {}] Latest {}: {} @ {}".format( self.pid_id, self.measure_type, self.last_measurement, local_timestamp)) self.last_measurement_success = True else: self.logger.warning("[PID {}] No data returned " "from influxdb".format(self.pid_id)) except Exception as except_msg: self.logger.exception("[PID {}] Failed to read " "measurement from the influxdb " "database: {}".format(self.pid_id, except_msg)) def manipulate_relays(self): """ Activate a relay based on PID output (control variable) and whether the manipulation directive is to raise, lower, or both. :rtype: None """ # If there was a measurement able to be retrieved from # influxdb database that was entered within the past minute if self.last_measurement_success: # Update setpoint if dynamic setpoints are enabled for this PID # and the current time is within one of the set time spans use_default_setpoint = True for each_setpt in self.pidsetpoints: if self.now_in_range(each_setpt.start_time, each_setpt.end_time): use_default_setpoint = False self.calculate_new_setpoint(each_setpt.start_time, each_setpt.end_time, each_setpt.start_setpoint, each_setpt.end_setpoint) self.logger.debug("[PID {}] New setpoint: {}".format(self.pid_id, self.set_point)) if use_default_setpoint: self.set_point = self.default_set_point self.addSetpointInfluxdb(self.pid_id, self.set_point) # Update PID and get control variable self.control_variable = self.update(self.last_measurement) # # PID control variable positive to raise environmental condition # if self.direction in ['raise', 'both'] and self.raise_relay_id: if self.control_variable > 0: # Ensure the relay on duration doesn't exceed the set maximum if (self.raise_max_duration and self.control_variable > self.raise_max_duration): self.raise_seconds_on = self.raise_max_duration else: self.raise_seconds_on = float("{0:.2f}".format(self.control_variable)) # Turn off lower_relay if active, because we're now raising if self.lower_relay_id: with session_scope(MYCODO_DB_PATH) as new_session: relay = new_session.query(Relay).filter( Relay.id == self.lower_relay_id).first() if relay.is_on(): self.control.relay_off(self.lower_relay_id) if self.raise_seconds_on > self.raise_min_duration: # Activate raise_relay for a duration self.logger.debug("[PID {}] Setpoint: {} " "Output: {} to relay {}".format( self.pid_id, self.set_point, self.control_variable, self.raise_relay_id)) self.control.relay_on(self.raise_relay_id, self.raise_seconds_on) else: self.control.relay_off(self.raise_relay_id) # # PID control variable negative to lower environmental condition # if self.direction in ['lower', 'both'] and self.lower_relay_id: if self.control_variable < 0: # Ensure the relay on duration doesn't exceed the set maximum if (self.lower_max_duration and abs(self.control_variable) > self.lower_max_duration): self.lower_seconds_on = self.lower_max_duration else: self.lower_seconds_on = abs(float("{0:.2f}".format(self.control_variable))) # Turn off raise_relay if active, because we're now lowering if self.raise_relay_id: with session_scope(MYCODO_DB_PATH) as new_session: relay = new_session.query(Relay).filter( Relay.id == self.raise_relay_id).first() if relay.is_on(): self.control.relay_off(self.raise_relay_id) if self.lower_seconds_on > self.lower_min_duration: # Activate lower_relay for a duration self.logger.debug("[PID {}] Setpoint: {} " "Output: {} to relay {}".format( self.pid_id, self.set_point, self.control_variable, self.lower_relay_id)) self.control.relay_on(self.lower_relay_id, self.lower_seconds_on) else: self.control.relay_off(self.lower_relay_id) else: if self.direction in ['raise', 'both'] and self.raise_relay_id: self.control.relay_off(self.raise_relay_id) if self.direction in ['lower', 'both'] and self.lower_relay_id: self.control.relay_off(self.lower_relay_id) def now_in_range(self, start_time, end_time): """ Check if the current time is between start_time and end_time :return: 1 is within range, 0 if not within range :rtype: int """ start_hour = int(start_time.split(":")[0]) start_min = int(start_time.split(":")[1]) end_hour = int(end_time.split(":")[0]) end_min = int(end_time.split(":")[1]) now_time = datetime.now().time() now_time = now_time.replace(second=0, microsecond=0) if ((start_hour < end_hour) or (start_hour == end_hour and start_min < end_min)): if now_time >= time(start_hour,start_min) and now_time <= time(end_hour,end_min): return 1 # Yes now within range else: if now_time >= time(start_hour,start_min) or now_time <= time(end_hour,end_min): return 1 # Yes now within range return 0 # No now not within range def calculate_new_setpoint(self, start_time, end_time, start_setpoint, end_setpoint): """ Calculate a dynamic setpoint that changes over time The current time must fall between the start_time and end_time. If there is only a start_setpoint, that is the only setpoint that can be returned. Based on where the current time falls between the start_time and the end_time, a setpoint between the start_setpoint and end_setpoint will be calculated. For example, if the time range is 12:00 to 1:00, and the setPoint range is 0 to 60, and the current time is 12:30, the calculated setpoint will be 30. :return: 0 if only a start setpoint is set, 1 if both start and end setpoints are set and the value between has been calculated. :rtype: int :param start_time: The start hour and minute of the time range :type start_time: str :param end_time: The end hour and minute of the time range :type end_time: str :param start_setpoint: The start setpoint :type start_setpoint: float :param end_setpoint: The end setpoint :type end_setpoint: float or None """ # Only a start_setpoint set for this time period if end_setpoint is None: self.set_point = start_setpoint return 0 # Split hour and minute into separate integers start_hour = int(start_time.split(":")[0]) start_min = int(start_time.split(":")[1]) end_hour = int(end_time.split(":")[0]) end_min = int(end_time.split(":")[1]) # Set the date and time format date_format = "%d %H:%M" # Add day in case end time is the next day # Convert string of 'day hour:minute' to actual date and time start_time_formatted = datetime.strptime("1 "+start_time, date_format) end_day_modifier = "1 " # End time is the same day if (start_hour > end_hour or (start_hour == end_hour and start_min > end_min)): end_day_modifier = "2 " # End time is the next day end_time_formatted = datetime.strptime(end_day_modifier+end_time, date_format) # Total number of minute between start time and end time diff = end_time_formatted-start_time_formatted diff_min = diff.seconds/60 # Find the difference between setpoints diff_setpoints = abs(end_setpoint-start_setpoint) # Total number of minute between start time and now now = datetime.now() if now.hour > start_hour: hours = now.hour-start_hour elif now.hour < start_hour: hours = now.hour+(24-start_hour) elif now.hour == start_hour: hours = 0 minutes = now.minute-start_min # May be negative total_minutes = (hours*60)+minutes # Based on the number of minutes between the start and end time and the # minutes passed since the start time, calculate the new setpoint mod_setpoint = total_minutes/float(diff_min/diff_setpoints) if end_setpoint < start_setpoint: # Setpoint decreases over duration new_setpoint = float("{0:.2f}".format(start_setpoint-mod_setpoint)) else: # Setpoint increases over duration new_setpoint = float("{0:.2f}".format(start_setpoint+mod_setpoint)) self.set_point = new_setpoint return 1 def addSetpointInfluxdb(self, pid_id, setpoint): """ Add a setpoint entry to InfluxDB :rtype: None """ write_db = threading.Thread( target=write_influxdb, args=(self.logger, INFLUXDB_HOST, INFLUXDB_PORT, INFLUXDB_USER, INFLUXDB_PASSWORD, INFLUXDB_DATABASE, 'pid', pid_id, 'setpoint', setpoint,)) write_db.start() def setPoint(self, set_point): """Initilize the setpoint of PID""" self.set_point = set_point self.Integrator = 0 self.Derivator = 0 def setIntegrator(self, Integrator): """Set the Integrator of the controller""" self.Integrator = Integrator def setDerivator(self, Derivator): """Set the Derivator of the controller""" self.Derivator = Derivator def setKp(self, P): """Set Kp gain of the controller""" self.Kp = P def setKi(self, I): """Set Ki gain of the controller""" self.Ki = I def setKd(self, D): """Set Kd gain of the controller""" self.Kd = D def getPoint(self): return self.set_point def getError(self): return self.error def getIntegrator(self): return self.Integrator def getDerivator(self): return self.Derivator def isRunning(self): return self.running def stopController(self): self.thread_shutdown_timer = timeit.default_timer() self.running = False
class PIDController(threading.Thread): """ Class to operate discrete PID controller """ def __init__(self, ready, pid_id): threading.Thread.__init__(self) self.logger = logging.getLogger("mycodo.pid_{id}".format(id=pid_id)) self.running = False self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.pid_id = pid_id self.control = DaemonControl() self.initialize_values() self.control_variable = 0 self.Derivator = 0 self.Integrator = 0 self.error = 0.0 self.P_value = None self.I_value = None self.D_value = None self.set_point = 0 self.lower_seconds_on = 0 self.raise_seconds_on = 0 self.last_measurement_success = False self.timer = t.time() + self.measure_interval # Check if a method is set for this PID if self.method_id: method = db_retrieve_table(MYCODO_DB_PATH, Method) method = method.filter(Method.method_id == self.method_id) method = method.filter(Method.method_order == 0).first() self.method_type = method.method_type self.method_start_time = method.start_time if self.method_type == 'Duration': if self.method_start_time == 'Ended': # Method has ended and hasn't been instructed to begin again pass elif self.method_start_time == 'Ready' or self.method_start_time is None: # Method has been instructed to begin with session_scope(MYCODO_DB_PATH) as db_session: mod_method = db_session.query(Method) mod_method = mod_method.filter( Method.method_id == self.method_id) mod_method = mod_method.filter( Method.method_order == 0).first() mod_method.start_time = datetime.datetime.now() self.method_start_time = mod_method.start_time db_session.commit() else: # Method neither instructed to begin or not to # Likely there was a daemon restart ot power failure # Resume method with saved start_time self.method_start_time = datetime.datetime.strptime( str(self.method_start_time), '%Y-%m-%d %H:%M:%S.%f') self.logger.warning( "Resuming method {id} started at {time}".format( id=self.method_id, time=self.method_start_time)) def run(self): try: self.running = True self.logger.info("Activated in {:.1f} ms".format( (timeit.default_timer() - self.thread_startup_timer) * 1000)) if self.activated == 2: self.logger.info("Paused") elif self.activated == 3: self.logger.info("Held") self.ready.set() while self.running: if t.time() > self.timer: self.timer = self.timer + self.measure_interval # self.activated: 0=inactive, 1=active, 2=pause, 3=hold # If active, retrieve sensor measurement and update PID output if self.activated == 1: self.get_last_measurement() if self.last_measurement_success: # Update setpoint if a method is selected if self.method_id != '': self.calculate_method_setpoint(self.method_id) write_influxdb_setpoint(self.pid_id, self.set_point) # Update PID and get control variable self.control_variable = self.update( self.last_measurement) # If active or on hold, activate relays if self.activated in [1, 3]: self.manipulate_relays() t.sleep(0.1) if self.raise_relay_id: self.control.relay_off(self.raise_relay_id) if self.lower_relay_id: self.control.relay_off(self.lower_relay_id) self.running = False self.logger.info("Deactivated in {:.1f} ms".format( (timeit.default_timer() - self.thread_shutdown_timer) * 1000)) except Exception as except_msg: self.logger.exception("Run Error: {err}".format(err=except_msg)) def initialize_values(self): """Set PID parameters""" pid = db_retrieve_table(MYCODO_DB_PATH, PID, device_id=self.pid_id) sensor = db_retrieve_table(MYCODO_DB_PATH, Sensor, device_id=pid.sensor_id) self.activated = pid.activated # 0=inactive, 1=active, 2=paused self.sensor_id = pid.sensor_id self.measure_type = pid.measure_type self.method_id = pid.method_id self.direction = pid.direction self.raise_relay_id = pid.raise_relay_id self.raise_min_duration = pid.raise_min_duration self.raise_max_duration = pid.raise_max_duration self.lower_relay_id = pid.lower_relay_id self.lower_min_duration = pid.lower_min_duration self.lower_max_duration = pid.lower_max_duration self.Kp = pid.p self.Ki = pid.i self.Kd = pid.d self.Integrator_min = pid.integrator_min self.Integrator_max = pid.integrator_max self.measure_interval = pid.period self.default_set_point = pid.setpoint self.set_point = pid.setpoint self.sensor_duration = sensor.period return "success" def update(self, current_value): """ Calculate PID output value from reference input and feedback :return: Manipulated, or control, variable. This is the PID output. :rtype: float :param current_value: The input, or process, variable (the actual measured condition by the sensor) :type current_value: float """ self.error = self.set_point - current_value # Calculate P-value self.P_value = self.Kp * self.error # Calculate I-value self.Integrator += self.error # First method for managing Integrator if self.Integrator > self.Integrator_max: self.Integrator = self.Integrator_max elif self.Integrator < self.Integrator_min: self.Integrator = self.Integrator_min # Second method for regulating Integrator # if self.measure_interval is not None: # if self.Integrator * self.Ki > self.measure_interval: # self.Integrator = self.measure_interval / self.Ki # elif self.Integrator * self.Ki < -self.measure_interval: # self.Integrator = -self.measure_interval / self.Ki self.I_value = self.Integrator * self.Ki # Calculate D-value self.D_value = self.Kd * (self.error - self.Derivator) self.Derivator = self.error # Produce output form P, I, and D values pid_value = self.P_value + self.I_value + self.D_value return pid_value def get_last_measurement(self): """ Retrieve the latest sensor measurement from InfluxDB :rtype: None """ self.last_measurement_success = False # Get latest measurement (from within the past minute) from influxdb try: if self.sensor_duration < 60: duration = 60 else: duration = int(self.sensor_duration * 1.5) self.last_measurement = read_last_influxdb(self.sensor_id, self.measure_type, duration) if self.last_measurement: measurement_list = list( self.last_measurement.get_points( measurement=self.measure_type)) self.last_time = measurement_list[0]['time'] self.last_measurement = measurement_list[0]['value'] utc_dt = datetime.datetime.strptime( self.last_time.split(".")[0], '%Y-%m-%dT%H:%M:%S') utc_timestamp = calendar.timegm(utc_dt.timetuple()) local_timestamp = str( datetime.datetime.fromtimestamp(utc_timestamp)) self.logger.debug("Latest {}: {} @ {}".format( self.measure_type, self.last_measurement, local_timestamp)) self.last_measurement_success = True else: self.logger.warning("No data returned from influxdb") except Exception as except_msg: self.logger.exception( "Failed to read measurement from the influxdb database: " "{err}".format(err=except_msg)) def manipulate_relays(self): """ Activate a relay based on PID output (control variable) and whether the manipulation directive is to raise, lower, or both. :rtype: None """ # If there was a measurement able to be retrieved from # influxdb database that was entered within the past minute if self.last_measurement_success: # # PID control variable positive to raise environmental condition # if self.direction in ['raise', 'both'] and self.raise_relay_id: if self.control_variable > 0: # Ensure the relay on duration doesn't exceed the set maximum if (self.raise_max_duration and self.control_variable > self.raise_max_duration): self.raise_seconds_on = self.raise_max_duration else: self.raise_seconds_on = float("{0:.2f}".format( self.control_variable)) # Turn off lower_relay if active, because we're now raising if self.lower_relay_id: relay = db_retrieve_table( MYCODO_DB_PATH, Relay, device_id=self.lower_relay_id) if relay.is_on(): self.control.relay_off(self.lower_relay_id) if self.raise_seconds_on > self.raise_min_duration: # Activate raise_relay for a duration self.logger.debug( "Setpoint: {sp} Output: {op} to relay " "{relay}".format(sp=self.set_point, op=self.control_variable, relay=self.raise_relay_id)) self.control.relay_on(self.raise_relay_id, self.raise_seconds_on) else: self.control.relay_off(self.raise_relay_id) # # PID control variable negative to lower environmental condition # if self.direction in ['lower', 'both'] and self.lower_relay_id: if self.control_variable < 0: # Ensure the relay on duration doesn't exceed the set maximum if (self.lower_max_duration and abs(self.control_variable) > self.lower_max_duration): self.lower_seconds_on = self.lower_max_duration else: self.lower_seconds_on = abs( float("{0:.2f}".format(self.control_variable))) # Turn off raise_relay if active, because we're now lowering if self.raise_relay_id: relay = db_retrieve_table( MYCODO_DB_PATH, Relay, device_id=self.raise_relay_id) if relay.is_on(): self.control.relay_off(self.raise_relay_id) if self.lower_seconds_on > self.lower_min_duration: # Activate lower_relay for a duration self.logger.debug("Setpoint: {sp} Output: {op} to " "relay {relay}".format( sp=self.set_point, op=self.control_variable, relay=self.lower_relay_id)) self.control.relay_on(self.lower_relay_id, self.lower_seconds_on) else: self.control.relay_off(self.lower_relay_id) else: if self.direction in ['raise', 'both'] and self.raise_relay_id: self.control.relay_off(self.raise_relay_id) if self.direction in ['lower', 'both'] and self.lower_relay_id: self.control.relay_off(self.lower_relay_id) def calculate_method_setpoint(self, method_id): method = db_retrieve_table(MYCODO_DB_PATH, Method) method_key = method.filter(Method.method_id == method_id) method_key = method_key.filter(Method.method_order == 0).first() method = method.filter(Method.method_id == method_id) method = method.filter(Method.relay_id == None) method = method.filter(Method.method_order > 0) method = method.order_by(Method.method_order.asc()).all() now = datetime.datetime.now() # Calculate where the current time/date is within the time/date method if method_key.method_type == 'Date': for each_method in method: start_time = datetime.datetime.strptime( each_method.start_time, '%Y-%m-%d %H:%M:%S') end_time = datetime.datetime.strptime(each_method.end_time, '%Y-%m-%d %H:%M:%S') if start_time < now < end_time: start_setpoint = each_method.start_setpoint if each_method.end_setpoint: end_setpoint = each_method.end_setpoint else: end_setpoint = each_method.start_setpoint setpoint_diff = abs(end_setpoint - start_setpoint) total_seconds = (end_time - start_time).total_seconds() part_seconds = (now - start_time).total_seconds() percent_total = part_seconds / total_seconds if start_setpoint < end_setpoint: new_setpoint = start_setpoint + (setpoint_diff * percent_total) else: new_setpoint = start_setpoint - (setpoint_diff * percent_total) self.logger.debug("[Method] Start: {} End: {}".format( start_time, end_time)) self.logger.debug("[Method] Start: {} End: {}".format( start_setpoint, end_setpoint)) self.logger.debug( "[Method] Total: {} Part total: {} ({}%)".format( total_seconds, part_seconds, percent_total)) self.logger.debug( "[Method] New Setpoint: {}".format(new_setpoint)) self.set_point = new_setpoint return 0 # Calculate where the current Hour:Minute:Seconds is within the Daily method elif method_key.method_type == 'Daily': daily_now = datetime.datetime.now().strftime('%H:%M:%S') daily_now = datetime.datetime.strptime(str(daily_now), '%H:%M:%S') for each_method in method: start_time = datetime.datetime.strptime( each_method.start_time, '%H:%M:%S') end_time = datetime.datetime.strptime(each_method.end_time, '%H:%M:%S') if start_time < daily_now < end_time: start_setpoint = each_method.start_setpoint if each_method.end_setpoint: end_setpoint = each_method.end_setpoint else: end_setpoint = each_method.start_setpoint setpoint_diff = abs(end_setpoint - start_setpoint) total_seconds = (end_time - start_time).total_seconds() part_seconds = (daily_now - start_time).total_seconds() percent_total = part_seconds / total_seconds if start_setpoint < end_setpoint: new_setpoint = start_setpoint + (setpoint_diff * percent_total) else: new_setpoint = start_setpoint - (setpoint_diff * percent_total) self.logger.debug("[Method] Start: {} End: {}".format( start_time.strftime('%H:%M:%S'), end_time.strftime('%H:%M:%S'))) self.logger.debug("[Method] Start: {} End: {}".format( start_setpoint, end_setpoint)) self.logger.debug( "[Method] Total: {} Part total: {} ({}%)".format( total_seconds, part_seconds, percent_total)) self.logger.debug( "[Method] New Setpoint: {}".format(new_setpoint)) self.set_point = new_setpoint return 0 # Calculate sine y-axis value from the x-axis (seconds of the day) elif method_key.method_type == 'DailySine': new_setpoint = sine_wave_y_out(method_key.amplitude, method_key.frequency, method_key.shift_angle, method_key.shift_y) self.set_point = new_setpoint return 0 # Calculate Bezier curve y-axis value from the x-axis (seconds of the day) elif method_key.method_type == 'DailyBezier': new_setpoint = bezier_curve_y_out(method_key.shift_angle, (method_key.x0, method_key.y0), (method_key.x1, method_key.y1), (method_key.x2, method_key.y2), (method_key.x3, method_key.y3)) self.set_point = new_setpoint return 0 # Calculate the duration in the method based on self.method_start_time elif method_key.method_type == 'Duration' and self.method_start_time != 'Ended': seconds_from_start = (now - self.method_start_time).total_seconds() total_sec = 0 previous_total_sec = 0 for each_method in method: total_sec += each_method.duration_sec if previous_total_sec <= seconds_from_start < total_sec: row_start_time = float( self.method_start_time.strftime( '%s')) + previous_total_sec row_since_start_sec = ( now - (self.method_start_time + datetime.timedelta( 0, previous_total_sec))).total_seconds() percent_row = row_since_start_sec / each_method.duration_sec start_setpoint = each_method.start_setpoint if each_method.end_setpoint: end_setpoint = each_method.end_setpoint else: end_setpoint = each_method.start_setpoint setpoint_diff = abs(end_setpoint - start_setpoint) if start_setpoint < end_setpoint: new_setpoint = start_setpoint + (setpoint_diff * percent_row) else: new_setpoint = start_setpoint - (setpoint_diff * percent_row) self.logger.debug( "[Method] Start: {} Seconds Since: {}".format( self.method_start_time, seconds_from_start)) self.logger.debug("[Method] Start time of row: {}".format( datetime.datetime.fromtimestamp(row_start_time))) self.logger.debug( "[Method] Sec since start of row: {}".format( row_since_start_sec)) self.logger.debug( "[Method] Percent of row: {}".format(percent_row)) self.logger.debug( "[Method] New Setpoint: {}".format(new_setpoint)) self.set_point = new_setpoint return 0 previous_total_sec = total_sec # Duration method has ended, reset start_time locally and in DB if self.method_start_time: with session_scope(MYCODO_DB_PATH) as db_session: mod_method = db_session.query(Method).filter( Method.method_id == self.method_id) mod_method = mod_method.filter( Method.method_order == 0).first() mod_method.start_time = 'Ended' db_session.commit() self.method_start_time = 'Ended' # Setpoint not needing to be calculated, use default setpoint self.set_point = self.default_set_point def pid_mod(self): if self.initialize_values(): return "success" else: return "error" def pid_hold(self): self.activated = 3 self.logger.info("Hold") return "success" def pid_pause(self): self.activated = 2 self.logger.info("Pause") return "success" def pid_resume(self): self.activated = 1 self.logger.info("Resume") return "success" def set_setpoint(self, set_point): """ Initilize the setpoint of PID """ self.set_point = set_point self.Integrator = 0 self.Derivator = 0 def set_integrator(self, Integrator): """ Set the Integrator of the controller """ self.Integrator = Integrator def set_derivator(self, Derivator): """ Set the Derivator of the controller """ self.Derivator = Derivator def set_kp(self, P): """ Set Kp gain of the controller """ self.Kp = P def set_ki(self, I): """ Set Ki gain of the controller """ self.Ki = I def set_kd(self, D): """ Set Kd gain of the controller """ self.Kd = D def get_setpoint(self): return self.set_point def get_error(self): return self.error def get_integrator(self): return self.Integrator def get_derivator(self): return self.Derivator def is_running(self): return self.running def stop_controller(self): self.thread_shutdown_timer = timeit.default_timer() self.running = False # Unset method start time if self.method_id: with session_scope(MYCODO_DB_PATH) as db_session: mod_method = db_session.query(Method) mod_method = mod_method.filter( Method.method_id == self.method_id) mod_method = mod_method.filter( Method.method_order == 0).first() mod_method.start_time = 'Ended' db_session.commit()
def page_camera(): """ Page to start/stop video stream or time-lapse, or capture a still image. Displays most recent still image and time-lapse image. """ if not flaskutils.user_has_permission('view_camera'): return redirect(url_for('general_routes.home')) form_camera = flaskforms.Camera() camera = Camera.query.all() # Check if a video stream is active for each_camera in camera: if each_camera.stream_started and not CameraStream().is_running(): each_camera.stream_started = False db.session.commit() if request.method == 'POST': if not flaskutils.user_has_permission('edit_settings'): return redirect(url_for('page_routes.page_camera')) control = DaemonControl() mod_camera = Camera.query.filter( Camera.id == form_camera.camera_id.data).first() if form_camera.capture_still.data: if mod_camera.stream_started: flash( gettext( u"Cannot capture still image if stream is active.")) return redirect('/camera') if CameraStream().is_running(): CameraStream().terminate_controller() # Stop camera stream time.sleep(2) camera_record('photo', mod_camera) elif form_camera.start_timelapse.data: if mod_camera.stream_started: flash(gettext(u"Cannot start time-lapse if stream is active.")) return redirect('/camera') now = time.time() mod_camera.timelapse_started = True mod_camera.timelapse_start_time = now mod_camera.timelapse_end_time = now + float( form_camera.timelapse_runtime_sec.data) mod_camera.timelapse_interval = form_camera.timelapse_interval.data mod_camera.timelapse_next_capture = now mod_camera.timelapse_capture_number = 0 db.session.commit() control.refresh_daemon_camera_settings() elif form_camera.pause_timelapse.data: mod_camera.timelapse_paused = True db.session.commit() control.refresh_daemon_camera_settings() elif form_camera.resume_timelapse.data: mod_camera.timelapse_paused = False db.session.commit() control.refresh_daemon_camera_settings() elif form_camera.stop_timelapse.data: mod_camera.timelapse_started = False mod_camera.timelapse_start_time = None mod_camera.timelapse_end_time = None mod_camera.timelapse_interval = None mod_camera.timelapse_next_capture = None mod_camera.timelapse_capture_number = None db.session.commit() control.refresh_daemon_camera_settings() elif form_camera.start_stream.data: if mod_camera.timelapse_started: flash(gettext(u"Cannot start stream if time-lapse is active.")) return redirect('/camera') elif CameraStream().is_running(): flash( gettext( u"Cannot start stream. The stream is already running.") ) return redirect('/camera') elif (not (mod_camera.camera_type == 'Raspberry Pi' and mod_camera.library == 'picamera')): flash( gettext(u"Streaming is only supported with the Raspberry" u" Pi camera using the picamera library.")) return redirect('/camera') elif Camera.query.filter_by(stream_started=True).count(): flash( gettext(u"Cannot start stream if another stream is " u"already in progress.")) return redirect('/camera') else: mod_camera.stream_started = True db.session.commit() elif form_camera.stop_stream.data: if CameraStream().is_running(): CameraStream().terminate_controller() mod_camera.stream_started = False db.session.commit() return redirect('/camera') # Get the full path and timestamps of latest still and time-lapse images latest_img_still_ts = {} latest_img_still = {} latest_img_tl_ts = {} latest_img_tl = {} for each_camera in camera: camera_path = os.path.join( PATH_CAMERAS, '{id}-{uid}'.format(id=each_camera.id, uid=each_camera.unique_id)) try: latest_still_img_full_path = max(glob.iglob( '{path}/still/Still-{cam_id}-*.jpg'.format( path=camera_path, cam_id=each_camera.id)), key=os.path.getmtime) except ValueError: latest_still_img_full_path = None if latest_still_img_full_path: ts = os.path.getmtime(latest_still_img_full_path) latest_img_still_ts[ each_camera.id] = datetime.datetime.fromtimestamp(ts).strftime( "%Y-%m-%d %H:%M:%S") latest_img_still[each_camera.id] = os.path.basename( latest_still_img_full_path) else: latest_img_still[each_camera.id] = None try: latest_time_lapse_img_full_path = max(glob.iglob( '{path}/timelapse/Timelapse-{cam_id}-*.jpg'.format( path=camera_path, cam_id=each_camera.id)), key=os.path.getmtime) except ValueError: latest_time_lapse_img_full_path = None if latest_time_lapse_img_full_path: ts = os.path.getmtime(latest_time_lapse_img_full_path) latest_img_tl_ts[each_camera.id] = datetime.datetime.fromtimestamp( ts).strftime("%Y-%m-%d %H:%M:%S") latest_img_tl[each_camera.id] = os.path.basename( latest_time_lapse_img_full_path) else: latest_img_tl[each_camera.id] = None time_now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') return render_template('pages/camera.html', camera=camera, form_camera=form_camera, latest_img_still=latest_img_still, latest_img_still_ts=latest_img_still_ts, latest_img_tl=latest_img_tl, latest_img_tl_ts=latest_img_tl_ts, time_now=time_now)
def page_info(): """ Display page with system information from command line tools """ if not flaskutils.user_has_permission('view_stats'): return redirect(url_for('general_routes.home')) uptime = subprocess.Popen("uptime", stdout=subprocess.PIPE, shell=True) (uptime_output, _) = uptime.communicate() uptime.wait() uname = subprocess.Popen("uname -a", stdout=subprocess.PIPE, shell=True) (uname_output, _) = uname.communicate() uname.wait() gpio = subprocess.Popen("gpio readall", stdout=subprocess.PIPE, shell=True) (gpio_output, _) = gpio.communicate() gpio.wait() df = subprocess.Popen("df -h", stdout=subprocess.PIPE, shell=True) (df_output, _) = df.communicate() df.wait() free = subprocess.Popen("free -h", stdout=subprocess.PIPE, shell=True) (free_output, _) = free.communicate() free.wait() ifconfig = subprocess.Popen("ifconfig -a", stdout=subprocess.PIPE, shell=True) (ifconfig_output, _) = ifconfig.communicate() ifconfig.wait() daemon_pid = None if os.path.exists(DAEMON_PID_FILE): with open(DAEMON_PID_FILE, 'r') as pid_file: daemon_pid = int(pid_file.read()) database_version = [] for each_ver in AlembicVersion.query.all(): database_version.append(each_ver.version_num) virtualenv_flask = False if hasattr(sys, 'real_prefix'): virtualenv_flask = True virtualenv_daemon = False pstree_output = None top_output = None daemon_up = daemon_active() if daemon_up: control = DaemonControl() ram_use_daemon = control.ram_use() virtualenv_daemon = control.is_in_virtualenv() pstree = subprocess.Popen("pstree -p {pid}".format(pid=daemon_pid), stdout=subprocess.PIPE, shell=True) (pstree_output, _) = pstree.communicate() pstree.wait() top = subprocess.Popen("top -bH -n 1 -p {pid}".format(pid=daemon_pid), stdout=subprocess.PIPE, shell=True) (top_output, _) = top.communicate() top.wait() else: ram_use_daemon = 0 ram_use_flask = resource.getrusage( resource.RUSAGE_SELF).ru_maxrss / float(1000) return render_template('pages/info.html', daemon_pid=daemon_pid, daemon_up=daemon_up, gpio_readall=gpio_output, database_version=database_version, df=df_output, free=free_output, ifconfig=ifconfig_output, pstree=pstree_output, ram_use_daemon=ram_use_daemon, ram_use_flask=ram_use_flask, top=top_output, uname=uname_output, uptime=uptime_output, virtualenv_daemon=virtualenv_daemon, virtualenv_flask=virtualenv_flask)
def __init__(self, ready, logger, pid_id): threading.Thread.__init__(self) self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.logger = logger self.pid_id = pid_id self.control = DaemonControl() with session_scope(MYCODO_DB_PATH) as new_session: pid = new_session.query(PID).filter(PID.id == self.pid_id).first() self.sensor_id = pid.sensor_id self.measure_type = pid.measure_type self.method_id = pid.method_id self.direction = pid.direction self.raise_relay_id = pid.raise_relay_id self.raise_min_duration = pid.raise_min_duration self.raise_max_duration = pid.raise_max_duration self.lower_relay_id = pid.lower_relay_id self.lower_min_duration = pid.lower_min_duration self.lower_max_duration = pid.lower_max_duration self.Kp = pid.p self.Ki = pid.i self.Kd = pid.d self.Integrator_min = pid.integrator_min self.Integrator_max = pid.integrator_max self.measure_interval = pid.period self.default_set_point = pid.setpoint self.set_point = pid.setpoint sensor = new_session.query(Sensor).filter(Sensor.id == self.sensor_id).first() self.sensor_duration = sensor.period self.Derivator = 0 self.Integrator = 0 self.error = 0.0 self.P_value = None self.I_value = None self.D_value = None self.raise_seconds_on = 0 self.timer = t.time()+self.measure_interval # Check if a method is set for this PID if self.method_id: with session_scope(MYCODO_DB_PATH) as new_session: method = new_session.query(Method) method = method.filter(Method.method_id == self.method_id) method = method.filter(Method.method_order == 0).first() self.method_type = method.method_type self.method_start_time = method.start_time if self.method_type == 'Duration': if self.method_start_time == 'Ended': # Method has ended and hasn't been instructed to begin again pass elif self.method_start_time == 'Ready' or self.method_start_time == None: # Method has been instructed to begin with session_scope(MYCODO_DB_PATH) as db_session: mod_method = db_session.query(Method) mod_method = mod_method.filter(Method.method_id == self.method_id) mod_method = mod_method.filter(Method.method_order == 0).first() mod_method.start_time = datetime.datetime.now() self.method_start_time = mod_method.start_time db_session.commit() else: # Method neither instructed to begin or not to # Likely there was a daemon restart ot power failure # Resume method with saved start_time self.method_start_time = datetime.datetime.strptime( self.method_start_time, '%Y-%m-%d %H:%M:%S.%f') self.logger.warning("[PID {}] Resuming method {} started at {}".format( self.pid_id, self.method_id, self.method_start_time))
def __init__(self, ready, pid_id): threading.Thread.__init__(self) self.logger = logging.getLogger("mycodo.pid_{id}".format(id=pid_id)) self.running = False self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.pid_id = pid_id self.pid_unique_id = db_retrieve_table_daemon( PID, device_id=self.pid_id).unique_id self.control = DaemonControl() self.control_variable = 0.0 self.derivator = 0.0 self.integrator = 0.0 self.error = 0.0 self.P_value = None self.I_value = None self.D_value = None self.set_point = 0.0 self.lower_seconds_on = 0.0 self.raise_seconds_on = 0.0 self.lower_duty_cycle = 0.0 self.raise_duty_cycle = 0.0 self.last_time = None self.last_measurement = None self.last_measurement_success = False self.is_activated = None self.is_held = None self.is_paused = None self.measurement = None self.method_id = None self.direction = None self.raise_output_id = None self.raise_min_duration = None self.raise_max_duration = None self.raise_min_off_duration = None self.lower_output_id = None self.lower_min_duration = None self.lower_max_duration = None self.lower_min_off_duration = None self.Kp = None self.Ki = None self.Kd = None self.integrator_min = None self.integrator_max = None self.period = None self.max_measure_age = None self.default_set_point = None self.set_point = None self.input_unique_id = None self.input_duration = None self.raise_output_type = None self.lower_output_type = None self.initialize_values() self.timer = t.time() + self.period # Check if a method is set for this PID self.method_start_act = None if self.method_id: method = db_retrieve_table_daemon(Method, device_id=self.method_id) method_data = db_retrieve_table_daemon(MethodData) method_data = method_data.filter( MethodData.method_id == self.method_id) method_data_repeat = method_data.filter( MethodData.duration_sec == 0).first() pid = db_retrieve_table_daemon(PID, device_id=self.pid_id) self.method_type = method.method_type self.method_start_act = pid.method_start_time self.method_start_time = None self.method_end_time = None if self.method_type == 'Duration': if self.method_start_act == 'Ended': # Method has ended and hasn't been instructed to begin again pass elif self.method_start_act == 'Ready' or self.method_start_act is None: # Method has been instructed to begin now = datetime.datetime.now() self.method_start_time = now if method_data_repeat and method_data_repeat.duration_end: self.method_end_time = now + datetime.timedelta( seconds=float(method_data_repeat.duration_end)) with session_scope(MYCODO_DB_PATH) as db_session: mod_pid = db_session.query(PID) mod_pid = mod_pid.filter(PID.id == self.pid_id).first() mod_pid.method_start_time = self.method_start_time mod_pid.method_end_time = self.method_end_time db_session.commit() else: # Method neither instructed to begin or not to # Likely there was a daemon restart ot power failure # Resume method with saved start_time self.method_start_time = datetime.datetime.strptime( str(pid.method_start_time), '%Y-%m-%d %H:%M:%S.%f') if method_data_repeat and method_data_repeat.duration_end: self.method_end_time = datetime.datetime.strptime( str(pid.method_end_time), '%Y-%m-%d %H:%M:%S.%f') if self.method_end_time > datetime.datetime.now(): self.logger.warning( "Resuming method {id}: started {start}, " "ends {end}".format( id=self.method_id, start=self.method_start_time, end=self.method_end_time)) else: self.method_start_act = 'Ended' else: self.method_start_act = 'Ended'
class TimerController(threading.Thread): """ class for controlling timers """ def __init__(self, ready, logger, timer_id): threading.Thread.__init__(self) self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.logger = logger self.timer_id = timer_id self.control = DaemonControl() with session_scope(MYCODO_DB_PATH) as new_session: timer = new_session.query(Timer).filter( Timer.id == self.timer_id).first() self.name = timer.name self.relay_id = timer.relay_id self.state = timer.state self.time = timer.time_on self.duration_on = timer.duration_on self.duration_off = timer.duration_off # Time of day split into hour and minute if self.time: time_split = self.time.split(":") self.hour = time_split[0] self.minute = time_split[1] else: self.hour = None self.minute = None self.duration_timer = time.time() self.date_timer_not_executed = True self.running = False def run(self): self.running = True self.logger.info("[Timer {}] Activated in {}ms".format( self.timer_id, (timeit.default_timer()-self.thread_startup_timer)*1000)) self.ready.set() while (self.running): # Timer is a simple on/off duration timer if self.duration_on and self.duration_off: if time.time() > self.duration_timer: self.duration_timer = time.time()+self.duration_on+self.duration_off self.logger.debug("[Timer {}] Turn relay {} on " "for {} seconds, then off for " "{} seconds".format(self.timer_id, self.relay_id, self.duration_on, self.duration_off)) relay_on = threading.Thread(target=self.control.relay_on, args=(self.relay_id, self.duration_on,)) relay_on.start() # Timer is set to react at a specific hour and minute of the day else: if (int(self.hour) == datetime.datetime.now().hour and int(self.minute) == datetime.datetime.now().minute): # Ensure this is triggered only once at this specific time if self.date_timer_not_executed: message = "[Timer {}] At {}, turn relay {} {}".format( self.timer_id, self.time, self.relay_id, self.state) if self.state == 'on' and self.duration_on: message += " for {} seconds".format( self.duration_on) self.logger.debug(message) modulate_relay = threading.Thread( target=self.control.relay_on_off, args=(self.relay_id, self.state, self.duration_on,)) modulate_relay.start() self.date_timer_not_executed = False elif not self.date_timer_not_executed: self.date_timer_not_executed = True time.sleep(1) self.control.relay_off(self.relay_id) self.running = False self.logger.info("[Timer {}] Deactivated in {}ms".format( self.timer_id, (timeit.default_timer()-self.thread_shutdown_timer)*1000)) def isRunning(self): return self.running def stopController(self): self.thread_shutdown_timer = timeit.default_timer() self.running = False
class TimerController(threading.Thread): """ class for controlling timers """ def __init__(self, ready, logger, timer_id): threading.Thread.__init__(self) self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.logger = logger self.timer_id = timer_id self.control = DaemonControl() with session_scope(MYCODO_DB_PATH) as new_session: timer = new_session.query(Timer).filter( Timer.id == self.timer_id).first() self.name = timer.name self.relay_id = timer.relay_id self.state = timer.state self.time = timer.time_on self.duration_on = timer.duration_on self.duration_off = timer.duration_off # Time of day split into hour and minute if self.time: time_split = self.time.split(":") self.hour = time_split[0] self.minute = time_split[1] else: self.hour = None self.minute = None self.duration_timer = time.time() self.date_timer_not_executed = True self.running = False def run(self): self.running = True self.logger.info("[Timer {}] Activated in {:.1f} ms".format( self.timer_id, (timeit.default_timer() - self.thread_startup_timer) * 1000)) self.ready.set() while (self.running): # Timer is a simple on/off duration timer if self.duration_on and self.duration_off: if time.time() > self.duration_timer: self.duration_timer = time.time( ) + self.duration_on + self.duration_off self.logger.debug("[Timer {}] Turn relay {} on " "for {} seconds, then off for " "{} seconds".format( self.timer_id, self.relay_id, self.duration_on, self.duration_off)) relay_on = threading.Thread(target=self.control.relay_on, args=( self.relay_id, self.duration_on, )) relay_on.start() # Timer is set to react at a specific hour and minute of the day else: if (int(self.hour) == datetime.datetime.now().hour and int( self.minute) == datetime.datetime.now().minute): # Ensure this is triggered only once at this specific time if self.date_timer_not_executed: message = "[Timer {}] At {}, turn relay {} {}".format( self.timer_id, self.time, self.relay_id, self.state) if self.state == 'on' and self.duration_on: message += " for {} seconds".format( self.duration_on) self.logger.debug(message) modulate_relay = threading.Thread( target=self.control.relay_on_off, args=( self.relay_id, self.state, self.duration_on, )) modulate_relay.start() self.date_timer_not_executed = False elif not self.date_timer_not_executed: self.date_timer_not_executed = True time.sleep(1) self.control.relay_off(self.relay_id) self.running = False self.logger.info("[Timer {}] Deactivated in {:.1f} ms".format( self.timer_id, (timeit.default_timer() - self.thread_shutdown_timer) * 1000)) def isRunning(self): return self.running def stopController(self): self.thread_shutdown_timer = timeit.default_timer() self.running = False
class PIDController(threading.Thread): """ Class to operate discrete PID controller """ def __init__(self, ready, pid_id): threading.Thread.__init__(self) self.logger = logging.getLogger("mycodo.pid_{id}".format(id=pid_id)) self.running = False self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.pid_id = pid_id self.pid_unique_id = db_retrieve_table_daemon( PID, device_id=self.pid_id).unique_id self.control = DaemonControl() self.control_variable = 0.0 self.derivator = 0.0 self.integrator = 0.0 self.error = 0.0 self.P_value = None self.I_value = None self.D_value = None self.set_point = 0.0 self.lower_seconds_on = 0.0 self.raise_seconds_on = 0.0 self.lower_duty_cycle = 0.0 self.raise_duty_cycle = 0.0 self.last_time = None self.last_measurement = None self.last_measurement_success = False self.is_activated = None self.is_held = None self.is_paused = None self.pid_type = None self.measurement = None self.method_id = None self.direction = None self.raise_relay_id = None self.raise_min_duration = None self.raise_max_duration = None self.raise_min_off_duration = None self.lower_relay_id = None self.lower_min_duration = None self.lower_max_duration = None self.lower_min_off_duration = None self.Kp = None self.Ki = None self.Kd = None self.integrator_min = None self.integrator_max = None self.period = None self.max_measure_age = None self.default_set_point = None self.set_point = None self.sensor_unique_id = None self.sensor_duration = None self.initialize_values() self.timer = t.time() + self.period # Check if a method is set for this PID if self.method_id: method = db_retrieve_table_daemon(Method, device_id=self.method_id) self.method_type = method.method_type self.method_start_time = method.start_time if self.method_type == 'Duration': if self.method_start_time == 'Ended': # Method has ended and hasn't been instructed to begin again pass elif self.method_start_time == 'Ready' or self.method_start_time is None: # Method has been instructed to begin with session_scope(MYCODO_DB_PATH) as db_session: mod_method = db_session.query(Method) mod_method = mod_method.filter( Method.id == self.method_id).first() mod_method.time_start = datetime.datetime.now() self.method_start_time = mod_method.start_time db_session.commit() else: # Method neither instructed to begin or not to # Likely there was a daemon restart ot power failure # Resume method with saved start_time self.method_start_time = datetime.datetime.strptime( str(self.method_start_time), '%Y-%m-%d %H:%M:%S.%f') self.logger.warning( "Resuming method {id} started at {time}".format( id=self.method_id, time=self.method_start_time)) def run(self): try: self.running = True self.logger.info("Activated in {:.1f} ms".format( (timeit.default_timer() - self.thread_startup_timer) * 1000)) if self.is_paused: self.logger.info("Paused") elif self.is_held: self.logger.info("Held") self.ready.set() while self.running: if t.time() > self.timer: # Ensure the timer ends in the future while t.time() > self.timer: self.timer = self.timer + self.period # If PID is active, retrieve sensor measurement and update PID output if self.is_activated and not self.is_paused: self.get_last_measurement() if self.last_measurement_success: # Update setpoint using a method if one is selected if self.method_id: self.calculate_method_setpoint(self.method_id) write_setpoint_db = threading.Thread( target=write_influxdb_value, args=( self.pid_unique_id, 'setpoint', self.set_point, )) write_setpoint_db.start() # Update PID and get control variable self.control_variable = self.update_pid_output( self.last_measurement) if self.pid_type == 'relay': pid_entry_type = 'pid_output' pid_entry_value = self.control_variable elif self.pid_type == 'pwm': pid_entry_type = 'duty_cycle' pid_entry_value = self.control_var_to_duty_cycle( abs(self.control_variable)) if self.control_variable < 0: pid_entry_value = -pid_entry_value else: pid_entry_type = None pid_entry_value = None if pid_entry_type: write_pid_out_db = threading.Thread( target=write_influxdb_value, args=( self.pid_unique_id, pid_entry_type, pid_entry_value, )) write_pid_out_db.start() # If PID is active or on hold, activate relays if ((self.is_activated and not self.is_paused) or (self.is_activated and self.is_held)): self.manipulate_output() t.sleep(0.1) # Turn off relay used in PID when the controller is deactivated if self.raise_relay_id and self.direction in ['raise', 'both']: self.control.relay_off(self.raise_relay_id, trigger_conditionals=True) if self.lower_relay_id and self.direction in ['lower', 'both']: self.control.relay_off(self.lower_relay_id, trigger_conditionals=True) self.running = False self.logger.info("Deactivated in {:.1f} ms".format( (timeit.default_timer() - self.thread_shutdown_timer) * 1000)) except Exception as except_msg: self.logger.exception("Run Error: {err}".format(err=except_msg)) def initialize_values(self): """Set PID parameters""" pid = db_retrieve_table_daemon(PID, device_id=self.pid_id) self.is_activated = pid.is_activated self.is_held = pid.is_held self.is_paused = pid.is_paused self.pid_type = pid.pid_type self.measurement = pid.measurement self.method_id = pid.method_id self.direction = pid.direction self.raise_relay_id = pid.raise_relay_id self.raise_min_duration = pid.raise_min_duration self.raise_max_duration = pid.raise_max_duration self.raise_min_off_duration = pid.raise_min_off_duration self.lower_relay_id = pid.lower_relay_id self.lower_min_duration = pid.lower_min_duration self.lower_max_duration = pid.lower_max_duration self.lower_min_off_duration = pid.lower_min_off_duration self.Kp = pid.p self.Ki = pid.i self.Kd = pid.d self.integrator_min = pid.integrator_min self.integrator_max = pid.integrator_max self.period = pid.period self.max_measure_age = pid.max_measure_age self.default_set_point = pid.setpoint self.set_point = pid.setpoint sensor = db_retrieve_table_daemon(Sensor, device_id=pid.sensor_id) self.sensor_unique_id = sensor.unique_id self.sensor_duration = sensor.period return "success" def update_pid_output(self, current_value): """ Calculate PID output value from reference input and feedback :return: Manipulated, or control, variable. This is the PID output. :rtype: float :param current_value: The input, or process, variable (the actual measured condition by the sensor) :type current_value: float """ self.error = self.set_point - current_value # Calculate P-value self.P_value = self.Kp * self.error # Calculate I-value self.integrator += self.error # First method for managing integrator if self.integrator > self.integrator_max: self.integrator = self.integrator_max elif self.integrator < self.integrator_min: self.integrator = self.integrator_min # Second method for regulating integrator # if self.period is not None: # if self.integrator * self.Ki > self.period: # self.integrator = self.period / self.Ki # elif self.integrator * self.Ki < -self.period: # self.integrator = -self.period / self.Ki self.I_value = self.integrator * self.Ki # Calculate D-value self.D_value = self.Kd * (self.error - self.derivator) self.derivator = self.error # Produce output form P, I, and D values pid_value = self.P_value + self.I_value + self.D_value return pid_value def get_last_measurement(self): """ Retrieve the latest sensor measurement from InfluxDB :rtype: None """ self.last_measurement_success = False # Get latest measurement (from within the past minute) from influxdb try: if self.sensor_duration < 60: duration = 60 else: duration = int(self.sensor_duration * 1.5) self.last_measurement = read_last_influxdb(self.sensor_unique_id, self.measurement, duration) if self.last_measurement: self.last_time = self.last_measurement[0] self.last_measurement = self.last_measurement[1] utc_dt = datetime.datetime.strptime( self.last_time.split(".")[0], '%Y-%m-%dT%H:%M:%S') utc_timestamp = calendar.timegm(utc_dt.timetuple()) local_timestamp = str( datetime.datetime.fromtimestamp(utc_timestamp)) self.logger.debug("Latest {meas}: {last} @ {ts}".format( meas=self.measurement, last=self.last_measurement, ts=local_timestamp)) if calendar.timegm( t.gmtime()) - utc_timestamp > self.max_measure_age: self.logger.error( "Last measurement was {last_sec} seconds ago, however" " the maximum measurement age is set to {max_sec}" " seconds.".format( last_sec=calendar.timegm(t.gmtime()) - utc_timestamp, max_sec=self.max_measure_age)) self.last_measurement_success = True else: self.logger.warning("No data returned from influxdb") except requests.ConnectionError: self.logger.error("Failed to read measurement from the " "influxdb database: Could not connect.") except Exception as except_msg: self.logger.exception( "Exception while reading measurement from the influxdb " "database: {err}".format(err=except_msg)) def manipulate_output(self): """ Activate output based on PID control variable and whether the manipulation directive is to raise, lower, or both. :rtype: None """ # If the last measurement was able to be retrieved and was entered within the past minute if self.last_measurement_success: # # PID control variable is positive, indicating a desire to raise # the environmental condition # if self.direction in ['raise', 'both'] and self.raise_relay_id: if self.control_variable > 0: # Turn off lower_relay if active, because we're now raising if (self.direction == 'both' and self.lower_relay_id and self.control.relay_state( self.lower_relay_id) != 'off'): self.control.relay_off(self.lower_relay_id) if self.pid_type == 'relay': # Ensure the relay on duration doesn't exceed the set maximum if (self.raise_max_duration and self.control_variable > self.raise_max_duration): self.raise_seconds_on = self.raise_max_duration else: self.raise_seconds_on = float("{0:.2f}".format( self.control_variable)) if self.raise_seconds_on > self.raise_min_duration: # Activate raise_relay for a duration self.logger.debug( "Setpoint: {sp} Output: {cv} to relay " "{id}".format(sp=self.set_point, cv=self.control_variable, id=self.raise_relay_id)) self.control.relay_on( self.raise_relay_id, duration=self.raise_seconds_on, min_off=self.raise_min_off_duration) elif self.pid_type == 'pwm': self.raise_duty_cycle = float("{0:.1f}".format( self.control_var_to_duty_cycle( self.control_variable))) # Ensure the duty cycle doesn't exceed the min/max if (self.raise_max_duration and self.raise_duty_cycle > self.raise_max_duration): self.raise_duty_cycle = self.raise_max_duration elif (self.raise_min_duration and self.raise_duty_cycle < self.raise_min_duration): self.raise_duty_cycle = self.raise_min_duration self.logger.debug( "Setpoint: {sp}, Control Variable: {cv}, Output: PWM output " "{id} to {dc:.1f}%".format( sp=self.set_point, cv=self.control_variable, id=self.raise_relay_id, dc=self.raise_duty_cycle)) # Activate pwm with calculated duty cycle self.control.relay_on(self.raise_relay_id, duty_cycle=self.raise_duty_cycle) else: if self.pid_type == 'relay': self.control.relay_off(self.raise_relay_id) elif self.pid_type == 'pwm': self.control.relay_on(self.raise_relay_id, duty_cycle=0) # # PID control variable is negative, indicating a desire to lower # the environmental condition # if self.direction in ['lower', 'both'] and self.lower_relay_id: if self.control_variable < 0: # Turn off raise_relay if active, because we're now raising if (self.direction == 'both' and self.raise_relay_id and self.control.relay_state( self.raise_relay_id) != 'off'): self.control.relay_off(self.raise_relay_id) if self.pid_type == 'relay': # Ensure the relay on duration doesn't exceed the set maximum if (self.lower_max_duration and abs(self.control_variable) > self.lower_max_duration): self.lower_seconds_on = self.lower_max_duration else: self.lower_seconds_on = abs( float("{0:.2f}".format(self.control_variable))) if self.lower_seconds_on > self.lower_min_duration: # Activate lower_relay for a duration self.logger.debug("Setpoint: {sp} Output: {cv} to " "relay {id}".format( sp=self.set_point, cv=self.control_variable, id=self.lower_relay_id)) self.control.relay_on( self.lower_relay_id, duration=self.lower_seconds_on, min_off=self.lower_min_off_duration) elif self.pid_type == 'pwm': self.lower_duty_cycle = float("{0:.1f}".format( self.control_var_to_duty_cycle( abs(self.control_variable)))) # Ensure the duty cycle doesn't exceed the min/max if (self.lower_max_duration and self.lower_duty_cycle > self.lower_max_duration): self.lower_duty_cycle = -self.lower_max_duration elif (self.lower_min_duration and self.lower_duty_cycle < self.lower_min_duration): self.lower_duty_cycle = -self.lower_min_duration self.logger.debug( "Setpoint: {sp}, Control Variable: {cv}, Output: PWM output " "{id} to {dc:.1f}%".format( sp=self.set_point, cv=self.control_variable, id=self.lower_relay_id, dc=self.lower_duty_cycle)) # Turn back negative for proper logging if self.control_variable < 0: self.lower_duty_cycle = -self.lower_duty_cycle # Activate pwm with calculated duty cycle self.control.relay_on(self.lower_relay_id, duty_cycle=self.lower_duty_cycle) else: if self.pid_type == 'relay': self.control.relay_off(self.lower_relay_id) elif self.pid_type == 'pwm': self.control.relay_on(self.lower_relay_id, duty_cycle=0) else: if self.direction in ['raise', 'both'] and self.raise_relay_id: self.control.relay_off(self.raise_relay_id) if self.direction in ['lower', 'both'] and self.lower_relay_id: self.control.relay_off(self.lower_relay_id) def calculate_method_setpoint(self, method_id): method = db_retrieve_table_daemon(Method) method_key = method.filter(Method.id == method_id).first() method_data = db_retrieve_table_daemon(MethodData) method_data = method_data.filter(MethodData.method_id == method_id) method_data_all = method_data.filter(MethodData.relay_id == None).all() method_data_first = method_data.filter( MethodData.relay_id == None).first() now = datetime.datetime.now() # Calculate where the current time/date is within the time/date method if method_key.method_type == 'Date': for each_method in method_data_all: start_time = datetime.datetime.strptime( each_method.time_start, '%Y-%m-%d %H:%M:%S') end_time = datetime.datetime.strptime(each_method.time_end, '%Y-%m-%d %H:%M:%S') if start_time < now < end_time: setpoint_start = each_method.setpoint_start if each_method.setpoint_end: setpoint_end = each_method.setpoint_end else: setpoint_end = each_method.setpoint_start setpoint_diff = abs(setpoint_end - setpoint_start) total_seconds = (end_time - start_time).total_seconds() part_seconds = (now - start_time).total_seconds() percent_total = part_seconds / total_seconds if setpoint_start < setpoint_end: new_setpoint = setpoint_start + (setpoint_diff * percent_total) else: new_setpoint = setpoint_start - (setpoint_diff * percent_total) self.logger.debug("[Method] Start: {} End: {}".format( start_time, end_time)) self.logger.debug("[Method] Start: {} End: {}".format( setpoint_start, setpoint_end)) self.logger.debug( "[Method] Total: {} Part total: {} ({}%)".format( total_seconds, part_seconds, percent_total)) self.logger.debug( "[Method] New Setpoint: {}".format(new_setpoint)) self.set_point = new_setpoint return 0 # Calculate where the current Hour:Minute:Seconds is within the Daily method elif method_key.method_type == 'Daily': daily_now = datetime.datetime.now().strftime('%H:%M:%S') daily_now = datetime.datetime.strptime(str(daily_now), '%H:%M:%S') for each_method in method_data_all: start_time = datetime.datetime.strptime( each_method.time_start, '%H:%M:%S') end_time = datetime.datetime.strptime(each_method.time_end, '%H:%M:%S') if start_time < daily_now < end_time: setpoint_start = each_method.setpoint_start if each_method.setpoint_end: setpoint_end = each_method.setpoint_end else: setpoint_end = each_method.setpoint_start setpoint_diff = abs(setpoint_end - setpoint_start) total_seconds = (end_time - start_time).total_seconds() part_seconds = (daily_now - start_time).total_seconds() percent_total = part_seconds / total_seconds if setpoint_start < setpoint_end: new_setpoint = setpoint_start + (setpoint_diff * percent_total) else: new_setpoint = setpoint_start - (setpoint_diff * percent_total) self.logger.debug("[Method] Start: {} End: {}".format( start_time.strftime('%H:%M:%S'), end_time.strftime('%H:%M:%S'))) self.logger.debug("[Method] Start: {} End: {}".format( setpoint_start, setpoint_end)) self.logger.debug( "[Method] Total: {} Part total: {} ({}%)".format( total_seconds, part_seconds, percent_total)) self.logger.debug( "[Method] New Setpoint: {}".format(new_setpoint)) self.set_point = new_setpoint return 0 # Calculate sine y-axis value from the x-axis (seconds of the day) elif method_key.method_type == 'DailySine': new_setpoint = sine_wave_y_out(method_data_first.amplitude, method_data_first.frequency, method_data_first.shift_angle, method_data_first.shift_y) self.set_point = new_setpoint return 0 # Calculate Bezier curve y-axis value from the x-axis (seconds of the day) elif method_key.method_type == 'DailyBezier': new_setpoint = bezier_curve_y_out( method_data_first.shift_angle, (method_data_first.x0, method_data_first.y0), (method_data_first.x1, method_data_first.y1), (method_data_first.x2, method_data_first.y2), (method_data_first.x3, method_data_first.y3)) self.set_point = new_setpoint return 0 # Calculate the duration in the method based on self.method_start_time elif method_key.method_type == 'Duration' and self.method_start_time != 'Ended': seconds_from_start = (now - self.method_start_time).total_seconds() total_sec = 0 previous_total_sec = 0 for each_method in method_data_all: total_sec += each_method.duration_sec if previous_total_sec <= seconds_from_start < total_sec: row_start_time = float( self.method_start_time.strftime( '%s')) + previous_total_sec row_since_start_sec = ( now - (self.method_start_time + datetime.timedelta( 0, previous_total_sec))).total_seconds() percent_row = row_since_start_sec / each_method.duration_sec setpoint_start = each_method.setpoint_start if each_method.setpoint_end: setpoint_end = each_method.setpoint_end else: setpoint_end = each_method.setpoint_start setpoint_diff = abs(setpoint_end - setpoint_start) if setpoint_start < setpoint_end: new_setpoint = setpoint_start + (setpoint_diff * percent_row) else: new_setpoint = setpoint_start - (setpoint_diff * percent_row) self.logger.debug( "[Method] Start: {} Seconds Since: {}".format( self.method_start_time, seconds_from_start)) self.logger.debug("[Method] Start time of row: {}".format( datetime.datetime.fromtimestamp(row_start_time))) self.logger.debug( "[Method] Sec since start of row: {}".format( row_since_start_sec)) self.logger.debug( "[Method] Percent of row: {}".format(percent_row)) self.logger.debug( "[Method] New Setpoint: {}".format(new_setpoint)) self.set_point = new_setpoint return 0 previous_total_sec = total_sec # Duration method has ended, reset method_start_time locally and in DB if self.method_start_time: with session_scope(MYCODO_DB_PATH) as db_session: mod_method = db_session.query(Method).filter( Method.id == self.method_id).first() mod_method.method_start_time = 'Ended' db_session.commit() self.method_start_time = 'Ended' # Setpoint not needing to be calculated, use default setpoint self.set_point = self.default_set_point def control_var_to_duty_cycle(self, control_variable): # Convert control variable to duty cycle if control_variable > self.period: return 100.0 else: return float((control_variable / self.period) * 100) def pid_mod(self): if self.initialize_values(): return "success" else: return "error" def pid_hold(self): self.is_held = True self.logger.info("Hold") return "success" def pid_pause(self): self.is_paused = True self.logger.info("Pause") return "success" def pid_resume(self): self.is_activated = True self.is_held = False self.is_paused = False self.logger.info("Resume") return "success" def set_setpoint(self, set_point): """ Initilize the setpoint of PID """ self.set_point = set_point self.integrator = 0 self.derivator = 0 def set_integrator(self, integrator): """ Set the integrator of the controller """ self.integrator = integrator def set_derivator(self, derivator): """ Set the derivator of the controller """ self.derivator = derivator def set_kp(self, p): """ Set Kp gain of the controller """ self.Kp = p def set_ki(self, i): """ Set Ki gain of the controller """ self.Ki = i def set_kd(self, d): """ Set Kd gain of the controller """ self.Kd = d def get_setpoint(self): return self.set_point def get_error(self): return self.error def get_integrator(self): return self.integrator def get_derivator(self): return self.derivator def is_running(self): return self.running def stop_controller(self): self.thread_shutdown_timer = timeit.default_timer() self.running = False # Unset method start time if self.method_id: with session_scope(MYCODO_DB_PATH) as db_session: mod_method = db_session.query(Method).filter( Method.id == self.method_id).first() mod_method.method_start_time = 'Ended' db_session.commit()
def __init__(self, ready, logger, pid_id): threading.Thread.__init__(self) self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.logger = logger self.pid_id = pid_id self.control = DaemonControl() with session_scope(MYCODO_DB_PATH) as new_session: pid = new_session.query(PID).filter(PID.id == self.pid_id).first() self.sensor_id = pid.sensor_id self.measure_type = pid.measure_type self.method_id = pid.method_id self.direction = pid.direction self.raise_relay_id = pid.raise_relay_id self.raise_min_duration = pid.raise_min_duration self.raise_max_duration = pid.raise_max_duration self.lower_relay_id = pid.lower_relay_id self.lower_min_duration = pid.lower_min_duration self.lower_max_duration = pid.lower_max_duration self.Kp = pid.p self.Ki = pid.i self.Kd = pid.d self.Integrator_min = pid.integrator_min self.Integrator_max = pid.integrator_max self.measure_interval = pid.period self.default_set_point = pid.setpoint self.set_point = pid.setpoint sensor = new_session.query(Sensor).filter( Sensor.id == self.sensor_id).first() self.sensor_duration = sensor.period self.Derivator = 0 self.Integrator = 0 self.error = 0.0 self.P_value = None self.I_value = None self.D_value = None self.raise_seconds_on = 0 self.timer = t.time() + self.measure_interval # Check if a method is set for this PID if self.method_id: with session_scope(MYCODO_DB_PATH) as new_session: method = new_session.query(Method) method = method.filter(Method.method_id == self.method_id) method = method.filter(Method.method_order == 0).first() self.method_type = method.method_type self.method_start_time = method.start_time if self.method_type == 'Duration': if self.method_start_time == 'Ended': # Method has ended and hasn't been instructed to begin again pass elif self.method_start_time == 'Ready' or self.method_start_time == None: # Method has been instructed to begin with session_scope(MYCODO_DB_PATH) as db_session: mod_method = db_session.query(Method) mod_method = mod_method.filter( Method.method_id == self.method_id) mod_method = mod_method.filter( Method.method_order == 0).first() mod_method.start_time = datetime.datetime.now() self.method_start_time = mod_method.start_time db_session.commit() else: # Method neither instructed to begin or not to # Likely there was a daemon restart ot power failure # Resume method with saved start_time self.method_start_time = datetime.datetime.strptime( self.method_start_time, '%Y-%m-%d %H:%M:%S.%f') self.logger.warning( "[PID {}] Resuming method {} started at {}".format( self.pid_id, self.method_id, self.method_start_time))
def __init__(self, ready, logger, sensor_id): threading.Thread.__init__(self) list_devices_i2c = [ 'ADS1x15', 'AM2315', 'ATLAS_PT1000', 'BMP', 'HTU21D', 'MCP342x', 'SHT2x', 'TMP006', 'TSL2561' ] self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.logger = logger self.lock = {} self.sensor_id = sensor_id self.control = DaemonControl() self.pause_loop = False self.verify_pause_loop = True self.setup_sensor_conditionals() with session_scope(MYCODO_DB_PATH) as new_session: sensor = new_session.query(Sensor) sensor = sensor.filter(Sensor.id == self.sensor_id).first() self.i2c_bus = sensor.i2c_bus self.location = sensor.location self.device_type = sensor.device self.sensor_type = sensor.device_type self.period = sensor.period self.multiplexer_address_raw = sensor.multiplexer_address self.multiplexer_bus = sensor.multiplexer_bus self.multiplexer_channel = sensor.multiplexer_channel self.adc_channel = sensor.adc_channel self.adc_gain = sensor.adc_gain self.adc_resolution = sensor.adc_resolution self.adc_measure = sensor.adc_measure self.adc_measure_units = sensor.adc_measure_units self.adc_volts_min = sensor.adc_volts_min self.adc_volts_max = sensor.adc_volts_max self.adc_units_min = sensor.adc_units_min self.adc_units_max = sensor.adc_units_max self.sht_clock_pin = sensor.sht_clock_pin self.sht_voltage = sensor.sht_voltage # Edge detection self.switch_edge = sensor.switch_edge self.switch_bouncetime = sensor.switch_bouncetime self.switch_reset_period = sensor.switch_reset_period # Relay that will activate prior to sensor read self.pre_relay_id = sensor.pre_relay_id self.pre_relay_duration = sensor.pre_relay_duration self.pre_relay_setup = False self.next_measurement = time.time() self.get_new_measurement = False self.measurement_acquired = False self.pre_relay_activated = False self.pre_relay_timer = time.time() relay = new_session.query(Relay).all() for each_relay in relay: # Check if relay ID actually exists if each_relay.id == self.pre_relay_id and self.pre_relay_duration: self.pre_relay_setup = True smtp = new_session.query(SMTP).first() self.smtp_max_count = smtp.hourly_max self.email_count = 0 self.allowed_to_send_notice = True # Convert string I2C address to base-16 int if self.device_type in list_devices_i2c: self.i2c_address = int(str(self.location), 16) # Set up multiplexer if enabled if self.device_type in list_devices_i2c and self.multiplexer_address_raw: self.multiplexer_address_string = self.multiplexer_address_raw self.multiplexer_address = int(str(self.multiplexer_address_raw), 16) self.multiplexer_lock_file = "/var/lock/mycodo_multiplexer_0x{:02X}.pid".format( self.multiplexer_address) self.multiplexer = TCA9548A(self.multiplexer_bus, self.multiplexer_address) else: self.multiplexer = None if self.device_type in ['ADS1x15', 'MCP342x'] and self.location: self.adc_lock_file = "/var/lock/mycodo_adc_bus{}_0x{:02X}.pid".format( self.i2c_bus, self.i2c_address) # Set up edge detection of a GPIO pin if self.device_type == 'EDGE': if self.switch_edge == 'rising': self.switch_edge_gpio = GPIO.RISING elif self.switch_edge == 'falling': self.switch_edge_gpio = GPIO.FALLING else: self.switch_edge_gpio = GPIO.BOTH # Set up analog-to-digital converter elif self.device_type == 'ADS1x15': self.adc = ADS1x15_read(self.i2c_address, self.i2c_bus, self.adc_channel, self.adc_gain) elif self.device_type == 'MCP342x': self.adc = MCP342x_read(self.i2c_address, self.i2c_bus, self.adc_channel, self.adc_gain, self.adc_resolution) else: self.adc = None self.device_recognized = True # Set up sensor if self.device_type in ['EDGE', 'ADS1x15', 'MCP342x']: self.measure_sensor = None elif self.device_type == 'RPiCPULoad': self.measure_sensor = RaspberryPiCPULoad() elif self.device_type == 'RPi': self.measure_sensor = RaspberryPiCPUTemp() elif self.device_type == 'DS18B20': self.measure_sensor = DS18B20(self.location) elif self.device_type == 'DHT11': self.measure_sensor = DHT11(pigpio.pi(), int(self.location)) elif self.device_type in ['DHT22', 'AM2302']: self.measure_sensor = DHT22(pigpio.pi(), int(self.location)) elif self.device_type == 'HTU21D': self.measure_sensor = HTU21D_read(self.i2c_bus) elif self.device_type == 'AM2315': self.measure_sensor = AM2315_read(self.i2c_bus) elif self.device_type == 'ATLAS_PT1000': self.measure_sensor = Atlas_PT1000(self.i2c_address, self.i2c_bus) elif self.device_type == 'K30': self.measure_sensor = K30() elif self.device_type == 'BMP': self.measure_sensor = BMP(self.i2c_bus) elif self.device_type == 'SHT1x_7x': self.measure_sensor = SHT1x_7x_read(self.location, self.sht_clock_pin, self.sht_voltage) elif self.device_type == 'SHT2x': self.measure_sensor = SHT2x_read(self.i2c_address, self.i2c_bus) elif self.device_type == 'TMP006': self.measure_sensor = TMP006_read(self.i2c_address, self.i2c_bus) elif self.device_type == 'TSL2561': self.measure_sensor = TSL2561_read(self.i2c_address, self.i2c_bus) else: self.device_recognized = False self.logger.debug("[Sensor {}] Device '{}' not " "recognized:".format(self.sensor_id, self.device_type)) raise Exception("{} is not a valid device type.".format( self.device_type)) self.edge_reset_timer = time.time() self.sensor_timer = time.time() self.running = False self.lastUpdate = None
class PIDController(threading.Thread): """ Class to operate discrete PID controller """ def __init__(self, ready, logger, pid_id): threading.Thread.__init__(self) self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.logger = logger self.pid_id = pid_id self.control = DaemonControl() with session_scope(MYCODO_DB_PATH) as new_session: pid = new_session.query(PID).filter(PID.id == self.pid_id).first() self.sensor_id = pid.sensor_id self.measure_type = pid.measure_type self.method_id = pid.method_id self.direction = pid.direction self.raise_relay_id = pid.raise_relay_id self.raise_min_duration = pid.raise_min_duration self.raise_max_duration = pid.raise_max_duration self.lower_relay_id = pid.lower_relay_id self.lower_min_duration = pid.lower_min_duration self.lower_max_duration = pid.lower_max_duration self.Kp = pid.p self.Ki = pid.i self.Kd = pid.d self.Integrator_min = pid.integrator_min self.Integrator_max = pid.integrator_max self.measure_interval = pid.period self.default_set_point = pid.setpoint self.set_point = pid.setpoint sensor = new_session.query(Sensor).filter(Sensor.id == self.sensor_id).first() self.sensor_duration = sensor.period self.Derivator = 0 self.Integrator = 0 self.error = 0.0 self.P_value = None self.I_value = None self.D_value = None self.raise_seconds_on = 0 self.timer = t.time()+self.measure_interval # Check if a method is set for this PID if self.method_id: with session_scope(MYCODO_DB_PATH) as new_session: method = new_session.query(Method) method = method.filter(Method.method_id == self.method_id) method = method.filter(Method.method_order == 0).first() self.method_type = method.method_type self.method_start_time = method.start_time if self.method_type == 'Duration': if self.method_start_time == 'Ended': # Method has ended and hasn't been instructed to begin again pass elif self.method_start_time == 'Ready' or self.method_start_time == None: # Method has been instructed to begin with session_scope(MYCODO_DB_PATH) as db_session: mod_method = db_session.query(Method) mod_method = mod_method.filter(Method.method_id == self.method_id) mod_method = mod_method.filter(Method.method_order == 0).first() mod_method.start_time = datetime.datetime.now() self.method_start_time = mod_method.start_time db_session.commit() else: # Method neither instructed to begin or not to # Likely there was a daemon restart ot power failure # Resume method with saved start_time self.method_start_time = datetime.datetime.strptime( self.method_start_time, '%Y-%m-%d %H:%M:%S.%f') self.logger.warning("[PID {}] Resuming method {} started at {}".format( self.pid_id, self.method_id, self.method_start_time)) def run(self): try: self.running = True self.logger.info("[PID {}] Activated in {:.1f} ms".format( self.pid_id, (timeit.default_timer()-self.thread_startup_timer)*1000)) self.ready.set() while (self.running): if t.time() > self.timer: self.timer = self.timer+self.measure_interval self.get_last_measurement() self.manipulate_relays() t.sleep(0.1) if self.raise_relay_id: self.control.relay_off(self.raise_relay_id) if self.lower_relay_id: self.control.relay_off(self.lower_relay_id) self.running = False self.logger.info("[PID {}] Deactivated in {:.1f} ms".format( self.pid_id, (timeit.default_timer()-self.thread_shutdown_timer)*1000)) except Exception as except_msg: self.logger.exception("[PID {}] Run Error: Up ID {}, Down ID " "{}: {}".format(self.pid_id, self.raise_relay_id, self.lower_relay_id, except_msg)) def update(self, current_value): """ Calculate PID output value from reference input and feedback :return: Manipulated, or control, variable. This is the PID output. :rtype: float :param current_value: The input, or process, variable (the actual measured condition by the sensor) :type current_value: float """ self.error = self.set_point - current_value # Calculate P-value self.P_value = self.Kp * self.error # Calculate I-value self.Integrator += self.error # First method for managing Integrator if self.Integrator > self.Integrator_max: self.Integrator = self.Integrator_max elif self.Integrator < self.Integrator_min: self.Integrator = self.Integrator_min # Second method for regulating Integrator # if self.measure_interval is not None: # if self.Integrator * self.Ki > self.measure_interval: # self.Integrator = self.measure_interval / self.Ki # elif self.Integrator * self.Ki < -self.measure_interval: # self.Integrator = -self.measure_interval / self.Ki self.I_value = self.Integrator * self.Ki # Calculate D-value self.D_value = self.Kd * (self.error - self.Derivator) self.Derivator = self.error # Produce output form P, I, and D values PID = self.P_value + self.I_value + self.D_value return PID def get_last_measurement(self): """ Retrieve the latest sensor measurement from InfluxDB :rtype: None """ self.last_measurement_success = False # Get latest measurement (from within the past minute) from influxdb try: if self.sensor_duration < 60: duration = 60 else: duration = int(self.sensor_duration*1.5) self.last_measurement = read_last_influxdb( INFLUXDB_HOST, INFLUXDB_PORT, INFLUXDB_USER, INFLUXDB_PASSWORD, INFLUXDB_DATABASE, self.sensor_id, self.measure_type, duration) if self.last_measurement: measurement_list = list(self.last_measurement.get_points( measurement=self.measure_type)) self.last_time = measurement_list[0]['time'] self.last_measurement = measurement_list[0]['value'] utc_dt = datetime.datetime.strptime(self.last_time.split(".")[0], '%Y-%m-%dT%H:%M:%S') utc_timestamp = calendar.timegm(utc_dt.timetuple()) local_timestamp = str(datetime.datetime.fromtimestamp(utc_timestamp)) self.logger.debug("[PID {}] Latest {}: {} @ {}".format( self.pid_id, self.measure_type, self.last_measurement, local_timestamp)) self.last_measurement_success = True else: self.logger.warning("[PID {}] No data returned " "from influxdb".format(self.pid_id)) except Exception as except_msg: self.logger.exception("[PID {}] Failed to read " "measurement from the influxdb " "database: {}".format(self.pid_id, except_msg)) def manipulate_relays(self): """ Activate a relay based on PID output (control variable) and whether the manipulation directive is to raise, lower, or both. :rtype: None """ # If there was a measurement able to be retrieved from # influxdb database that was entered within the past minute if self.last_measurement_success: # Update setpoint if a method is selected if self.method_id != '': self.calculate_method_setpoint(self.method_id) self.addSetpointInfluxdb(self.pid_id, self.set_point) # Update PID and get control variable self.control_variable = self.update(self.last_measurement) # # PID control variable positive to raise environmental condition # if self.direction in ['raise', 'both'] and self.raise_relay_id: if self.control_variable > 0: # Ensure the relay on duration doesn't exceed the set maximum if (self.raise_max_duration and self.control_variable > self.raise_max_duration): self.raise_seconds_on = self.raise_max_duration else: self.raise_seconds_on = float("{0:.2f}".format(self.control_variable)) # Turn off lower_relay if active, because we're now raising if self.lower_relay_id: with session_scope(MYCODO_DB_PATH) as new_session: relay = new_session.query(Relay).filter( Relay.id == self.lower_relay_id).first() if relay.is_on(): self.control.relay_off(self.lower_relay_id) if self.raise_seconds_on > self.raise_min_duration: # Activate raise_relay for a duration self.logger.debug("[PID {}] Setpoint: {} " "Output: {} to relay {}".format( self.pid_id, self.set_point, self.control_variable, self.raise_relay_id)) self.control.relay_on(self.raise_relay_id, self.raise_seconds_on) else: self.control.relay_off(self.raise_relay_id) # # PID control variable negative to lower environmental condition # if self.direction in ['lower', 'both'] and self.lower_relay_id: if self.control_variable < 0: # Ensure the relay on duration doesn't exceed the set maximum if (self.lower_max_duration and abs(self.control_variable) > self.lower_max_duration): self.lower_seconds_on = self.lower_max_duration else: self.lower_seconds_on = abs(float("{0:.2f}".format(self.control_variable))) # Turn off raise_relay if active, because we're now lowering if self.raise_relay_id: with session_scope(MYCODO_DB_PATH) as new_session: relay = new_session.query(Relay).filter( Relay.id == self.raise_relay_id).first() if relay.is_on(): self.control.relay_off(self.raise_relay_id) if self.lower_seconds_on > self.lower_min_duration: # Activate lower_relay for a duration self.logger.debug("[PID {}] Setpoint: {} " "Output: {} to relay {}".format( self.pid_id, self.set_point, self.control_variable, self.lower_relay_id)) self.control.relay_on(self.lower_relay_id, self.lower_seconds_on) else: self.control.relay_off(self.lower_relay_id) else: if self.direction in ['raise', 'both'] and self.raise_relay_id: self.control.relay_off(self.raise_relay_id) if self.direction in ['lower', 'both'] and self.lower_relay_id: self.control.relay_off(self.lower_relay_id) def calculate_method_setpoint(self, method_id): with session_scope(MYCODO_DB_PATH) as new_session: method = new_session.query(Method) new_session.expunge_all() new_session.close() method_key = method.filter(Method.method_id == method_id) method_key = method_key.filter(Method.method_order == 0).first() method = method.filter(Method.method_id == method_id) method = method.filter(Method.relay_id == None) method = method.filter(Method.method_order > 0) method = method.order_by(Method.method_order.asc()).all() now = datetime.datetime.now() # Calculate where the current time/date is within the time/date method if method_key.method_type == 'Date': for each_method in method: start_time = datetime.datetime.strptime(each_method.start_time, '%Y-%m-%d %H:%M:%S') end_time = end_time = datetime.datetime.strptime(each_method.end_time, '%Y-%m-%d %H:%M:%S') if start_time < now < end_time: start_setpoint = each_method.start_setpoint if each_method.end_setpoint: end_setpoint = each_method.end_setpoint else: end_setpoint = each_method.start_setpoint setpoint_diff = abs(end_setpoint-start_setpoint) total_seconds = (end_time-start_time).total_seconds() part_seconds = (now-start_time).total_seconds() percent_total = part_seconds/total_seconds if start_setpoint < end_setpoint: new_setpoint = start_setpoint+(setpoint_diff*percent_total) else: new_setpoint = start_setpoint-(setpoint_diff*percent_total) self.logger.debug("[Method] Start: {} End: {}".format( start_time, end_time)) self.logger.debug("[Method] Start: {} End: {}".format( start_setpoint, end_setpoint)) self.logger.debug("[Method] Total: {} Part total: {} ({}%)".format( total_seconds, part_seconds, percent_total)) self.logger.debug("[Method] New Setpoint: {}".format( new_setpoint)) self.set_point = new_setpoint return 0 # Calculate where the current Hour:Minute:Seconds is within the Daily method elif method_key.method_type == 'Daily': daily_now = datetime.datetime.now().strftime('%H:%M:%S') daily_now = datetime.datetime.strptime(str(daily_now), '%H:%M:%S') for each_method in method: start_time = datetime.datetime.strptime(each_method.start_time, '%H:%M:%S') end_time = end_time = datetime.datetime.strptime(each_method.end_time, '%H:%M:%S') if start_time < daily_now < end_time: start_setpoint = each_method.start_setpoint if each_method.end_setpoint: end_setpoint = each_method.end_setpoint else: end_setpoint = each_method.start_setpoint setpoint_diff = abs(end_setpoint-start_setpoint) total_seconds = (end_time-start_time).total_seconds() part_seconds = (daily_now-start_time).total_seconds() percent_total = part_seconds/total_seconds if start_setpoint < end_setpoint: new_setpoint = start_setpoint+(setpoint_diff*percent_total) else: new_setpoint = start_setpoint-(setpoint_diff*percent_total) self.logger.debug("[Method] Start: {} End: {}".format( start_time.strftime('%H:%M:%S'), end_time.strftime('%H:%M:%S'))) self.logger.debug("[Method] Start: {} End: {}".format( start_setpoint, end_setpoint)) self.logger.debug("[Method] Total: {} Part total: {} ({}%)".format( total_seconds, part_seconds, percent_total)) self.logger.debug("[Method] New Setpoint: {}".format( new_setpoint)) self.set_point = new_setpoint return 0 # Calculate sine y-axis value from the x-axis (seconds of the day) elif method_key.method_type == 'DailySine': new_setpoint = sine_wave_y_out(method_key.amplitude, method_key.frequency, method_key.shift_angle, method_key.shift_y) self.set_point = new_setpoint return 0 # Calculate Bezier curve y-axis value from the x-axis (seconds of the day) elif method_key.method_type == 'DailyBezier': new_setpoint = bezier_curve_y_out(method_key.shift_angle, (method_key.x0, method_key.y0), (method_key.x1, method_key.y1), (method_key.x2, method_key.y2), (method_key.x3, method_key.y3)) self.set_point = new_setpoint return 0 # Calculate the duration in the method based on self.method_start_time elif method_key.method_type == 'Duration' and self.method_start_time != 'Ended': seconds_from_start = (now-self.method_start_time).total_seconds() total_sec = 0 previous_total_sec = 0 for each_method in method: total_sec += each_method.duration_sec if previous_total_sec <= seconds_from_start < total_sec: row_start_time = float(self.method_start_time.strftime('%s'))+previous_total_sec row_since_start_sec = (now-(self.method_start_time+datetime.timedelta(0, previous_total_sec))).total_seconds() percent_row = row_since_start_sec/each_method.duration_sec start_setpoint = each_method.start_setpoint if each_method.end_setpoint: end_setpoint = each_method.end_setpoint else: end_setpoint = each_method.start_setpoint setpoint_diff = abs(end_setpoint-start_setpoint) if start_setpoint < end_setpoint: new_setpoint = start_setpoint+(setpoint_diff*percent_row) else: new_setpoint = start_setpoint-(setpoint_diff*percent_row) self.logger.debug("[Method] Start: {} Seconds Since: {}".format( self.method_start_time, seconds_from_start)) self.logger.debug("[Method] Start time of row: {}".format( datetime.datetime.fromtimestamp(row_start_time))) self.logger.debug("[Method] Sec since start of row: {}".format( row_since_start_sec)) self.logger.debug("[Method] Percent of row: {}".format( percent_row)) self.logger.debug("[Method] New Setpoint: {}".format( new_setpoint)) self.set_point = new_setpoint return 0 previous_total_sec = total_sec # Duration method has ended, reset start_time locally and in DB if self.method_start_time: with session_scope(MYCODO_DB_PATH) as db_session: mod_method = db_session.query(Method).filter( Method.method_id == self.method_id) mod_method = mod_method.filter(Method.method_order == 0).first() mod_method.start_time = 'Ended' db_session.commit() self.method_start_time = 'Ended' # Setpoint not needing to be calculated, use default setpoint self.set_point = self.default_set_point def addSetpointInfluxdb(self, pid_id, setpoint): """ Add a setpoint entry to InfluxDB :rtype: None """ write_db = threading.Thread( target=write_influxdb_value, args=(self.logger, INFLUXDB_HOST, INFLUXDB_PORT, INFLUXDB_USER, INFLUXDB_PASSWORD, INFLUXDB_DATABASE, 'pid', pid_id, 'setpoint', setpoint,)) write_db.start() def setPoint(self, set_point): """Initilize the setpoint of PID""" self.set_point = set_point self.Integrator = 0 self.Derivator = 0 def setIntegrator(self, Integrator): """Set the Integrator of the controller""" self.Integrator = Integrator def setDerivator(self, Derivator): """Set the Derivator of the controller""" self.Derivator = Derivator def setKp(self, P): """Set Kp gain of the controller""" self.Kp = P def setKi(self, I): """Set Ki gain of the controller""" self.Ki = I def setKd(self, D): """Set Kd gain of the controller""" self.Kd = D def getPoint(self): return self.set_point def getError(self): return self.error def getIntegrator(self): return self.Integrator def getDerivator(self): return self.Derivator def isRunning(self): return self.running def stopController(self): self.thread_shutdown_timer = timeit.default_timer() self.running = False # Unset method start time if self.method_id: with session_scope(MYCODO_DB_PATH) as db_session: mod_method = db_session.query(Method) mod_method = mod_method.filter(Method.method_id == self.method_id) mod_method = mod_method.filter(Method.method_order == 0).first() mod_method.start_time = 'Ended' db_session.commit()
def __init__(self, ready, sensor_id): threading.Thread.__init__(self) self.logger = logging.getLogger( "mycodo.sensor_{id}".format(id=sensor_id)) self.stop_iteration_counter = 0 self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.lock = {} self.measurement = None self.updateSuccess = False self.sensor_id = sensor_id self.control = DaemonControl() self.pause_loop = False self.verify_pause_loop = True self.cond_id = {} self.cond_action_id = {} self.cond_name = {} self.cond_is_activated = {} self.cond_if_sensor_period = {} self.cond_if_sensor_measurement = {} self.cond_if_sensor_edge_select = {} self.cond_if_sensor_edge_detected = {} self.cond_if_sensor_gpio_state = {} self.cond_if_sensor_direction = {} self.cond_if_sensor_setpoint = {} self.cond_do_relay_id = {} self.cond_do_relay_state = {} self.cond_do_relay_duration = {} self.cond_execute_command = {} self.cond_email_notify = {} self.cond_do_lcd_id = {} self.cond_do_camera_id = {} self.cond_timer = {} self.smtp_wait_timer = {} self.setup_sensor_conditionals() sensor = db_retrieve_table_daemon(Sensor, device_id=self.sensor_id) self.sensor_sel = sensor self.unique_id = sensor.unique_id self.i2c_bus = sensor.i2c_bus self.location = sensor.location self.power_relay_id = sensor.power_relay_id self.measurements = sensor.measurements self.device = sensor.device self.interface = sensor.interface self.device_loc = sensor.device_loc self.baud_rate = sensor.baud_rate self.period = sensor.period self.resolution = sensor.resolution self.sensitivity = sensor.sensitivity self.mux_address_raw = sensor.multiplexer_address self.mux_bus = sensor.multiplexer_bus self.mux_chan = sensor.multiplexer_channel self.adc_chan = sensor.adc_channel self.adc_gain = sensor.adc_gain self.adc_resolution = sensor.adc_resolution self.adc_measure = sensor.adc_measure self.adc_measure_units = sensor.adc_measure_units self.adc_volts_min = sensor.adc_volts_min self.adc_volts_max = sensor.adc_volts_max self.adc_units_min = sensor.adc_units_min self.adc_units_max = sensor.adc_units_max self.sht_clock_pin = sensor.sht_clock_pin self.sht_voltage = sensor.sht_voltage # Edge detection self.switch_edge = sensor.switch_edge self.switch_bouncetime = sensor.switch_bouncetime self.switch_reset_period = sensor.switch_reset_period # Relay that will activate prior to sensor read self.pre_relay_id = sensor.pre_relay_id self.pre_relay_duration = sensor.pre_relay_duration self.pre_relay_setup = False self.next_measurement = time.time() self.get_new_measurement = False self.trigger_cond = False self.measurement_acquired = False self.pre_relay_activated = False self.pre_relay_timer = time.time() relay = db_retrieve_table_daemon(Relay, entry='all') for each_relay in relay: # Check if relay ID actually exists if each_relay.id == self.pre_relay_id and self.pre_relay_duration: self.pre_relay_setup = True smtp = db_retrieve_table_daemon(SMTP, entry='first') self.smtp_max_count = smtp.hourly_max self.email_count = 0 self.allowed_to_send_notice = True # Convert string I2C address to base-16 int if self.device in LIST_DEVICES_I2C: self.i2c_address = int(str(self.location), 16) # Set up multiplexer if enabled if self.device in LIST_DEVICES_I2C and self.mux_address_raw: self.mux_address_string = self.mux_address_raw self.mux_address = int(str(self.mux_address_raw), 16) self.mux_lock = "/var/lock/mycodo_multiplexer_0x{i2c:02X}.pid".format( i2c=self.mux_address) self.multiplexer = TCA9548A(self.mux_bus, self.mux_address) else: self.multiplexer = None if self.device in ['ADS1x15', 'MCP342x'] and self.location: self.adc_lock_file = "/var/lock/mycodo_adc_bus{bus}_0x{i2c:02X}.pid".format( bus=self.i2c_bus, i2c=self.i2c_address) # Set up edge detection of a GPIO pin if self.device == 'EDGE': if self.switch_edge == 'rising': self.switch_edge_gpio = GPIO.RISING elif self.switch_edge == 'falling': self.switch_edge_gpio = GPIO.FALLING else: self.switch_edge_gpio = GPIO.BOTH # Set up analog-to-digital converter elif self.device == 'ADS1x15': self.adc = ADS1x15Read(self.i2c_address, self.i2c_bus, self.adc_chan, self.adc_gain) elif self.device == 'MCP342x': self.adc = MCP342xRead(self.i2c_address, self.i2c_bus, self.adc_chan, self.adc_gain, self.adc_resolution) else: self.adc = None self.device_recognized = True # Set up sensors or devices if self.device in ['EDGE', 'ADS1x15', 'MCP342x']: self.measure_sensor = None elif self.device == 'MYCODO_RAM': self.measure_sensor = MycodoRam() elif self.device == 'RPiCPULoad': self.measure_sensor = RaspberryPiCPULoad() elif self.device == 'RPi': self.measure_sensor = RaspberryPiCPUTemp() elif self.device == 'RPiFreeSpace': self.measure_sensor = RaspberryPiFreeSpace(self.location) elif self.device == 'AM2302': self.measure_sensor = DHT22Sensor(self.sensor_id, int(self.location)) elif self.device == 'AM2315': self.measure_sensor = AM2315Sensor(self.sensor_id, self.i2c_bus, power=self.power_relay_id) elif self.device == 'ATLAS_PH_I2C': self.measure_sensor = AtlaspHSensor(self.interface, i2c_address=self.i2c_address, i2c_bus=self.i2c_bus, sensor_sel=self.sensor_sel) elif self.device == 'ATLAS_PH_UART': self.measure_sensor = AtlaspHSensor(self.interface, device_loc=self.device_loc, baud_rate=self.baud_rate, sensor_sel=self.sensor_sel) elif self.device == 'ATLAS_PT1000_I2C': self.measure_sensor = AtlasPT1000Sensor( self.interface, i2c_address=self.i2c_address, i2c_bus=self.i2c_bus) elif self.device == 'ATLAS_PT1000_UART': self.measure_sensor = AtlasPT1000Sensor(self.interface, device_loc=self.device_loc, baud_rate=self.baud_rate) elif self.device == 'BH1750': self.measure_sensor = BH1750Sensor(self.i2c_address, self.i2c_bus, self.resolution, self.sensitivity) elif self.device == 'BME280': self.measure_sensor = BME280Sensor(self.i2c_address, self.i2c_bus) # TODO: BMP is an old designation and will be removed in the future elif self.device in ['BMP', 'BMP180']: self.measure_sensor = BMP180Sensor(self.i2c_bus) elif self.device == 'BMP280': self.measure_sensor = BMP280Sensor(self.i2c_address, self.i2c_bus) elif self.device == 'CHIRP': self.measure_sensor = ChirpSensor(self.i2c_address, self.i2c_bus) elif self.device == 'DS18B20': self.measure_sensor = DS18B20Sensor(self.location) elif self.device == 'DHT11': self.measure_sensor = DHT11Sensor(self.sensor_id, int(self.location), power=self.power_relay_id) elif self.device == 'DHT22': self.measure_sensor = DHT22Sensor(self.sensor_id, int(self.location), power=self.power_relay_id) elif self.device == 'HTU21D': self.measure_sensor = HTU21DSensor(self.i2c_bus) elif self.device == 'K30_UART': self.measure_sensor = K30Sensor(self.device_loc, baud_rate=self.baud_rate) elif self.device == 'MH_Z16_I2C': self.measure_sensor = MHZ16Sensor(self.interface, i2c_address=self.i2c_address, i2c_bus=self.i2c_bus) elif self.device == 'MH_Z16_UART': self.measure_sensor = MHZ16Sensor(self.interface, device_loc=self.device_loc, baud_rate=self.baud_rate) elif self.device == 'MH_Z19_UART': self.measure_sensor = MHZ19Sensor(self.device_loc, baud_rate=self.baud_rate) elif self.device == 'SHT1x_7x': self.measure_sensor = SHT1x7xSensor(int(self.location), self.sht_clock_pin, self.sht_voltage) elif self.device == 'SHT2x': self.measure_sensor = SHT2xSensor(self.i2c_address, self.i2c_bus) elif self.device == 'TMP006': self.measure_sensor = TMP006Sensor(self.i2c_address, self.i2c_bus) elif self.device == 'TSL2561': self.measure_sensor = TSL2561Sensor(self.i2c_address, self.i2c_bus) elif self.device == 'TSL2591': self.measure_sensor = TSL2591Sensor(self.i2c_address, self.i2c_bus) else: self.device_recognized = False self.logger.debug( "Device '{device}' not recognized".format(device=self.device)) raise Exception("'{device}' is not a valid device type.".format( device=self.device)) self.edge_reset_timer = time.time() self.sensor_timer = time.time() self.running = False self.lastUpdate = None
class PIDController(threading.Thread): """ Class to operate discrete PID controller """ def __init__(self, ready, pid_id): threading.Thread.__init__(self) self.logger = logging.getLogger("mycodo.pid_{id}".format(id=pid_id)) self.running = False self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.pid_id = pid_id self.pid_unique_id = db_retrieve_table_daemon( PID, device_id=self.pid_id).unique_id self.control = DaemonControl() self.control_variable = 0.0 self.derivator = 0.0 self.integrator = 0.0 self.error = 0.0 self.P_value = None self.I_value = None self.D_value = None self.set_point = 0.0 self.lower_seconds_on = 0.0 self.raise_seconds_on = 0.0 self.lower_duty_cycle = 0.0 self.raise_duty_cycle = 0.0 self.last_time = None self.last_measurement = None self.last_measurement_success = False self.is_activated = None self.is_held = None self.is_paused = None self.measurement = None self.method_id = None self.direction = None self.raise_output_id = None self.raise_min_duration = None self.raise_max_duration = None self.raise_min_off_duration = None self.lower_output_id = None self.lower_min_duration = None self.lower_max_duration = None self.lower_min_off_duration = None self.Kp = None self.Ki = None self.Kd = None self.integrator_min = None self.integrator_max = None self.period = None self.max_measure_age = None self.default_set_point = None self.set_point = None self.input_unique_id = None self.input_duration = None self.raise_output_type = None self.lower_output_type = None self.initialize_values() self.timer = t.time() + self.period # Check if a method is set for this PID self.method_start_act = None if self.method_id: method = db_retrieve_table_daemon(Method, device_id=self.method_id) method_data = db_retrieve_table_daemon(MethodData) method_data = method_data.filter( MethodData.method_id == self.method_id) method_data_repeat = method_data.filter( MethodData.duration_sec == 0).first() pid = db_retrieve_table_daemon(PID, device_id=self.pid_id) self.method_type = method.method_type self.method_start_act = pid.method_start_time self.method_start_time = None self.method_end_time = None if self.method_type == 'Duration': if self.method_start_act == 'Ended': # Method has ended and hasn't been instructed to begin again pass elif self.method_start_act == 'Ready' or self.method_start_act is None: # Method has been instructed to begin now = datetime.datetime.now() self.method_start_time = now if method_data_repeat and method_data_repeat.duration_end: self.method_end_time = now + datetime.timedelta( seconds=float(method_data_repeat.duration_end)) with session_scope(MYCODO_DB_PATH) as db_session: mod_pid = db_session.query(PID) mod_pid = mod_pid.filter(PID.id == self.pid_id).first() mod_pid.method_start_time = self.method_start_time mod_pid.method_end_time = self.method_end_time db_session.commit() else: # Method neither instructed to begin or not to # Likely there was a daemon restart ot power failure # Resume method with saved start_time self.method_start_time = datetime.datetime.strptime( str(pid.method_start_time), '%Y-%m-%d %H:%M:%S.%f') if method_data_repeat and method_data_repeat.duration_end: self.method_end_time = datetime.datetime.strptime( str(pid.method_end_time), '%Y-%m-%d %H:%M:%S.%f') if self.method_end_time > datetime.datetime.now(): self.logger.warning( "Resuming method {id}: started {start}, " "ends {end}".format( id=self.method_id, start=self.method_start_time, end=self.method_end_time)) else: self.method_start_act = 'Ended' else: self.method_start_act = 'Ended' def run(self): try: self.running = True self.logger.info("Activated in {:.1f} ms".format( (timeit.default_timer() - self.thread_startup_timer) * 1000)) if self.is_paused: self.logger.info("Paused") elif self.is_held: self.logger.info("Held") self.ready.set() while self.running: if (self.method_start_act == 'Ended' and self.method_type == 'Duration'): self.stop_controller(ended_normally=False, deactivate_pid=True) self.logger.warning( "Method has ended. " "Activate the PID controller to start it again.") elif t.time() > self.timer: # Ensure the timer ends in the future while t.time() > self.timer: self.timer = self.timer + self.period # If PID is active, retrieve input measurement and update PID output if self.is_activated and not self.is_paused: self.get_last_measurement() if self.last_measurement_success: # Update setpoint using a method if one is selected if self.method_id: this_controller = db_retrieve_table_daemon( PID, device_id=self.pid_id) setpoint, ended = calculate_method_setpoint( self.method_id, PID, this_controller, Method, MethodData, self.logger) if ended: self.method_start_act = 'Ended' if setpoint is not None: self.set_point = setpoint else: self.set_point = self.default_set_point write_setpoint_db = threading.Thread( target=write_influxdb_value, args=( self.pid_unique_id, 'setpoint', self.set_point, )) write_setpoint_db.start() # Update PID and get control variable self.control_variable = self.update_pid_output( self.last_measurement) # If PID is active or on hold, activate outputs if ((self.is_activated and not self.is_paused) or (self.is_activated and self.is_held)): self.manipulate_output() t.sleep(0.1) # Turn off output used in PID when the controller is deactivated if self.raise_output_id and self.direction in ['raise', 'both']: self.control.relay_off(self.raise_output_id, trigger_conditionals=True) if self.lower_output_id and self.direction in ['lower', 'both']: self.control.relay_off(self.lower_output_id, trigger_conditionals=True) self.running = False self.logger.info("Deactivated in {:.1f} ms".format( (timeit.default_timer() - self.thread_shutdown_timer) * 1000)) except Exception as except_msg: self.logger.exception("Run Error: {err}".format(err=except_msg)) def initialize_values(self): """Set PID parameters""" pid = db_retrieve_table_daemon(PID, device_id=self.pid_id) self.is_activated = pid.is_activated self.is_held = pid.is_held self.is_paused = pid.is_paused self.method_id = pid.method_id self.direction = pid.direction self.raise_output_id = pid.raise_relay_id self.raise_min_duration = pid.raise_min_duration self.raise_max_duration = pid.raise_max_duration self.raise_min_off_duration = pid.raise_min_off_duration self.lower_output_id = pid.lower_relay_id self.lower_min_duration = pid.lower_min_duration self.lower_max_duration = pid.lower_max_duration self.lower_min_off_duration = pid.lower_min_off_duration self.Kp = pid.p self.Ki = pid.i self.Kd = pid.d self.integrator_min = pid.integrator_min self.integrator_max = pid.integrator_max self.period = pid.period self.max_measure_age = pid.max_measure_age self.default_set_point = pid.setpoint self.set_point = pid.setpoint input_unique_id = pid.measurement.split(',')[0] self.measurement = pid.measurement.split(',')[1] input_dev = db_retrieve_table_daemon(Input, unique_id=input_unique_id) self.input_unique_id = input_dev.unique_id self.input_duration = input_dev.period try: self.raise_output_type = db_retrieve_table_daemon( Output, device_id=self.raise_output_id).relay_type except AttributeError: self.raise_output_type = None try: self.lower_output_type = db_retrieve_table_daemon( Output, device_id=self.lower_output_id).relay_type except AttributeError: self.lower_output_type = None return "success" def update_pid_output(self, current_value): """ Calculate PID output value from reference input and feedback :return: Manipulated, or control, variable. This is the PID output. :rtype: float :param current_value: The input, or process, variable (the actual measured condition by the input) :type current_value: float """ self.error = self.set_point - current_value # Calculate P-value self.P_value = self.Kp * self.error # Calculate I-value self.integrator += self.error # First method for managing integrator if self.integrator > self.integrator_max: self.integrator = self.integrator_max elif self.integrator < self.integrator_min: self.integrator = self.integrator_min # Second method for regulating integrator # if self.period is not None: # if self.integrator * self.Ki > self.period: # self.integrator = self.period / self.Ki # elif self.integrator * self.Ki < -self.period: # self.integrator = -self.period / self.Ki self.I_value = self.integrator * self.Ki # Calculate D-value self.D_value = self.Kd * (self.error - self.derivator) self.derivator = self.error # Produce output form P, I, and D values pid_value = self.P_value + self.I_value + self.D_value return pid_value def get_last_measurement(self): """ Retrieve the latest input measurement from InfluxDB :rtype: None """ self.last_measurement_success = False # Get latest measurement (from within the past minute) from influxdb try: if self.input_duration < 60: duration = 60 else: duration = int(self.input_duration * 1.5) self.last_measurement = read_last_influxdb(self.input_unique_id, self.measurement, duration) if self.last_measurement: self.last_time = self.last_measurement[0] self.last_measurement = self.last_measurement[1] utc_dt = datetime.datetime.strptime( self.last_time.split(".")[0], '%Y-%m-%dT%H:%M:%S') utc_timestamp = calendar.timegm(utc_dt.timetuple()) local_timestamp = str( datetime.datetime.fromtimestamp(utc_timestamp)) self.logger.debug("Latest {meas}: {last} @ {ts}".format( meas=self.measurement, last=self.last_measurement, ts=local_timestamp)) if calendar.timegm( t.gmtime()) - utc_timestamp > self.max_measure_age: self.logger.error( "Last measurement was {last_sec} seconds ago, however" " the maximum measurement age is set to {max_sec}" " seconds.".format( last_sec=calendar.timegm(t.gmtime()) - utc_timestamp, max_sec=self.max_measure_age)) self.last_measurement_success = True else: self.logger.warning("No data returned from influxdb") except requests.ConnectionError: self.logger.error("Failed to read measurement from the " "influxdb database: Could not connect.") except Exception as except_msg: self.logger.exception( "Exception while reading measurement from the influxdb " "database: {err}".format(err=except_msg)) def manipulate_output(self): """ Activate output based on PID control variable and whether the manipulation directive is to raise, lower, or both. :rtype: None """ # If the last measurement was able to be retrieved and was entered within the past minute if self.last_measurement_success: # # PID control variable is positive, indicating a desire to raise # the environmental condition # if self.direction in ['raise', 'both'] and self.raise_output_id: if self.control_variable > 0: # Turn off lower_output if active, because we're now raising if (self.direction == 'both' and self.lower_output_id and self.control.relay_state( self.lower_output_id) != 'off'): self.control.relay_off(self.lower_output_id) # Determine if the output should be PWM or a duration if self.raise_output_type == 'pwm': self.raise_duty_cycle = float("{0:.1f}".format( self.control_var_to_duty_cycle( self.control_variable))) # Ensure the duty cycle doesn't exceed the min/max if (self.raise_max_duration and self.raise_duty_cycle > self.raise_max_duration): self.raise_duty_cycle = self.raise_max_duration elif (self.raise_min_duration and self.raise_duty_cycle < self.raise_min_duration): self.raise_duty_cycle = self.raise_min_duration self.logger.debug( "Setpoint: {sp}, Control Variable: {cv}, Output: PWM output " "{id} to {dc:.1f}%".format( sp=self.set_point, cv=self.control_variable, id=self.raise_output_id, dc=self.raise_duty_cycle)) # Activate pwm with calculated duty cycle self.control.relay_on(self.raise_output_id, duty_cycle=self.raise_duty_cycle) pid_entry_value = self.control_var_to_duty_cycle( abs(self.control_variable)) if self.control_variable < 0: pid_entry_value = -pid_entry_value self.write_pid_output_influxdb('duty_cycle', pid_entry_value) elif self.raise_output_type in [ 'command', 'wired', 'wireless_433MHz_pi_switch' ]: # Ensure the output on duration doesn't exceed the set maximum if (self.raise_max_duration and self.control_variable > self.raise_max_duration): self.raise_seconds_on = self.raise_max_duration else: self.raise_seconds_on = float("{0:.2f}".format( self.control_variable)) if self.raise_seconds_on > self.raise_min_duration: # Activate raise_output for a duration self.logger.debug( "Setpoint: {sp} Output: {cv} to output " "{id}".format(sp=self.set_point, cv=self.control_variable, id=self.raise_output_id)) self.control.relay_on( self.raise_output_id, duration=self.raise_seconds_on, min_off=self.raise_min_off_duration) self.write_pid_output_influxdb('pid_output', self.control_variable) else: if self.raise_output_type == 'pwm': self.control.relay_on(self.raise_output_id, duty_cycle=0) else: self.control.relay_off(self.raise_output_id) # # PID control variable is negative, indicating a desire to lower # the environmental condition # if self.direction in ['lower', 'both'] and self.lower_output_id: if self.control_variable < 0: # Turn off raise_output if active, because we're now raising if (self.direction == 'both' and self.raise_output_id and self.control.relay_state( self.raise_output_id) != 'off'): self.control.relay_off(self.raise_output_id) # Determine if the output should be PWM or a duration if self.lower_output_type == 'pwm': self.lower_duty_cycle = float("{0:.1f}".format( self.control_var_to_duty_cycle( abs(self.control_variable)))) # Ensure the duty cycle doesn't exceed the min/max if (self.lower_max_duration and self.lower_duty_cycle > self.lower_max_duration): self.lower_duty_cycle = self.lower_max_duration elif (self.lower_min_duration and self.lower_duty_cycle < self.lower_min_duration): self.lower_duty_cycle = self.lower_min_duration self.logger.debug( "Setpoint: {sp}, Control Variable: {cv}, " "Output: PWM output {id} to {dc:.1f}%".format( sp=self.set_point, cv=self.control_variable, id=self.lower_output_id, dc=self.lower_duty_cycle)) # Turn back negative for proper logging self.lower_duty_cycle = -self.lower_duty_cycle # Activate pwm with calculated duty cycle self.control.relay_on(self.lower_output_id, duty_cycle=self.lower_duty_cycle) pid_entry_value = self.control_var_to_duty_cycle( abs(self.control_variable)) pid_entry_value = -pid_entry_value self.write_pid_output_influxdb('duty_cycle', pid_entry_value) elif self.lower_output_type in [ 'command', 'wired', 'wireless_433MHz_pi_switch' ]: # Ensure the output on duration doesn't exceed the set maximum if (self.lower_max_duration and abs(self.control_variable) > self.lower_max_duration): self.lower_seconds_on = self.lower_max_duration else: self.lower_seconds_on = float("{0:.2f}".format( self.control_variable)) if abs(self.lower_seconds_on ) > self.lower_min_duration: # Activate lower_output for a duration self.logger.debug("Setpoint: {sp} Output: {cv} to " "output {id}".format( sp=self.set_point, cv=self.control_variable, id=self.lower_output_id)) self.control.relay_on( self.lower_output_id, duration=self.lower_seconds_on, min_off=self.lower_min_off_duration) self.write_pid_output_influxdb('pid_output', self.control_variable) else: if self.lower_output_type == 'pwm': self.control.relay_on(self.lower_output_id, duty_cycle=0) else: self.control.relay_off(self.lower_output_id) else: if self.direction in ['raise', 'both'] and self.raise_output_id: self.control.relay_off(self.raise_output_id) if self.direction in ['lower', 'both'] and self.lower_output_id: self.control.relay_off(self.lower_output_id) def control_var_to_duty_cycle(self, control_variable): # Convert control variable to duty cycle if control_variable > self.period: return 100.0 else: return float((control_variable / self.period) * 100) def write_pid_output_influxdb(self, pid_entry_type, pid_entry_value): write_pid_out_db = threading.Thread(target=write_influxdb_value, args=( self.pid_unique_id, pid_entry_type, pid_entry_value, )) write_pid_out_db.start() def pid_mod(self): if self.initialize_values(): return "success" else: return "error" def pid_hold(self): self.is_held = True self.logger.info("Hold") return "success" def pid_pause(self): self.is_paused = True self.logger.info("Pause") return "success" def pid_resume(self): self.is_activated = True self.is_held = False self.is_paused = False self.logger.info("Resume") return "success" def set_setpoint(self, set_point): """ Initilize the setpoint of PID """ self.set_point = set_point self.integrator = 0 self.derivator = 0 def set_integrator(self, integrator): """ Set the integrator of the controller """ self.integrator = integrator def set_derivator(self, derivator): """ Set the derivator of the controller """ self.derivator = derivator def set_kp(self, p): """ Set Kp gain of the controller """ self.Kp = p def set_ki(self, i): """ Set Ki gain of the controller """ self.Ki = i def set_kd(self, d): """ Set Kd gain of the controller """ self.Kd = d def get_setpoint(self): return self.set_point def get_error(self): return self.error def get_integrator(self): return self.integrator def get_derivator(self): return self.derivator def is_running(self): return self.running def stop_controller(self, ended_normally=True, deactivate_pid=False): self.thread_shutdown_timer = timeit.default_timer() self.running = False # Unset method start time if self.method_id and ended_normally: with session_scope(MYCODO_DB_PATH) as db_session: mod_pid = db_session.query(PID).filter( PID.id == self.pid_id).first() mod_pid.method_start_time = 'Ended' mod_pid.method_end_time = None db_session.commit() if deactivate_pid: with session_scope(MYCODO_DB_PATH) as db_session: mod_pid = db_session.query(PID).filter( PID.id == self.pid_id).first() mod_pid.is_activated = False db_session.commit()
def __init__(self, ready, sensor_id): threading.Thread.__init__(self) self.logger = logging.getLogger( "mycodo.sensor_{id}".format(id=sensor_id)) list_devices_i2c = [ 'ADS1x15', 'AM2315', 'ATLAS_PT1000', 'BME280', 'BMP', 'CHIRP', 'HTU21D', 'MCP342x', 'SHT2x', 'TMP006', 'TSL2561' ] self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.lock = {} self.measurement = None self.updateSuccess = False self.sensor_id = sensor_id self.control = DaemonControl() self.pause_loop = False self.verify_pause_loop = True self.cond_id = {} self.cond_name = {} self.cond_activated = {} self.cond_period = {} self.cond_measurement_type = {} self.cond_edge_select = {} self.cond_edge_detected = {} self.cond_gpio_state = {} self.cond_direction = {} self.cond_setpoint = {} self.cond_relay_id = {} self.cond_relay_state = {} self.cond_relay_on_duration = {} self.cond_execute_command = {} self.cond_email_notify = {} self.cond_flash_lcd = {} self.cond_camera_record = {} self.cond_timer = {} self.smtp_wait_timer = {} self.setup_sensor_conditionals() sensor = db_retrieve_table(MYCODO_DB_PATH, Sensor, device_id=self.sensor_id) self.i2c_bus = sensor.i2c_bus self.location = sensor.location self.device = sensor.device self.sensor_type = sensor.device_type self.period = sensor.period self.multiplexer_address_raw = sensor.multiplexer_address self.multiplexer_bus = sensor.multiplexer_bus self.multiplexer_channel = sensor.multiplexer_channel self.adc_channel = sensor.adc_channel self.adc_gain = sensor.adc_gain self.adc_resolution = sensor.adc_resolution self.adc_measure = sensor.adc_measure self.adc_measure_units = sensor.adc_measure_units self.adc_volts_min = sensor.adc_volts_min self.adc_volts_max = sensor.adc_volts_max self.adc_units_min = sensor.adc_units_min self.adc_units_max = sensor.adc_units_max self.sht_clock_pin = sensor.sht_clock_pin self.sht_voltage = sensor.sht_voltage # Edge detection self.switch_edge = sensor.switch_edge self.switch_bouncetime = sensor.switch_bouncetime self.switch_reset_period = sensor.switch_reset_period # Relay that will activate prior to sensor read self.pre_relay_id = sensor.pre_relay_id self.pre_relay_duration = sensor.pre_relay_duration self.pre_relay_setup = False self.next_measurement = time.time() self.get_new_measurement = False self.measurement_acquired = False self.pre_relay_activated = False self.pre_relay_timer = time.time() relay = db_retrieve_table(MYCODO_DB_PATH, Relay, entry='all') for each_relay in relay: # Check if relay ID actually exists if each_relay.id == self.pre_relay_id and self.pre_relay_duration: self.pre_relay_setup = True smtp = db_retrieve_table(MYCODO_DB_PATH, SMTP, entry='first') self.smtp_max_count = smtp.hourly_max self.email_count = 0 self.allowed_to_send_notice = True # Convert string I2C address to base-16 int if self.device in list_devices_i2c: self.i2c_address = int(str(self.location), 16) # Set up multiplexer if enabled if self.device in list_devices_i2c and self.multiplexer_address_raw: self.multiplexer_address_string = self.multiplexer_address_raw self.multiplexer_address = int(str(self.multiplexer_address_raw), 16) self.multiplexer_lock_file = "/var/lock/mycodo_multiplexer_0x{:02X}.pid".format( self.multiplexer_address) self.multiplexer = TCA9548A(self.multiplexer_bus, self.multiplexer_address) else: self.multiplexer = None if self.device in ['ADS1x15', 'MCP342x'] and self.location: self.adc_lock_file = "/var/lock/mycodo_adc_bus{}_0x{:02X}.pid".format( self.i2c_bus, self.i2c_address) # Set up edge detection of a GPIO pin if self.device == 'EDGE': if self.switch_edge == 'rising': self.switch_edge_gpio = GPIO.RISING elif self.switch_edge == 'falling': self.switch_edge_gpio = GPIO.FALLING else: self.switch_edge_gpio = GPIO.BOTH # Set up analog-to-digital converter elif self.device == 'ADS1x15': self.adc = ADS1x15Read(self.i2c_address, self.i2c_bus, self.adc_channel, self.adc_gain) elif self.device == 'MCP342x': self.adc = MCP342xRead(self.i2c_address, self.i2c_bus, self.adc_channel, self.adc_gain, self.adc_resolution) else: self.adc = None self.device_recognized = True # Set up sensor if self.device in ['EDGE', 'ADS1x15', 'MCP342x']: self.measure_sensor = None elif self.device == 'RPiCPULoad': self.measure_sensor = RaspberryPiCPULoad() elif self.device == 'RPi': self.measure_sensor = RaspberryPiCPUTemp() elif self.device == 'CHIRP': self.measure_sensor = ChirpSensor(self.i2c_address, self.i2c_bus) elif self.device == 'DS18B20': self.measure_sensor = DS18B20Sensor(self.location) elif self.device == 'DHT11': self.measure_sensor = DHT11Sensor(self.sensor_id, int(self.location)) elif self.device in ['DHT22', 'AM2302']: self.measure_sensor = DHT22Sensor(self.sensor_id, int(self.location)) elif self.device == 'HTU21D': self.measure_sensor = HTU21DSensor(self.i2c_bus) elif self.device == 'AM2315': self.measure_sensor = AM2315Sensor(self.i2c_bus) elif self.device == 'ATLAS_PT1000': self.measure_sensor = AtlasPT1000Sensor(self.i2c_address, self.i2c_bus) elif self.device == 'K30': self.measure_sensor = K30Sensor() elif self.device == 'BME280': self.measure_sensor = BME280Sensor(self.i2c_address, self.i2c_bus) elif self.device == 'BMP': self.measure_sensor = BMPSensor(self.i2c_bus) elif self.device == 'SHT1x_7x': self.measure_sensor = SHT1x7xSensor(self.location, self.sht_clock_pin, self.sht_voltage) elif self.device == 'SHT2x': self.measure_sensor = SHT2xSensor(self.i2c_address, self.i2c_bus) elif self.device == 'TMP006': self.measure_sensor = TMP006Sensor(self.i2c_address, self.i2c_bus) elif self.device == 'TSL2561': self.measure_sensor = TSL2561Sensor(self.i2c_address, self.i2c_bus) else: self.device_recognized = False self.logger.debug( "Device '{device}' not recognized".format(device=self.device)) raise Exception("{device} is not a valid device type.".format( device=self.device)) self.edge_reset_timer = time.time() self.sensor_timer = time.time() self.running = False self.lastUpdate = None
class TimerController(threading.Thread): """ class for controlling timers """ def __init__(self, ready, timer_id): threading.Thread.__init__(self) self.logger = logging.getLogger( "mycodo.timer_{id}".format(id=timer_id)) self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.timer_id = timer_id self.control = DaemonControl() timer = db_retrieve_table_daemon(Timer, device_id=self.timer_id) self.timer_type = timer.timer_type self.output_unique_id = timer.relay_id self.method_id = timer.method_id self.method_period = timer.method_period self.state = timer.state self.time_start = timer.time_start self.time_end = timer.time_end self.duration_on = timer.duration_on self.duration_off = timer.duration_off self.output_id = db_retrieve_table_daemon( Output, unique_id=self.output_unique_id).id # Time of day split into hour and minute if self.time_start: time_split = self.time_start.split(":") self.start_hour = time_split[0] self.start_minute = time_split[1] else: self.start_hour = None self.start_minute = None if self.time_end: time_split = self.time_end.split(":") self.end_hour = time_split[0] self.end_minute = time_split[1] else: self.end_hour = None self.end_minute = None self.duration_timer = time.time() self.pwm_method_timer = time.time() self.date_timer_not_executed = True self.running = False if self.method_id: method = db_retrieve_table_daemon(Method, device_id=self.method_id) method_data = db_retrieve_table_daemon(MethodData) method_data = method_data.filter( MethodData.method_id == self.method_id) method_data_repeat = method_data.filter( MethodData.duration_sec == 0).first() self.method_type = method.method_type self.method_start_act = timer.method_start_time self.method_start_time = None self.method_end_time = None if self.method_type == 'Duration': if self.method_start_act == 'Ended': self.stop_controller(ended_normally=False, deactivate_timer=True) self.logger.warning( "Method has ended. " "Activate the Timer controller to start it again.") elif self.method_start_act == 'Ready' or self.method_start_act is None: # Method has been instructed to begin now = datetime.datetime.now() self.method_start_time = now if method_data_repeat and method_data_repeat.duration_end: self.method_end_time = now + datetime.timedelta( seconds=float(method_data_repeat.duration_end)) with session_scope(MYCODO_DB_PATH) as db_session: mod_timer = db_session.query(Timer) mod_timer = mod_timer.filter( Timer.id == self.timer_id).first() mod_timer.method_start_time = self.method_start_time mod_timer.method_end_time = self.method_end_time db_session.commit() else: # Method neither instructed to begin or not to # Likely there was a daemon restart ot power failure # Resume method with saved start_time self.method_start_time = datetime.datetime.strptime( str(timer.method_start_time), '%Y-%m-%d %H:%M:%S.%f') if method_data_repeat and method_data_repeat.duration_end: self.method_end_time = datetime.datetime.strptime( str(timer.method_end_time), '%Y-%m-%d %H:%M:%S.%f') if self.method_end_time > datetime.datetime.now(): self.logger.warning( "Resuming method {id}: started {start}, " "ends {end}".format(id=self.method_id, start=self.method_start_time, end=self.method_end_time)) else: self.method_start_act = 'Ended' else: self.method_start_act = 'Ended' def run(self): self.running = True self.logger.info("Activated in {:.1f} ms".format( (timeit.default_timer() - self.thread_startup_timer) * 1000)) self.ready.set() while self.running: # Timer is set to react at a specific hour and minute of the day if self.timer_type == 'time': if (int(self.start_hour) == datetime.datetime.now().hour and int(self.start_minute) == datetime.datetime.now().minute): # Ensure this is triggered only once at this specific time if self.date_timer_not_executed: self.date_timer_not_executed = False message = "At {st}, turn Output {id} {state}".format( st=self.time_start, id=self.output_id, state=self.state) if self.state == 'on' and self.duration_on: message += " for {sec} seconds".format( sec=self.duration_on) else: self.duration_on = 0 self.logger.debug(message) modulate_output = threading.Thread( target=self.control.output_on_off, args=( self.output_id, self.state, ), kwargs={'duration': self.duration_on}) modulate_output.start() elif not self.date_timer_not_executed: self.date_timer_not_executed = True # Timer is set to react at a specific time duration of the day elif self.timer_type == 'timespan': if time_between_range(self.time_start, self.time_end): current_output_state = self.control.relay_state( self.output_id) if self.state != current_output_state: message = "Output {output} should be {state}, but is " \ "{cstate}. Turning {state}.".format( output=self.output_id, state=self.state, cstate=current_output_state) modulate_output = threading.Thread( target=self.control.output_on_off, args=( self.output_id, self.state, )) modulate_output.start() self.logger.debug(message) # Timer is a simple on/off duration timer elif self.timer_type == 'duration': if time.time() > self.duration_timer: self.duration_timer = (time.time() + self.duration_on + self.duration_off) self.logger.debug("Turn Output {output} on for {onsec} " "seconds, then off for {offsec} " "seconds".format( output=self.output_id, onsec=self.duration_on, offsec=self.duration_off)) output_on = threading.Thread(target=self.control.relay_on, args=( self.output_id, self.duration_on, )) output_on.start() # Timer is a PWM Method timer elif self.timer_type == 'pwm_method': try: if time.time() > self.pwm_method_timer: if self.method_start_act == 'Ended': self.stop_controller(ended_normally=False, deactivate_timer=True) self.logger.info( "Method has ended. " "Activate the Timer controller to start it again." ) else: this_controller = db_retrieve_table_daemon( Timer, device_id=self.timer_id) setpoint, ended = calculate_method_setpoint( self.method_id, Timer, this_controller, Method, MethodData, self.logger) if ended: self.method_start_act = 'Ended' if setpoint > 100: setpoint = 100 elif setpoint < 0: setpoint = 0 self.logger.debug( "Turn Output {output} to a PWM duty cycle of " "{dc:.1f} %".format(output=self.output_id, dc=setpoint)) # Activate pwm with calculated duty cycle self.control.relay_on(self.output_id, duty_cycle=setpoint) self.pwm_method_timer = time.time( ) + self.method_period except Exception: self.logger.exception(1) time.sleep(0.1) self.control.relay_off(self.output_id) self.running = False self.logger.info("Deactivated in {:.1f} ms".format( (timeit.default_timer() - self.thread_shutdown_timer) * 1000)) def is_running(self): return self.running def stop_controller(self, ended_normally=True, deactivate_timer=False): self.thread_shutdown_timer = timeit.default_timer() self.running = False # Unset method start time if self.method_id and ended_normally: with session_scope(MYCODO_DB_PATH) as db_session: mod_timer = db_session.query(Timer).filter( Timer.id == self.timer_id).first() mod_timer.method_start_time = 'Ended' mod_timer.method_end_time = None db_session.commit() if deactivate_timer: with session_scope(MYCODO_DB_PATH) as db_session: mod_timer = db_session.query(Timer).filter( Timer.id == self.timer_id).first() mod_timer.is_activated = False db_session.commit()
def __init__(self, ready, timer_id): threading.Thread.__init__(self) self.logger = logging.getLogger( "mycodo.timer_{id}".format(id=timer_id)) self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.timer_id = timer_id self.control = DaemonControl() timer = db_retrieve_table_daemon(Timer, device_id=self.timer_id) self.timer_type = timer.timer_type self.output_unique_id = timer.relay_id self.method_id = timer.method_id self.method_period = timer.method_period self.state = timer.state self.time_start = timer.time_start self.time_end = timer.time_end self.duration_on = timer.duration_on self.duration_off = timer.duration_off self.output_id = db_retrieve_table_daemon( Output, unique_id=self.output_unique_id).id # Time of day split into hour and minute if self.time_start: time_split = self.time_start.split(":") self.start_hour = time_split[0] self.start_minute = time_split[1] else: self.start_hour = None self.start_minute = None if self.time_end: time_split = self.time_end.split(":") self.end_hour = time_split[0] self.end_minute = time_split[1] else: self.end_hour = None self.end_minute = None self.duration_timer = time.time() self.pwm_method_timer = time.time() self.date_timer_not_executed = True self.running = False if self.method_id: method = db_retrieve_table_daemon(Method, device_id=self.method_id) method_data = db_retrieve_table_daemon(MethodData) method_data = method_data.filter( MethodData.method_id == self.method_id) method_data_repeat = method_data.filter( MethodData.duration_sec == 0).first() self.method_type = method.method_type self.method_start_act = timer.method_start_time self.method_start_time = None self.method_end_time = None if self.method_type == 'Duration': if self.method_start_act == 'Ended': self.stop_controller(ended_normally=False, deactivate_timer=True) self.logger.warning( "Method has ended. " "Activate the Timer controller to start it again.") elif self.method_start_act == 'Ready' or self.method_start_act is None: # Method has been instructed to begin now = datetime.datetime.now() self.method_start_time = now if method_data_repeat and method_data_repeat.duration_end: self.method_end_time = now + datetime.timedelta( seconds=float(method_data_repeat.duration_end)) with session_scope(MYCODO_DB_PATH) as db_session: mod_timer = db_session.query(Timer) mod_timer = mod_timer.filter( Timer.id == self.timer_id).first() mod_timer.method_start_time = self.method_start_time mod_timer.method_end_time = self.method_end_time db_session.commit() else: # Method neither instructed to begin or not to # Likely there was a daemon restart ot power failure # Resume method with saved start_time self.method_start_time = datetime.datetime.strptime( str(timer.method_start_time), '%Y-%m-%d %H:%M:%S.%f') if method_data_repeat and method_data_repeat.duration_end: self.method_end_time = datetime.datetime.strptime( str(timer.method_end_time), '%Y-%m-%d %H:%M:%S.%f') if self.method_end_time > datetime.datetime.now(): self.logger.warning( "Resuming method {id}: started {start}, " "ends {end}".format(id=self.method_id, start=self.method_start_time, end=self.method_end_time)) else: self.method_start_act = 'Ended' else: self.method_start_act = 'Ended'
class TimerController(threading.Thread): """ class for controlling timers """ def __init__(self, ready, timer_id): threading.Thread.__init__(self) self.logger = logging.getLogger( "mycodo.timer_{id}".format(id=timer_id)) self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.timer_id = timer_id self.control = DaemonControl() timer = db_retrieve_table_daemon(Timer, device_id=self.timer_id) self.timer_type = timer.timer_type self.relay_unique_id = timer.relay_id self.state = timer.state self.time_start = timer.time_start self.time_end = timer.time_end self.duration_on = timer.duration_on self.duration_off = timer.duration_off self.relay_id = db_retrieve_table_daemon( Relay, unique_id=self.relay_unique_id).id # Time of day split into hour and minute if self.time_start: time_split = self.time_start.split(":") self.start_hour = time_split[0] self.start_minute = time_split[1] else: self.start_hour = None self.start_minute = None if self.time_end: time_split = self.time_end.split(":") self.end_hour = time_split[0] self.end_minute = time_split[1] else: self.end_hour = None self.end_minute = None self.duration_timer = time.time() self.date_timer_not_executed = True self.running = False def run(self): self.running = True self.logger.info("Activated in {:.1f} ms".format( (timeit.default_timer() - self.thread_startup_timer) * 1000)) self.ready.set() while self.running: # Timer is set to react at a specific hour and minute of the day if self.timer_type == 'time': if (int(self.start_hour) == datetime.datetime.now().hour and int(self.start_minute) == datetime.datetime.now().minute): # Ensure this is triggered only once at this specific time if self.date_timer_not_executed: message = "At {st}, turn relay {id} {state}".format( st=self.time_start, id=self.relay_unique_id, state=self.state) if self.state == 'on' and self.duration_on: message += " for {sec} seconds".format( sec=self.duration_on) else: self.duration_on = 0 self.logger.debug(message) modulate_relay = threading.Thread( target=self.control.relay_on_off, args=( self.relay_id, self.state, ), kwargs={'duration': self.duration_on}) modulate_relay.start() self.date_timer_not_executed = False elif not self.date_timer_not_executed: self.date_timer_not_executed = True # Timer is set to react at a specific time duration of the day elif self.timer_type == 'timespan': if time_between_range(self.time_start, self.time_end): current_relay_state = self.control.relay_state( self.relay_id) if self.state != current_relay_state: message = "Relay {relay} should be {state}, but is " \ "{cstate}. Turning {state}.".format( relay=self.relay_unique_id, state=self.state, cstate=current_relay_state) modulate_relay = threading.Thread( target=self.control.relay_on_off, args=( self.relay_id, self.state, )) modulate_relay.start() self.logger.debug(message) # Timer is a simple on/off duration timer elif self.timer_type == 'duration': if time.time() > self.duration_timer: self.duration_timer = (time.time() + self.duration_on + self.duration_off) self.logger.debug("Turn relay {relay} on for {onsec} " "seconds, then off for {offsec} " "seconds".format( relay=self.relay_unique_id, onsec=self.duration_on, offsec=self.duration_off)) relay_on = threading.Thread(target=self.control.relay_on, args=( self.relay_id, self.duration_on, )) relay_on.start() time.sleep(0.1) self.control.relay_off(self.relay_id) self.running = False self.logger.info("Deactivated in {:.1f} ms".format( (timeit.default_timer() - self.thread_shutdown_timer) * 1000)) def is_running(self): return self.running def stop_controller(self): self.thread_shutdown_timer = timeit.default_timer() self.running = False
class PIDController(threading.Thread): """ Class to operate discrete PID controller """ def __init__(self, ready, logger, pid_id): threading.Thread.__init__(self) self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.logger = logger self.pid_id = pid_id self.control = DaemonControl() with session_scope(MYCODO_DB_PATH) as new_session: pid = new_session.query(PID).filter(PID.id == self.pid_id).first() self.sensor_id = pid.sensor_id self.measure_type = pid.measure_type self.method_id = pid.method_id self.direction = pid.direction self.raise_relay_id = pid.raise_relay_id self.raise_min_duration = pid.raise_min_duration self.raise_max_duration = pid.raise_max_duration self.lower_relay_id = pid.lower_relay_id self.lower_min_duration = pid.lower_min_duration self.lower_max_duration = pid.lower_max_duration self.Kp = pid.p self.Ki = pid.i self.Kd = pid.d self.Integrator_min = pid.integrator_min self.Integrator_max = pid.integrator_max self.measure_interval = pid.period self.default_set_point = pid.setpoint self.set_point = pid.setpoint sensor = new_session.query(Sensor).filter(Sensor.id == self.sensor_id).first() self.sensor_duration = sensor.period self.Derivator = 0 self.Integrator = 0 self.error = 0.0 self.P_value = None self.I_value = None self.D_value = None self.raise_seconds_on = 0 self.timer = t.time()+self.measure_interval # Check if a method is set for this PID if self.method_id: with session_scope(MYCODO_DB_PATH) as new_session: method = new_session.query(Method) method = method.filter(Method.method_id == self.method_id) method = method.filter(Method.method_order == 0).first() self.method_type = method.method_type self.method_start_time = method.start_time if self.method_type == 'Duration': if self.method_start_time == 'Ended': # Method has ended and hasn't been instructed to begin again pass elif self.method_start_time == 'Ready' or self.method_start_time == None: # Method has been instructed to begin with session_scope(MYCODO_DB_PATH) as db_session: mod_method = db_session.query(Method) mod_method = mod_method.filter(Method.method_id == self.method_id) mod_method = mod_method.filter(Method.method_order == 0).first() mod_method.start_time = datetime.datetime.now() self.method_start_time = mod_method.start_time db_session.commit() else: # Method neither instructed to begin or not to # Likely there was a daemon restart ot power failure # Resume method with saved start_time self.method_start_time = datetime.datetime.strptime( self.method_start_time, '%Y-%m-%d %H:%M:%S.%f') self.logger.warning("[PID {}] Resuming method {} started at {}".format( self.pid_id, self.method_id, self.method_start_time)) def run(self): try: self.running = True self.logger.info("[PID {}] Activated in {:.1f} ms".format( self.pid_id, (timeit.default_timer()-self.thread_startup_timer)*1000)) self.ready.set() while (self.running): if t.time() > self.timer: self.timer = self.timer+self.measure_interval self.get_last_measurement() self.manipulate_relays() t.sleep(0.1) if self.raise_relay_id: self.control.relay_off(self.raise_relay_id) if self.lower_relay_id: self.control.relay_off(self.lower_relay_id) self.running = False self.logger.info("[PID {}] Deactivated in {:.1f} ms".format( self.pid_id, (timeit.default_timer()-self.thread_shutdown_timer)*1000)) except Exception as except_msg: self.logger.exception("[PID {}] Error: {}".format(self.pid_id, except_msg)) def update(self, current_value): """ Calculate PID output value from reference input and feedback :return: Manipulated, or control, variable. This is the PID output. :rtype: float :param current_value: The input, or process, variable (the actual measured condition by the sensor) :type current_value: float """ self.error = self.set_point - current_value # Calculate P-value self.P_value = self.Kp * self.error # Calculate I-value self.Integrator += self.error # First method for managing Integrator if self.Integrator > self.Integrator_max: self.Integrator = self.Integrator_max elif self.Integrator < self.Integrator_min: self.Integrator = self.Integrator_min # Second method for regulating Integrator # if self.measure_interval is not None: # if self.Integrator * self.Ki > self.measure_interval: # self.Integrator = self.measure_interval / self.Ki # elif self.Integrator * self.Ki < -self.measure_interval: # self.Integrator = -self.measure_interval / self.Ki self.I_value = self.Integrator * self.Ki # Calculate D-value self.D_value = self.Kd * (self.error - self.Derivator) self.Derivator = self.error # Produce output form P, I, and D values PID = self.P_value + self.I_value + self.D_value return PID def get_last_measurement(self): """ Retrieve the latest sensor measurement from InfluxDB :rtype: None """ self.last_measurement_success = False # Get latest measurement (from within the past minute) from influxdb try: if self.sensor_duration/60 < 1: duration = 1 else: duration = self.sensor_duration/60*1.5 self.last_measurement = read_last_influxdb( INFLUXDB_HOST, INFLUXDB_PORT, INFLUXDB_USER, INFLUXDB_PASSWORD, INFLUXDB_DATABASE, self.sensor_id, self.measure_type, duration) if self.last_measurement: measurement_list = list(self.last_measurement.get_points( measurement=self.measure_type)) self.last_time = measurement_list[0]['time'] self.last_measurement = measurement_list[0]['value'] utc_dt = datetime.datetime.strptime(self.last_time.split(".")[0], '%Y-%m-%dT%H:%M:%S') utc_timestamp = calendar.timegm(utc_dt.timetuple()) local_timestamp = str(datetime.datetime.fromtimestamp(utc_timestamp)) self.logger.debug("[PID {}] Latest {}: {} @ {}".format( self.pid_id, self.measure_type, self.last_measurement, local_timestamp)) self.last_measurement_success = True else: self.logger.warning("[PID {}] No data returned " "from influxdb".format(self.pid_id)) except Exception as except_msg: self.logger.exception("[PID {}] Failed to read " "measurement from the influxdb " "database: {}".format(self.pid_id, except_msg)) def manipulate_relays(self): """ Activate a relay based on PID output (control variable) and whether the manipulation directive is to raise, lower, or both. :rtype: None """ # If there was a measurement able to be retrieved from # influxdb database that was entered within the past minute if self.last_measurement_success: # Update setpoint if a method is selected if self.method_id != '': self.calculate_method_setpoint(self.method_id) self.addSetpointInfluxdb(self.pid_id, self.set_point) # Update PID and get control variable self.control_variable = self.update(self.last_measurement) # # PID control variable positive to raise environmental condition # if self.direction in ['raise', 'both'] and self.raise_relay_id: if self.control_variable > 0: # Ensure the relay on duration doesn't exceed the set maximum if (self.raise_max_duration and self.control_variable > self.raise_max_duration): self.raise_seconds_on = self.raise_max_duration else: self.raise_seconds_on = float("{0:.2f}".format(self.control_variable)) # Turn off lower_relay if active, because we're now raising if self.lower_relay_id: with session_scope(MYCODO_DB_PATH) as new_session: relay = new_session.query(Relay).filter( Relay.id == self.lower_relay_id).first() if relay.is_on(): self.control.relay_off(self.lower_relay_id) if self.raise_seconds_on > self.raise_min_duration: # Activate raise_relay for a duration self.logger.debug("[PID {}] Setpoint: {} " "Output: {} to relay {}".format( self.pid_id, self.set_point, self.control_variable, self.raise_relay_id)) self.control.relay_on(self.raise_relay_id, self.raise_seconds_on) else: self.control.relay_off(self.raise_relay_id) # # PID control variable negative to lower environmental condition # if self.direction in ['lower', 'both'] and self.lower_relay_id: if self.control_variable < 0: # Ensure the relay on duration doesn't exceed the set maximum if (self.lower_max_duration and abs(self.control_variable) > self.lower_max_duration): self.lower_seconds_on = self.lower_max_duration else: self.lower_seconds_on = abs(float("{0:.2f}".format(self.control_variable))) # Turn off raise_relay if active, because we're now lowering if self.raise_relay_id: with session_scope(MYCODO_DB_PATH) as new_session: relay = new_session.query(Relay).filter( Relay.id == self.raise_relay_id).first() if relay.is_on(): self.control.relay_off(self.raise_relay_id) if self.lower_seconds_on > self.lower_min_duration: # Activate lower_relay for a duration self.logger.debug("[PID {}] Setpoint: {} " "Output: {} to relay {}".format( self.pid_id, self.set_point, self.control_variable, self.lower_relay_id)) self.control.relay_on(self.lower_relay_id, self.lower_seconds_on) else: self.control.relay_off(self.lower_relay_id) else: if self.direction in ['raise', 'both'] and self.raise_relay_id: self.control.relay_off(self.raise_relay_id) if self.direction in ['lower', 'both'] and self.lower_relay_id: self.control.relay_off(self.lower_relay_id) def now_in_range(self, start_time, end_time): """ Check if the current time is between start_time and end_time :return: 1 is within range, 0 if not within range :rtype: int """ start_hour = int(start_time.split(":")[0]) start_min = int(start_time.split(":")[1]) end_hour = int(end_time.split(":")[0]) end_min = int(end_time.split(":")[1]) now_time = datetime.datetime.now().time() now_time = now_time.replace(second=0, microsecond=0) if ((start_hour < end_hour) or (start_hour == end_hour and start_min < end_min)): if now_time >= datetime.time(start_hour, start_min) and now_time <= datetime.time(end_hour, end_min): return 1 # Yes now within range else: if now_time >= datetime.time(start_hour, start_min) or now_time <= datetime.time(end_hour, end_min): return 1 # Yes now within range return 0 # No now not within range def calculate_method_setpoint(self, method_id): with session_scope(MYCODO_DB_PATH) as new_session: method = new_session.query(Method) new_session.expunge_all() new_session.close() method_key = method.filter(Method.method_id == method_id) method_key = method_key.filter(Method.method_order == 0).first() method = method.filter(Method.method_id == method_id) method = method.filter(Method.relay_id == None) method = method.filter(Method.method_order > 0) method = method.order_by(Method.method_order.asc()).all() now = datetime.datetime.now() # Calculate where the current time/date is within the time/date method if method_key.method_type == 'Date': for each_method in method: start_time = datetime.datetime.strptime(each_method.start_time, '%Y-%m-%d %H:%M:%S') end_time = end_time = datetime.datetime.strptime(each_method.end_time, '%Y-%m-%d %H:%M:%S') if start_time < now < end_time: start_setpoint = each_method.start_setpoint if each_method.end_setpoint: end_setpoint = each_method.end_setpoint else: end_setpoint = each_method.start_setpoint setpoint_diff = abs(end_setpoint-start_setpoint) total_seconds = (end_time-start_time).total_seconds() part_seconds = (now-start_time).total_seconds() percent_total = part_seconds/total_seconds if start_setpoint < end_setpoint: new_setpoint = start_setpoint+(setpoint_diff*percent_total) else: new_setpoint = start_setpoint-(setpoint_diff*percent_total) self.logger.debug("[Method] Start: {} End: {}".format( start_time, end_time)) self.logger.debug("[Method] Start: {} End: {}".format( start_setpoint, end_setpoint)) self.logger.debug("[Method] Total: {} Part total: {} ({}%)".format( total_seconds, part_seconds, percent_total)) self.logger.debug("[Method] New Setpoint: {}".format( new_setpoint)) self.set_point = new_setpoint return 0 # Calculate where the current Hour:Minute:Seconds is within the Daily method elif method_key.method_type == 'Daily': daily_now = datetime.datetime.now().strftime('%H:%M:%S') daily_now = datetime.datetime.strptime(str(daily_now), '%H:%M:%S') for each_method in method: start_time = datetime.datetime.strptime(each_method.start_time, '%H:%M:%S') end_time = end_time = datetime.datetime.strptime(each_method.end_time, '%H:%M:%S') if start_time < daily_now < end_time: start_setpoint = each_method.start_setpoint if each_method.end_setpoint: end_setpoint = each_method.end_setpoint else: end_setpoint = each_method.start_setpoint setpoint_diff = abs(end_setpoint-start_setpoint) total_seconds = (end_time-start_time).total_seconds() part_seconds = (daily_now-start_time).total_seconds() percent_total = part_seconds/total_seconds if start_setpoint < end_setpoint: new_setpoint = start_setpoint+(setpoint_diff*percent_total) else: new_setpoint = start_setpoint-(setpoint_diff*percent_total) self.logger.debug("[Method] Start: {} End: {}".format( start_time.strftime('%H:%M:%S'), end_time.strftime('%H:%M:%S'))) self.logger.debug("[Method] Start: {} End: {}".format( start_setpoint, end_setpoint)) self.logger.debug("[Method] Total: {} Part total: {} ({}%)".format( total_seconds, part_seconds, percent_total)) self.logger.debug("[Method] New Setpoint: {}".format( new_setpoint)) self.set_point = new_setpoint return 0 # Calculate sine y-axis value from the x-axis (seconds of the day) elif method_key.method_type == 'DailySine': new_setpoint = sine_wave_y_out(method_key.amplitude, method_key.frequency, method_key.shift_angle, method_key.shift_y) self.set_point = new_setpoint return 0 # Calculate Bezier curve y-axis value from the x-axis (seconds of the day) elif method_key.method_type == 'DailyBezier': new_setpoint = bezier_curve_y_out(method_key.shift_angle, (method_key.x0, method_key.y0), (method_key.x1, method_key.y1), (method_key.x2, method_key.y2), (method_key.x3, method_key.y3)) self.set_point = new_setpoint return 0 # Calculate the duration in the method based on self.method_start_time elif method_key.method_type == 'Duration' and self.method_start_time != 'Ended': seconds_from_start = (now-self.method_start_time).total_seconds() total_sec = 0 previous_total_sec = 0 for each_method in method: total_sec += each_method.duration_sec if previous_total_sec <= seconds_from_start < total_sec: row_start_time = float(self.method_start_time.strftime('%s'))+previous_total_sec row_since_start_sec = (now-(self.method_start_time+datetime.timedelta(0, previous_total_sec))).total_seconds() percent_row = row_since_start_sec/each_method.duration_sec start_setpoint = each_method.start_setpoint if each_method.end_setpoint: end_setpoint = each_method.end_setpoint else: end_setpoint = each_method.start_setpoint setpoint_diff = abs(end_setpoint-start_setpoint) if start_setpoint < end_setpoint: new_setpoint = start_setpoint+(setpoint_diff*percent_row) else: new_setpoint = start_setpoint-(setpoint_diff*percent_row) self.logger.debug("[Method] Start: {} Seconds Since: {}".format( self.method_start_time, seconds_from_start)) self.logger.debug("[Method] Start time of row: {}".format( datetime.datetime.fromtimestamp(row_start_time))) self.logger.debug("[Method] Sec since start of row: {}".format( row_since_start_sec)) self.logger.debug("[Method] Percent of row: {}".format( percent_row)) self.logger.debug("[Method] New Setpoint: {}".format( new_setpoint)) self.set_point = new_setpoint return 0 previous_total_sec = total_sec # Duration method has ended, reset start_time locally and in DB if self.method_start_time: with session_scope(MYCODO_DB_PATH) as db_session: mod_method = db_session.query(Method).filter( Method.method_id == self.method_id) mod_method = mod_method.filter(Method.method_order == 0).first() mod_method.start_time = 'Ended' db_session.commit() self.method_start_time = 'Ended' # Setpoint not needing to be calculated, use default setpoint self.set_point = self.default_set_point def addSetpointInfluxdb(self, pid_id, setpoint): """ Add a setpoint entry to InfluxDB :rtype: None """ write_db = threading.Thread( target=write_influxdb_value, args=(self.logger, INFLUXDB_HOST, INFLUXDB_PORT, INFLUXDB_USER, INFLUXDB_PASSWORD, INFLUXDB_DATABASE, 'pid', pid_id, 'setpoint', setpoint,)) write_db.start() def setPoint(self, set_point): """Initilize the setpoint of PID""" self.set_point = set_point self.Integrator = 0 self.Derivator = 0 def setIntegrator(self, Integrator): """Set the Integrator of the controller""" self.Integrator = Integrator def setDerivator(self, Derivator): """Set the Derivator of the controller""" self.Derivator = Derivator def setKp(self, P): """Set Kp gain of the controller""" self.Kp = P def setKi(self, I): """Set Ki gain of the controller""" self.Ki = I def setKd(self, D): """Set Kd gain of the controller""" self.Kd = D def getPoint(self): return self.set_point def getError(self): return self.error def getIntegrator(self): return self.Integrator def getDerivator(self): return self.Derivator def isRunning(self): return self.running def stopController(self): self.thread_shutdown_timer = timeit.default_timer() self.running = False # Unset method start time if self.method_id: with session_scope(MYCODO_DB_PATH) as db_session: mod_method = db_session.query(Method) mod_method = mod_method.filter(Method.method_id == self.method_id) mod_method = mod_method.filter(Method.method_order == 0).first() mod_method.start_time = 'Ended' db_session.commit()