コード例 #1
0
ファイル: utils_output.py プロジェクト: ciscomonkey/Mycodo
def output_on_off(form_output):
    action = '{action} {controller}'.format(
        action=gettext("Actuate"),
        controller=TRANSLATIONS['output']['title'])
    error = []

    try:
        control = DaemonControl()
        output = Output.query.filter_by(unique_id=form_output.output_id.data).first()
        if output.output_type == 'wired' and int(form_output.output_pin.data) == 0:
            error.append(gettext("Cannot modulate output with a GPIO of 0"))
        elif form_output.on_submit.data:
            if output.output_type in ['wired',
                                      'wireless_rpi_rf',
                                      'command']:
                if float(form_output.sec_on.data) <= 0:
                    error.append(gettext("Value must be greater than 0"))
                else:
                    return_value = control.output_on(form_output.output_id.data,
                                                     duration=float(form_output.sec_on.data))
                    flash(gettext("Output turned on for %(sec)s seconds: %(rvalue)s",
                                  sec=form_output.sec_on.data,
                                  rvalue=return_value),
                          "success")
            if output.output_type == 'pwm':
                if int(form_output.output_pin.data) == 0:
                    error.append(gettext("Invalid pin"))
                if output.pwm_hertz <= 0:
                    error.append(gettext("PWM Hertz must be a positive value"))
                if float(form_output.pwm_duty_cycle_on.data) <= 0:
                    error.append(gettext("PWM duty cycle must be a positive value"))
                if not error:
                    return_value = control.output_on(
                        form_output.output_id.data,
                        duty_cycle=float(form_output.pwm_duty_cycle_on.data))
                    flash(gettext("PWM set to %(dc)s %% at %(hertz)s Hz: %(rvalue)s",
                                  dc=float(form_output.pwm_duty_cycle_on.data),
                                  hertz=output.pwm_hertz,
                                  rvalue=return_value),
                          "success")
        elif form_output.turn_on.data:
            return_value = control.output_on(form_output.output_id.data, 0)
            flash(gettext("Output turned on: %(rvalue)s",
                          rvalue=return_value), "success")
        elif form_output.turn_off.data:
            return_value = control.output_off(form_output.output_id.data)
            flash(gettext("Output turned off: %(rvalue)s",
                          rvalue=return_value), "success")
    except ValueError as except_msg:
        error.append('{err}: {msg}'.format(
            err=gettext("Invalid value"),
            msg=except_msg))
    except Exception as except_msg:
        error.append(except_msg)

    flash_success_errors(error, action, url_for('routes_page.page_output'))
コード例 #2
0
ファイル: am2315.py プロジェクト: ciscomonkey/Mycodo
class InputModule(AbstractInput):
    """
    A sensor support class that measures the AM2315's humidity and temperature
    and calculates the dew point

    """
    def __init__(self, input_dev, testing=False):
        super(InputModule, self).__init__()
        self.logger = logging.getLogger('mycodo.inputs.am2315')
        self.powered = False
        self.am = None

        if not testing:
            from mycodo.mycodo_client import DaemonControl
            self.logger = logging.getLogger(
                'mycodo.am2315_{id}'.format(id=input_dev.unique_id.split('-')[0]))

            self.device_measurements = db_retrieve_table_daemon(
                DeviceMeasurements).filter(
                    DeviceMeasurements.device_id == input_dev.unique_id)

            self.i2c_bus = input_dev.i2c_bus
            self.power_output_id = input_dev.power_output_id
            self.control = DaemonControl()
            self.start_sensor()
            self.am = AM2315(self.i2c_bus)

    def get_measurement(self):
        """ Gets the humidity and temperature """
        return_dict = measurements_dict.copy()

        temperature = None
        humidity = None
        dew_point = None
        measurements_success = False

        # Ensure if the power pin turns off, it is turned back on
        if (self.power_output_id and
                db_retrieve_table_daemon(Output, unique_id=self.power_output_id) and
                self.control.output_state(self.power_output_id) == 'off'):
            self.logger.error(
                'Sensor power output {rel} detected as being off. '
                'Turning on.'.format(rel=self.power_output_id))
            self.start_sensor()
            time.sleep(2)

        # Try twice to get measurement. This prevents an anomaly where
        # the first measurement fails if the sensor has just been powered
        # for the first time.
        for _ in range(2):
            dew_point, humidity, temperature = self.return_measurements()
            if dew_point is not None:
                measurements_success = True
                break
            time.sleep(2)

        # Measurement failure, power cycle the sensor (if enabled)
        # Then try two more times to get a measurement
        if self.power_output_id and not measurements_success:
            self.stop_sensor()
            time.sleep(2)
            self.start_sensor()
            for _ in range(2):
                dew_point, humidity, temperature = self.return_measurements()
                if dew_point is not None:
                    measurements_success = True
                    break
                time.sleep(2)

        if measurements_success:
            if self.is_enabled(0):
                return_dict[0]['value'] = temperature

            if self.is_enabled(1):
                return_dict[1]['value'] = humidity

            if (self.is_enabled(2) and
                    self.is_enabled(0) and
                    self.is_enabled(1)):
                return_dict[2]['value'] = calculate_dewpoint(
                    return_dict[0]['value'], return_dict[1]['value'])

            if (self.is_enabled(3) and
                    self.is_enabled(0) and
                    self.is_enabled(1)):
                return_dict[3]['value'] = calculate_vapor_pressure_deficit(
                    return_dict[0]['value'], return_dict[1]['value'])

            return return_dict
        else:
            self.logger.debug("Could not acquire a measurement")

    def return_measurements(self):
        # Retry measurement if CRC fails
        for num_measure in range(3):
            humidity, temperature = self.am.data()
            if humidity is None:
                self.logger.debug(
                    "Measurement {num} returned failed CRC".format(
                        num=num_measure))
                pass
            else:
                dew_pt = calculate_dewpoint(temperature, humidity)
                return dew_pt, humidity, temperature
            time.sleep(2)

        self.logger.error("All measurements returned failed CRC")
        return None, None, None

    def start_sensor(self):
        """ Turn the sensor on """
        if self.power_output_id:
            self.logger.info("Turning on sensor")
            self.control.output_on(self.power_output_id, 0)
            time.sleep(2)
            self.powered = True

    def stop_sensor(self):
        """ Turn the sensor off """
        if self.power_output_id:
            self.logger.info("Turning off sensor")
            self.control.output_off(self.power_output_id)
            self.powered = False
