コード例 #1
0
ファイル: controller_timer.py プロジェクト: qwekw/Mycodo
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
コード例 #2
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()
コード例 #3
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()
コード例 #4
0
ファイル: controller_timer.py プロジェクト: zsole2/Mycodo
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.timer_type = timer.timer_type
            self.name = timer.name
            self.relay_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

        # 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("[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 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 = "[Timer {}] At {}, turn relay {} {}".format(
                            self.timer_id,
                            self.time_start,
                            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

            # 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 = "[Timer {}] Relay {} should be {}, but is {}. Turning {}.".format(
                            self.timer_id,
                            self.relay_id,
                            self.state,
                            current_relay_state,
                            self.state)
                        modulate_relay = threading.Thread(
                            target=self.control.relay_on_off,
                            args=(self.relay_id,
                                  self.state,
                                  0,))
                        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("[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()

            time.sleep(0.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
コード例 #5
0
ファイル: controller_pid.py プロジェクト: qwekw/Mycodo
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()
コード例 #6
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.timer_type = timer.timer_type
            self.name = timer.name
            self.relay_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

        # 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("[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 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 = "[Timer {}] At {}, turn relay {} {}".format(
                            self.timer_id, self.time_start, 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

            # 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 = "[Timer {}] Relay {} should be {}, but is {}. Turning {}.".format(
                            self.timer_id, self.relay_id, self.state,
                            current_relay_state, self.state)
                        modulate_relay = threading.Thread(
                            target=self.control.relay_on_off,
                            args=(
                                self.relay_id,
                                self.state,
                                0,
                            ))
                        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("[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()

            time.sleep(0.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