示例#1
0
    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
示例#2
0
    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, 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
示例#4
0
    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
示例#5
0
    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
示例#7
0
    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
示例#8
0
    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
示例#9
0
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
示例#10
0
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()
示例#11
0
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)
示例#12
0
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)
示例#13
0
    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))
示例#14
0
    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'
示例#15
0
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
示例#16
0
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
示例#17
0
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()
示例#18
0
    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))
示例#19
0
    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
示例#20
0
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()
示例#21
0
    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
示例#22
0
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()
示例#23
0
    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
示例#24
0
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()
示例#25
0
    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'
示例#26
0
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
示例#27
0
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()