コード例 #3
0
class PIDController(AbstractController, threading.Thread):
    """
    Class to operate discrete PID controller in Mycodo
    """
    def __init__(self, ready, unique_id):
        threading.Thread.__init__(self)
        super(PIDController, self).__init__(ready, unique_id=unique_id, name=__name__)

        self.unique_id = unique_id
        self.sample_rate = None

        self.control = DaemonControl()

        self.device_measurements = None
        self.device_id = None
        self.measurement_id = None
        self.raise_output_type = None
        self.lower_output_type = None
        self.log_level_debug = None
        self.PID_Controller = None
        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.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.setpoint_tracking_type = None
        self.setpoint_tracking_id = None
        self.setpoint_tracking_max_age = 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 = 0
        self.Ki = 0
        self.Kd = 0
        self.integrator_min = None
        self.integrator_max = None
        self.period = 0
        self.start_offset = 0
        self.max_measure_age = None
        self.default_setpoint = None
        self.setpoint = 0
        self.store_lower_as_negative = None
        self.first_start = None
        self.timer = None

        # Hysteresis options
        self.band = None
        self.allow_raising = False
        self.allow_lowering = False

        # PID Autotune
        self.autotune = None
        self.autotune_activated = False
        self.autotune_debug = False
        self.autotune_noiseband = 0
        self.autotune_outstep = 0
        self.autotune_timestamp = None

        # Check if a method is set for this PID
        self.method_type = None
        self.method_start_act = None
        self.method_start_time = None
        self.method_end_time = None

    def loop(self):
        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 time.time() > self.timer:
            while time.time() > self.timer:
                self.timer = self.timer + self.period
            self.attempt_execute(self.check_pid)

    def run_finally(self):
        # Turn off output used in PID when the controller is deactivated
        if self.raise_output_id and self.direction in ['raise', 'both']:
            self.control.output_off(
                self.raise_output_id,
                trigger_conditionals=True)
        if self.lower_output_id and self.direction in ['lower', 'both']:
            self.control.output_off(
                self.lower_output_id,
                trigger_conditionals=True)

    def initialize_variables(self):
        """Set PID parameters"""
        self.sample_rate = db_retrieve_table_daemon(
            Misc, entry='first').sample_rate_controller_pid

        self.device_measurements = db_retrieve_table_daemon(
            DeviceMeasurements)

        pid = db_retrieve_table_daemon(PID, unique_id=self.unique_id)

        self.device_id = pid.measurement.split(',')[0]
        self.measurement_id = pid.measurement.split(',')[1]

        self.is_activated = pid.is_activated
        self.is_held = pid.is_held
        self.is_paused = pid.is_paused
        self.log_level_debug = pid.log_level_debug
        self.setpoint_tracking_type = pid.setpoint_tracking_type
        self.setpoint_tracking_id = pid.setpoint_tracking_id
        self.setpoint_tracking_max_age = pid.setpoint_tracking_max_age
        self.direction = pid.direction
        self.raise_output_id = pid.raise_output_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_output_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.start_offset = pid.start_offset
        self.max_measure_age = pid.max_measure_age
        self.default_setpoint = pid.setpoint
        self.setpoint = pid.setpoint
        self.band = pid.band
        self.store_lower_as_negative = pid.store_lower_as_negative
        self.first_start = True
        self.timer = time.time() + self.start_offset

        # Autotune
        self.autotune_activated = pid.autotune_activated
        self.autotune_noiseband = pid.autotune_noiseband
        self.autotune_outstep = pid.autotune_outstep

        self.set_log_level_debug(self.log_level_debug)

        try:
            self.raise_output_type = db_retrieve_table_daemon(
                Output, unique_id=self.raise_output_id).output_type
        except AttributeError:
            self.raise_output_type = None

        try:
            self.lower_output_type = db_retrieve_table_daemon(
                Output, unique_id=self.lower_output_id).output_type
        except AttributeError:
            self.lower_output_type = None

        # Initialize PID Controller
        self.PID_Controller = PIDControl(
            self.period,
            self.Kp, self.Ki, self.Kd,
            integrator_min=self.integrator_min,
            integrator_max=self.integrator_max)

        # If activated, initialize PID Autotune
        if self.autotune_activated:
            self.autotune_timestamp = time.time()
            try:
                self.autotune = PIDAutotune(
                    self.setpoint,
                    out_step=self.autotune_outstep,
                    sampletime=self.period,
                    out_min=0,
                    out_max=self.period,
                    noiseband=self.autotune_noiseband)
            except Exception as msg:
                self.logger.error(msg)
                self.stop_controller(deactivate_pid=True)

        if self.setpoint_tracking_type == 'method' and self.setpoint_tracking_id != '':
            self.setup_method(self.setpoint_tracking_id)

        if self.is_paused:
            self.logger.info("Starting Paused")
        elif self.is_held:
            self.logger.info("Starting Held")

        self.logger.info("PID Settings: {}".format(self.pid_parameters_str()))

        return "success"

    def check_pid(self):
        """ Get measurement and apply to PID controller """
        # If PID is active, retrieve measurement and update
        # the control variable.
        # A PID on hold will sustain the current output and
        # not update the control variable.
        if self.is_activated and (not self.is_paused or not self.is_held):
            self.get_last_measurement()

            if self.last_measurement_success:
                if self.setpoint_tracking_type == 'method' and self.setpoint_tracking_id != '':
                    # Update setpoint using a method
                    this_pid = db_retrieve_table_daemon(
                        PID, unique_id=self.unique_id)
                    setpoint, ended = calculate_method_setpoint(
                        self.setpoint_tracking_id,
                        PID,
                        this_pid,
                        Method,
                        MethodData,
                        self.logger)
                    if ended:
                        self.method_start_act = 'Ended'
                    if setpoint is not None:
                        self.setpoint = setpoint
                    else:
                        self.setpoint = self.default_setpoint

                if self.setpoint_tracking_type == 'input-math' and self.setpoint_tracking_id != '':
                    # Update setpoint using an Input or Math
                    device_id = self.setpoint_tracking_id.split(',')[0]
                    measurement_id = self.setpoint_tracking_id.split(',')[1]

                    measurement = get_measurement(measurement_id)
                    if not measurement:
                        return False, None

                    last_measurement = read_last_influxdb(
                        device_id,
                        measurement.unit,
                        measurement.measurement,
                        measurement.channel,
                        self.setpoint_tracking_max_age)

                    if last_measurement[1] is not None:
                        self.setpoint = last_measurement[1]
                    else:
                        self.logger.debug(
                            "Could not find measurement for Setpoint "
                            "Tracking. Max Age of {} exceeded for measuring "
                            "device ID {} (measurement {})".format(
                                self.setpoint_tracking_max_age,
                                device_id,
                                measurement_id))
                        self.setpoint = None

                # If autotune activated, determine control variable (output) from autotune
                if self.autotune_activated:
                    if not self.autotune.run(self.last_measurement):
                        self.control_variable = self.autotune.output

                        if self.autotune_debug:
                            self.logger.info('')
                            self.logger.info("state: {}".format(self.autotune.state))
                            self.logger.info("output: {}".format(self.autotune.output))
                    else:
                        # Autotune has finished
                        timestamp = time.time() - self.autotune_timestamp
                        self.logger.info('')
                        self.logger.info('time:  {0} min'.format(round(timestamp / 60)))
                        self.logger.info('state: {0}'.format(self.autotune.state))

                        if self.autotune.state == PIDAutotune.STATE_SUCCEEDED:
                            for rule in self.autotune.tuning_rules:
                                params = self.autotune.get_pid_parameters(rule)
                                self.logger.info('')
                                self.logger.info('rule: {0}'.format(rule))
                                self.logger.info('Kp: {0}'.format(params.Kp))
                                self.logger.info('Ki: {0}'.format(params.Ki))
                                self.logger.info('Kd: {0}'.format(params.Kd))

                        self.stop_controller(deactivate_pid=True)
                else:
                    # Calculate new control variable (output) from PID Controller

                    # Original PID method
                    self.control_variable = self.update_pid_output(
                        self.last_measurement)

                    # New PID method (untested)
                    # self.control_variable = self.PID_Controller.calc(
                    #     self.last_measurement, self.setpoint)

                self.write_pid_values()  # Write variables to database

        # Is PID in a state that allows manipulation of outputs
        if (self.is_activated and
                self.setpoint is not None and
                (not self.is_paused or self.is_held)):
            self.manipulate_output()

    def setup_method(self, method_id):
        """ Initialize method variables to start running a method """
        self.setpoint_tracking_id = ''

        method = db_retrieve_table_daemon(Method, unique_id=method_id)
        method_data = db_retrieve_table_daemon(MethodData)
        method_data = method_data.filter(MethodData.method_id == method_id)
        method_data_repeat = method_data.filter(
            MethodData.duration_sec == 0).first()
        pid = db_retrieve_table_daemon(PID, unique_id=self.unique_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).filter(
                        PID.unique_id == self.unique_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=method_id,
                                start=self.method_start_time,
                                end=self.method_end_time))
                    else:
                        self.method_start_act = 'Ended'
                else:
                    self.method_start_act = 'Ended'

        self.setpoint_tracking_id = method_id
        self.logger.debug("Method enabled: {id}".format(id=self.setpoint_tracking_id))

    def write_pid_values(self):
        """ Write PID values to the measurement database """
        if self.band:
            setpoint_band_lower = self.setpoint - self.band
            setpoint_band_upper = self.setpoint + self.band
        else:
            setpoint_band_lower = None
            setpoint_band_upper = None

        list_measurements = [
            self.setpoint,
            setpoint_band_lower,
            setpoint_band_upper,
            self.P_value,
            self.I_value,
            self.D_value
        ]

        measurement_dict = {}
        measurements = self.device_measurements.filter(
            DeviceMeasurements.device_id == self.unique_id).all()
        for each_channel, each_measurement in enumerate(measurements):
            if (each_measurement.channel not in measurement_dict and
                    each_measurement.channel < len(list_measurements)):

                # If setpoint, get unit from PID measurement
                if each_measurement.measurement_type == 'setpoint':
                    setpoint_pid = db_retrieve_table_daemon(
                        PID, unique_id=each_measurement.device_id)
                    if setpoint_pid and ',' in setpoint_pid.measurement:
                        pid_measurement = setpoint_pid.measurement.split(',')[1]
                        setpoint_measurement = db_retrieve_table_daemon(
                            DeviceMeasurements, unique_id=pid_measurement)
                        if setpoint_measurement:
                            conversion = db_retrieve_table_daemon(
                                Conversion, unique_id=setpoint_measurement.conversion_id)
                            _, unit, _ = return_measurement_info(
                                setpoint_measurement, conversion)
                            measurement_dict[each_channel] = {
                                'measurement': each_measurement.measurement,
                                'unit': unit,
                                'value': list_measurements[each_channel]
                            }
                else:
                    measurement_dict[each_channel] = {
                        'measurement': each_measurement.measurement,
                        'unit': each_measurement.unit,
                        'value': list_measurements[each_channel]
                    }

        add_measurements_influxdb(self.unique_id, measurement_dict)

    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
        """
        # Determine if hysteresis is enabled and if the PID should be applied
        setpoint = self.check_hysteresis(current_value)

        if setpoint is None:
            # Prevent PID variables form being manipulated and
            # restrict PID from operating.
            return 0

        self.error = setpoint - 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

        # Prevent large initial D-value
        if self.first_start:
            self.derivator = self.error
            self.first_start = False

        # 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

        self.logger.debug(
            "PID: Input: {inp}, "
            "Output: P: {p}, I: {i}, D: {d}, Out: {o}".format(
            inp=current_value, p=self.P_value, i=self.I_value, d=self.D_value, o=pid_value))

        return pid_value

    def check_hysteresis(self, measure):
        """
        Determine if hysteresis is enabled and if the PID should be applied

        :return: float if the setpoint if the PID should be applied, None to
            restrict the PID
        :rtype: float or None

        :param measure: The PID input (or process) variable
        :type measure: float
        """
        if self.band == 0:
            # If band is disabled, return setpoint
            return self.setpoint

        band_min = self.setpoint - self.band
        band_max = self.setpoint + self.band

        if self.direction == 'raise':
            if (measure < band_min or
                    (band_min < measure < band_max and self.allow_raising)):
                self.allow_raising = True
                setpoint = band_max  # New setpoint
                return setpoint  # Apply the PID
            elif measure > band_max:
                self.allow_raising = False
            return None  # Restrict the PID

        elif self.direction == 'lower':
            if (measure > band_max or
                    (band_min < measure < band_max and self.allow_lowering)):
                self.allow_lowering = True
                setpoint = band_min  # New setpoint
                return setpoint  # Apply the PID
            elif measure < band_min:
                self.allow_lowering = False
            return None  # Restrict the PID

        elif self.direction == 'both':
            if measure < band_min:
                setpoint = band_min  # New setpoint
                if not self.allow_raising:
                    # Reset integrator and derivator upon direction switch
                    self.integrator = 0.0
                    self.derivator = 0.0
                    self.allow_raising = True
                    self.allow_lowering = False
            elif measure > band_max:
                setpoint = band_max  # New setpoint
                if not self.allow_lowering:
                    # Reset integrator and derivator upon direction switch
                    self.integrator = 0.0
                    self.derivator = 0.0
                    self.allow_raising = False
                    self.allow_lowering = True
            else:
                return None  # Restrict the PID
            return setpoint  # Apply the PID

    def get_last_measurement(self):
        """
        Retrieve the latest input measurement from InfluxDB

        :rtype: None
        """
        self.last_measurement_success = False

        # Get latest measurement from influxdb
        try:
            device_measurement = get_measurement(self.measurement_id)

            if device_measurement:
                conversion = db_retrieve_table_daemon(
                    Conversion, unique_id=device_measurement.conversion_id)
            else:
                conversion = None
            channel, unit, measurement = return_measurement_info(
                device_measurement, conversion)

            self.last_measurement = read_last_influxdb(
                self.device_id,
                unit,
                measurement,
                channel,
                int(self.max_measure_age))

            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 (CH{ch}, Unit: {unit}): {last} @ {ts}".format(
                    ch=channel,
                    unit=unit,
                    last=self.last_measurement,
                    ts=local_timestamp))
                if calendar.timegm(time.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(time.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:
                    # Determine if the output should be PWM or a duration
                    if self.raise_output_type in OUTPUTS_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.setpoint,
                                cv=self.control_variable,
                                id=self.raise_output_id,
                                dc=self.raise_duty_cycle))

                        # Activate pwm with calculated duty cycle
                        self.control.output_on(
                            self.raise_output_id,
                            duty_cycle=self.raise_duty_cycle)

                        self.write_pid_output_influxdb(
                            'percent', 'duty_cycle', 7,
                            self.control_var_to_duty_cycle(self.control_variable))

                    elif self.raise_output_type in ['command',
                                                    'python',
                                                    'wired',
                                                    'wireless_rpi_rf']:
                        # 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.setpoint,
                                    cv=self.control_variable,
                                    id=self.raise_output_id))
                            self.control.output_on(
                                self.raise_output_id,
                                amount=self.raise_seconds_on,
                                min_off=self.raise_min_off_duration)

                        self.write_pid_output_influxdb(
                            's', 'duration_time', 6,
                            self.control_variable)

                    elif self.raise_output_type == 'atlas_ezo_pmp':
                        # Activate raise_output for a volume (ml)
                        self.logger.debug(
                            "Setpoint: {sp} Output: {cv} ml to output "
                            "{id}".format(
                                sp=self.setpoint,
                                cv=self.control_variable,
                                id=self.raise_output_id))
                        self.control.output_on(
                            self.raise_output_id,
                            amount=self.control_variable,
                            min_off=self.raise_min_off_duration)

                    self.write_pid_output_influxdb(
                        's', 'duration_time', 6,
                        self.control_variable)

                else:
                    # Turn PWM Off if PWM Output
                    if self.raise_output_type in OUTPUTS_PWM:
                        self.control.output_on(
                            self.raise_output_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_output_id:

                if self.control_variable < 0:
                    # Determine if the output should be PWM or a duration
                    if self.lower_output_type in OUTPUTS_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.setpoint,
                                cv=self.control_variable,
                                id=self.lower_output_id,
                                dc=self.lower_duty_cycle))

                        if self.store_lower_as_negative:
                            stored_duty_cycle = -abs(self.lower_duty_cycle)
                            stored_control_variable = -self.control_var_to_duty_cycle(abs(self.control_variable))
                        else:
                            stored_duty_cycle = abs(self.lower_duty_cycle)
                            stored_control_variable = self.control_var_to_duty_cycle(abs(self.control_variable))

                        # Activate pwm with calculated duty cycle
                        self.control.output_on(
                            self.lower_output_id,
                            duty_cycle=stored_duty_cycle)

                        self.write_pid_output_influxdb(
                            'percent', 'duty_cycle', 7,
                            stored_control_variable)

                    elif self.lower_output_type in ['command',
                                                    'python',
                                                    'wired',
                                                    'wireless_rpi_rf']:
                        # 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(
                                abs(self.control_variable)))

                        if self.store_lower_as_negative:
                            stored_amount_on = -abs(self.lower_seconds_on)
                            stored_control_variable = -abs(self.control_variable)
                        else:
                            stored_amount_on = abs(self.lower_seconds_on)
                            stored_control_variable = abs(self.control_variable)

                        if 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.setpoint,
                                                cv=self.control_variable,
                                                id=self.lower_output_id))

                            self.control.output_on(
                                self.lower_output_id,
                                amount=stored_amount_on,
                                min_off=self.lower_min_off_duration)

                        self.write_pid_output_influxdb(
                            's', 'duration_time', 6,
                            stored_control_variable)

                    elif self.lower_output_type == 'atlas_ezo_pmp':
                        if self.store_lower_as_negative:
                            stored_amount_on = -abs(self.lower_seconds_on)
                            stored_control_variable = -abs(self.control_variable)
                        else:
                            stored_amount_on = abs(self.lower_seconds_on)
                            stored_control_variable = abs(self.control_variable)

                        # Activate lower_output for a volume (ml)
                        self.logger.debug("Setpoint: {sp} Output: {cv} to "
                                          "output {id}".format(
                            sp=self.setpoint,
                            cv=self.control_variable,
                            id=self.lower_output_id))

                        self.control.output_on(
                            self.lower_output_id,
                            amount=stored_amount_on,
                            min_off=self.lower_min_off_duration)

                        self.write_pid_output_influxdb(
                            's', 'duration_time', 6,
                            stored_control_variable)

                else:
                    # Turn PWM Off if PWM Output
                    if self.lower_output_type in OUTPUTS_PWM:
                        self.control.output_on(
                            self.lower_output_id,
                            duty_cycle=0)

        else:
            self.logger.debug("Last measurement unsuccessful. Turning outputs off.")
            if self.direction in ['raise', 'both'] and self.raise_output_id:
                self.control.output_off(
                    self.raise_output_id)
            if self.direction in ['lower', 'both'] and self.lower_output_id:
                self.control.output_off(
                    self.lower_output_id)

    def pid_parameters_str(self):
        return "Device ID: {did}, " \
               "Measurement ID: {mid}, " \
               "Direction: {dir}, " \
               "Period: {per}, " \
               "Setpoint: {sp}, " \
               "Band: {band}, " \
               "Kp: {kp}, " \
               "Ki: {ki}, " \
               "Kd: {kd}, " \
               "Integrator Min: {imn}, " \
               "Integrator Max {imx}, " \
               "Output Raise: {opr}, " \
               "Output Raise Min On: {oprmnon}, " \
               "Output Raise Max On: {oprmxon}, " \
               "Output Raise Min Off: {oprmnoff}, " \
               "Output Lower: {opl}, " \
               "Output Lower Min On: {oplmnon}, " \
               "Output Lower Max On: {oplmxon}, " \
               "Output Lower Min Off: {oplmnoff}, " \
               "Setpoint Tracking Type: {sptt}, " \
               "Setpoint Tracking ID: {spt}".format(
            did=self.device_id,
            mid=self.measurement_id,
            dir=self.direction,
            per=self.period,
            sp=self.setpoint,
            band=self.band,
            kp=self.Kp,
            ki=self.Ki,
            kd=self.Kd,
            imn=self.integrator_min,
            imx=self.integrator_max,
            opr=self.raise_output_id,
            oprmnon=self.raise_min_duration,
            oprmxon=self.raise_max_duration,
            oprmnoff=self.raise_min_off_duration,
            opl=self.lower_output_id,
            oplmnon=self.lower_min_duration,
            oplmxon=self.lower_max_duration,
            oplmnoff=self.lower_min_off_duration,
            sptt=self.setpoint_tracking_type,
            spt=self.setpoint_tracking_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, unit, measurement, channel, value):
        write_pid_out_db = threading.Thread(
            target=write_influxdb_value,
            args=(self.unique_id,
                  unit,
                  value,),
            kwargs={'measure': measurement,
                    'channel': channel})
        write_pid_out_db.start()

    def pid_mod(self):
        if self.initialize_variables():
            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, setpoint):
        """ Set the setpoint of PID """
        self.setpoint = float(setpoint)
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.unique_id).first()
            mod_pid.setpoint = setpoint
            db_session.commit()
        return "Setpoint set to {sp}".format(sp=setpoint)

    def set_method(self, method_id):
        """ Set the method of PID """
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.unique_id).first()
            mod_pid.setpoint_tracking_id = method_id

            if method_id == '':
                self.setpoint_tracking_id = ''
                db_session.commit()
            else:
                mod_pid.method_start_time = 'Ready'
                mod_pid.method_end_time = None
                db_session.commit()
                self.setup_method(method_id)

        return "Method set to {me}".format(me=method_id)

    def set_integrator(self, integrator):
        """ Set the integrator of the controller """
        self.integrator = float(integrator)
        return "Integrator set to {i}".format(i=self.integrator)

    def set_derivator(self, derivator):
        """ Set the derivator of the controller """
        self.derivator = float(derivator)
        return "Derivator set to {d}".format(d=self.derivator)

    def set_kp(self, p):
        """ Set Kp gain of the controller """
        self.Kp = float(p)
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.unique_id).first()
            mod_pid.p = p
            db_session.commit()
        return "Kp set to {kp}".format(kp=self.Kp)

    def set_ki(self, i):
        """ Set Ki gain of the controller """
        self.Ki = float(i)
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.unique_id).first()
            mod_pid.i = i
            db_session.commit()
        return "Ki set to {ki}".format(ki=self.Ki)

    def set_kd(self, d):
        """ Set Kd gain of the controller """
        self.Kd = float(d)
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.unique_id).first()
            mod_pid.d = d
            db_session.commit()
        return "Kd set to {kd}".format(kd=self.Kd)

    def get_setpoint(self):
        return self.setpoint

    def get_error(self):
        return self.error

    def get_integrator(self):
        return self.integrator

    def get_derivator(self):
        return self.derivator

    def get_kp(self):
        return self.Kp

    def get_ki(self):
        return self.Ki

    def get_kd(self):
        return self.Kd

    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.setpoint_tracking_type == 'method' and
                self.setpoint_tracking_id != '' and
                ended_normally):
            with session_scope(MYCODO_DB_PATH) as db_session:
                mod_pid = db_session.query(PID).filter(
                    PID.unique_id == self.unique_id).first()
                mod_pid.method_start_time = 'Ended'
                mod_pid.method_end_time = None
                db_session.commit()

        # Deactivate PID and Autotune
        if deactivate_pid:
            with session_scope(MYCODO_DB_PATH) as db_session:
                mod_pid = db_session.query(PID).filter(
                    PID.unique_id == self.unique_id).first()
                mod_pid.is_activated = False
                mod_pid.autotune_activated = False
                db_session.commit()
コード例 #4
0
def camera_record(record_type,
                  unique_id,
                  duration_sec=None,
                  tmp_filename=None):
    """
    Record still image from cameras
    :param record_type:
    :param unique_id:
    :param duration_sec:
    :param tmp_filename:
    :return:
    """
    daemon_control = None
    settings = db_retrieve_table_daemon(Camera, unique_id=unique_id)
    timestamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
    assure_path_exists(PATH_CAMERAS)
    camera_path = assure_path_exists(
        os.path.join(PATH_CAMERAS, '{uid}'.format(uid=settings.unique_id)))
    if record_type == 'photo':
        if settings.path_still != '':
            save_path = settings.path_still
        else:
            save_path = assure_path_exists(os.path.join(camera_path, 'still'))
        filename = 'Still-{cam_id}-{cam}-{ts}.jpg'.format(
            cam_id=settings.id, cam=settings.name,
            ts=timestamp).replace(" ", "_")
    elif record_type == 'timelapse':
        if settings.path_timelapse != '':
            save_path = settings.path_timelapse
        else:
            save_path = assure_path_exists(
                os.path.join(camera_path, 'timelapse'))
        start = datetime.datetime.fromtimestamp(
            settings.timelapse_start_time).strftime("%Y-%m-%d_%H-%M-%S")
        filename = 'Timelapse-{cam_id}-{cam}-{st}-img-{cn:05d}.jpg'.format(
            cam_id=settings.id,
            cam=settings.name,
            st=start,
            cn=settings.timelapse_capture_number).replace(" ", "_")
    elif record_type == 'video':
        if settings.path_video != '':
            save_path = settings.path_video
        else:
            save_path = assure_path_exists(os.path.join(camera_path, 'video'))
        filename = 'Video-{cam}-{ts}.h264'.format(cam=settings.name,
                                                  ts=timestamp).replace(
                                                      " ", "_")
    else:
        return

    assure_path_exists(save_path)

    if tmp_filename:
        filename = tmp_filename

    path_file = os.path.join(save_path, filename)

    # Turn on output, if configured
    if settings.output_id:
        daemon_control = DaemonControl()
        daemon_control.output_on(settings.output_id)

    # Pause while the output remains on for the specified duration.
    # Used for instance to allow fluorescent lights to fully turn on before
    # capturing an image.
    if settings.output_duration:
        time.sleep(settings.output_duration)

    if settings.library == 'picamera':
        # Try 5 times to access the pi camera (in case another process is accessing it)
        for _ in range(5):
            try:
                with picamera.PiCamera() as camera:
                    camera.resolution = (settings.width, settings.height)
                    camera.hflip = settings.hflip
                    camera.vflip = settings.vflip
                    camera.rotation = settings.rotation
                    camera.brightness = int(settings.brightness)
                    camera.contrast = int(settings.contrast)
                    camera.exposure_compensation = int(settings.exposure)
                    camera.saturation = int(settings.saturation)
                    camera.start_preview()
                    time.sleep(2)  # Camera warm-up time

                    if record_type in ['photo', 'timelapse']:
                        camera.capture(path_file, use_video_port=False)
                    elif record_type == 'video':
                        camera.start_recording(path_file,
                                               format='h264',
                                               quality=20)
                        camera.wait_recording(duration_sec)
                        camera.stop_recording()
                    else:
                        return
                    break
            except picamera.exc.PiCameraMMALError:
                logger.error(
                    "The camera is already open by picamera. Retrying 4 times."
                )
            time.sleep(1)

    elif settings.library == 'fswebcam':
        cmd = "/usr/bin/fswebcam --device {dev} --resolution {w}x{h} --set brightness={bt}% " \
              "--no-banner --save {file}".format(dev=settings.device,
                                                 w=settings.width,
                                                 h=settings.height,
                                                 bt=settings.brightness,
                                                 file=path_file)
        if settings.hflip:
            cmd += " --flip h"
        if settings.vflip:
            cmd += " --flip h"
        if settings.rotation:
            cmd += " --rotate {angle}".format(angle=settings.rotation)
        if settings.custom_options:
            cmd += " " + settings.custom_options

        out, err, status = cmd_output(cmd, stdout_pipe=False)
        # logger.error("TEST01: {}; {}; {}; {}".format(cmd, out, err, status))

    # Turn off output, if configured
    if settings.output_id and daemon_control:
        daemon_control.output_off(settings.output_id)

    try:
        set_user_grp(path_file, 'mycodo', 'mycodo')
        return save_path, filename
    except Exception as e:
        logger.exception(
            "Exception raised in 'camera_record' when setting user grp: "
            "{err}".format(err=e))
コード例 #5
0
ファイル: bang_bang.py プロジェクト: vinodyadav7490/Mycodo
class CustomModule(AbstractController, threading.Thread):
    """
    Class to operate custom controller
    """
    def __init__(self, ready, unique_id, testing=False):
        threading.Thread.__init__(self)
        super(CustomModule, self).__init__(ready,
                                           unique_id=unique_id,
                                           name=__name__)

        self.unique_id = unique_id
        self.log_level_debug = None
        self.control_variable = None
        self.timestamp = None
        self.timer = None
        self.control = DaemonControl()
        self.outputIsOn = False
        self.timer_loop = time.time()

        # Initialize custom options
        self.measurement_device_id = None
        self.measurement_measurement_id = None
        self.output_device_id = None
        self.output_measurement_id = None
        self.output_channel_id = None
        self.setpoint = None
        self.hysteresis = None
        self.direction = None
        self.output_channel = None
        self.update_period = None

        # Set custom options
        custom_function = db_retrieve_table_daemon(CustomController,
                                                   unique_id=unique_id)
        self.setup_custom_options(FUNCTION_INFORMATION['custom_options'],
                                  custom_function)

        self.output_channel = self.get_output_channel_from_channel_id(
            self.output_channel_id)

        self.initialize_variables()

    def initialize_variables(self):
        controller = db_retrieve_table_daemon(CustomController,
                                              unique_id=self.unique_id)
        self.log_level_debug = controller.log_level_debug
        self.set_log_level_debug(self.log_level_debug)

        self.timestamp = time.time()

    def run(self):
        try:
            if self.output_channel is None:
                self.logger.error(
                    "Cannot start bang-bang controller: Could not find output channel."
                )
                self.deactivate_self()
                return

            self.logger.info("Activated in {:.1f} ms".format(
                (timeit.default_timer() - self.thread_startup_timer) * 1000))

            self.ready.set()
            self.running = True
            self.timer = time.time()

            self.logger.info(
                "Bang-Bang controller started with options: "
                "Measurement Device: {}, Measurement: {}, Output: {}, "
                "Output_Channel: {}, Setpoint: {}, Hysteresis: {}, "
                "Direction: {}, Period: {}".format(
                    self.measurement_device_id,
                    self.measurement_measurement_id, self.output_device_id,
                    self.output_channel, self.setpoint, self.hysteresis,
                    self.direction, self.update_period))

            # Start a loop
            while self.running:
                if self.timer_loop < time.time():
                    while self.timer_loop < time.time():
                        self.timer_loop += self.update_period
                    self.loop()
                time.sleep(0.1)
        except:
            self.logger.exception("Run Error")
        finally:
            self.run_finally()
            self.running = False
            if self.thread_shutdown_timer:
                self.logger.info("Deactivated in {:.1f} ms".format(
                    (timeit.default_timer() - self.thread_shutdown_timer) *
                    1000))
            else:
                self.logger.error("Deactivated unexpectedly")

    def loop(self):
        last_measurement = self.get_last_measurement(
            self.measurement_device_id, self.measurement_measurement_id)[1]
        outputState = self.control.output_state(self.output_device_id,
                                                self.output_channel)

        self.logger.info("Input: {}, output: {}, target: {}, hyst: {}".format(
            last_measurement, outputState, self.setpoint, self.hysteresis))

        if self.direction == 'raise':
            if outputState == 'on':
                # looking to turn output off
                if last_measurement > (self.setpoint + self.hysteresis):
                    self.control.output_off(self.output_device_id,
                                            output_channel=self.output_channel)
            else:
                # looking to turn output on
                if last_measurement < (self.setpoint - self.hysteresis):
                    self.control.output_on(self.output_device_id,
                                           output_channel=self.output_channel)
        elif self.direction == 'lower':
            if outputState == 'on':
                # looking to turn output off
                if last_measurement < (self.setpoint - self.hysteresis):
                    self.control.output_off(self.output_device_id,
                                            output_channel=self.output_channel)
            else:
                # looking to turn output on
                if last_measurement > (self.setpoint + self.hysteresis):
                    self.control.output_on(self.output_device_id,
                                           output_channel=self.output_channel)
        else:
            self.logger.info("Unknown controller direction: {}".format(
                self.direction))

    def deactivate_self(self):
        self.logger.info("Deactivating bang-bang controller")

        with session_scope(MYCODO_DB_PATH) as new_session:
            mod_cont = new_session.query(CustomController).filter(
                CustomController.unique_id == self.unique_id).first()
            mod_cont.is_activated = False
            new_session.commit()

        deactivate_controller = threading.Thread(
            target=self.control.controller_deactivate, args=(self.unique_id, ))
        deactivate_controller.start()

    def pre_stop(self):
        self.control.output_off(self.output_device_id, self.output_channel)
コード例 #6
0
ファイル: am2315.py プロジェクト: stardawg/Mycodo
class InputModule(AbstractInput):
    """
    A sensor support class that measures the AM2315's humidity and temperature
    and calculates the dew point

    """
    def __init__(self, input_dev, testing=False):
        super(InputModule, self).__init__(input_dev,
                                          testing=testing,
                                          name=__name__)

        self.sensor = None
        self.powered = False
        self.control = None
        self.power_output_id = None

        if not testing:
            self.initialize_input()

    def initialize_input(self):
        from mycodo.mycodo_client import DaemonControl

        self.power_output_id = self.input_dev.power_output_id
        self.control = DaemonControl()
        self.start_input()
        self.sensor = AM2315(self.input_dev.i2c_bus)

    def get_measurement(self):
        """ Gets the humidity and temperature """
        if not self.sensor:
            self.logger.error("Input not set up")
            return

        self.return_dict = copy.deepcopy(measurements_dict)

        temperature = None
        humidity = None
        dew_point = None
        measurements_success = False

        # Ensure if the power pin turns off, it is turned back on
        if (self.power_output_id and db_retrieve_table_daemon(
                Output, unique_id=self.power_output_id)
                and self.control.output_state(self.power_output_id) == 'off'):
            self.logger.error(
                'Sensor power output {rel} detected as being off. Turning on.'.
                format(rel=self.power_output_id))
            self.start_input()
            time.sleep(2)

        # Try twice to get measurement. This prevents an anomaly where
        # the first measurement fails if the sensor has just been powered
        # for the first time.
        for _ in range(2):
            dew_point, humidity, temperature = self.return_measurements()
            if dew_point is not None:
                measurements_success = True
                break
            time.sleep(2)

        # Measurement failure, power cycle the sensor (if enabled)
        # Then try two more times to get a measurement
        if self.power_output_id and not measurements_success:
            self.stop_input()
            time.sleep(2)
            self.start_input()
            for _ in range(2):
                dew_point, humidity, temperature = self.return_measurements()
                if dew_point is not None:
                    measurements_success = True
                    break
                time.sleep(2)

        if measurements_success:
            self.value_set(0, temperature)
            self.value_set(1, humidity)

            if self.is_enabled(0) and self.is_enabled(1):
                self.value_set(
                    2, calculate_dewpoint(self.value_get(0),
                                          self.value_get(1)))
                self.value_set(
                    3,
                    calculate_vapor_pressure_deficit(self.value_get(0),
                                                     self.value_get(1)))

            return self.return_dict
        else:
            self.logger.debug("Could not acquire a measurement")

    def return_measurements(self):
        # Retry measurement if CRC fails
        for num_measure in range(3):
            humidity, temperature = self.sensor.data()
            if humidity is None:
                self.logger.debug(
                    "Measurement {num} returned failed CRC".format(
                        num=num_measure))
            else:
                dew_pt = calculate_dewpoint(temperature, humidity)
                return dew_pt, humidity, temperature
            time.sleep(2)

        self.logger.error("All measurements returned failed CRC")
        return None, None, None

    def start_input(self):
        """ Turn the sensor on """
        if self.power_output_id:
            self.logger.info("Turning on sensor")
            self.control.output_on(self.power_output_id)
            time.sleep(2)
            self.powered = True

    def stop_input(self):
        """ Turn the sensor off """
        if self.power_output_id:
            self.logger.info("Turning off sensor")
            self.control.output_off(self.power_output_id)
            self.powered = False
コード例 #7
0
class PIDController(AbstractController, threading.Thread):
    """
    Class to operate discrete PID controller in Mycodo
    """
    def __init__(self, ready, unique_id):
        threading.Thread.__init__(self)
        super(PIDController, self).__init__(ready,
                                            unique_id=unique_id,
                                            name=__name__)

        self.unique_id = unique_id
        self.sample_rate = None
        self.dict_outputs = None

        self.control = DaemonControl()

        self.PID_Controller = None
        self.setpoint = None

        self.device_measurements = None
        self.device_id = None
        self.measurement_id = None
        self.log_level_debug = None
        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.setpoint_tracking_type = None
        self.setpoint_tracking_id = None
        self.setpoint_tracking_max_age = None
        self.raise_output_id = None
        self.raise_output_channel_id = None
        self.raise_output_channel = None
        self.raise_output_type = None
        self.raise_min_duration = None
        self.raise_max_duration = None
        self.raise_min_off_duration = None
        self.raise_always_min_pwm = None
        self.lower_output_id = None
        self.lower_output_channel_id = None
        self.lower_output_channel = None
        self.lower_output_type = None
        self.lower_min_duration = None
        self.lower_max_duration = None
        self.lower_min_off_duration = None
        self.lower_always_min_pwm = None
        self.period = 0
        self.start_offset = 0
        self.max_measure_age = None
        self.store_lower_as_negative = None
        self.timer = 0

        # Check if a method is set for this PID
        self.method_type = None
        self.method_start_act = None
        self.method_start_time = None
        self.method_end_time = None

    def loop(self):
        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 time.time() > self.timer:
            while time.time() > self.timer:
                self.timer = self.timer + self.period
            self.attempt_execute(self.check_pid)

    def run_finally(self):
        # Turn off output used in PID when the controller is deactivated
        if self.raise_output_id and self.PID_Controller.direction in [
                'raise', 'both'
        ]:
            self.control.output_off(self.raise_output_id,
                                    output_channel=self.raise_output_channel,
                                    trigger_conditionals=True)
        if self.lower_output_id and self.PID_Controller.direction in [
                'lower', 'both'
        ]:
            self.control.output_off(self.lower_output_id,
                                    output_channel=self.lower_output_channel,
                                    trigger_conditionals=True)

    def initialize_variables(self):
        """Set PID parameters"""
        self.dict_outputs = parse_output_information()

        self.sample_rate = db_retrieve_table_daemon(
            Misc, entry='first').sample_rate_controller_pid

        self.device_measurements = db_retrieve_table_daemon(DeviceMeasurements)

        pid = db_retrieve_table_daemon(PID, unique_id=self.unique_id)

        self.log_level_debug = pid.log_level_debug
        self.set_log_level_debug(self.log_level_debug)

        self.device_id = pid.measurement.split(',')[0]
        self.measurement_id = pid.measurement.split(',')[1]
        self.is_activated = pid.is_activated
        self.is_held = pid.is_held
        self.is_paused = pid.is_paused
        self.setpoint_tracking_type = pid.setpoint_tracking_type
        self.setpoint_tracking_id = pid.setpoint_tracking_id
        self.setpoint_tracking_max_age = pid.setpoint_tracking_max_age
        if pid.raise_output_id and "," in pid.raise_output_id:
            self.raise_output_id = pid.raise_output_id.split(",")[0]
            self.raise_output_channel_id = pid.raise_output_id.split(",")[1]
            output_channel = db_retrieve_table_daemon(
                OutputChannel, unique_id=self.raise_output_channel_id)
            self.raise_output_channel = output_channel.channel
        self.raise_output_type = pid.raise_output_type
        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.raise_always_min_pwm = pid.raise_always_min_pwm
        if pid.lower_output_id and "," in pid.lower_output_id:
            self.lower_output_id = pid.lower_output_id.split(",")[0]
            self.lower_output_channel_id = pid.lower_output_id.split(",")[1]
            output_channel = db_retrieve_table_daemon(
                OutputChannel, unique_id=self.lower_output_channel_id)
            self.lower_output_channel = output_channel.channel
        self.lower_output_type = pid.lower_output_type
        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.lower_always_min_pwm = pid.lower_always_min_pwm
        self.period = pid.period
        self.start_offset = pid.start_offset
        self.max_measure_age = pid.max_measure_age
        self.store_lower_as_negative = pid.store_lower_as_negative
        self.timer = time.time() + self.start_offset
        self.setpoint = pid.setpoint

        # Initialize PID Controller
        if self.PID_Controller is None:
            self.PID_Controller = PIDControl(self.logger, pid.setpoint, pid.p,
                                             pid.i, pid.d, pid.direction,
                                             pid.band, pid.integrator_min,
                                             pid.integrator_max)
        else:
            # Set PID options
            self.PID_Controller.setpoint = pid.setpoint
            self.PID_Controller.Kp = pid.p
            self.PID_Controller.Ki = pid.i
            self.PID_Controller.Kd = pid.d
            self.PID_Controller.direction = pid.direction
            self.PID_Controller.band = pid.band
            self.PID_Controller.integrator_min = pid.integrator_min
            self.PID_Controller.integrator_max = pid.integrator_max
            self.PID_Controller.first_start = True

        if self.setpoint_tracking_type == 'method' and self.setpoint_tracking_id != '':
            self.setup_method(self.setpoint_tracking_id)

        if self.is_paused:
            self.logger.info("Starting Paused")
        elif self.is_held:
            self.logger.info("Starting Held")

        self.logger.info("PID Settings: {}".format(self.pid_parameters_str()))

        return "success"

    def check_pid(self):
        """ Get measurement and apply to PID controller """
        # If PID is active, retrieve measurement and update
        # the control variable.
        # A PID on hold will sustain the current output and
        # not update the control variable.
        if self.is_activated and (not self.is_paused or not self.is_held):
            self.get_last_measurement_pid()

            if self.last_measurement_success:
                if self.setpoint_tracking_type == 'method' and self.setpoint_tracking_id != '':
                    # Update setpoint using a method
                    this_pid = db_retrieve_table_daemon(
                        PID, unique_id=self.unique_id)
                    new_setpoint, ended = calculate_method_setpoint(
                        self.setpoint_tracking_id, PID, this_pid, Method,
                        MethodData, self.logger)
                    if ended:
                        self.method_start_act = 'Ended'
                    if new_setpoint is not None:
                        self.PID_Controller.setpoint = new_setpoint
                    else:
                        self.PID_Controller.setpoint = self.setpoint

                if self.setpoint_tracking_type == 'input-math' and self.setpoint_tracking_id != '':
                    # Update setpoint using an Input or Math
                    device_id = self.setpoint_tracking_id.split(',')[0]
                    measurement_id = self.setpoint_tracking_id.split(',')[1]

                    measurement = get_measurement(measurement_id)
                    if not measurement:
                        return False, None

                    conversion = db_retrieve_table_daemon(
                        Conversion, unique_id=measurement.conversion_id)
                    channel, unit, measurement = return_measurement_info(
                        measurement, conversion)

                    last_measurement = read_last_influxdb(
                        device_id,
                        unit,
                        channel,
                        measure=measurement,
                        duration_sec=self.setpoint_tracking_max_age)

                    if last_measurement[1] is not None:
                        self.PID_Controller.setpoint = last_measurement[1]
                    else:
                        self.logger.debug(
                            "Could not find measurement for Setpoint "
                            "Tracking. Max Age of {} exceeded for measuring "
                            "device ID {} (measurement {})".format(
                                self.setpoint_tracking_max_age, device_id,
                                measurement_id))
                        self.PID_Controller.setpoint = None

                # Calculate new control variable (output) from PID Controller
                self.PID_Controller.update_pid_output(self.last_measurement)

                self.write_pid_values()  # Write variables to database

        # Is PID in a state that allows manipulation of outputs
        if (self.is_activated and self.PID_Controller.setpoint is not None
                and (not self.is_paused or self.is_held)):
            self.manipulate_output()

    def setup_method(self, method_id):
        """ Initialize method variables to start running a method """
        self.setpoint_tracking_id = ''

        method = db_retrieve_table_daemon(Method, unique_id=method_id)
        method_data = db_retrieve_table_daemon(MethodData)
        method_data = method_data.filter(MethodData.method_id == method_id)
        method_data_repeat = method_data.filter(
            MethodData.duration_sec == 0).first()
        pid = db_retrieve_table_daemon(PID, unique_id=self.unique_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).filter(
                        PID.unique_id == self.unique_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=method_id,
                                    start=self.method_start_time,
                                    end=self.method_end_time))
                    else:
                        self.method_start_act = 'Ended'
                else:
                    self.method_start_act = 'Ended'

        self.setpoint_tracking_id = method_id
        self.logger.debug(
            "Method enabled: {id}".format(id=self.setpoint_tracking_id))

    def write_pid_values(self):
        """ Write PID values to the measurement database """
        if self.PID_Controller.band:
            setpoint_band_lower = self.PID_Controller.setpoint - self.PID_Controller.band
            setpoint_band_upper = self.PID_Controller.setpoint + self.PID_Controller.band
        else:
            setpoint_band_lower = None
            setpoint_band_upper = None

        list_measurements = [
            self.PID_Controller.setpoint, setpoint_band_lower,
            setpoint_band_upper, self.PID_Controller.P_value,
            self.PID_Controller.I_value, self.PID_Controller.D_value
        ]

        measurement_dict = {}
        measurements = self.device_measurements.filter(
            DeviceMeasurements.device_id == self.unique_id).all()
        for each_channel, each_measurement in enumerate(measurements):
            if (each_measurement.channel not in measurement_dict
                    and each_measurement.channel < len(list_measurements)):

                # If setpoint, get unit from PID measurement
                if each_measurement.measurement_type == 'setpoint':
                    setpoint_pid = db_retrieve_table_daemon(
                        PID, unique_id=each_measurement.device_id)
                    if setpoint_pid and ',' in setpoint_pid.measurement:
                        pid_measurement = setpoint_pid.measurement.split(
                            ',')[1]
                        setpoint_measurement = db_retrieve_table_daemon(
                            DeviceMeasurements, unique_id=pid_measurement)
                        if setpoint_measurement:
                            conversion = db_retrieve_table_daemon(
                                Conversion,
                                unique_id=setpoint_measurement.conversion_id)
                            _, unit, _ = return_measurement_info(
                                setpoint_measurement, conversion)
                            measurement_dict[each_channel] = {
                                'measurement': each_measurement.measurement,
                                'unit': unit,
                                'value': list_measurements[each_channel]
                            }
                else:
                    measurement_dict[each_channel] = {
                        'measurement': each_measurement.measurement,
                        'unit': each_measurement.unit,
                        'value': list_measurements[each_channel]
                    }

        add_measurements_influxdb(self.unique_id, measurement_dict)

    def get_last_measurement_pid(self):
        """
        Retrieve the latest input measurement from InfluxDB

        :rtype: None
        """
        self.last_measurement_success = False

        # Get latest measurement from influxdb
        try:
            device_measurement = get_measurement(self.measurement_id)

            if device_measurement:
                conversion = db_retrieve_table_daemon(
                    Conversion, unique_id=device_measurement.conversion_id)
            else:
                conversion = None
            channel, unit, measurement = return_measurement_info(
                device_measurement, conversion)

            self.last_measurement = read_last_influxdb(
                self.device_id,
                unit,
                channel,
                measure=measurement,
                duration_sec=int(self.max_measure_age))

            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 (CH{ch}, Unit: {unit}): {last} @ {ts}".format(
                        ch=channel,
                        unit=unit,
                        last=self.last_measurement,
                        ts=local_timestamp))
                if calendar.timegm(
                        time.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(time.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.PID_Controller.direction in ['raise', 'both'
                                                 ] and self.raise_output_id:

                if self.PID_Controller.control_variable > 0:
                    # 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.PID_Controller.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} CH{ch} to {dc:.1f}%"
                            .format(sp=self.PID_Controller.setpoint,
                                    cv=self.PID_Controller.control_variable,
                                    id=self.raise_output_id,
                                    ch=self.raise_output_channel,
                                    dc=self.raise_duty_cycle))

                        # Activate pwm with calculated duty cycle
                        self.control.output_on(
                            self.raise_output_id,
                            output_type='pwm',
                            amount=self.raise_duty_cycle,
                            output_channel=self.raise_output_channel)

                        self.write_pid_output_influxdb(
                            'percent', 'duty_cycle', 7,
                            self.control_var_to_duty_cycle(
                                self.PID_Controller.control_variable))

                    elif self.raise_output_type == 'on_off':
                        # Ensure the output on duration doesn't exceed the set maximum
                        if (self.raise_max_duration
                                and self.PID_Controller.control_variable >
                                self.raise_max_duration):
                            self.raise_seconds_on = self.raise_max_duration
                        else:
                            self.raise_seconds_on = float("{0:.2f}".format(
                                self.PID_Controller.control_variable))

                        if self.raise_seconds_on > self.raise_min_duration:
                            # Activate raise_output for a duration
                            self.logger.debug(
                                "Setpoint: {sp} Output: {cv} sec to output {id} CH{ch}"
                                .format(
                                    sp=self.PID_Controller.setpoint,
                                    cv=self.PID_Controller.control_variable,
                                    id=self.raise_output_id,
                                    ch=self.raise_output_channel))

                            self.control.output_on(
                                self.raise_output_id,
                                output_type='sec',
                                amount=self.raise_seconds_on,
                                min_off=self.raise_min_off_duration,
                                output_channel=self.raise_output_channel)

                        self.write_pid_output_influxdb(
                            's', 'duration_time', 6,
                            self.PID_Controller.control_variable)

                    elif self.raise_output_type == 'value':
                        # Activate raise_output for a value
                        self.logger.debug(
                            "Setpoint: {sp} Output: {cv} to output {id} CH{ch}"
                            .format(sp=self.PID_Controller.setpoint,
                                    cv=self.PID_Controller.control_variable,
                                    id=self.raise_output_id,
                                    ch=self.raise_output_channel))

                        self.control.output_on(
                            self.raise_output_id,
                            output_type='value',
                            amount=self.PID_Controller.control_variable,
                            min_off=self.raise_min_off_duration,
                            output_channel=self.raise_output_channel)

                        self.write_pid_output_influxdb(
                            'none', 'unitless', 9,
                            self.PID_Controller.control_variable)

                    elif self.raise_output_type == 'volume':
                        # Activate raise_output for a volume (ml)
                        self.logger.debug(
                            "Setpoint: {sp} Output: {cv} ml to output {id} CH{ch}"
                            .format(sp=self.PID_Controller.setpoint,
                                    cv=self.PID_Controller.control_variable,
                                    id=self.raise_output_id,
                                    ch=self.raise_output_channel))

                        self.control.output_on(
                            self.raise_output_id,
                            output_type='vol',
                            amount=self.PID_Controller.control_variable,
                            min_off=self.raise_min_off_duration,
                            output_channel=self.raise_output_channel)

                        self.write_pid_output_influxdb(
                            'ml', 'volume', 8,
                            self.PID_Controller.control_variable)

                elif self.raise_output_type == 'pwm' and not self.raise_always_min_pwm:
                    # Turn PWM Off if PWM Output and not instructed to always be at least min
                    self.control.output_on(
                        self.raise_output_id,
                        output_type='pwm',
                        amount=0,
                        output_channel=self.raise_output_channel)

            #
            # PID control variable is negative, indicating a desire to lower
            # the environmental condition
            #
            if self.PID_Controller.direction in ['lower', 'both'
                                                 ] and self.lower_output_id:

                if self.PID_Controller.control_variable < 0:
                    # 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.PID_Controller.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} CH{ch} to {dc:.1f}%"
                            .format(sp=self.PID_Controller.setpoint,
                                    cv=self.PID_Controller.control_variable,
                                    id=self.lower_output_id,
                                    ch=self.lower_output_channel,
                                    dc=self.lower_duty_cycle))

                        if self.store_lower_as_negative:
                            stored_duty_cycle = -abs(self.lower_duty_cycle)
                            stored_control_variable = -self.control_var_to_duty_cycle(
                                abs(self.PID_Controller.control_variable))
                        else:
                            stored_duty_cycle = abs(self.lower_duty_cycle)
                            stored_control_variable = self.control_var_to_duty_cycle(
                                abs(self.PID_Controller.control_variable))

                        # Activate pwm with calculated duty cycle
                        self.control.output_on(
                            self.lower_output_id,
                            output_type='pwm',
                            amount=stored_duty_cycle,
                            output_channel=self.lower_output_channel)

                        self.write_pid_output_influxdb(
                            'percent', 'duty_cycle', 7,
                            stored_control_variable)

                    elif self.lower_output_type == 'on_off':
                        # Ensure the output on duration doesn't exceed the set maximum
                        if (self.lower_max_duration
                                and abs(self.PID_Controller.control_variable) >
                                self.lower_max_duration):
                            self.lower_seconds_on = self.lower_max_duration
                        else:
                            self.lower_seconds_on = float("{0:.2f}".format(
                                abs(self.PID_Controller.control_variable)))

                        if self.store_lower_as_negative:
                            stored_amount_on = -abs(self.lower_seconds_on)
                            stored_control_variable = -abs(
                                self.PID_Controller.control_variable)
                        else:
                            stored_amount_on = abs(self.lower_seconds_on)
                            stored_control_variable = abs(
                                self.PID_Controller.control_variable)

                        if self.lower_seconds_on > self.lower_min_duration:
                            # Activate lower_output for a duration
                            self.logger.debug(
                                "Setpoint: {sp} Output: {cv} sec to output {id} CH{ch}"
                                .format(
                                    sp=self.PID_Controller.setpoint,
                                    cv=self.PID_Controller.control_variable,
                                    id=self.lower_output_id,
                                    ch=self.lower_output_channel))

                            self.control.output_on(
                                self.lower_output_id,
                                output_type='sec',
                                amount=stored_amount_on,
                                min_off=self.lower_min_off_duration,
                                output_channel=self.lower_output_channel)

                        self.write_pid_output_influxdb(
                            's', 'duration_time', 6, stored_control_variable)

                    elif self.lower_output_type == 'value':
                        if self.store_lower_as_negative:
                            stored_amount_on = -abs(self.lower_seconds_on)
                            stored_control_variable = -abs(
                                self.PID_Controller.control_variable)
                        else:
                            stored_amount_on = abs(self.lower_seconds_on)
                            stored_control_variable = abs(
                                self.PID_Controller.control_variable)

                        # Activate lower_output for a value
                        self.logger.debug(
                            "Setpoint: {sp} Output: {cv} to output {id} CH{ch}"
                            .format(sp=self.PID_Controller.setpoint,
                                    cv=self.PID_Controller.control_variable,
                                    id=self.lower_output_id,
                                    ch=self.lower_output_channel))

                        self.control.output_on(
                            self.lower_output_id,
                            output_type='value',
                            amount=stored_amount_on,
                            min_off=self.lower_min_off_duration,
                            output_channel=self.lower_output_channel)

                        self.write_pid_output_influxdb(
                            'none', 'unitless', 9, stored_control_variable)

                    elif self.lower_output_type == 'volume':
                        if self.store_lower_as_negative:
                            stored_amount_on = -abs(self.lower_seconds_on)
                            stored_control_variable = -abs(
                                self.PID_Controller.control_variable)
                        else:
                            stored_amount_on = abs(self.lower_seconds_on)
                            stored_control_variable = abs(
                                self.PID_Controller.control_variable)

                        # Activate lower_output for a volume (ml)
                        self.logger.debug(
                            "Setpoint: {sp} Output: {cv} ml to output {id} CH{ch}"
                            .format(sp=self.PID_Controller.setpoint,
                                    cv=self.PID_Controller.control_variable,
                                    id=self.lower_output_id,
                                    ch=self.lower_output_channel))

                        self.control.output_on(
                            self.lower_output_id,
                            output_type='vol',
                            amount=stored_amount_on,
                            min_off=self.lower_min_off_duration,
                            output_channel=self.lower_output_channel)

                        self.write_pid_output_influxdb(
                            'ml', 'volume', 8, stored_control_variable)

                elif self.lower_output_type == 'pwm' and not self.lower_always_min_pwm:
                    # Turn PWM Off if PWM Output and not instructed to always be at least min
                    self.control.output_on(
                        self.lower_output_id,
                        output_type='pwm',
                        amount=0,
                        output_channel=self.lower_output_channel)

        else:
            self.logger.debug(
                "Last measurement unsuccessful. Turning outputs off.")
            if self.PID_Controller.direction in ['raise', 'both'
                                                 ] and self.raise_output_id:
                self.control.output_off(
                    self.raise_output_id,
                    output_channel=self.raise_output_channel)
            if self.PID_Controller.direction in ['lower', 'both'
                                                 ] and self.lower_output_id:
                self.control.output_off(
                    self.lower_output_id,
                    output_channel=self.lower_output_channel)

    def pid_parameters_str(self):
        return "Device ID: {did}, " \
               "Measurement ID: {mid}, " \
               "Direction: {dir}, " \
               "Period: {per}, " \
               "Setpoint: {sp}, " \
               "Band: {band}, " \
               "Kp: {kp}, " \
               "Ki: {ki}, " \
               "Kd: {kd}, " \
               "Integrator Min: {imn}, " \
               "Integrator Max {imx}, " \
               "Output Raise: {opr}, " \
               "Output Raise Channel: {oprc}, " \
               "Output Raise Type: {oprt}, " \
               "Output Raise Min On: {oprmnon}, " \
               "Output Raise Max On: {oprmxon}, " \
               "Output Raise Min Off: {oprmnoff}, " \
               "Output Raise Always Min: {opramn}, " \
               "Output Lower: {opl}, " \
               "Output Lower Channel: {oplc}, " \
               "Output Lower Type: {oplt}, " \
               "Output Lower Min On: {oplmnon}, " \
               "Output Lower Max On: {oplmxon}, " \
               "Output Lower Min Off: {oplmnoff}, " \
               "Output Lower Always Min: {oplamn}, " \
               "Setpoint Tracking Type: {sptt}, " \
               "Setpoint Tracking ID: {spt}".format(
                    did=self.device_id,
                    mid=self.measurement_id,
                    dir=self.PID_Controller.direction,
                    per=self.period,
                    sp=self.PID_Controller.setpoint,
                    band=self.PID_Controller.band,
                    kp=self.PID_Controller.Kp,
                    ki=self.PID_Controller.Ki,
                    kd=self.PID_Controller.Kd,
                    imn=self.PID_Controller.integrator_min,
                    imx=self.PID_Controller.integrator_max,
                    opr=self.raise_output_id,
                    oprc=self.raise_output_channel,
                    oprt=self.raise_output_type,
                    oprmnon=self.raise_min_duration,
                    oprmxon=self.raise_max_duration,
                    oprmnoff=self.raise_min_off_duration,
                    opramn=self.raise_always_min_pwm,
                    opl=self.lower_output_id,
                    oplc=self.lower_output_channel,
                    oplt=self.lower_output_type,
                    oplmnon=self.lower_min_duration,
                    oplmxon=self.lower_max_duration,
                    oplmnoff=self.lower_min_off_duration,
                    oplamn=self.lower_always_min_pwm,
                    sptt=self.setpoint_tracking_type,
                    spt=self.setpoint_tracking_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)

    @staticmethod
    def return_output_channel(output_channel_id):
        output_channel = db_retrieve_table_daemon(OutputChannel,
                                                  unique_id=output_channel_id)
        if output_channel and output_channel.channel is not None:
            return output_channel.channel

    def write_pid_output_influxdb(self, unit, measurement, channel, value):
        write_pid_out_db = threading.Thread(target=write_influxdb_value,
                                            args=(
                                                self.unique_id,
                                                unit,
                                                value,
                                            ),
                                            kwargs={
                                                'measure': measurement,
                                                'channel': channel
                                            })
        write_pid_out_db.start()

    def pid_mod(self):
        if self.initialize_variables():
            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, setpoint):
        """ Set the setpoint of PID """
        self.PID_Controller.setpoint = float(setpoint)
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.unique_id).first()
            mod_pid.setpoint = setpoint
            db_session.commit()
        return "Setpoint set to {sp}".format(sp=setpoint)

    def set_method(self, method_id):
        """ Set the method of PID """
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.unique_id).first()
            mod_pid.setpoint_tracking_id = method_id

            if method_id == '':
                self.setpoint_tracking_id = ''
                db_session.commit()
            else:
                mod_pid.method_start_time = 'Ready'
                mod_pid.method_end_time = None
                db_session.commit()
                self.setup_method(method_id)

        return "Method set to {me}".format(me=method_id)

    def set_integrator(self, integrator):
        """ Set the integrator of the controller """
        self.PID_Controller.integrator = float(integrator)
        return "Integrator set to {i}".format(i=self.PID_Controller.integrator)

    def set_derivator(self, derivator):
        """ Set the derivator of the controller """
        self.PID_Controller.derivator = float(derivator)
        return "Derivator set to {d}".format(d=self.PID_Controller.derivator)

    def set_kp(self, p):
        """ Set Kp gain of the controller """
        self.PID_Controller.Kp = float(p)
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.unique_id).first()
            mod_pid.p = p
            db_session.commit()
        return "Kp set to {kp}".format(kp=self.PID_Controller.Kp)

    def set_ki(self, i):
        """ Set Ki gain of the controller """
        self.PID_Controller.Ki = float(i)
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.unique_id).first()
            mod_pid.i = i
            db_session.commit()
        return "Ki set to {ki}".format(ki=self.PID_Controller.Ki)

    def set_kd(self, d):
        """ Set Kd gain of the controller """
        self.PID_Controller.Kd = float(d)
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.unique_id).first()
            mod_pid.d = d
            db_session.commit()
        return "Kd set to {kd}".format(kd=self.PID_Controller.Kd)

    def get_setpoint(self):
        return self.PID_Controller.setpoint

    def get_setpoint_band(self):
        return self.PID_Controller.setpoint_band

    def get_error(self):
        return self.PID_Controller.error

    def get_integrator(self):
        return self.PID_Controller.integrator

    def get_derivator(self):
        return self.PID_Controller.derivator

    def get_kp(self):
        return self.PID_Controller.Kp

    def get_ki(self):
        return self.PID_Controller.Ki

    def get_kd(self):
        return self.PID_Controller.Kd

    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.setpoint_tracking_type == 'method'
                and self.setpoint_tracking_id != '' and ended_normally):
            with session_scope(MYCODO_DB_PATH) as db_session:
                mod_pid = db_session.query(PID).filter(
                    PID.unique_id == self.unique_id).first()
                mod_pid.method_start_time = 'Ended'
                mod_pid.method_end_time = None
                db_session.commit()

        # Deactivate PID and Autotune
        if deactivate_pid:
            with session_scope(MYCODO_DB_PATH) as db_session:
                mod_pid = db_session.query(PID).filter(
                    PID.unique_id == self.unique_id).first()
                mod_pid.is_activated = False
                mod_pid.autotune_activated = False
                db_session.commit()
コード例 #8
0
ファイル: dht22.py プロジェクト: marianzange/Mycodo
class InputModule(AbstractInput):
    """
    A sensor support class that measures the DHT22's humidity and temperature
    and calculates the dew point

    An adaptation of DHT22 code from https://github.com/joan2937/pigpio

    The sensor is also known as the AM2302.
    The sensor can be powered from the Pi 3.3-volt or 5-volt rail.
    Powering from the 3.3-volt rail is simpler and safer.  You may need
    to power from 5 if the sensor is connected via a long cable.
    For 3.3-volt operation connect pin 1 to 3.3 volts and pin 4 to ground.
    Connect pin 2 to a gpio.
    For 5-volt operation connect pin 1 to the 5 volts and pin 4 to ground.
    The following pin 2 connection works for me.  Use at YOUR OWN RISK.

    5V--5K_resistor--+--10K_resistor--Ground
                     |
    DHT22 pin 2 -----+
                     |
    gpio ------------+

    """
    def __init__(self, input_dev, testing=False):
        """
        Instantiate with the Pi and gpio to which the DHT22 output
        pin is connected.

        Optionally a gpio used to power the sensor may be specified.
        This gpio will be set high to power the sensor.  If the sensor
        locks it will be power cycled to restart the readings.

        Taking readings more often than about once every two seconds will
        eventually cause the DHT22 to hang.  A 3 second interval seems OK.
        """
        super(InputModule, self).__init__()
        self.logger = logging.getLogger('mycodo.inputs.dht22')
        self.temp_temperature = None
        self.temp_humidity = None
        self.temp_dew_point = None
        self.temp_vpd = None
        self.power_output_id = None
        self.powered = False
        self.pi = None

        if not testing:
            import pigpio
            from mycodo.mycodo_client import DaemonControl
            self.logger = logging.getLogger(
                'mycodo.dht22_{id}'.format(id=input_dev.unique_id.split('-')[0]))

            self.device_measurements = db_retrieve_table_daemon(
                DeviceMeasurements).filter(
                    DeviceMeasurements.device_id == input_dev.unique_id)

            self.power_output_id = input_dev.power_output_id

            self.control = DaemonControl()
            self.pigpio = pigpio
            self.pi = self.pigpio.pi()

            self.gpio = int(input_dev.gpio_location)
            self.bad_CS = 0  # Bad checksum count
            self.bad_SM = 0  # Short message count
            self.bad_MM = 0  # Missing message count
            self.bad_SR = 0  # Sensor reset count

            # Power cycle if timeout > MAX_NO_RESPONSE
            self.MAX_NO_RESPONSE = 3
            self.no_response = None
            self.tov = None
            self.high_tick = None
            self.bit = None
            self.either_edge_cb = None

        self.start_sensor()

    def get_measurement(self):
        """ Gets the humidity and temperature """
        return_dict = measurements_dict.copy()

        if not self.pi.connected:  # Check if pigpiod is running
            self.logger.error('Could not connect to pigpiod. '
                              'Ensure it is running and try again.')
            return None, None, None

        # Ensure if the power pin turns off, it is turned back on
        if (self.power_output_id and
                db_retrieve_table_daemon(Output, unique_id=self.power_output_id) and
                self.control.output_state(self.power_output_id) == 'off'):
            self.logger.error(
                'Sensor power output {rel} detected as being off. '
                'Turning on.'.format(rel=self.power_output_id))
            self.start_sensor()
            time.sleep(2)

        # Try twice to get measurement. This prevents an anomaly where
        # the first measurement fails if the sensor has just been powered
        # for the first time.
        for _ in range(4):
            self.measure_sensor()
            if self.temp_dew_point is not None:
                if self.is_enabled(0):
                    return_dict[0]['value'] = self.temp_temperature
                if self.is_enabled(1):
                    return_dict[1]['value'] = self.temp_humidity
                if (self.is_enabled(2) and
                        self.is_enabled(0) and
                        self.is_enabled(1)):
                    return_dict[2]['value'] = self.temp_dew_point
                if (self.is_enabled(3) and
                        self.is_enabled(0) and
                        self.is_enabled(1)):
                    return_dict[3]['value'] = self.temp_vpd
                return return_dict  # success - no errors
            time.sleep(2)

        # Measurement failure, power cycle the sensor (if enabled)
        # Then try two more times to get a measurement
        if self.power_output_id is not None and self.running:
            self.stop_sensor()
            time.sleep(3)
            self.start_sensor()
            for _ in range(2):
                self.measure_sensor()
                if self.temp_dew_point is not None:
                    if self.is_enabled(0):
                        return_dict[0]['value'] = self.temp_temperature
                    if self.is_enabled(1):
                        return_dict[1]['value'] = self.temp_humidity
                    if (self.is_enabled(2) and
                            self.is_enabled(0) and
                            self.is_enabled(1)):
                        return_dict[2]['value'] = self.temp_dew_point
                    if (self.is_enabled(3) and
                            self.is_enabled(0) and
                            self.is_enabled(1)):
                        return_dict[3]['value'] = self.temp_vpd
                    return return_dict  # success - no errors
                time.sleep(2)

        self.logger.debug("Could not acquire a measurement")
        return None

    def measure_sensor(self):
        self.temp_temperature = None
        self.temp_humidity = None
        self.temp_dew_point = None
        self.temp_vpd = None

        initialized = False

        try:
            self.close()
            time.sleep(0.2)
            self.setup()
            time.sleep(0.2)
            initialized = True
        except Exception as except_msg:
            self.logger.error(
                "Could not initialize sensor. Check if it's connected "
                "properly and pigpiod is running. Error: {msg}".format(
                    msg=except_msg))

        if initialized:
            try:
                self.pi.write(self.gpio, self.pigpio.LOW)
                time.sleep(0.017)  # 17 ms
                self.pi.set_mode(self.gpio, self.pigpio.INPUT)
                self.pi.set_watchdog(self.gpio, 200)
                time.sleep(0.2)
                if (self.temp_humidity is not None and
                        self.temp_temperature is not None):
                    self.temp_dew_point = calculate_dewpoint(
                        self.temp_temperature, self.temp_humidity)
                    self.temp_vpd = calculate_vapor_pressure_deficit(
                        self.temp_temperature, self.temp_humidity)
            except Exception as e:
                self.logger.exception(
                    "Exception when taking a reading: {err}".format(
                        err=e))
            finally:
                self.close()

    def setup(self):
        """
        Clears the internal gpio pull-up/down resistor.
        Kills any watchdogs.
        Setup callbacks
        """
        self.no_response = 0
        self.tov = None
        self.high_tick = 0
        self.bit = 40
        self.either_edge_cb = None
        self.pi.set_pull_up_down(self.gpio, self.pigpio.PUD_OFF)
        self.pi.set_watchdog(self.gpio, 0)  # Kill any watchdogs
        self.register_callbacks()

    def register_callbacks(self):
        """ Monitors RISING_EDGE changes using callback """
        self.either_edge_cb = self.pi.callback(self.gpio,
                                               self.pigpio.EITHER_EDGE,
                                               self.either_edge_callback)

    def either_edge_callback(self, gpio, level, tick):
        """
        Either Edge callbacks, called each time the gpio edge changes.
        Accumulate the 40 data bits from the DHT22 sensor.

        Format into 5 bytes, humidity high,
        humidity low, temperature high, temperature low, checksum.
        """
        level_handlers = {
            self.pigpio.FALLING_EDGE: self._edge_fall,
            self.pigpio.RISING_EDGE: self._edge_rise,
            self.pigpio.EITHER_EDGE: self._edge_either
        }
        handler = level_handlers[level]
        diff = self.pigpio.tickDiff(self.high_tick, tick)
        handler(tick, diff)

    def _edge_rise(self, tick, diff):
        """ Handle Rise signal """
        # Edge length determines if bit is 1 or 0.
        if diff >= 50:
            val = 1
            if diff >= 200:  # Bad bit?
                self.CS = 256  # Force bad checksum.
        else:
            val = 0

        if self.bit >= 40:  # Message complete.
            self.bit = 40
        elif self.bit >= 32:  # In checksum byte.
            self.CS = (self.CS << 1) + val
            if self.bit == 39:
                # 40th bit received.
                self.pi.set_watchdog(self.gpio, 0)
                self.no_response = 0
                total = self.hH + self.hL + self.tH + self.tL
                if (total & 255) == self.CS:  # Is checksum ok?
                    self.temp_humidity = ((self.hH << 8) + self.hL) * 0.1
                    if self.tH & 128:  # Negative temperature.
                        mult = -0.1
                        self.tH &= 127
                    else:
                        mult = 0.1
                    self.temp_temperature = ((self.tH << 8) + self.tL) * mult
                    self.tov = time.time()
                else:
                    self.bad_CS += 1
        elif self.bit >= 24:  # in temp low byte
            self.tL = (self.tL << 1) + val
        elif self.bit >= 16:  # in temp high byte
            self.tH = (self.tH << 1) + val
        elif self.bit >= 8:  # in humidity low byte
            self.hL = (self.hL << 1) + val
        elif self.bit >= 0:  # in humidity high byte
            self.hH = (self.hH << 1) + val
        self.bit += 1

    def _edge_fall(self, tick, diff):
        """ Handle Fall signal """
        # Edge length determines if bit is 1 or 0.
        self.high_tick = tick
        if diff <= 250000:
            return
        self.bit = -2
        self.hH = 0
        self.hL = 0
        self.tH = 0
        self.tL = 0
        self.CS = 0

    def _edge_either(self, tick, diff):
        """ Handle Either signal or Timeout """
        self.pi.set_watchdog(self.gpio, 0)
        if self.bit < 8:  # Too few data bits received.
            self.bad_MM += 1  # Bump missing message count.
            self.no_response += 1
            if self.no_response > self.MAX_NO_RESPONSE:
                self.no_response = 0
                self.bad_SR += 1  # Bump sensor reset count.
                if self.power_output_id is not None:
                    self.logger.error(
                        "Invalid data, power cycling sensor.")
                    self.stop_sensor()
                    time.sleep(2)
                    self.start_sensor()
        elif self.bit < 39:  # Short message received.
            self.bad_SM += 1  # Bump short message count.
            self.no_response = 0
        else:  # Full message received.
            self.no_response = 0

    def staleness(self):
        """ Return time since measurement made """
        if self.tov is not None:
            return time.time() - self.tov
        else:
            return -999

    def bad_checksum(self):
        """ Return count of messages received with bad checksums """
        return self.bad_CS

    def short_message(self):
        """ Return count of short messages """
        return self.bad_SM

    def missing_message(self):
        """ Return count of missing messages """
        return self.bad_MM

    def sensor_resets(self):
        """ Return count of power cycles because of sensor hangs """
        return self.bad_SR

    def close(self):
        """ Stop reading sensor, remove callbacks """
        self.pi.set_watchdog(self.gpio, 0)
        if self.either_edge_cb:
            self.either_edge_cb.cancel()
            self.either_edge_cb = None

    def start_sensor(self):
        """ Turn the sensor on """
        if self.power_output_id:
            self.logger.info("Turning on sensor")
            self.control.output_on(self.power_output_id, 0)
            time.sleep(2)
            self.powered = True

    def stop_sensor(self):
        """ Turn the sensor off """
        if self.power_output_id:
            self.logger.info("Turning off sensor")
            self.control.output_off(self.power_output_id)
            self.powered = False
コード例 #9
0
class CustomModule(AbstractFunction):
    """
    Class to operate custom controller
    """
    def __init__(self, function, testing=False):
        super(CustomModule, self).__init__(function,
                                           testing=testing,
                                           name=__name__)

        self.control_variable = None
        self.timestamp = None
        self.control = DaemonControl()
        self.outputIsOn = False
        self.timer_loop = time.time()

        # Initialize custom options
        self.measurement_device_id = None
        self.measurement_measurement_id = None
        self.output_device_id = None
        self.output_measurement_id = None
        self.output_channel_id = None
        self.setpoint = None
        self.hysteresis = None
        self.direction = None
        self.output_channel = None
        self.update_period = None

        # Set custom options
        custom_function = db_retrieve_table_daemon(CustomController,
                                                   unique_id=self.unique_id)
        self.setup_custom_options(FUNCTION_INFORMATION['custom_options'],
                                  custom_function)

        if not testing:
            self.initialize_variables()

    def initialize_variables(self):
        self.timestamp = time.time()

        self.output_channel = self.get_output_channel_from_channel_id(
            self.output_channel_id)

        self.logger.info(
            "Bang-Bang controller started with options: "
            "Measurement Device: {}, Measurement: {}, Output: {}, "
            "Output_Channel: {}, Setpoint: {}, Hysteresis: {}, "
            "Direction: {}, Period: {}".format(
                self.measurement_device_id, self.measurement_measurement_id,
                self.output_device_id, self.output_channel, self.setpoint,
                self.hysteresis, self.direction, self.update_period))

    def loop(self):
        if self.timer_loop > time.time():
            return

        while self.timer_loop < time.time():
            self.timer_loop += self.update_period

        if self.output_channel is None:
            self.logger.error(
                "Cannot run bang-bang controller: Check output channel.")
            return

        last_measurement = self.get_last_measurement(
            self.measurement_device_id, self.measurement_measurement_id)[1]
        outputState = self.control.output_state(self.output_device_id,
                                                self.output_channel)

        self.logger.info("Input: {}, output: {}, target: {}, hyst: {}".format(
            last_measurement, outputState, self.setpoint, self.hysteresis))

        if self.direction == 'raise':
            if last_measurement > (self.setpoint + self.hysteresis):
                if outputState == 'on':
                    self.control.output_off(self.output_device_id,
                                            output_channel=self.output_channel)
            else:
                if last_measurement < (self.setpoint - self.hysteresis):
                    self.control.output_on(self.output_device_id,
                                           output_channel=self.output_channel)
        elif self.direction == 'lower':
            if last_measurement < (self.setpoint - self.hysteresis):
                if outputState == 'on':
                    self.control.output_off(self.output_device_id,
                                            output_channel=self.output_channel)
            else:
                if last_measurement > (self.setpoint + self.hysteresis):
                    self.control.output_on(self.output_device_id,
                                           output_channel=self.output_channel)
        else:
            self.logger.info("Unknown controller direction: '{}'".format(
                self.direction))

    def stop_function(self):
        self.control.output_off(self.output_device_id, self.output_channel)
コード例 #10
0
ファイル: camera.py プロジェクト: ciscomonkey/Mycodo
def camera_record(record_type, unique_id, duration_sec=None, tmp_filename=None):
    """
    Record still image from cameras
    :param record_type:
    :param unique_id:
    :param duration_sec:
    :param tmp_filename:
    :return:
    """
    daemon_control = None
    settings = db_retrieve_table_daemon(Camera, unique_id=unique_id)
    timestamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
    assure_path_exists(PATH_CAMERAS)
    camera_path = assure_path_exists(
        os.path.join(PATH_CAMERAS, '{uid}'.format(uid=settings.unique_id)))
    if record_type == 'photo':
        if settings.path_still != '':
            save_path = settings.path_still
        else:
            save_path = assure_path_exists(os.path.join(camera_path, 'still'))
        filename = 'Still-{cam_id}-{cam}-{ts}.jpg'.format(
            cam_id=settings.id,
            cam=settings.name,
            ts=timestamp).replace(" ", "_")
    elif record_type == 'timelapse':
        if settings.path_timelapse != '':
            save_path = settings.path_timelapse
        else:
            save_path = assure_path_exists(os.path.join(camera_path, 'timelapse'))
        start = datetime.datetime.fromtimestamp(
            settings.timelapse_start_time).strftime("%Y-%m-%d_%H-%M-%S")
        filename = 'Timelapse-{cam_id}-{cam}-{st}-img-{cn:05d}.jpg'.format(
            cam_id=settings.id,
            cam=settings.name,
            st=start,
            cn=settings.timelapse_capture_number).replace(" ", "_")
    elif record_type == 'video':
        if settings.path_video != '':
            save_path = settings.path_video
        else:
            save_path = assure_path_exists(os.path.join(camera_path, 'video'))
        filename = 'Video-{cam}-{ts}.h264'.format(
            cam=settings.name,
            ts=timestamp).replace(" ", "_")
    else:
        return

    assure_path_exists(save_path)

    if tmp_filename:
        filename = tmp_filename

    path_file = os.path.join(save_path, filename)

    # Turn on output, if configured
    if settings.output_id:
        daemon_control = DaemonControl()
        daemon_control.output_on(settings.output_id)

    # Pause while the output remains on for the specified duration.
    # Used for instance to allow fluorescent lights to fully turn on before
    # capturing an image.
    if settings.output_duration:
        time.sleep(settings.output_duration)

    if settings.library == 'picamera':
        # Try 5 times to access the pi camera (in case another process is accessing it)
        for _ in range(5):
            try:
                with picamera.PiCamera() as camera:
                    camera.resolution = (settings.width, settings.height)
                    camera.hflip = settings.hflip
                    camera.vflip = settings.vflip
                    camera.rotation = settings.rotation
                    camera.brightness = int(settings.brightness)
                    camera.contrast = int(settings.contrast)
                    camera.exposure_compensation = int(settings.exposure)
                    camera.saturation = int(settings.saturation)
                    camera.start_preview()
                    time.sleep(2)  # Camera warm-up time

                    if record_type in ['photo', 'timelapse']:
                        camera.capture(path_file, use_video_port=False)
                    elif record_type == 'video':
                        camera.start_recording(path_file, format='h264', quality=20)
                        camera.wait_recording(duration_sec)
                        camera.stop_recording()
                    else:
                        return
                    break
            except picamera.exc.PiCameraMMALError:
                logger.error("The camera is already open by picamera. Retrying 4 times.")
            time.sleep(1)

    elif settings.library == 'fswebcam':
        cmd = "/usr/bin/fswebcam --device {dev} --resolution {w}x{h} --set brightness={bt}% " \
              "--no-banner --save {file}".format(dev=settings.device,
                                                 w=settings.width,
                                                 h=settings.height,
                                                 bt=settings.brightness,
                                                 file=path_file)
        if settings.hflip:
            cmd += " --flip h"
        if settings.vflip:
            cmd += " --flip h"
        if settings.rotation:
            cmd += " --rotate {angle}".format(angle=settings.rotation)
        if settings.custom_options:
            cmd += " " + settings.custom_options

        out, err, status = cmd_output(cmd, stdout_pipe=False)
        # logger.error("TEST01: {}; {}; {}; {}".format(cmd, out, err, status))

    # Turn off output, if configured
    if settings.output_id and daemon_control:
        daemon_control.output_off(settings.output_id)

    try:
        set_user_grp(path_file, 'mycodo', 'mycodo')
        return save_path, filename
    except Exception as e:
        logger.exception(
            "Exception raised in 'camera_record' when setting user grp: "
            "{err}".format(err=e))
コード例 #11
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.split('-')[0]))

        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.sample_rate = db_retrieve_table_daemon(
            Misc, entry='first').sample_rate_controller_pid

        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.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_setpoint = None
        self.setpoint = None
        self.store_lower_as_negative = None

        # Hysteresis options
        self.band = None
        self.allow_raising = False
        self.allow_lowering = False

        self.dev_unique_id = None
        self.input_duration = None

        self.raise_output_type = None
        self.lower_output_type = None

        self.first_start = True
        self.timer = 0

        self.initialize_values()

        # Check if a method is set for this PID
        self.method_start_act = None
        if self.method_id != '':
            self.setup_method(self.method_id)

    def run(self):
        try:
            self.running = True
            startup_str = "Activated in {:.1f} ms".format(
                (timeit.default_timer() - self.thread_startup_timer) * 1000)
            if self.is_paused:
                startup_str += ", started Paused"
            elif self.is_held:
                startup_str += ", started Held"
            self.logger.info(startup_str)
            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 time.time() > self.timer:
                    self.check_pid()

                time.sleep(self.sample_rate)
        except Exception as except_msg:
            self.logger.exception("Run Error: {err}".format(err=except_msg))
        finally:
            # Turn off output used in PID when the controller is deactivated
            if self.raise_output_id and self.direction in ['raise', 'both']:
                self.control.output_off(self.raise_output_id,
                                        trigger_conditionals=True)
            if self.lower_output_id and self.direction in ['lower', 'both']:
                self.control.output_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))

    def initialize_values(self):
        """Set PID parameters"""
        pid = db_retrieve_table_daemon(PID, unique_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_output_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_output_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_setpoint = pid.setpoint
        self.setpoint = pid.setpoint
        self.band = pid.band
        self.store_lower_as_negative = pid.store_lower_as_negative

        dev_unique_id = pid.measurement.split(',')[0]
        self.measurement = pid.measurement.split(',')[1]

        input_dev = db_retrieve_table_daemon(Input, unique_id=dev_unique_id)
        math = db_retrieve_table_daemon(Math, unique_id=dev_unique_id)
        if input_dev:
            self.dev_unique_id = input_dev.unique_id
            self.input_duration = input_dev.period
        elif math:
            self.dev_unique_id = math.unique_id
            self.input_duration = math.period

        try:
            self.raise_output_type = db_retrieve_table_daemon(
                Output, unique_id=self.raise_output_id).output_type
        except AttributeError:
            self.raise_output_type = None
        try:
            self.lower_output_type = db_retrieve_table_daemon(
                Output, unique_id=self.lower_output_id).output_type
        except AttributeError:
            self.lower_output_type = None

        return "success"

    def check_pid(self):
        """ Get measurement and apply to PID controller """
        # Ensure the timer ends in the future
        while time.time() > self.timer:
            self.timer = self.timer + self.period

        # If PID is active, retrieve measurement and update
        # the control variable.
        # A PID on hold will sustain the current output and
        # not update the control variable.
        if self.is_activated and (not self.is_paused or not self.is_held):
            self.get_last_measurement()

            if self.last_measurement_success:
                if self.method_id != '':
                    # Update setpoint using a method
                    this_pid = db_retrieve_table_daemon(PID,
                                                        unique_id=self.pid_id)
                    setpoint, ended = calculate_method_setpoint(
                        self.method_id, PID, this_pid, Method, MethodData,
                        self.logger)
                    if ended:
                        self.method_start_act = 'Ended'
                    if setpoint is not None:
                        self.setpoint = setpoint
                    else:
                        self.setpoint = self.default_setpoint

                self.write_setpoint_band()  # Write variables to database

                # Calculate new control variable
                self.control_variable = self.update_pid_output(
                    self.last_measurement)

                self.write_pid_values()  # Write variables to database

        # Is PID in a state that allows manipulation of outputs
        if self.is_activated and (not self.is_paused or self.is_held):
            self.manipulate_output()

    def setup_method(self, method_id):
        """ Initialize method variables to start running a method """
        self.method_id = ''

        method = db_retrieve_table_daemon(Method, unique_id=method_id)
        method_data = db_retrieve_table_daemon(MethodData)
        method_data = method_data.filter(MethodData.method_id == method_id)
        method_data_repeat = method_data.filter(
            MethodData.duration_sec == 0).first()
        pid = db_retrieve_table_daemon(PID, unique_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).filter(
                        PID.unique_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=method_id,
                                                start=self.method_start_time,
                                                end=self.method_end_time))
                    else:
                        self.method_start_act = 'Ended'
                else:
                    self.method_start_act = 'Ended'

            self.method_id = method_id

    def write_setpoint_band(self):
        """ Write setpoint and band values to measurement database """
        write_setpoint_db = threading.Thread(target=write_influxdb_value,
                                             args=(
                                                 self.pid_id,
                                                 'setpoint',
                                                 self.setpoint,
                                             ))
        write_setpoint_db.start()

        if self.band:
            band_min = self.setpoint - self.band
            write_setpoint_db = threading.Thread(target=write_influxdb_value,
                                                 args=(
                                                     self.pid_id,
                                                     'setpoint_band_min',
                                                     band_min,
                                                 ))
            write_setpoint_db.start()

            band_max = self.setpoint + self.band
            write_setpoint_db = threading.Thread(target=write_influxdb_value,
                                                 args=(
                                                     self.pid_id,
                                                     'setpoint_band_max',
                                                     band_max,
                                                 ))
            write_setpoint_db.start()

    def write_pid_values(self):
        """ Write p, i, and d values to the measurement database """
        write_setpoint_db = threading.Thread(target=write_influxdb_value,
                                             args=(
                                                 self.pid_id,
                                                 'pid_p_value',
                                                 self.P_value,
                                             ))
        write_setpoint_db.start()

        write_setpoint_db = threading.Thread(target=write_influxdb_value,
                                             args=(
                                                 self.pid_id,
                                                 'pid_i_value',
                                                 self.I_value,
                                             ))
        write_setpoint_db.start()

        write_setpoint_db = threading.Thread(target=write_influxdb_value,
                                             args=(
                                                 self.pid_id,
                                                 'pid_d_value',
                                                 self.D_value,
                                             ))
        write_setpoint_db.start()

    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
        """
        # Determine if hysteresis is enabled and if the PID should be applied
        setpoint = self.check_hysteresis(current_value)

        if setpoint is None:
            # Prevent PID variables form being manipulated and
            # restrict PID from operating.
            return 0

        self.error = setpoint - 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

        # Prevent large initial D-value
        if self.first_start:
            self.derivator = self.error
            self.first_start = False

        # 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 check_hysteresis(self, measure):
        """
        Determine if hysteresis is enabled and if the PID should be applied

        :return: float if the setpoint if the PID should be applied, None to
            restrict the PID
        :rtype: float or None

        :param measure: The PID input (or process) variable
        :type measure: float
        """
        if self.band == 0:
            # If band is disabled, return setpoint
            return self.setpoint

        band_min = self.setpoint - self.band
        band_max = self.setpoint + self.band

        if self.direction == 'raise':
            if (measure < band_min
                    or (band_min < measure < band_max and self.allow_raising)):
                self.allow_raising = True
                setpoint = band_max  # New setpoint
                return setpoint  # Apply the PID
            elif measure > band_max:
                self.allow_raising = False
            return None  # Restrict the PID

        elif self.direction == 'lower':
            if (measure > band_max or
                (band_min < measure < band_max and self.allow_lowering)):
                self.allow_lowering = True
                setpoint = band_min  # New setpoint
                return setpoint  # Apply the PID
            elif measure < band_min:
                self.allow_lowering = False
            return None  # Restrict the PID

        elif self.direction == 'both':
            if measure < band_min:
                setpoint = band_min  # New setpoint
                if not self.allow_raising:
                    # Reset integrator and derivator upon direction switch
                    self.integrator = 0.0
                    self.derivator = 0.0
                    self.allow_raising = True
                    self.allow_lowering = False
            elif measure > band_max:
                setpoint = band_max  # New setpoint
                if not self.allow_lowering:
                    # Reset integrator and derivator upon direction switch
                    self.integrator = 0.0
                    self.derivator = 0.0
                    self.allow_raising = False
                    self.allow_lowering = True
            else:
                return None  # Restrict the PID
            return setpoint  # Apply the PID

    def get_last_measurement(self):
        """
        Retrieve the latest input measurement from InfluxDB

        :rtype: None
        """
        self.last_measurement_success = False
        # Get latest measurement from influxdb
        try:
            self.last_measurement = read_last_influxdb(
                self.dev_unique_id, self.measurement,
                int(self.max_measure_age))
            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(
                        time.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(time.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:
                    # 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.setpoint,
                                cv=self.control_variable,
                                id=self.raise_output_id,
                                dc=self.raise_duty_cycle))

                        # Activate pwm with calculated duty cycle
                        self.control.output_on(
                            self.raise_output_id,
                            duty_cycle=self.raise_duty_cycle)

                        self.write_pid_output_influxdb(
                            'duty_cycle',
                            self.control_var_to_duty_cycle(
                                self.control_variable))

                    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.setpoint,
                                              cv=self.control_variable,
                                              id=self.raise_output_id))
                            self.control.output_on(
                                self.raise_output_id,
                                duration=self.raise_seconds_on,
                                min_off=self.raise_min_off_duration)

                        self.write_pid_output_influxdb('duration_sec',
                                                       self.control_variable)

                else:
                    if self.raise_output_type == 'pwm':
                        self.control.output_on(self.raise_output_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_output_id:

                if self.control_variable < 0:
                    # 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.setpoint,
                                cv=self.control_variable,
                                id=self.lower_output_id,
                                dc=self.lower_duty_cycle))

                        if self.store_lower_as_negative:
                            stored_duty_cycle = -abs(self.lower_duty_cycle)
                            stored_control_variable = -self.control_var_to_duty_cycle(
                                abs(self.control_variable))
                        else:
                            stored_duty_cycle = abs(self.lower_duty_cycle)
                            stored_control_variable = self.control_var_to_duty_cycle(
                                abs(self.control_variable))

                        # Activate pwm with calculated duty cycle
                        self.control.output_on(self.lower_output_id,
                                               duty_cycle=stored_duty_cycle)

                        self.write_pid_output_influxdb(
                            'duty_cycle', stored_control_variable)

                    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(
                                abs(self.control_variable)))

                        if self.store_lower_as_negative:
                            stored_seconds_on = -abs(self.lower_seconds_on)
                            stored_control_variable = -abs(
                                self.control_variable)
                        else:
                            stored_seconds_on = abs(self.lower_seconds_on)
                            stored_control_variable = abs(
                                self.control_variable)

                        if 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.setpoint,
                                                  cv=self.control_variable,
                                                  id=self.lower_output_id))

                            self.control.output_on(
                                self.lower_output_id,
                                duration=stored_seconds_on,
                                min_off=self.lower_min_off_duration)

                        self.write_pid_output_influxdb(
                            'duration_sec', stored_control_variable)

                else:
                    if self.lower_output_type == 'pwm':
                        self.control.output_on(self.lower_output_id,
                                               duty_cycle=0)

        else:
            if self.direction in ['raise', 'both'] and self.raise_output_id:
                self.control.output_off(self.raise_output_id)
            if self.direction in ['lower', 'both'] and self.lower_output_id:
                self.control.output_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_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, setpoint):
        """ Set the setpoint of PID """
        self.setpoint = float(setpoint)
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.pid_id).first()
            mod_pid.setpoint = setpoint
            db_session.commit()
        return "Setpoint set to {sp}".format(sp=setpoint)

    def set_method(self, method_id):
        """ Set the method of PID """
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.pid_id).first()
            mod_pid.method_id = method_id

            if method_id == '':
                self.method_id = ''
                db_session.commit()
            else:
                mod_pid.method_start_time = 'Ready'
                mod_pid.method_end_time = None
                db_session.commit()
                self.setup_method(method_id)

        return "Method set to {me}".format(me=method_id)

    def set_integrator(self, integrator):
        """ Set the integrator of the controller """
        self.integrator = float(integrator)
        return "Integrator set to {i}".format(i=self.integrator)

    def set_derivator(self, derivator):
        """ Set the derivator of the controller """
        self.derivator = float(derivator)
        return "Derivator set to {d}".format(d=self.derivator)

    def set_kp(self, p):
        """ Set Kp gain of the controller """
        self.Kp = float(p)
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.pid_id).first()
            mod_pid.p = p
            db_session.commit()
        return "Kp set to {kp}".format(kp=self.Kp)

    def set_ki(self, i):
        """ Set Ki gain of the controller """
        self.Ki = float(i)
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.pid_id).first()
            mod_pid.i = i
            db_session.commit()
        return "Ki set to {ki}".format(ki=self.Ki)

    def set_kd(self, d):
        """ Set Kd gain of the controller """
        self.Kd = float(d)
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.pid_id).first()
            mod_pid.d = d
            db_session.commit()
        return "Kd set to {kd}".format(kd=self.Kd)

    def get_setpoint(self):
        return self.setpoint

    def get_error(self):
        return self.error

    def get_integrator(self):
        return self.integrator

    def get_derivator(self):
        return self.derivator

    def get_kp(self):
        return self.Kp

    def get_ki(self):
        return self.Ki

    def get_kd(self):
        return self.Kd

    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.unique_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.unique_id == self.pid_id).first()
                mod_pid.is_activated = False
                db_session.commit()
コード例 #12
0
class InputModule(AbstractInput):
    """
    A sensor support class that measures the DHT11's humidity and temperature
    and calculates the dew point

    The DHT11 class is a stripped version of the DHT22 sensor code by joan2937.
    You can find the initial implementation here:
    - https://github.com/srounet/pigpio/tree/master/EXAMPLES/Python/DHT22_AM2302_SENSOR

    """
    def __init__(self, input_dev, testing=False):
        """
        :param gpio: gpio pin number
        :type gpio: int
        :param power: Power pin number
        :type power: int

        Instantiate with the Pi and gpio to which the DHT11 output
        pin is connected.

        Optionally a gpio used to power the sensor may be specified.
        This gpio will be set high to power the sensor.

        """
        super(InputModule, self).__init__()
        self.logger = logging.getLogger('mycodo.inputs.dht11')
        self.temp_temperature = 0
        self.temp_humidity = 0
        self.temp_dew_point = None
        self.temp_vpd = None
        self.power_output_id = None
        self.powered = False

        if not testing:
            import pigpio
            from mycodo.mycodo_client import DaemonControl
            self.logger = logging.getLogger('mycodo.dht11_{id}'.format(
                id=input_dev.unique_id.split('-')[0]))

            self.device_measurements = db_retrieve_table_daemon(
                DeviceMeasurements).filter(
                    DeviceMeasurements.device_id == input_dev.unique_id)

            self.gpio = int(input_dev.gpio_location)
            self.power_output_id = input_dev.power_output_id

            self.control = DaemonControl()
            self.pigpio = pigpio
            self.pi = self.pigpio.pi()

            self.high_tick = None
            self.bit = None
            self.either_edge_cb = None

        self.start_sensor()

    def get_measurement(self):
        """ Gets the humidity and temperature """
        return_dict = measurements_dict.copy()

        if not self.pi.connected:  # Check if pigpiod is running
            self.logger.error("Could not connect to pigpiod."
                              "Ensure it is running and try again.")
            return None, None, None

        import pigpio
        self.pigpio = pigpio

        # Ensure if the power pin turns off, it is turned back on
        if (self.power_output_id and db_retrieve_table_daemon(
                Output, unique_id=self.power_output_id)
                and self.control.output_state(self.power_output_id) == 'off'):
            self.logger.error(
                'Sensor power output {rel} detected as being off. '
                'Turning on.'.format(rel=self.power_output_id))
            self.start_sensor()
            time.sleep(2)

        # Try twice to get measurement. This prevents an anomaly where
        # the first measurement fails if the sensor has just been powered
        # for the first time.
        for _ in range(2):
            self.measure_sensor()
            if self.temp_dew_point is not None:
                if self.is_enabled(0):
                    return_dict[0]['value'] = self.temp_temperature
                if self.is_enabled(1):
                    return_dict[1]['value'] = self.temp_humidity
                if (self.is_enabled(2) and self.is_enabled(0)
                        and self.is_enabled(1)):
                    return_dict[2]['value'] = self.temp_dew_point
                if (self.is_enabled(3) and self.is_enabled(0)
                        and self.is_enabled(1)):
                    return_dict[3]['value'] = self.temp_vpd
                return return_dict  # success - no errors
            time.sleep(2)

        # Measurement failure, power cycle the sensor (if enabled)
        # Then try two more times to get a measurement
        if self.power_output_id is not None and self.running:
            self.stop_sensor()
            time.sleep(2)
            self.start_sensor()
            for _ in range(2):
                self.measure_sensor()
                if self.temp_dew_point is not None:
                    if self.is_enabled(0):
                        return_dict[0]['value'] = self.temp_temperature
                    if self.is_enabled(1):
                        return_dict[1]['value'] = self.temp_humidity
                    if (self.is_enabled(2) and self.is_enabled(0)
                            and self.is_enabled(1)):
                        return_dict[2]['value'] = self.temp_dew_point
                    if (self.is_enabled(3) and self.is_enabled(0)
                            and self.is_enabled(1)):
                        return_dict[3]['value'] = self.temp_vpd
                    return return_dict  # success - no errors
                time.sleep(2)

        self.logger.error("Could not acquire a measurement")
        return None

    def measure_sensor(self):
        self.temp_temperature = 0
        self.temp_humidity = 0
        self.temp_dew_point = None
        self.temp_vpd = None

        try:
            try:
                self.setup()
            except Exception as except_msg:
                self.logger.error(
                    'Could not initialize sensor. Check if gpiod is running. '
                    'Error: {msg}'.format(msg=except_msg))
            self.pi.write(self.gpio, self.pigpio.LOW)
            time.sleep(0.017)  # 17 ms
            self.pi.set_mode(self.gpio, self.pigpio.INPUT)
            self.pi.set_watchdog(self.gpio, 200)
            time.sleep(0.2)
            if self.temp_humidity != 0:
                self.temp_dew_point = calculate_dewpoint(
                    self.temp_temperature, self.temp_humidity)
                self.temp_vpd = calculate_vapor_pressure_deficit(
                    self.temp_temperature, self.temp_humidity)
        except Exception as e:
            self.logger.error(
                "Exception raised when taking a reading: {err}".format(err=e))
        finally:
            self.close()
            return (self.temp_dew_point, self.temp_humidity,
                    self.temp_temperature)

    def setup(self):
        """
        Clears the internal gpio pull-up/down resistor.
        Kills any watchdogs.
        Setup callbacks
        """
        self.high_tick = 0
        self.bit = 40
        self.either_edge_cb = None
        self.pi.set_pull_up_down(self.gpio, self.pigpio.PUD_OFF)
        self.pi.set_watchdog(self.gpio, 0)
        self.register_callbacks()

    def register_callbacks(self):
        """ Monitors RISING_EDGE changes using callback """
        self.either_edge_cb = self.pi.callback(self.gpio,
                                               self.pigpio.EITHER_EDGE,
                                               self.either_edge_callback)

    def either_edge_callback(self, gpio, level, tick):
        """
        Either Edge callbacks, called each time the gpio edge changes.
        Accumulate the 40 data bits from the DHT11 sensor.
        """
        level_handlers = {
            self.pigpio.FALLING_EDGE: self._edge_fall,
            self.pigpio.RISING_EDGE: self._edge_rise,
            self.pigpio.EITHER_EDGE: self._edge_either
        }
        handler = level_handlers[level]
        diff = self.pigpio.tickDiff(self.high_tick, tick)
        handler(tick, diff)

    def _edge_rise(self, tick, diff):
        """ Handle Rise signal """
        val = 0
        if diff >= 50:
            val = 1
        if diff >= 200:  # Bad bit?
            self.checksum = 256  # Force bad checksum
        if self.bit >= 40:  # Message complete
            self.bit = 40
        elif self.bit >= 32:  # In checksum byte
            self.checksum = (self.checksum << 1) + val
            if self.bit == 39:
                # 40th bit received
                self.pi.set_watchdog(self.gpio, 0)
                total = self.temp_humidity + self.temp_temperature
                # is checksum ok ?
                if not (total & 255) == self.checksum:
                    # For some reason the port from python 2 to python 3 causes
                    # this bad checksum error to happen during every read
                    # TODO: Investigate how to properly check the checksum in python 3
                    self.logger.debug(
                        "Exception raised when taking a reading: "
                        "Bad Checksum.")
        elif 16 <= self.bit < 24:  # in temperature byte
            self.temp_temperature = (self.temp_temperature << 1) + val
        elif 0 <= self.bit < 8:  # in humidity byte
            self.temp_humidity = (self.temp_humidity << 1) + val
        self.bit += 1

    def _edge_fall(self, tick, diff):
        """ Handle Fall signal """
        self.high_tick = tick
        if diff <= 250000:
            return
        self.bit = -2
        self.checksum = 0
        self.temp_temperature = 0
        self.temp_humidity = 0

    def _edge_either(self, tick, diff):
        """ Handle Either signal """
        self.pi.set_watchdog(self.gpio, 0)

    def close(self):
        """ Stop reading sensor, remove callbacks """
        self.pi.set_watchdog(self.gpio, 0)
        if self.either_edge_cb:
            self.either_edge_cb.cancel()
            self.either_edge_cb = None

    def start_sensor(self):
        """ Power the sensor """
        if self.power_output_id:
            self.logger.info("Turning on sensor")
            self.control.output_on(self.power_output_id, 0)
            time.sleep(2)
            self.powered = True

    def stop_sensor(self):
        """ Depower the sensor """
        if self.power_output_id:
            self.logger.info("Turning off sensor")
            self.control.output_off(self.power_output_id)
            self.powered = False
コード例 #13
0
def output_on_off(form_output):
    action = '{action} {controller}'.format(action=gettext("Actuate"),
                                            controller=gettext("Output"))
    error = []

    try:
        control = DaemonControl()
        output = Output.query.filter_by(
            unique_id=form_output.output_id.data).first()
        if output.output_type == 'wired' and int(
                form_output.output_pin.data) == 0:
            error.append(gettext("Cannot modulate output with a GPIO of 0"))
        elif form_output.on_submit.data:
            if output.output_type in [
                    'wired', 'wireless_433MHz_pi_switch', 'command'
            ]:
                if float(form_output.sec_on.data) <= 0:
                    error.append(gettext("Value must be greater than 0"))
                else:
                    return_value = control.output_on(
                        form_output.output_id.data,
                        duration=float(form_output.sec_on.data))
                    flash(
                        gettext(
                            "Output turned on for %(sec)s seconds: %(rvalue)s",
                            sec=form_output.sec_on.data,
                            rvalue=return_value), "success")
            if output.output_type == 'pwm':
                if int(form_output.output_pin.data) == 0:
                    error.append(gettext("Invalid pin"))
                if output.pwm_hertz <= 0:
                    error.append(gettext("PWM Hertz must be a positive value"))
                if float(form_output.pwm_duty_cycle_on.data) <= 0:
                    error.append(
                        gettext("PWM duty cycle must be a positive value"))
                if not error:
                    return_value = control.output_on(
                        form_output.output_id.data,
                        duty_cycle=float(form_output.pwm_duty_cycle_on.data))
                    flash(
                        gettext(
                            "PWM set to %(dc)s%% at %(hertz)s Hz: %(rvalue)s",
                            dc=float(form_output.pwm_duty_cycle_on.data),
                            hertz=output.pwm_hertz,
                            rvalue=return_value), "success")
        elif form_output.turn_on.data:
            return_value = control.output_on(form_output.output_id.data, 0)
            flash(gettext("Output turned on: %(rvalue)s", rvalue=return_value),
                  "success")
        elif form_output.turn_off.data:
            return_value = control.output_off(form_output.output_id.data)
            flash(
                gettext("Output turned off: %(rvalue)s", rvalue=return_value),
                "success")
    except ValueError as except_msg:
        error.append('{err}: {msg}'.format(err=gettext("Invalid value"),
                                           msg=except_msg))
    except Exception as except_msg:
        error.append(except_msg)

    flash_success_errors(error, action, url_for('routes_page.page_output'))
コード例 #14
0
ファイル: controller_pid.py プロジェクト: ciscomonkey/Mycodo
class PIDController(threading.Thread):
    """
    Class to operate discrete PID controller in Mycodo

    """
    def __init__(self, ready, pid_id):
        threading.Thread.__init__(self)

        self.logger = logging.getLogger("mycodo.pid_{id}".format(
            id=pid_id.split('-')[0]))

        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.sample_rate = db_retrieve_table_daemon(
            Misc, entry='first').sample_rate_controller_pid

        self.device_measurements = db_retrieve_table_daemon(DeviceMeasurements)

        self.PID_Controller = None
        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.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.start_offset = None
        self.max_measure_age = None
        self.default_setpoint = None
        self.setpoint = None
        self.store_lower_as_negative = None

        # Hysteresis options
        self.band = None
        self.allow_raising = False
        self.allow_lowering = False

        # PID Autotune
        self.autotune = None
        self.autotune_activated = False
        self.autotune_debug = False
        self.autotune_noiseband = None
        self.autotune_outstep = None
        self.autotune_timestamp = None

        self.device_id = None
        self.measurement_id = None

        self.input_duration = None

        self.raise_output_type = None
        self.lower_output_type = None

        self.first_start = True

        self.initialize_values()

        self.timer = time.time() + self.start_offset

        # Check if a method is set for this PID
        self.method_type = None
        self.method_start_act = None
        self.method_start_time = None
        self.method_end_time = None
        if self.method_id != '':
            self.setup_method(self.method_id)

    def run(self):
        try:
            self.running = True
            startup_str = "Activated in {time:.1f} ms".format(
                time=(timeit.default_timer() - self.thread_startup_timer) * 1000)
            if self.is_paused:
                startup_str += ", started Paused"
            elif self.is_held:
                startup_str += ", started Held"
            self.logger.info(startup_str)

            # Initialize PID Controller
            self.PID_Controller = PIDControl(
                self.period,
                self.Kp, self.Ki, self.Kd,
                integrator_min=self.integrator_min,
                integrator_max=self.integrator_max)

            # If activated, initialize PID Autotune
            if self.autotune_activated:
                self.autotune_timestamp = time.time()
                try:
                    self.autotune = PIDAutotune(
                        self.setpoint,
                        out_step=self.autotune_outstep,
                        sampletime=self.period,
                        out_min=0,
                        out_max=self.period,
                        noiseband=self.autotune_noiseband)
                except Exception as msg:
                    self.logger.error(msg)
                    self.stop_controller(deactivate_pid=True)

            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 time.time() > self.timer:
                    self.check_pid()

                time.sleep(self.sample_rate)
        except Exception as except_msg:
            self.logger.exception("Run Error: {err}".format(
                err=except_msg))
        finally:
            # Turn off output used in PID when the controller is deactivated
            if self.raise_output_id and self.direction in ['raise', 'both']:
                self.control.output_off(self.raise_output_id, trigger_conditionals=True)
            if self.lower_output_id and self.direction in ['lower', 'both']:
                self.control.output_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))

    def initialize_values(self):
        """Set PID parameters"""
        pid = db_retrieve_table_daemon(PID, unique_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_output_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_output_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.start_offset = pid.start_offset
        self.max_measure_age = pid.max_measure_age
        self.default_setpoint = pid.setpoint
        self.setpoint = pid.setpoint
        self.band = pid.band
        self.store_lower_as_negative = pid.store_lower_as_negative

        # Autotune
        self.autotune_activated = pid.autotune_activated
        self.autotune_noiseband = pid.autotune_noiseband
        self.autotune_outstep = pid.autotune_outstep

        self.device_id = pid.measurement.split(',')[0]
        self.measurement_id = pid.measurement.split(',')[1]

        input_dev = db_retrieve_table_daemon(Input, unique_id=self.device_id)
        math = db_retrieve_table_daemon(Math, unique_id=self.device_id)
        if input_dev:
            self.input_duration = input_dev.period
        elif math:
            self.input_duration = math.period

        try:
            self.raise_output_type = db_retrieve_table_daemon(
                Output, unique_id=self.raise_output_id).output_type
        except AttributeError:
            self.raise_output_type = None
        try:
            self.lower_output_type = db_retrieve_table_daemon(
                Output, unique_id=self.lower_output_id).output_type
        except AttributeError:
            self.lower_output_type = None

        self.logger.info("PID Settings: {}".format(self.pid_parameters_str()))

        return "success"

    def check_pid(self):
        """ Get measurement and apply to PID controller """
        # Ensure the timer ends in the future
        while time.time() > self.timer:
            self.timer = self.timer + self.period

        # If PID is active, retrieve measurement and update
        # the control variable.
        # A PID on hold will sustain the current output and
        # not update the control variable.
        if self.is_activated and (not self.is_paused or not self.is_held):
            self.get_last_measurement()

            if self.last_measurement_success:
                if self.method_id != '':
                    # Update setpoint using a method
                    this_pid = db_retrieve_table_daemon(
                        PID, unique_id=self.pid_id)
                    setpoint, ended = calculate_method_setpoint(
                        self.method_id,
                        PID,
                        this_pid,
                        Method,
                        MethodData,
                        self.logger)
                    if ended:
                        self.method_start_act = 'Ended'
                    if setpoint is not None:
                        self.setpoint = setpoint
                    else:
                        self.setpoint = self.default_setpoint

                # If autotune activated, determine control variable (output) from autotune
                if self.autotune_activated:
                    if not self.autotune.run(self.last_measurement):
                        self.control_variable = self.autotune.output

                        if self.autotune_debug:
                            self.logger.info('')
                            self.logger.info("state: {}".format(self.autotune.state))
                            self.logger.info("output: {}".format(self.autotune.output))
                    else:
                        # Autotune has finished
                        timestamp = time.time() - self.autotune_timestamp
                        self.logger.info('')
                        self.logger.info('time:  {0} min'.format(round(timestamp / 60)))
                        self.logger.info('state: {0}'.format(self.autotune.state))

                        if self.autotune.state == PIDAutotune.STATE_SUCCEEDED:
                            for rule in self.autotune.tuning_rules:
                                params = self.autotune.get_pid_parameters(rule)
                                self.logger.info('')
                                self.logger.info('rule: {0}'.format(rule))
                                self.logger.info('Kp: {0}'.format(params.Kp))
                                self.logger.info('Ki: {0}'.format(params.Ki))
                                self.logger.info('Kd: {0}'.format(params.Kd))

                        self.stop_controller(deactivate_pid=True)
                else:
                    # Calculate new control variable (output) from PID Controller

                    # Original PID method
                    self.control_variable = self.update_pid_output(
                        self.last_measurement)

                    # New PID method (untested)
                    # self.control_variable = self.PID_Controller.calc(
                    #     self.last_measurement, self.setpoint)

                self.write_pid_values()  # Write variables to database

        # Is PID in a state that allows manipulation of outputs
        if self.is_activated and (not self.is_paused or self.is_held):
            self.manipulate_output()

    def setup_method(self, method_id):
        """ Initialize method variables to start running a method """
        self.method_id = ''

        method = db_retrieve_table_daemon(Method, unique_id=method_id)
        method_data = db_retrieve_table_daemon(MethodData)
        method_data = method_data.filter(MethodData.method_id == method_id)
        method_data_repeat = method_data.filter(MethodData.duration_sec == 0).first()
        pid = db_retrieve_table_daemon(PID, unique_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).filter(
                        PID.unique_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=method_id,
                                start=self.method_start_time,
                                end=self.method_end_time))
                    else:
                        self.method_start_act = 'Ended'
                else:
                    self.method_start_act = 'Ended'

        self.method_id = method_id

    def write_pid_values(self):
        """ Write PID values to the measurement database """
        if self.band:
            setpoint_band_lower = self.setpoint - self.band
            setpoint_band_upper = self.setpoint + self.band
        else:
            setpoint_band_lower = None
            setpoint_band_upper = None

        list_measurements = [
            self.setpoint,
            setpoint_band_lower,
            setpoint_band_upper,
            self.P_value,
            self.I_value,
            self.D_value
        ]

        measurement_dict = {}
        measurements = self.device_measurements.filter(
            DeviceMeasurements.device_id == self.pid_id).all()
        for each_channel, each_measurement in enumerate(measurements):
            if (each_measurement.channel not in measurement_dict and
                    each_measurement.channel < len(list_measurements)):

                # If setpoint, get unit from PID measurement
                if each_measurement.measurement_type == 'setpoint':
                    setpoint_pid = db_retrieve_table_daemon(
                        PID, unique_id=each_measurement.device_id)
                    if setpoint_pid and ',' in setpoint_pid.measurement:
                        pid_measurement = setpoint_pid.measurement.split(',')[1]
                        setpoint_measurement = db_retrieve_table_daemon(
                            DeviceMeasurements, unique_id=pid_measurement)
                        if setpoint_measurement:
                            conversion = db_retrieve_table_daemon(
                                Conversion, unique_id=setpoint_measurement.conversion_id)
                            _, unit, _ = return_measurement_info(
                                setpoint_measurement, conversion)
                            measurement_dict[each_channel] = {
                                'measurement': each_measurement.measurement,
                                'unit': unit,
                                'value': list_measurements[each_channel]
                            }
                else:
                    measurement_dict[each_channel] = {
                        'measurement': each_measurement.measurement,
                        'unit': each_measurement.unit,
                        'value': list_measurements[each_channel]
                    }

        add_measurements_influxdb(self.pid_id, measurement_dict)

    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
        """
        # Determine if hysteresis is enabled and if the PID should be applied
        setpoint = self.check_hysteresis(current_value)

        if setpoint is None:
            # Prevent PID variables form being manipulated and
            # restrict PID from operating.
            return 0

        self.error = setpoint - 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

        # Prevent large initial D-value
        if self.first_start:
            self.derivator = self.error
            self.first_start = False

        # 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 check_hysteresis(self, measure):
        """
        Determine if hysteresis is enabled and if the PID should be applied

        :return: float if the setpoint if the PID should be applied, None to
            restrict the PID
        :rtype: float or None

        :param measure: The PID input (or process) variable
        :type measure: float
        """
        if self.band == 0:
            # If band is disabled, return setpoint
            return self.setpoint

        band_min = self.setpoint - self.band
        band_max = self.setpoint + self.band

        if self.direction == 'raise':
            if (measure < band_min or
                    (band_min < measure < band_max and self.allow_raising)):
                self.allow_raising = True
                setpoint = band_max  # New setpoint
                return setpoint  # Apply the PID
            elif measure > band_max:
                self.allow_raising = False
            return None  # Restrict the PID

        elif self.direction == 'lower':
            if (measure > band_max or
                    (band_min < measure < band_max and self.allow_lowering)):
                self.allow_lowering = True
                setpoint = band_min  # New setpoint
                return setpoint  # Apply the PID
            elif measure < band_min:
                self.allow_lowering = False
            return None  # Restrict the PID

        elif self.direction == 'both':
            if measure < band_min:
                setpoint = band_min  # New setpoint
                if not self.allow_raising:
                    # Reset integrator and derivator upon direction switch
                    self.integrator = 0.0
                    self.derivator = 0.0
                    self.allow_raising = True
                    self.allow_lowering = False
            elif measure > band_max:
                setpoint = band_max  # New setpoint
                if not self.allow_lowering:
                    # Reset integrator and derivator upon direction switch
                    self.integrator = 0.0
                    self.derivator = 0.0
                    self.allow_raising = False
                    self.allow_lowering = True
            else:
                return None  # Restrict the PID
            return setpoint  # Apply the PID

    def get_last_measurement(self):
        """
        Retrieve the latest input measurement from InfluxDB

        :rtype: None
        """
        self.last_measurement_success = False

        # Get latest measurement from influxdb
        try:
            device_measurement = get_measurement(self.measurement_id)

            if device_measurement:
                conversion = db_retrieve_table_daemon(
                    Conversion, unique_id=device_measurement.conversion_id)
            else:
                conversion = None
            channel, unit, measurement = return_measurement_info(
                device_measurement, conversion)

            self.last_measurement = read_last_influxdb(
                self.device_id,
                unit,
                measurement,
                channel,
                int(self.max_measure_age))

            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 (CH{ch}, Unit: {unit}): {last} @ {ts}".format(
                    ch=channel,
                    unit=unit,
                    last=self.last_measurement,
                    ts=local_timestamp))
                if calendar.timegm(time.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(time.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:
                    # Determine if the output should be PWM or a duration
                    if self.raise_output_type in ['pwm',
                                                  'command_pwm',
                                                  'python_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.setpoint,
                                cv=self.control_variable,
                                id=self.raise_output_id,
                                dc=self.raise_duty_cycle))

                        # Activate pwm with calculated duty cycle
                        self.control.output_on(self.raise_output_id,
                                               duty_cycle=self.raise_duty_cycle)

                        self.write_pid_output_influxdb(
                            'percent', 'duty_cycle', 7,
                            self.control_var_to_duty_cycle(self.control_variable))

                    elif self.raise_output_type in ['command',
                                                    'python',
                                                    'wired',
                                                    'wireless_rpi_rf']:
                        # 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.setpoint,
                                    cv=self.control_variable,
                                    id=self.raise_output_id))
                            self.control.output_on(
                                self.raise_output_id,
                                duration=self.raise_seconds_on,
                                min_off=self.raise_min_off_duration)

                        self.write_pid_output_influxdb(
                            's', 'duration_time', 6,
                            self.control_variable)

                else:
                    if self.raise_output_type in ['pwm',
                                                  'command_pwm',
                                                  'python_pwm']:
                        self.control.output_on(self.raise_output_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_output_id:

                if self.control_variable < 0:
                    # Determine if the output should be PWM or a duration
                    if self.lower_output_type in ['pwm',
                                                  'command_pwm',
                                                  'python_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.setpoint,
                                cv=self.control_variable,
                                id=self.lower_output_id,
                                dc=self.lower_duty_cycle))

                        if self.store_lower_as_negative:
                            stored_duty_cycle = -abs(self.lower_duty_cycle)
                            stored_control_variable = -self.control_var_to_duty_cycle(abs(self.control_variable))
                        else:
                            stored_duty_cycle = abs(self.lower_duty_cycle)
                            stored_control_variable = self.control_var_to_duty_cycle(abs(self.control_variable))

                        # Activate pwm with calculated duty cycle
                        self.control.output_on(
                            self.lower_output_id,
                            duty_cycle=stored_duty_cycle)

                        self.write_pid_output_influxdb(
                            'percent', 'duty_cycle', 7,
                            stored_control_variable)

                    elif self.lower_output_type in ['command',
                                                    'python',
                                                    'wired',
                                                    'wireless_rpi_rf']:
                        # 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(
                                abs(self.control_variable)))

                        if self.store_lower_as_negative:
                            stored_seconds_on = -abs(self.lower_seconds_on)
                            stored_control_variable = -abs(self.control_variable)
                        else:
                            stored_seconds_on = abs(self.lower_seconds_on)
                            stored_control_variable = abs(self.control_variable)

                        if 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.setpoint,
                                                cv=self.control_variable,
                                                id=self.lower_output_id))

                            self.control.output_on(
                                self.lower_output_id,
                                duration=stored_seconds_on,
                                min_off=self.lower_min_off_duration)

                        self.write_pid_output_influxdb(
                            's', 'duration_time', 6,
                            stored_control_variable)

                else:
                    if self.lower_output_type in ['pwm',
                                                  'command_pwm',
                                                  'python_pwm']:
                        self.control.output_on(self.lower_output_id,
                                               duty_cycle=0)

        else:
            if self.direction in ['raise', 'both'] and self.raise_output_id:
                self.control.output_off(self.raise_output_id)
            if self.direction in ['lower', 'both'] and self.lower_output_id:
                self.control.output_off(self.lower_output_id)

    def pid_parameters_str(self):
        return "Device ID: {did}, " \
               "Measurement ID: {mid}, " \
               "Direction: {dir}, " \
               "Period: {per}, " \
               "Setpoint: {sp}, " \
               "Band: {band}, " \
               "Kp: {kp}, " \
               "Ki: {ki}, " \
               "Kd: {kd}, " \
               "Integrator Min: {imn}, " \
               "Integrator Max {imx}, " \
               "Output Raise: {opr}, " \
               "Output Raise Min On: {oprmnon}, " \
               "Output Raise Max On: {oprmxon}, " \
               "Output Raise Min Off: {oprmnoff}, " \
               "Output Lower: {opl}, " \
               "Output Lower Min On: {oplmnon}, " \
               "Output Lower Max On: {oplmxon}, " \
               "Output Lower Min Off: {oplmnoff}, " \
               "Setpoint Tracking: {spt}".format(
            did=self.device_id,
            mid=self.measurement_id,
            dir=self.direction,
            per=self.period,
            sp=self.setpoint,
            band=self.band,
            kp=self.Kp,
            ki=self.Ki,
            kd=self.Kd,
            imn=self.integrator_min,
            imx=self.integrator_max,
            opr=self.raise_output_id,
            oprmnon=self.raise_min_duration,
            oprmxon=self.raise_max_duration,
            oprmnoff=self.raise_min_off_duration,
            opl=self.lower_output_id,
            oplmnon=self.lower_min_duration,
            oplmxon=self.lower_max_duration,
            oplmnoff=self.lower_min_off_duration,
            spt=self.method_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, unit, measurement, channel, value):
        write_pid_out_db = threading.Thread(
            target=write_influxdb_value,
            args=(self.pid_id,
                  unit,
                  value,),
            kwargs={'measure': measurement,
                    'channel': channel})
        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, setpoint):
        """ Set the setpoint of PID """
        self.setpoint = float(setpoint)
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.pid_id).first()
            mod_pid.setpoint = setpoint
            db_session.commit()
        return "Setpoint set to {sp}".format(sp=setpoint)

    def set_method(self, method_id):
        """ Set the method of PID """
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.pid_id).first()
            mod_pid.method_id = method_id

            if method_id == '':
                self.method_id = ''
                db_session.commit()
            else:
                mod_pid.method_start_time = 'Ready'
                mod_pid.method_end_time = None
                db_session.commit()
                self.setup_method(method_id)

        return "Method set to {me}".format(me=method_id)

    def set_integrator(self, integrator):
        """ Set the integrator of the controller """
        self.integrator = float(integrator)
        return "Integrator set to {i}".format(i=self.integrator)

    def set_derivator(self, derivator):
        """ Set the derivator of the controller """
        self.derivator = float(derivator)
        return "Derivator set to {d}".format(d=self.derivator)

    def set_kp(self, p):
        """ Set Kp gain of the controller """
        self.Kp = float(p)
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.pid_id).first()
            mod_pid.p = p
            db_session.commit()
        return "Kp set to {kp}".format(kp=self.Kp)

    def set_ki(self, i):
        """ Set Ki gain of the controller """
        self.Ki = float(i)
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.pid_id).first()
            mod_pid.i = i
            db_session.commit()
        return "Ki set to {ki}".format(ki=self.Ki)

    def set_kd(self, d):
        """ Set Kd gain of the controller """
        self.Kd = float(d)
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.pid_id).first()
            mod_pid.d = d
            db_session.commit()
        return "Kd set to {kd}".format(kd=self.Kd)

    def get_setpoint(self):
        return self.setpoint

    def get_error(self):
        return self.error

    def get_integrator(self):
        return self.integrator

    def get_derivator(self):
        return self.derivator

    def get_kp(self):
        return self.Kp

    def get_ki(self):
        return self.Ki

    def get_kd(self):
        return self.Kd

    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.unique_id == self.pid_id).first()
                mod_pid.method_start_time = 'Ended'
                mod_pid.method_end_time = None
                db_session.commit()

        # Deactivate PID and Autotune
        if deactivate_pid:
            with session_scope(MYCODO_DB_PATH) as db_session:
                mod_pid = db_session.query(PID).filter(
                    PID.unique_id == self.pid_id).first()
                mod_pid.is_activated = False
                mod_pid.autotune_activated = False
                db_session.commit()
コード例 #15
0
ファイル: controller_pid.py プロジェクト: rosaLux161/Mycodo
class PIDController(AbstractController, threading.Thread):
    """
    Class to operate discrete PID controller in Mycodo
    """
    def __init__(self, ready, unique_id):
        threading.Thread.__init__(self)
        super().__init__(ready, unique_id=unique_id, name=__name__)

        self.unique_id = unique_id
        self.sample_rate = None
        self.dict_outputs = None

        self.control = DaemonControl()

        self.PID_Controller = None
        self.setpoint = None

        self.device_measurements = None
        self.device_id = None
        self.measurement_id = None
        self.log_level_debug = None
        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.setpoint_tracking_type = None
        self.setpoint_tracking_id = None
        self.setpoint_tracking_max_age = None
        self.raise_output_id = None
        self.raise_output_channel_id = None
        self.raise_output_channel = None
        self.raise_output_type = None
        self.raise_min_duration = None
        self.raise_max_duration = None
        self.raise_min_off_duration = None
        self.raise_always_min_pwm = None
        self.lower_output_id = None
        self.lower_output_channel_id = None
        self.lower_output_channel = None
        self.lower_output_type = None
        self.lower_min_duration = None
        self.lower_max_duration = None
        self.lower_min_off_duration = None
        self.lower_always_min_pwm = None
        self.period = 0
        self.start_offset = 0
        self.max_measure_age = None
        self.send_lower_as_negative = None
        self.store_lower_as_negative = None
        self.timer = 0

        # Check if a method is set for this PID
        self.method_type = None
        self.method_start_time = None
        self.method_end_time = None

    def loop(self):
        if time.time() > self.timer:
            while time.time() > self.timer:
                self.timer = self.timer + self.period
            self.attempt_execute(self.check_pid)

    def run_finally(self):
        # Turn off output used in PID when the controller is deactivated
        if self.raise_output_id and self.PID_Controller.direction in [
                'raise', 'both'
        ]:
            self.control.output_off(self.raise_output_id,
                                    output_channel=self.raise_output_channel,
                                    trigger_conditionals=True)
        if self.lower_output_id and self.PID_Controller.direction in [
                'lower', 'both'
        ]:
            self.control.output_off(self.lower_output_id,
                                    output_channel=self.lower_output_channel,
                                    trigger_conditionals=True)

    def initialize_variables(self):
        """Set PID parameters."""
        self.dict_outputs = parse_output_information()

        self.sample_rate = db_retrieve_table_daemon(
            Misc, entry='first').sample_rate_controller_pid

        self.device_measurements = db_retrieve_table_daemon(DeviceMeasurements)

        pid = db_retrieve_table_daemon(PID, unique_id=self.unique_id)

        self.log_level_debug = pid.log_level_debug
        self.set_log_level_debug(self.log_level_debug)

        self.device_id = pid.measurement.split(',')[0]
        self.measurement_id = pid.measurement.split(',')[1]
        self.is_activated = pid.is_activated
        self.is_held = pid.is_held
        self.is_paused = pid.is_paused
        self.setpoint_tracking_type = pid.setpoint_tracking_type
        self.setpoint_tracking_id = pid.setpoint_tracking_id
        self.setpoint_tracking_max_age = pid.setpoint_tracking_max_age
        if pid.raise_output_id and "," in pid.raise_output_id:
            self.raise_output_id = pid.raise_output_id.split(",")[0]
            self.raise_output_channel_id = pid.raise_output_id.split(",")[1]
            output_channel = db_retrieve_table_daemon(
                OutputChannel, unique_id=self.raise_output_channel_id)
            self.raise_output_channel = output_channel.channel
        self.raise_output_type = pid.raise_output_type
        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.raise_always_min_pwm = pid.raise_always_min_pwm
        if pid.lower_output_id and "," in pid.lower_output_id:
            self.lower_output_id = pid.lower_output_id.split(",")[0]
            self.lower_output_channel_id = pid.lower_output_id.split(",")[1]
            output_channel = db_retrieve_table_daemon(
                OutputChannel, unique_id=self.lower_output_channel_id)
            self.lower_output_channel = output_channel.channel
        self.lower_output_type = pid.lower_output_type
        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.lower_always_min_pwm = pid.lower_always_min_pwm
        self.period = pid.period
        self.start_offset = pid.start_offset
        self.max_measure_age = pid.max_measure_age
        self.send_lower_as_negative = pid.send_lower_as_negative
        self.store_lower_as_negative = pid.store_lower_as_negative
        self.timer = time.time() + self.start_offset
        self.setpoint = pid.setpoint

        # Initialize PID Controller
        if self.PID_Controller is None:
            self.PID_Controller = PIDControl(self.logger, pid.setpoint, pid.p,
                                             pid.i, pid.d, pid.direction,
                                             pid.band, pid.integrator_min,
                                             pid.integrator_max)
        else:
            # Set PID options
            self.PID_Controller.setpoint = pid.setpoint
            self.PID_Controller.Kp = pid.p
            self.PID_Controller.Ki = pid.i
            self.PID_Controller.Kd = pid.d
            self.PID_Controller.direction = pid.direction
            self.PID_Controller.band = pid.band
            self.PID_Controller.integrator_min = pid.integrator_min
            self.PID_Controller.integrator_max = pid.integrator_max
            self.PID_Controller.first_start = True

        if self.setpoint_tracking_type == 'method' and self.setpoint_tracking_id != '':
            self.setup_method(self.setpoint_tracking_id)

        if self.is_paused:
            self.logger.info("Starting Paused")
        elif self.is_held:
            self.logger.info("Starting Held")

        self.logger.info(f"PID Settings: {self.pid_parameters_str()}")

        self.ready.set()
        self.running = True

        return "success"

    def check_pid(self):
        """Get measurement and apply to PID controller."""
        # If PID is active, retrieve measurement and update
        # the control variable.
        # A PID on hold will sustain the current output and
        # not update the control variable.
        if self.is_activated and (not self.is_paused or not self.is_held):
            self.get_last_measurement_pid()

            if self.last_measurement_success:
                if self.setpoint_tracking_type == 'method' and self.setpoint_tracking_id != '':
                    # Update setpoint using a method
                    this_pid = db_retrieve_table_daemon(
                        PID, unique_id=self.unique_id)

                    now = datetime.datetime.now()

                    method = load_method_handler(self.setpoint_tracking_id,
                                                 self.logger)
                    new_setpoint, ended = method.calculate_setpoint(
                        now, this_pid.method_start_time)
                    self.logger.debug(
                        f"Method {self.setpoint_tracking_id} {method} {now} {this_pid.method_start_time}"
                    )

                    if ended:
                        # point in time is out of method range
                        with session_scope(MYCODO_DB_PATH) as db_session:
                            # Overwrite this_controller with committable connection
                            this_pid = db_session.query(PID).filter(
                                PID.unique_id == self.unique_id).first()

                            self.logger.debug("Ended")
                            # Duration method has ended, reset method_start_time locally and in DB
                            this_pid.method_start_time = None
                            this_pid.method_end_time = None
                            this_pid.is_activated = False
                            db_session.commit()

                            self.is_activated = False
                            self.stop_controller()

                            db_session.commit()

                    if new_setpoint is not None:
                        self.logger.debug(
                            f"New setpoint = {new_setpoint} {ended}")
                        self.PID_Controller.setpoint = new_setpoint
                    else:
                        self.logger.debug(
                            f"New setpoint = default {self.setpoint} {ended}")
                        self.PID_Controller.setpoint = self.setpoint

                if self.setpoint_tracking_type == 'input-math' and self.setpoint_tracking_id != '':
                    # Update setpoint using an Input
                    device_id = self.setpoint_tracking_id.split(',')[0]
                    measurement_id = self.setpoint_tracking_id.split(',')[1]

                    measurement = get_measurement(measurement_id)
                    if not measurement:
                        return False, None

                    conversion = db_retrieve_table_daemon(
                        Conversion, unique_id=measurement.conversion_id)
                    channel, unit, measurement = return_measurement_info(
                        measurement, conversion)

                    last_measurement = read_influxdb_single(
                        device_id,
                        unit,
                        channel,
                        measure=measurement,
                        duration_sec=self.setpoint_tracking_max_age,
                        value='LAST')

                    if last_measurement[1] is not None:
                        self.PID_Controller.setpoint = last_measurement[1]
                    else:
                        self.logger.debug(
                            "Could not find measurement for Setpoint "
                            f"Tracking. Max Age of {self.setpoint_tracking_max_age} exceeded for measuring "
                            f"device ID {device_id} (measurement {measurement_id})"
                        )
                        self.PID_Controller.setpoint = None

                # Calculate new control variable (output) from PID Controller
                self.PID_Controller.update_pid_output(self.last_measurement)

                self.write_pid_values()  # Write variables to database

        # Is PID in a state that allows manipulation of outputs
        if (self.is_activated and self.PID_Controller.setpoint is not None
                and (not self.is_paused or self.is_held)):
            self.manipulate_output()

    def setup_method(self, method_id):
        """Initialize method variables to start running a method."""
        self.setpoint_tracking_id = ''

        method = load_method_handler(method_id, self.logger)

        this_controller = db_retrieve_table_daemon(PID,
                                                   unique_id=self.unique_id)
        self.method_type = method.method_type

        if parse_db_time(this_controller.method_start_time) is None:
            self.method_start_time = datetime.datetime.now()
            self.method_end_time = method.determine_end_time(
                self.method_start_time)

            self.logger.info(
                f"Starting method {self.method_start_time} {self.method_end_time}"
            )

            with session_scope(MYCODO_DB_PATH) as db_session:
                this_controller = db_session.query(PID)
                this_controller = this_controller.filter(
                    PID.unique_id == self.unique_id).first()
                this_controller.method_start_time = self.method_start_time
                this_controller.method_end_time = self.method_end_time
                db_session.commit()
        else:
            # already running, potentially the daemon has been restarted
            self.method_start_time = this_controller.method_start_time
            self.method_end_time = this_controller.method_end_time

        self.setpoint_tracking_id = method_id
        self.logger.debug(f"Method enabled: {self.setpoint_tracking_id}")

    def stop_method(self):
        self.method_start_time = None
        self.method_end_time = None
        with session_scope(MYCODO_DB_PATH) as db_session:
            this_controller = db_session.query(PID)
            this_controller = this_controller.filter(
                PID.unique_id == self.unique_id).first()
            this_controller.is_activated = False
            this_controller.method_start_time = None
            this_controller.method_end_time = None
            db_session.commit()
        self.stop_controller()
        self.is_activated = False
        self.logger.warning(
            "Method has ended. "
            "Activate the Trigger controller to start it again.")

    def write_pid_values(self):
        """Write PID values to the measurement database"""
        if self.PID_Controller.band:
            setpoint_band_lower = self.PID_Controller.setpoint - self.PID_Controller.band
            setpoint_band_upper = self.PID_Controller.setpoint + self.PID_Controller.band
        else:
            setpoint_band_lower = None
            setpoint_band_upper = None

        list_measurements = [
            self.PID_Controller.setpoint, setpoint_band_lower,
            setpoint_band_upper, self.PID_Controller.P_value,
            self.PID_Controller.I_value, self.PID_Controller.D_value
        ]

        measurement_dict = {}
        measurements = self.device_measurements.filter(
            DeviceMeasurements.device_id == self.unique_id).all()
        for each_channel, each_measurement in enumerate(measurements):
            if (each_measurement.channel not in measurement_dict
                    and each_measurement.channel < len(list_measurements)):

                # If setpoint, get unit from PID measurement
                if each_measurement.measurement_type == 'setpoint':
                    setpoint_pid = db_retrieve_table_daemon(
                        PID, unique_id=each_measurement.device_id)
                    if setpoint_pid and ',' in setpoint_pid.measurement:
                        pid_measurement = setpoint_pid.measurement.split(
                            ',')[1]
                        setpoint_measurement = db_retrieve_table_daemon(
                            DeviceMeasurements, unique_id=pid_measurement)
                        if setpoint_measurement:
                            conversion = db_retrieve_table_daemon(
                                Conversion,
                                unique_id=setpoint_measurement.conversion_id)
                            _, unit, _ = return_measurement_info(
                                setpoint_measurement, conversion)
                            measurement_dict[each_channel] = {
                                'measurement': each_measurement.measurement,
                                'unit': unit,
                                'value': list_measurements[each_channel]
                            }
                else:
                    measurement_dict[each_channel] = {
                        'measurement': each_measurement.measurement,
                        'unit': each_measurement.unit,
                        'value': list_measurements[each_channel]
                    }

        add_measurements_influxdb(self.unique_id, measurement_dict)

    def get_last_measurement_pid(self):
        """
        Retrieve the latest input measurement from InfluxDB

        :rtype: None
        """
        self.last_measurement_success = False

        # Get latest measurement from influxdb
        try:
            device_measurement = get_measurement(self.measurement_id)

            if device_measurement:
                conversion = db_retrieve_table_daemon(
                    Conversion, unique_id=device_measurement.conversion_id)
            else:
                conversion = None
            channel, unit, measurement = return_measurement_info(
                device_measurement, conversion)

            self.last_measurement = read_influxdb_single(
                self.device_id,
                unit,
                channel,
                measure=measurement,
                duration_sec=int(self.max_measure_age),
                value='LAST')

            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(
                    f"Latest (CH{channel}, Unit: {unit}): {self.last_measurement} @ {local_timestamp}"
                )
                if calendar.timegm(
                        time.gmtime()) - utc_timestamp > self.max_measure_age:
                    sec = calendar.timegm(time.gmtime()) - utc_timestamp
                    self.logger.error(
                        f"Last measurement was {sec} seconds ago, however the maximum "
                        f"measurement age is set to {self.max_measure_age} seconds."
                    )
                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:
            self.logger.exception(
                "Exception while reading measurement from the influxdb database"
            )

    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.PID_Controller.direction in ['raise', 'both'
                                                 ] and self.raise_output_id:

                if self.PID_Controller.control_variable > 0:

                    if self.raise_output_type == 'pwm':
                        raise_duty_cycle = self.control_var_to_duty_cycle(
                            self.PID_Controller.control_variable)

                        # Ensure the duty cycle doesn't exceed the min/max
                        if (self.raise_max_duration and
                                raise_duty_cycle > self.raise_max_duration):
                            raise_duty_cycle = self.raise_max_duration
                        elif (self.raise_min_duration
                              and raise_duty_cycle < self.raise_min_duration):
                            raise_duty_cycle = self.raise_min_duration

                        self.logger.debug(
                            f"Setpoint: {self.PID_Controller.setpoint}, "
                            f"Control Variable: {self.PID_Controller.control_variable}, "
                            f"Output: PWM output {self.raise_output_id} "
                            f"CH{self.raise_output_channel} to {raise_duty_cycle:.1f}%"
                        )

                        # Activate pwm with calculated duty cycle
                        self.control.output_on(
                            self.raise_output_id,
                            output_type='pwm',
                            amount=raise_duty_cycle,
                            output_channel=self.raise_output_channel)

                        self.write_pid_output_influxdb(
                            'percent', 'duty_cycle', 7,
                            self.control_var_to_duty_cycle(
                                self.PID_Controller.control_variable))

                    elif self.raise_output_type == 'on_off':
                        raise_seconds_on = self.PID_Controller.control_variable

                        # Ensure the output on duration doesn't exceed the set maximum
                        if (self.raise_max_duration and
                                raise_seconds_on > self.raise_max_duration):
                            raise_seconds_on = self.raise_max_duration

                        if raise_seconds_on >= self.raise_min_duration:
                            # Activate raise_output for a duration
                            self.logger.debug(
                                f"Setpoint: {self.PID_Controller.setpoint} "
                                f"Output: {self.PID_Controller.control_variable} sec to "
                                f"output {self.raise_output_id} CH{self.raise_output_channel}"
                            )

                            self.control.output_on(
                                self.raise_output_id,
                                output_type='sec',
                                amount=raise_seconds_on,
                                min_off=self.raise_min_off_duration,
                                output_channel=self.raise_output_channel)

                        self.write_pid_output_influxdb(
                            's', 'duration_time', 6,
                            self.PID_Controller.control_variable)

                    elif self.raise_output_type == 'value':
                        raise_value = self.PID_Controller.control_variable

                        # Ensure the duty cycle doesn't exceed the min/max
                        if (self.raise_max_duration
                                and raise_value > self.raise_max_duration):
                            raise_value = self.raise_max_duration

                        if raise_value >= self.raise_min_duration:
                            # Activate raise_output for a value
                            self.logger.debug(
                                f"Setpoint: {self.PID_Controller.setpoint} "
                                f"Output: {self.PID_Controller.control_variable} to "
                                f"output {self.raise_output_id} CH{self.raise_output_channel}"
                            )

                            self.control.output_on(
                                self.raise_output_id,
                                output_type='value',
                                amount=raise_value,
                                min_off=self.raise_min_off_duration,
                                output_channel=self.raise_output_channel)

                        self.write_pid_output_influxdb('none', 'unitless', 9,
                                                       raise_value)

                    elif self.raise_output_type == 'volume':
                        raise_volume = self.PID_Controller.control_variable

                        # Ensure the duty cycle doesn't exceed the min/max
                        if (self.raise_max_duration
                                and raise_volume > self.raise_max_duration):
                            raise_volume = self.raise_max_duration

                        if raise_volume >= self.raise_min_duration:
                            # Activate raise_output for a volume (ml)
                            self.logger.debug(
                                f"Setpoint: {self.PID_Controller.setpoint} "
                                f"Output: {self.PID_Controller.control_variable} ml to "
                                f"output {self.raise_output_id} CH{self.raise_output_channel}"
                            )

                            self.control.output_on(
                                self.raise_output_id,
                                output_type='vol',
                                amount=raise_volume,
                                min_off=self.raise_min_off_duration,
                                output_channel=self.raise_output_channel)

                        self.write_pid_output_influxdb(
                            'ml', 'volume', 8,
                            self.PID_Controller.control_variable)

                elif self.raise_output_type == 'pwm' and not self.raise_always_min_pwm:
                    # Turn PWM Off if PWM Output and not instructed to always be at least min
                    self.control.output_on(
                        self.raise_output_id,
                        output_type='pwm',
                        amount=0,
                        output_channel=self.raise_output_channel)

            #
            # PID control variable is negative, indicating a desire to lower
            # the environmental condition
            #
            if self.PID_Controller.direction in ['lower', 'both'
                                                 ] and self.lower_output_id:

                if self.PID_Controller.control_variable < 0:

                    if self.lower_output_type == 'pwm':
                        lower_duty_cycle = self.control_var_to_duty_cycle(
                            abs(self.PID_Controller.control_variable))

                        # Ensure the duty cycle doesn't exceed the min/max
                        if (self.lower_max_duration and
                                lower_duty_cycle > self.lower_max_duration):
                            lower_duty_cycle = self.lower_max_duration
                        elif (self.lower_min_duration
                              and lower_duty_cycle < self.lower_min_duration):
                            lower_duty_cycle = self.lower_min_duration

                        self.logger.debug(
                            f"Setpoint: {self.PID_Controller.setpoint}, "
                            f"Control Variable: {self.PID_Controller.control_variable}, "
                            f"Output: PWM output {self.lower_output_id} "
                            f"CH{self.lower_output_channel} to {lower_duty_cycle:.1f}%"
                        )

                        if self.store_lower_as_negative:
                            store_duty_cycle = -self.control_var_to_duty_cycle(
                                abs(self.PID_Controller.control_variable))
                        else:
                            store_duty_cycle = self.control_var_to_duty_cycle(
                                abs(self.PID_Controller.control_variable))

                        if self.send_lower_as_negative:
                            send_duty_cycle = -abs(lower_duty_cycle)
                        else:
                            send_duty_cycle = abs(lower_duty_cycle)

                        # Activate pwm with calculated duty cycle
                        self.control.output_on(
                            self.lower_output_id,
                            output_type='pwm',
                            amount=send_duty_cycle,
                            output_channel=self.lower_output_channel)

                        self.write_pid_output_influxdb('percent', 'duty_cycle',
                                                       7, store_duty_cycle)

                    elif self.lower_output_type == 'on_off':
                        lower_seconds_on = abs(
                            self.PID_Controller.control_variable)

                        # Ensure the output on duration doesn't exceed the set maximum
                        if (self.lower_max_duration and
                                lower_seconds_on > self.lower_max_duration):
                            lower_seconds_on = self.lower_max_duration

                        if self.store_lower_as_negative:
                            store_amount_on = -abs(
                                self.PID_Controller.control_variable)
                        else:
                            store_amount_on = abs(
                                self.PID_Controller.control_variable)

                        if self.send_lower_as_negative:
                            send_amount_on = -lower_seconds_on
                        else:
                            send_amount_on = lower_seconds_on

                        if lower_seconds_on >= self.lower_min_duration:
                            # Activate lower_output for a duration
                            self.logger.debug(
                                f"Setpoint: {self.PID_Controller.setpoint} "
                                f"Output: {self.PID_Controller.control_variable} sec to "
                                f"output {self.lower_output_id} CH{self.lower_output_channel}"
                            )

                            self.control.output_on(
                                self.lower_output_id,
                                output_type='sec',
                                amount=send_amount_on,
                                min_off=self.lower_min_off_duration,
                                output_channel=self.lower_output_channel)

                        self.write_pid_output_influxdb('s', 'duration_time', 6,
                                                       store_amount_on)

                    elif self.lower_output_type == 'value':
                        lower_value = abs(self.PID_Controller.control_variable)

                        # Ensure the output value doesn't exceed the set maximum
                        if (self.lower_max_duration
                                and lower_value > self.lower_max_duration):
                            lower_value = self.lower_max_duration

                        if self.store_lower_as_negative:
                            store_value = -abs(
                                self.PID_Controller.control_variable)
                        else:
                            store_value = abs(
                                self.PID_Controller.control_variable)

                        if self.send_lower_as_negative:
                            send_value = -lower_value
                        else:
                            send_value = lower_value

                        if lower_value >= self.lower_min_duration:
                            # Activate lower_output for a value
                            self.logger.debug(
                                f"Setpoint: {self.PID_Controller.setpoint} "
                                f"Output: {self.PID_Controller.control_variable} to "
                                f"output {self.lower_output_id} CH{self.lower_output_channel}"
                            )

                            self.control.output_on(
                                self.lower_output_id,
                                output_type='value',
                                amount=send_value,
                                min_off=self.lower_min_off_duration,
                                output_channel=self.lower_output_channel)

                        self.write_pid_output_influxdb('none', 'unitless', 9,
                                                       store_value)

                    elif self.lower_output_type == 'volume':
                        lower_volume = abs(
                            self.PID_Controller.control_variable)

                        # Ensure the output volume doesn't exceed the set maximum
                        if (self.lower_max_duration
                                and lower_volume > self.lower_max_duration):
                            lower_volume = self.lower_max_duration

                        if self.store_lower_as_negative:
                            store_volume = -abs(
                                self.PID_Controller.control_variable)
                        else:
                            store_volume = abs(
                                self.PID_Controller.control_variable)

                        if self.send_lower_as_negative:
                            send_volume = -lower_volume
                        else:
                            send_volume = lower_volume

                        if lower_volume >= self.lower_min_duration:
                            # Activate lower_output for a volume (ml)
                            self.logger.debug(
                                f"Setpoint: {self.PID_Controller.setpoint} "
                                f"Output: {self.PID_Controller.control_variable} ml to "
                                f"output {self.lower_output_id} CH{self.lower_output_channel}"
                            )

                            self.control.output_on(
                                self.lower_output_id,
                                output_type='vol',
                                amount=send_volume,
                                min_off=self.lower_min_off_duration,
                                output_channel=self.lower_output_channel)

                        self.write_pid_output_influxdb('ml', 'volume', 8,
                                                       store_volume)

                elif self.lower_output_type == 'pwm' and not self.lower_always_min_pwm:
                    # Turn PWM Off if PWM Output and not instructed to always be at least min
                    self.control.output_on(
                        self.lower_output_id,
                        output_type='pwm',
                        amount=0,
                        output_channel=self.lower_output_channel)

        else:
            self.logger.debug(
                "Last measurement unsuccessful. Turning outputs off.")
            if self.PID_Controller.direction in ['raise', 'both'
                                                 ] and self.raise_output_id:
                self.control.output_off(
                    self.raise_output_id,
                    output_channel=self.raise_output_channel)
            if self.PID_Controller.direction in ['lower', 'both'
                                                 ] and self.lower_output_id:
                self.control.output_off(
                    self.lower_output_id,
                    output_channel=self.lower_output_channel)

    def pid_parameters_str(self):
        return f"Device ID: {self.device_id}, " \
               f"Measurement ID: {self.measurement_id}, " \
               f"Direction: {self.PID_Controller.direction}, " \
               f"Period: {self.period}, " \
               f"Setpoint: {self.PID_Controller.setpoint}, " \
               f"Band: {self.PID_Controller.band}, " \
               f"Kp: {self.PID_Controller.Kp}, " \
               f"Ki: {self.PID_Controller.Ki}, " \
               f"Kd: {self.PID_Controller.Kd}, " \
               f"Integrator Min: {self.PID_Controller.integrator_min}, " \
               f"Integrator Max {self.PID_Controller.integrator_max}, " \
               f"Output Raise: {self.raise_output_id}, " \
               f"Output Raise Channel: {self.raise_output_channel}, " \
               f"Output Raise Type: {self.raise_output_type}, " \
               f"Output Raise Min On: {self.raise_min_duration}, " \
               f"Output Raise Max On: {self.raise_max_duration}, " \
               f"Output Raise Min Off: {self.raise_min_off_duration}, " \
               f"Output Raise Always Min: {self.raise_always_min_pwm}, " \
               f"Output Lower: {self.lower_output_id}, " \
               f"Output Lower Channel: {self.lower_output_channel}, " \
               f"Output Lower Type: {self.lower_output_type}, " \
               f"Output Lower Min On: {self.lower_min_duration}, " \
               f"Output Lower Max On: {self.lower_max_duration}, " \
               f"Output Lower Min Off: {self.lower_min_off_duration}, " \
               f"Output Lower Always Min: {self.lower_always_min_pwm}, " \
               f"Setpoint Tracking Type: {self.setpoint_tracking_type}, " \
               f"Setpoint Tracking ID: {self.setpoint_tracking_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)

    @staticmethod
    def return_output_channel(output_channel_id):
        output_channel = db_retrieve_table_daemon(OutputChannel,
                                                  unique_id=output_channel_id)
        if output_channel and output_channel.channel is not None:
            return output_channel.channel

    def write_pid_output_influxdb(self, unit, measurement, channel, value):
        write_pid_out_db = threading.Thread(target=write_influxdb_value,
                                            args=(
                                                self.unique_id,
                                                unit,
                                                value,
                                            ),
                                            kwargs={
                                                'measure': measurement,
                                                'channel': channel
                                            })
        write_pid_out_db.start()

    def pid_mod(self):
        if self.initialize_variables():
            return "success"
        else:
            return "error"

    def pid_hold(self):
        self.is_held = True
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.unique_id).first()
            mod_pid.is_held = True
            db_session.commit()
        self.logger.info("PID Held")
        return "success"

    def pid_pause(self):
        self.is_paused = True
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.unique_id).first()
            mod_pid.is_paused = True
            db_session.commit()
        self.logger.info("PID Paused")
        return "success"

    def pid_resume(self):
        self.is_activated = True
        self.is_held = False
        self.is_paused = False
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.unique_id).first()
            mod_pid.is_activated = True
            mod_pid.is_held = False
            mod_pid.is_paused = False
            db_session.commit()
        self.logger.info("PID Resumed")
        return "success"

    def set_setpoint(self, setpoint):
        """Set the setpoint of PID."""
        self.PID_Controller.setpoint = float(setpoint)
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.unique_id).first()
            mod_pid.setpoint = setpoint
            db_session.commit()
        return f"Setpoint set to {setpoint}"

    def set_method(self, method_id):
        """Set the method of PID."""
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.unique_id).first()
            mod_pid.setpoint_tracking_id = method_id

            if method_id == '':
                self.setpoint_tracking_id = ''
                db_session.commit()
            else:
                mod_pid.method_start_time = None
                mod_pid.method_end_time = None
                db_session.commit()
                self.setup_method(method_id)

        return f"Method set to {method_id}"

    def set_integrator(self, integrator):
        """Set the integrator of the controller."""
        self.PID_Controller.integrator = float(integrator)
        return f"Integrator set to {self.PID_Controller.integrator}"

    def set_derivator(self, derivator):
        """Set the derivator of the controller."""
        self.PID_Controller.derivator = float(derivator)
        return f"Derivator set to {self.PID_Controller.derivator}"

    def set_kp(self, p):
        """Set Kp gain of the controller."""
        self.PID_Controller.Kp = float(p)
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.unique_id).first()
            mod_pid.p = p
            db_session.commit()
        return f"Kp set to {self.PID_Controller.Kp}"

    def set_ki(self, i):
        """Set Ki gain of the controller."""
        self.PID_Controller.Ki = float(i)
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.unique_id).first()
            mod_pid.i = i
            db_session.commit()
        return f"Ki set to {self.PID_Controller.Ki}"

    def set_kd(self, d):
        """Set Kd gain of the controller."""
        self.PID_Controller.Kd = float(d)
        with session_scope(MYCODO_DB_PATH) as db_session:
            mod_pid = db_session.query(PID).filter(
                PID.unique_id == self.unique_id).first()
            mod_pid.d = d
            db_session.commit()
        return f"Kd set to {self.PID_Controller.Kd}"

    def get_setpoint(self):
        return self.PID_Controller.setpoint

    def get_setpoint_band(self):
        return self.PID_Controller.setpoint_band

    def get_error(self):
        return self.PID_Controller.error

    def get_integrator(self):
        return self.PID_Controller.integrator

    def get_derivator(self):
        return self.PID_Controller.derivator

    def get_kp(self):
        return self.PID_Controller.Kp

    def get_ki(self):
        return self.PID_Controller.Ki

    def get_kd(self):
        return self.PID_Controller.Kd

    def function_status(self):
        total = self.PID_Controller.P_value + self.PID_Controller.I_value + self.PID_Controller.D_value
        return_dict = {
            'string_status':
            "This info is being returned from the PID Controller."
            f"\nCurrent time: {datetime.datetime.now()}"
            f"\nControl Variable: {total:.4f} = "
            f"{self.PID_Controller.P_value:.4f} (P), "
            f"{self.PID_Controller.I_value:.4f} (I), "
            f"{self.PID_Controller.D_value:.4f} (D)",
            'error': []
        }
        return return_dict

    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.setpoint_tracking_type == 'method'
                and self.setpoint_tracking_id != '' and ended_normally):
            with session_scope(MYCODO_DB_PATH) as db_session:
                mod_pid = db_session.query(PID).filter(
                    PID.unique_id == self.unique_id).first()
                mod_pid.method_start_time = None
                mod_pid.method_end_time = None
                db_session.commit()

        # Deactivate PID and Autotune
        if deactivate_pid:
            with session_scope(MYCODO_DB_PATH) as db_session:
                mod_pid = db_session.query(PID).filter(
                    PID.unique_id == self.unique_id).first()
                mod_pid.is_activated = False
                mod_pid.autotune_activated = False
                db_session.commit()
コード例 #16
0
ファイル: camera.py プロジェクト: thorpulse/Mycodo
def camera_record(record_type,
                  unique_id,
                  duration_sec=None,
                  tmp_filename=None):
    """
    Record still image from cameras
    :param record_type:
    :param unique_id:
    :param duration_sec:
    :param tmp_filename:
    :return:
    """
    daemon_control = None
    settings = db_retrieve_table_daemon(Camera, unique_id=unique_id)
    timestamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
    assure_path_exists(PATH_CAMERAS)
    camera_path = assure_path_exists(
        os.path.join(PATH_CAMERAS, '{uid}'.format(uid=settings.unique_id)))
    if record_type == 'photo':
        if settings.path_still != '':
            save_path = settings.path_still
        else:
            save_path = assure_path_exists(os.path.join(camera_path, 'still'))
        filename = 'Still-{cam_id}-{cam}-{ts}.jpg'.format(
            cam_id=settings.id, cam=settings.name,
            ts=timestamp).replace(" ", "_")
    elif record_type == 'timelapse':
        if settings.path_timelapse != '':
            save_path = settings.path_timelapse
        else:
            save_path = assure_path_exists(
                os.path.join(camera_path, 'timelapse'))
        start = datetime.datetime.fromtimestamp(
            settings.timelapse_start_time).strftime("%Y-%m-%d_%H-%M-%S")
        filename = 'Timelapse-{cam_id}-{cam}-{st}-img-{cn:05d}.jpg'.format(
            cam_id=settings.id,
            cam=settings.name,
            st=start,
            cn=settings.timelapse_capture_number).replace(" ", "_")
    elif record_type == 'video':
        if settings.path_video != '':
            save_path = settings.path_video
        else:
            save_path = assure_path_exists(os.path.join(camera_path, 'video'))
        filename = 'Video-{cam}-{ts}.h264'.format(cam=settings.name,
                                                  ts=timestamp).replace(
                                                      " ", "_")
    else:
        return

    assure_path_exists(save_path)

    if tmp_filename:
        filename = tmp_filename

    path_file = os.path.join(save_path, filename)

    # Turn on output, if configured
    output_already_on = False
    if settings.output_id:
        daemon_control = DaemonControl()
        if daemon_control.output_state(settings.output_id) == "on":
            output_already_on = True
        else:
            daemon_control.output_on(settings.output_id)

    # Pause while the output remains on for the specified duration.
    # Used for instance to allow fluorescent lights to fully turn on before
    # capturing an image.
    if settings.output_duration:
        time.sleep(settings.output_duration)

    if settings.library == 'picamera':
        import picamera
        # Try 5 times to access the pi camera (in case another process is accessing it)
        for _ in range(5):
            try:
                with picamera.PiCamera() as camera:
                    camera.resolution = (settings.width, settings.height)
                    camera.hflip = settings.hflip
                    camera.vflip = settings.vflip
                    camera.rotation = settings.rotation
                    camera.brightness = int(settings.brightness)
                    camera.contrast = int(settings.contrast)
                    camera.exposure_compensation = int(settings.exposure)
                    camera.saturation = int(settings.saturation)
                    camera.shutter_speed = settings.picamera_shutter_speed
                    camera.sharpness = settings.picamera_sharpness
                    camera.iso = settings.picamera_iso
                    camera.awb_mode = settings.picamera_awb
                    if settings.picamera_awb == 'off':
                        camera.awb_gains = (settings.picamera_awb_gain_red,
                                            settings.picamera_awb_gain_blue)
                    camera.exposure_mode = settings.picamera_exposure_mode
                    camera.meter_mode = settings.picamera_meter_mode
                    camera.image_effect = settings.picamera_image_effect

                    camera.start_preview()
                    time.sleep(2)  # Camera warm-up time

                    if record_type in ['photo', 'timelapse']:
                        camera.capture(path_file, use_video_port=False)
                    elif record_type == 'video':
                        camera.start_recording(path_file,
                                               format='h264',
                                               quality=20)
                        camera.wait_recording(duration_sec)
                        camera.stop_recording()
                    else:
                        return
                    break
            except picamera.exc.PiCameraMMALError:
                logger.error(
                    "The camera is already open by picamera. Retrying 4 times."
                )
            time.sleep(1)

    elif settings.library == 'fswebcam':
        cmd = "/usr/bin/fswebcam --device {dev} --resolution {w}x{h} --set brightness={bt}% " \
              "--no-banner --save {file}".format(dev=settings.device,
                                                 w=settings.width,
                                                 h=settings.height,
                                                 bt=settings.brightness,
                                                 file=path_file)
        if settings.hflip:
            cmd += " --flip h"
        if settings.vflip:
            cmd += " --flip h"
        if settings.rotation:
            cmd += " --rotate {angle}".format(angle=settings.rotation)
        if settings.custom_options:
            cmd += " {}".format(settings.custom_options)

        out, err, status = cmd_output(cmd, stdout_pipe=False, user='******')
        logger.debug("Camera debug message: "
                     "cmd: {}; out: {}; error: {}; status: {}".format(
                         cmd, out, err, status))

    elif settings.library == 'opencv':
        import cv2
        import imutils
        cap = cv2.VideoCapture(settings.opencv_device)
        cap.set(cv2.CAP_PROP_FRAME_WIDTH, settings.width)
        cap.set(cv2.CAP_PROP_FRAME_HEIGHT, settings.height)
        cap.set(cv2.CAP_PROP_EXPOSURE, settings.exposure)
        cap.set(cv2.CAP_PROP_GAIN, settings.gain)
        cap.set(cv2.CAP_PROP_BRIGHTNESS, settings.brightness)
        cap.set(cv2.CAP_PROP_CONTRAST, settings.contrast)
        cap.set(cv2.CAP_PROP_HUE, settings.hue)
        cap.set(cv2.CAP_PROP_SATURATION, settings.saturation)

        # Check if image can be read
        if not cap.read():
            logger.error("Cannot detect USB camera with device '{dev}'".format(
                dev=settings.opencv_device))
            return

        # Discard a few frames to allow camera to adjust to settings
        for _ in range(2):
            cap.read()

        if record_type in ['photo', 'timelapse']:
            edited = False
            _, img_orig = cap.read()
            cap.release()

            img_edited = img_orig.copy()

            if any((settings.hflip, settings.vflip, settings.rotation)):
                edited = True

            if settings.hflip and settings.vflip:
                img_edited = cv2.flip(img_orig, -1)
            elif settings.hflip:
                img_edited = cv2.flip(img_orig, 1)
            elif settings.vflip:
                img_edited = cv2.flip(img_orig, 0)

            if settings.rotation:
                img_edited = imutils.rotate_bound(img_orig, settings.rotation)

            if edited:
                cv2.imwrite(path_file, img_edited)
            else:
                cv2.imwrite(path_file, img_orig)

        elif record_type == 'video':
            # TODO: opencv video recording is currently not working. No idea why. Try to fix later.
            try:
                cap = cv2.VideoCapture(settings.opencv_device)
                fourcc = cv2.CV_FOURCC('X', 'V', 'I', 'D')
                resolution = (settings.width, settings.height)
                out = cv2.VideoWriter(path_file, fourcc, 20.0, resolution)

                time_end = time.time() + duration_sec
                while cap.isOpened() and time.time() < time_end:
                    ret, frame = cap.read()
                    if ret:
                        # write the frame
                        out.write(frame)
                        if cv2.waitKey(1) & 0xFF == ord('q'):
                            break
                    else:
                        break
                cap.release()
                out.release()
                cv2.destroyAllWindows()
            except Exception as e:
                logger.exception("Exception raised while recording video: "
                                 "{err}".format(err=e))
        else:
            return
    try:
        set_user_grp(path_file, 'mycodo', 'mycodo')
    except Exception as e:
        logger.exception(
            "Exception raised in 'camera_record' when setting user grp: "
            "{err}".format(err=e))

    # Turn off output, if configured
    if settings.output_id and daemon_control:
        if not output_already_on:
            daemon_control.output_off(settings.output_id)

    try:
        set_user_grp(path_file, 'mycodo', 'mycodo')
        return save_path, filename
    except Exception as e:
        logger.exception(
            "Exception raised in 'camera_record' when setting user grp: "
            "{err}".format(err=e))
コード例 #17
0
class CustomModule(AbstractController, threading.Thread):
    """
    Class to operate custom controller
    """
    def __init__(self, ready, unique_id, testing=False):
        threading.Thread.__init__(self)
        super(CustomModule, self).__init__(ready,
                                           unique_id=unique_id,
                                           name=__name__)

        self.unique_id = unique_id
        self.log_level_debug = None

        self.control = DaemonControl()

        # Initialize custom options
        self.start_offset = None
        self.period = None
        self.input_temperature_condenser_device_id = None
        self.input_temperature_condenser_measurement_id = None
        self.input_temperature_condenser_max_age = None
        self.input_temperature_room_device_id = None
        self.input_temperature_room_measurement_id = None
        self.input_temperature_room_max_age = None
        self.output_ac_id = None
        self.output_ac_sensor_heater_id = None
        self.setpoint_temperature = None

        # Set custom options
        custom_controller = db_retrieve_table_daemon(CustomController,
                                                     unique_id=unique_id)
        self.setup_custom_options(CONTROLLER_INFORMATION['custom_options'],
                                  custom_controller)

        if not testing:
            pass
            # import controller-specific modules here

    def get_ac_condenser_temperature(self):
        """Get condenser temperature"""
        last_measurement = self.get_last_measurement(
            self.input_temperature_condenser_device_id,
            self.input_temperature_condenser_measurement_id,
            max_age=self.input_temperature_condenser_max_age)

        if last_measurement:
            self.logger.debug(
                "Most recent timestamp and measurement for "
                "input_temperature_condenser: {timestamp}, {meas}".format(
                    timestamp=last_measurement[0], meas=last_measurement[1]))
            return last_measurement
        else:
            self.logger.debug(
                "Could not find a measurement in the database for "
                "input_temperature_condenser device ID {} and measurement "
                "ID {}".format(
                    self.input_temperature_condenser_device_id,
                    self.input_temperature_condenser_measurement_id))

    def get_room_temperature(self):
        """Get condenser temperature"""
        last_measurement = self.get_last_measurement(
            self.input_temperature_room_device_id,
            self.input_temperature_room_measurement_id,
            max_age=self.input_temperature_room_max_age)

        if last_measurement:
            self.logger.debug(
                "Most recent timestamp and measurement for "
                "input_temperature_room: {timestamp}, {meas}".format(
                    timestamp=last_measurement[0], meas=last_measurement[1]))
            return last_measurement
        else:
            self.logger.debug(
                "Could not find a measurement in the database for "
                "input_temperature_room device ID {} and measurement "
                "ID {}".format(self.input_temperature_room_device_id,
                               self.input_temperature_room_measurement_id))

    def run(self):
        try:
            self.logger.info("Activated in {:.1f} ms".format(
                (timeit.default_timer() - self.thread_startup_timer) * 1000))

            self.ready.set()
            self.running = True

            start_offset_timer = time.time() + self.start_offset
            while self.running and time.time() < start_offset_timer:
                time.sleep(1)
            if not self.running:
                return

            # try to get measurement from sensor
            temperature_condenser = self.get_ac_condenser_temperature()
            temperature_room = self.get_room_temperature()
            if not temperature_condenser or not temperature_room:
                return

            # Turn Output output_ac on
            self.logger.debug("Turning output_ac with ID {} on".format(
                self.output_ac_id))
            self.control.output_on(self.output_ac_id)

            # Start a loop
            while self.running:
                temperature_condenser = self.get_ac_condenser_temperature()
                temperature_room = self.get_room_temperature()

                if temperature_room > self.setpoint_temperature and temperature_condenser > 0:
                    # Turn Output output_ac_sensor_heater on
                    self.logger.debug(
                        "Turning output_ac_sensor_heater with ID {} on".format(
                            self.output_ac_sensor_heater_id))
                    self.control.output_on(self.output_ac_sensor_heater_id)
                else:
                    # Turn Output output_ac_sensor_heater off
                    self.logger.debug(
                        "Turning output_ac_sensor_heater with ID {} off".
                        format(self.output_ac_sensor_heater_id))
                    self.control.output_off(self.output_ac_sensor_heater_id)

                time.sleep(self.period)
        except:
            self.logger.exception("Run Error")
        finally:
            self.run_finally()
            self.running = False
            if self.thread_shutdown_timer:
                self.logger.info("Deactivated in {:.1f} ms".format(
                    (timeit.default_timer() - self.thread_shutdown_timer) *
                    1000))
            else:
                self.logger.error("Deactivated unexpectedly")

    def loop(self):
        pass

    def initialize_variables(self):
        controller = db_retrieve_table_daemon(CustomController,
                                              unique_id=self.unique_id)
        self.log_level_debug = controller.log_level_debug
        self.set_log_level_debug(self.log_level_debug)

    def run_finally(self):
        # Turn Output output_ac off
        self.logger.debug("Turning output_ac with ID {} off".format(
            self.output_ac_id))
        self.control.output_off(self.output_ac_id)

        # Turn Output output_ac_sensor_heater off
        self.logger.debug(
            "Turning output_ac_sensor_heater with ID {} off".format(
                self.output_ac_sensor_heater_id))
        self.control.output_off(self.output_ac_sensor_heater_id)
コード例 #18
0
ファイル: camera.py プロジェクト: sjoerdschouten/Mycodo
def camera_record(record_type, unique_id, duration_sec=None, tmp_filename=None):
    """
    Record still image from cameras
    :param record_type:
    :param unique_id:
    :param duration_sec:
    :param tmp_filename:
    :return:
    """
    daemon_control = None
    settings = db_retrieve_table_daemon(Camera, unique_id=unique_id)
    timestamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
    assure_path_exists(PATH_CAMERAS)
    camera_path = assure_path_exists(
        os.path.join(PATH_CAMERAS, '{uid}'.format(uid=settings.unique_id)))
    if record_type == 'photo':
        if settings.path_still:
            save_path = settings.path_still
        else:
            save_path = assure_path_exists(os.path.join(camera_path, 'still'))
        # TODO: next major version, remove cam id (unique_id is already in path)
        filename = 'Still-{cam_id}-{cam}-{ts}.jpg'.format(
            cam_id=settings.id,
            cam=settings.name,
            ts=timestamp).replace(" ", "_")
    elif record_type == 'timelapse':
        if settings.path_timelapse:
            save_path = settings.path_timelapse
        else:
            save_path = assure_path_exists(os.path.join(camera_path, 'timelapse'))
        start = datetime.datetime.fromtimestamp(
            settings.timelapse_start_time).strftime("%Y-%m-%d_%H-%M-%S")
        # TODO: next major version, remove cam id (unique_id is already in path)
        filename = 'Timelapse-{cam_id}-{cam}-{st}-img-{cn:05d}.jpg'.format(
            cam_id=settings.id,
            cam=settings.name,
            st=start,
            cn=settings.timelapse_capture_number).replace(" ", "_")
    elif record_type == 'video':
        if settings.path_video:
            save_path = settings.path_video
        else:
            save_path = assure_path_exists(os.path.join(camera_path, 'video'))
        filename = 'Video-{cam}-{ts}.h264'.format(
            cam=settings.name,
            ts=timestamp).replace(" ", "_")
    else:
        return

    assure_path_exists(save_path)

    if tmp_filename:
        filename = tmp_filename

    path_file = os.path.join(save_path, filename)

    # Turn on output, if configured
    output_already_on = False
    output_id = None
    output_channel_id = None
    output_channel = None
    if settings.output_id and ',' in settings.output_id:
        output_id = settings.output_id.split(",")[0]
        output_channel_id = settings.output_id.split(",")[1]
        output_channel = db_retrieve_table_daemon(OutputChannel, unique_id=output_channel_id)

    if output_id and output_channel:
        daemon_control = DaemonControl()
        if daemon_control.output_state(output_id, output_channel=output_channel.channel) == "on":
            output_already_on = True
        else:
            daemon_control.output_on(output_id, output_channel=output_channel.channel)

    # Pause while the output remains on for the specified duration.
    # Used for instance to allow fluorescent lights to fully turn on before
    # capturing an image.
    if settings.output_duration:
        time.sleep(settings.output_duration)

    if settings.library == 'picamera':
        try:
            import picamera

            # Try 5 times to access the pi camera (in case another process is accessing it)
            for _ in range(5):
                try:
                    with picamera.PiCamera() as camera:
                        camera.resolution = (settings.width, settings.height)
                        camera.hflip = settings.hflip
                        camera.vflip = settings.vflip
                        camera.rotation = settings.rotation
                        camera.brightness = int(settings.brightness)
                        camera.contrast = int(settings.contrast)
                        camera.exposure_compensation = int(settings.exposure)
                        camera.saturation = int(settings.saturation)
                        camera.shutter_speed = settings.picamera_shutter_speed
                        camera.sharpness = settings.picamera_sharpness
                        camera.iso = settings.picamera_iso
                        camera.awb_mode = settings.picamera_awb
                        if settings.picamera_awb == 'off':
                            camera.awb_gains = (settings.picamera_awb_gain_red,
                                                settings.picamera_awb_gain_blue)
                        camera.exposure_mode = settings.picamera_exposure_mode
                        camera.meter_mode = settings.picamera_meter_mode
                        camera.image_effect = settings.picamera_image_effect

                        camera.start_preview()
                        time.sleep(2)  # Camera warm-up time

                        if record_type in ['photo', 'timelapse']:
                            camera.capture(path_file, use_video_port=False)
                        elif record_type == 'video':
                            camera.start_recording(path_file, format='h264', quality=20)
                            camera.wait_recording(duration_sec)
                            camera.stop_recording()
                        else:
                            return
                        break
                except picamera.exc.PiCameraMMALError:
                    logger.error("The camera is already open by picamera. Retrying 4 times.")
                time.sleep(1)
        except:
            logger.exception("picamera")

    elif settings.library == 'fswebcam':
        try:
            cmd = "/usr/bin/fswebcam --device {dev} --resolution {w}x{h} --set brightness={bt}% " \
                  "--no-banner --save {file}".format(dev=settings.device,
                                                     w=settings.width,
                                                     h=settings.height,
                                                     bt=settings.brightness,
                                                     file=path_file)
            if settings.hflip:
                cmd += " --flip h"
            if settings.vflip:
                cmd += " --flip h"
            if settings.rotation:
                cmd += " --rotat {angle}".format(angle=settings.rotation)
            if settings.custom_options:
                cmd += " {}".format(settings.custom_options)

            out, err, status = cmd_output(cmd, stdout_pipe=False, user='******')
            logger.debug(
                "Camera debug message: "
                "cmd: {}; out: {}; error: {}; status: {}".format(
                    cmd, out, err, status))
        except:
            logger.exception("fswebcam")

    elif settings.library == 'raspistill':
        try:
            cmd = "/usr/bin/raspistill -w {w} -h {h} --brightness {bt} " \
                  "-o {file}".format(w=settings.width,
                                     h=settings.height,
                                     bt=settings.brightness,
                                     file=path_file)

            if settings.contrast is not None:
                cmd += " --contrast {}".format(int(settings.contrast))
            if settings.saturation is not None:
                cmd += " --saturation {}".format(int(settings.saturation))
            if settings.picamera_sharpness is not None:
                cmd += " --sharpness {}".format(int(settings.picamera_sharpness))
            if settings.picamera_iso not in [0, None]:
                cmd += " --ISO {}".format(int(settings.picamera_iso))
            if settings.picamera_awb is not None:
                cmd += " --awb {}".format(settings.picamera_awb)
            if settings.hflip:
                cmd += " --hflip"
            if settings.vflip:
                cmd += " --vflip"
            if settings.rotation:
                cmd += " --rotation {angle}".format(angle=settings.rotation)
            if settings.custom_options:
                cmd += " {}".format(settings.custom_options)

            out, err, status = cmd_output(cmd, stdout_pipe=False, user='******')
            logger.debug(
                "Camera debug message: "
                "cmd: {}; out: {}; error: {}; status: {}".format(
                    cmd, out, err, status))
        except:
            logger.exception("raspistill")

    elif settings.library == 'opencv':
        try:
            import cv2
            import imutils

            cap = cv2.VideoCapture(settings.opencv_device)
            cap.set(cv2.CAP_PROP_FRAME_WIDTH, settings.width)
            cap.set(cv2.CAP_PROP_FRAME_HEIGHT, settings.height)
            cap.set(cv2.CAP_PROP_EXPOSURE, settings.exposure)
            cap.set(cv2.CAP_PROP_GAIN, settings.gain)
            cap.set(cv2.CAP_PROP_BRIGHTNESS, settings.brightness)
            cap.set(cv2.CAP_PROP_CONTRAST, settings.contrast)
            cap.set(cv2.CAP_PROP_HUE, settings.hue)
            cap.set(cv2.CAP_PROP_SATURATION, settings.saturation)

            # Check if image can be read
            status, _ = cap.read()
            if not status:
                logger.error(
                    "Cannot detect USB camera with device '{dev}'".format(
                        dev=settings.opencv_device))
                return

            # Discard a few frames to allow camera to adjust to settings
            for _ in range(2):
                cap.read()

            if record_type in ['photo', 'timelapse']:
                edited = False
                status, img_orig = cap.read()
                cap.release()

                if not status:
                    logger.error("Could not acquire image")
                    return

                img_edited = img_orig.copy()

                if any((settings.hflip, settings.vflip, settings.rotation)):
                    edited = True

                if settings.hflip and settings.vflip:
                    img_edited = cv2.flip(img_orig, -1)
                elif settings.hflip:
                    img_edited = cv2.flip(img_orig, 1)
                elif settings.vflip:
                    img_edited = cv2.flip(img_orig, 0)

                if settings.rotation:
                    img_edited = imutils.rotate_bound(img_orig, settings.rotation)

                if edited:
                    cv2.imwrite(path_file, img_edited)
                else:
                    cv2.imwrite(path_file, img_orig)

            elif record_type == 'video':
                # TODO: opencv video recording is currently not working. No idea why. Try to fix later.
                try:
                    cap = cv2.VideoCapture(settings.opencv_device)
                    fourcc = cv2.CV_FOURCC('X', 'V', 'I', 'D')
                    resolution = (settings.width, settings.height)
                    out = cv2.VideoWriter(path_file, fourcc, 20.0, resolution)

                    time_end = time.time() + duration_sec
                    while cap.isOpened() and time.time() < time_end:
                        ret, frame = cap.read()
                        if ret:
                            # write the frame
                            out.write(frame)
                            if cv2.waitKey(1) & 0xFF == ord('q'):
                                break
                        else:
                            break
                    cap.release()
                    out.release()
                    cv2.destroyAllWindows()
                except Exception as e:
                    logger.exception(
                        "Exception raised while recording video: "
                        "{err}".format(err=e))
            else:
                return
        except:
            logger.exception("opencv")

    elif settings.library == 'http_address':
        try:
            import cv2
            import imutils
            from urllib.error import HTTPError
            from urllib.parse import urlparse
            from urllib.request import urlretrieve

            if record_type in ['photo', 'timelapse']:
                path_tmp = "/tmp/tmpimg.jpg"

                # Get filename and extension, if available
                a = urlparse(settings.url_still)
                filename = os.path.basename(a.path)
                if filename:
                    path_tmp = "/tmp/{}".format(filename)

                try:
                    os.remove(path_tmp)
                except FileNotFoundError:
                    pass

                try:
                    urlretrieve(settings.url_still, path_tmp)
                except HTTPError as err:
                    logger.error(err)
                except Exception as err:
                    logger.exception(err)

                try:
                    img_orig = cv2.imread(path_tmp)

                    if img_orig is not None and img_orig.shape is not None:
                        if any((settings.hflip, settings.vflip, settings.rotation)):
                            img_edited = None
                            if settings.hflip and settings.vflip:
                                img_edited = cv2.flip(img_orig, -1)
                            elif settings.hflip:
                                img_edited = cv2.flip(img_orig, 1)
                            elif settings.vflip:
                                img_edited = cv2.flip(img_orig, 0)

                            if settings.rotation:
                                img_edited = imutils.rotate_bound(img_orig, settings.rotation)

                            if img_edited:
                                cv2.imwrite(path_file, img_edited)
                        else:
                            cv2.imwrite(path_file, img_orig)
                    else:
                        os.rename(path_tmp, path_file)
                except Exception as err:
                    logger.error("Could not convert, rotate, or invert image: {}".format(err))
                    try:
                        os.rename(path_tmp, path_file)
                    except FileNotFoundError:
                        logger.error("Camera image not found")

            elif record_type == 'video':
                pass  # No video (yet)
        except:
            logger.exception("http:address")

    elif settings.library == 'http_address_requests':
        try:
            import cv2
            import imutils
            import requests

            if record_type in ['photo', 'timelapse']:
                path_tmp = "/tmp/tmpimg.jpg"
                try:
                    os.remove(path_tmp)
                except FileNotFoundError:
                    pass

                try:
                    r = requests.get(settings.url_still)
                    if r.status_code == 200:
                        open(path_tmp, 'wb').write(r.content)
                    else:
                        logger.error("Could not download image. Status code: {}".format(r.status_code))
                except requests.HTTPError as err:
                    logger.error("HTTPError: {}".format(err))
                except Exception as err:
                    logger.exception(err)

                try:
                    img_orig = cv2.imread(path_tmp)

                    if img_orig is not None and img_orig.shape is not None:
                        if any((settings.hflip, settings.vflip, settings.rotation)):
                            if settings.hflip and settings.vflip:
                                img_edited = cv2.flip(img_orig, -1)
                            elif settings.hflip:
                                img_edited = cv2.flip(img_orig, 1)
                            elif settings.vflip:
                                img_edited = cv2.flip(img_orig, 0)

                            if settings.rotation:
                                img_edited = imutils.rotate_bound(img_orig, settings.rotation)

                            cv2.imwrite(path_file, img_edited)
                        else:
                            cv2.imwrite(path_file, img_orig)
                    else:
                        os.rename(path_tmp, path_file)
                except Exception as err:
                    logger.error("Could not convert, rotate, or invert image: {}".format(err))
                    try:
                        os.rename(path_tmp, path_file)
                    except FileNotFoundError:
                        logger.error("Camera image not found")

            elif record_type == 'video':
                pass  # No video (yet)
        except:
            logger.exception("http_address_requests")

    try:
        set_user_grp(path_file, 'mycodo', 'mycodo')
    except Exception as e:
        logger.exception(
            "Exception raised in 'camera_record' when setting user grp: "
            "{err}".format(err=e))

    # Turn off output, if configured
    if output_id and output_channel and daemon_control and not output_already_on:
        daemon_control.output_off(output_id, output_channel=output_channel.channel)

    try:
        set_user_grp(path_file, 'mycodo', 'mycodo')
        return save_path, filename
    except Exception as e:
        logger.exception(
            "Exception raised in 'camera_record' when setting user grp: "
            "{err}".format(err=e))
コード例 #19
0
ファイル: am2315.py プロジェクト: matteliot/Mycodo
class AM2315Sensor(AbstractInput):
    """
    A sensor support class that measures the AM2315's humidity and temperature
    and calculates the dew point

    """
    def __init__(self, input_dev, testing=False):
        super(AM2315Sensor, self).__init__()
        self.logger = logging.getLogger('mycodo.inputs.am2315')
        self._dew_point = None
        self._humidity = None
        self._temperature = None
        self.powered = False
        self.am = None

        if not testing:
            from mycodo.mycodo_client import DaemonControl
            self.logger = logging.getLogger(
                'mycodo.inputs.am2315_{id}'.format(id=input_dev.id))
            self.i2c_bus = input_dev.i2c_bus
            self.power_output_id = input_dev.power_output_id
            self.control = DaemonControl()
            self.start_sensor()
            self.am = AM2315(self.i2c_bus)

    def __repr__(self):
        """  Representation of object """
        return "<{cls}(dewpoint={dpt})(humidity={hum})(temperature={temp})>".format(
            cls=type(self).__name__,
            dpt="{0:.2f}".format(self._dew_point),
            hum="{0:.2f}".format(self._humidity),
            temp="{0:.2f}".format(self._temperature))

    def __str__(self):
        """ Return measurement information """
        return "Dew Point: {dpt}, Humidity: {hum}, Temperature: {temp}".format(
            dpt="{0:.2f}".format(self._dew_point),
            hum="{0:.2f}".format(self._humidity),
            temp="{0:.2f}".format(self._temperature))

    def __iter__(self):  # must return an iterator
        """ AM2315Sensor iterates through live measurement readings """
        return self

    def next(self):
        """ Get next measurement reading """
        if self.read():  # raised an error
            raise StopIteration  # required
        return dict(dewpoint=float('{0:.2f}'.format(self._dew_point)),
                    humidity=float('{0:.2f}'.format(self._humidity)),
                    temperature=float('{0:.2f}'.format(self._temperature)))

    @property
    def dew_point(self):
        """ AM2315 dew point in Celsius """
        if self._dew_point is None:  # update if needed
            self.read()
        return self._dew_point

    @property
    def humidity(self):
        """ AM2315 relative humidity in percent """
        if self._humidity is None:  # update if needed
            self.read()
        return self._humidity

    @property
    def temperature(self):
        """ AM2315 temperature in Celsius """
        if self._temperature is None:  # update if needed
            self.read()
        return self._temperature

    def get_measurement(self):
        """ Gets the humidity and temperature """
        self._dew_point = None
        self._humidity = None
        self._temperature = None

        # Ensure if the power pin turns off, it is turned back on
        if (self.power_output_id and
                db_retrieve_table_daemon(Output, unique_id=self.power_output_id) and
                self.control.output_state(self.power_output_id) == 'off'):
            self.logger.error(
                'Sensor power output {rel} detected as being off. '
                'Turning on.'.format(rel=self.power_output_id))
            self.start_sensor()
            time.sleep(2)

        # Try twice to get measurement. This prevents an anomaly where
        # the first measurement fails if the sensor has just been powered
        # for the first time.
        for _ in range(2):
            dew_point, humidity, temperature = self.return_measurements()
            if dew_point is not None:
                return dew_point, humidity, temperature  # success - no errors
            time.sleep(2)

        # Measurement failure, power cycle the sensor (if enabled)
        # Then try two more times to get a measurement
        if self.power_output_id:
            self.stop_sensor()
            time.sleep(2)
            self.start_sensor()
            for _ in range(2):
                dew_point, humidity, temperature = self.return_measurements()
                if dew_point is not None:
                    return dew_point, humidity, temperature  # success
                time.sleep(2)

        self.logger.debug("Could not acquire a measurement")
        return None, None, None

    def return_measurements(self):
        # Retry measurement if CRC fails
        for num_measure in range(3):
            humidity, temperature = self.am.data()
            if humidity is None:
                self.logger.debug(
                    "Measurement {num} returned failed CRC".format(
                        num=num_measure))
                pass
            else:
                dew_pt = dewpoint(temperature, humidity)
                return dew_pt, humidity, temperature
            time.sleep(2)

        self.logger.error("All measurements returned failed CRC")
        return None, None, None

    def read(self):
        """
        Takes a reading from the AM2315 and updates the self.dew_point,
        self._humidity, and self._temperature values

        :returns: None on success or 1 on error
        """
        try:
            (self._dew_point,
             self._humidity,
             self._temperature) = self.get_measurement()
            if self._dew_point is not None:
                return  # success - no errors
        except Exception as e:
            self.logger.exception(
                "{cls} raised an exception when taking a reading: "
                "{err}".format(cls=type(self).__name__, err=e))
        return 1

    def start_sensor(self):
        """ Turn the sensor on """
        if self.power_output_id:
            self.logger.info("Turning on sensor")
            self.control.output_on(self.power_output_id, 0)
            time.sleep(2)
            self.powered = True

    def stop_sensor(self):
        """ Turn the sensor off """
        if self.power_output_id:
            self.logger.info("Turning off sensor")
            self.control.output_off(self.power_output_id)
            self.powered = False