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 setup_lcd_line(self, display_id, line, device_id, measurement_id): if measurement_id == 'output': device_measurement = db_retrieve_table_daemon( Output, unique_id=device_id) elif measurement_id in ['BLANK', 'IP']: device_measurement = None else: device_measurement = db_retrieve_table_daemon( DeviceMeasurements, unique_id=measurement_id) if device_measurement: conversion = db_retrieve_table_daemon( Conversion, unique_id=device_measurement.conversion_id) channel, unit, measurement = return_measurement_info( device_measurement, conversion) else: channel = None unit = None measurement = None self.lcd_line[display_id][line]['setup'] = False self.lcd_line[display_id][line]['id'] = device_id self.lcd_line[display_id][line]['name'] = None self.lcd_line[display_id][line]['unit'] = unit self.lcd_line[display_id][line]['measure'] = measurement self.lcd_line[display_id][line]['channel'] = channel if 'time' in measurement_id: self.lcd_line[display_id][line]['measure'] = 'time' elif measurement_id in ['BLANK', 'IP']: self.lcd_line[display_id][line]['measure'] = measurement_id self.lcd_line[display_id][line]['name'] = '' if not device_id: return if unit in self.dict_units: self.lcd_line[display_id][line]['unit'] = unit else: self.lcd_line[display_id][line]['unit'] = '' # Determine the name controllers = [ Output, PID, Input, Math ] for each_controller in controllers: controller_found = db_retrieve_table_daemon(each_controller, unique_id=device_id) if controller_found: self.lcd_line[display_id][line]['name'] = controller_found.name if (self.lcd_line[display_id][line]['measure'] in ['BLANK', 'IP', 'time'] or None not in [self.lcd_line[display_id][line]['name'], self.lcd_line[display_id][line]['unit']]): self.lcd_line[display_id][line]['setup'] = True
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 refresh_daemon_misc_settings(self): old_time = self.output_usage_report_next_gen self.output_usage_report_next_gen = next_schedule( self.output_usage_report_span, self.output_usage_report_day, self.output_usage_report_hour) try: self.logger.debug("Refreshing misc settings") misc = db_retrieve_table_daemon(Misc, entry='first') self.opt_out_statistics = misc.stats_opt_out self.enable_upgrade_check = misc.enable_upgrade_check self.output_usage_report_gen = misc.output_usage_report_gen self.output_usage_report_span = misc.output_usage_report_span self.output_usage_report_day = misc.output_usage_report_day self.output_usage_report_hour = misc.output_usage_report_hour if (self.output_usage_report_gen and old_time != self.output_usage_report_next_gen): str_next_report = time.strftime( '%c', time.localtime(self.output_usage_report_next_gen)) self.logger.debug( "Generating next output usage report {time_date}".format( time_date=str_next_report)) except Exception as except_msg: message = "Could not refresh misc settings:" \ " {err}".format(err=except_msg) self.logger.exception(message)
def __init__(self, input_dev, testing=False): super(InputModule, self).__init__() self.logger = logging.getLogger('mycodo.mcp3008') self.acquiring_measurement = False self.adc = None if not testing: import Adafruit_MCP3008 self.logger = logging.getLogger( 'mycodo.mcp3008_{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.pin_clock = input_dev.pin_clock self.pin_cs = input_dev.pin_cs self.pin_miso = input_dev.pin_miso self.pin_mosi = input_dev.pin_mosi self.scale_from_max = input_dev.scale_from_max self.adc = Adafruit_MCP3008.MCP3008(clk=self.pin_clock, cs=self.pin_cs, miso=self.pin_miso, mosi=self.pin_mosi)
def get_method_output(self, method_id): """ Get output variable from method """ this_controller = db_retrieve_table_daemon( Trigger, unique_id=self.function_id) setpoint, ended = calculate_method_setpoint( method_id, Trigger, this_controller, Method, MethodData, self.logger) if setpoint is not None: if setpoint > 100: setpoint = 100 elif setpoint < 0: setpoint = 0 if ended: with session_scope(MYCODO_DB_PATH) as db_session: mod_conditional = db_session.query(Trigger) mod_conditional = mod_conditional.filter( Trigger.unique_id == self.function_id).first() mod_conditional.is_activated = False db_session.commit() self.is_activated = False self.stop_controller() return setpoint, ended
def __init__(self, input_dev, testing=False): super(InputModule, self).__init__() self.logger = logging.getLogger("mycodo.inputs.amg8833") self.save_image = False self.temp_max = None self.temp_min = None self.report = False self.scale = 2 self.nx = 8 self.ny = 8 if not testing: from Adafruit_AMG88xx import Adafruit_AMG88xx self.logger = logging.getLogger( "mycodo.ds18b20_{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.Adafruit_AMG88xx = Adafruit_AMG88xx self.i2c_address = int(str(input_dev.i2c_location), 16) self.i2c_bus = input_dev.i2c_bus self.input_dev = input_dev self.sensor = self.Adafruit_AMG88xx(address=self.i2c_address, busnum=self.i2c_bus) time.sleep(.1) # wait for it to boot
def __init__(self, input_dev, mode=BMP280_STANDARD, testing=False): super(InputModule, self).__init__() self.logger = logging.getLogger("mycodo.inputs.bmp280") if not testing: import Adafruit_GPIO.I2C as I2C self.logger = logging.getLogger( "mycodo.bmp280_{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_address = int(str(input_dev.i2c_location), 16) self.i2c_bus = input_dev.i2c_bus if mode not in [BMP280_ULTRALOWPOWER, BMP280_STANDARD, BMP280_HIGHRES, BMP280_ULTRAHIGHRES]: raise ValueError( 'Unexpected mode value {0}. Set mode to one of ' 'BMP280_ULTRALOWPOWER, BMP280_STANDARD, BMP280_HIGHRES, ' 'or BMP280_ULTRAHIGHRES'.format(mode)) self._mode = mode # Create I2C device. i2c = I2C self._device = i2c.get_i2c_device(self.i2c_address, busnum=self.i2c_bus) # Load calibration values. self._load_calibration() self._tfine = 0
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 output_state(output_id): output = db_retrieve_table_daemon(Output, unique_id=output_id) GPIO.setmode(GPIO.BCM) if GPIO.input(output.pin) == output.trigger: gpio_state = 'On' else: gpio_state = 'Off' return gpio_state
def __init__(self, input_dev, testing=False): super(InputModule, self).__init__() self.logger = logging.getLogger("mycodo.inputs.winsen_zh03b") self.fan_is_on = False if not testing: import serial import binascii self.logger = logging.getLogger( "mycodo.winsen_zh03b_{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.binascii = binascii self.uart_location = input_dev.uart_location self.baud_rate = input_dev.baud_rate # Check if device is valid self.serial_device = is_device(self.uart_location) self.fan_modulate = True self.fan_seconds = 50.0 if input_dev.custom_options: for each_option in input_dev.custom_options.split(';'): option = each_option.split(',')[0] value = each_option.split(',')[1] if option == 'fan_modulate': self.fan_modulate = bool(value) elif option == 'fan_seconds': self.fan_seconds = float(value) if self.serial_device: try: self.ser = serial.Serial( port=self.serial_device, baudrate=self.baud_rate, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, timeout=10 ) self.ser.flushInput() if not self.fan_modulate: self.dormant_mode('run') time.sleep(0.1) except serial.SerialException: self.logger.exception('Opening serial') else: self.logger.error( 'Could not open "{dev}". ' 'Check the device location is correct.'.format( dev=self.uart_location))
def get_measurement(measurement_id): """ Find measurement """ device_measurement = db_retrieve_table_daemon( DeviceMeasurements).filter( DeviceMeasurements.unique_id == measurement_id).first() if device_measurement: return device_measurement else: return None
def refresh_daemon_camera_settings(self): try: self.logger.debug("Refreshing camera settings") self.camera = db_retrieve_table_daemon( Camera, entry='all') except Exception as except_msg: self.camera = [] message = "Could not read camera table:" \ " {err}".format(err=except_msg) self.logger.exception(message)
def convert_from_x_to_y_unit(unit_from, unit_to, in_value): conversion = db_retrieve_table_daemon(Conversion) conversion = conversion.filter(Conversion.convert_unit_from == unit_from) conversion = conversion.filter(Conversion.convert_unit_to == unit_to).first() if conversion: replaced_str = conversion.equation.replace('x', str(in_value)) return float('{0:.5f}'.format(eval(replaced_str))) else: logger.error("Conversion not found for {uf} to {ut}".format( uf=unit_to, ut=unit_from))
def get_condition_measurement(sql_condition): device_id = sql_condition.measurement.split(',')[0] measurement_id = sql_condition.measurement.split(',')[1] device_measurement = db_retrieve_table_daemon( DeviceMeasurements, unique_id=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) if None in [channel, unit]: logger.error( "Could not determine channel or unit from measurement ID: " "{}".format(measurement_id)) return max_age = sql_condition.max_age # Check Measurement Conditions if sql_condition.condition_type == 'measurement': # Check if there hasn't been a measurement in the last set number # of seconds. If not, trigger conditional last_measurement = get_last_measurement( device_id, unit, measurement, channel, max_age) return last_measurement # If the edge detection variable is set, calling this function will # trigger an edge detection event. This will merely produce the correct # message based on the edge detection settings. elif sql_condition.condition_type == 'gpio_state': try: GPIO.setmode(GPIO.BCM) GPIO.setup(int(sql_condition.gpio_pin), GPIO.IN) gpio_state = GPIO.input(int(sql_condition.gpio_pin)) except: gpio_state = None logger.error("Exception reading the GPIO pin") return gpio_state
def __init__(self, input_dev, testing=False): super(InputModule, self).__init__() self.logger = logging.getLogger("mycodo.inputs.raspi") if not testing: self.logger = logging.getLogger( "mycodo.raspi_{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)
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 convert_units(conversion_id, measure_value): """ Convert from one unit to another, such as ppm to ppb. See UNIT_CONVERSIONS in config_devices_units.py for available conversions. :param conversion_id: conversion ID :param measure_value: The value to convert :return: converted value """ conversion = db_retrieve_table_daemon(Conversion, unique_id=conversion_id) replaced_str = conversion.equation.replace('x', str(measure_value)) return float('{0:.5f}'.format(eval(replaced_str)))
def edge_detected(self, bcm_pin): """ Callback function from GPIO.add_event_detect() for when an edge is detected Write rising (1) or falling (-1) edge to influxdb database Trigger any conditionals that match the rising/falling/both edge :param bcm_pin: BMC pin of rising/falling edge (required parameter) :return: None """ gpio_state = GPIO.input(int(self.gpio_location)) if time.time() > self.edge_reset_timer: self.edge_reset_timer = time.time()+self.switch_reset_period if (self.switch_edge == 'rising' or (self.switch_edge == 'both' and gpio_state)): rising_or_falling = 1 # Rising edge detected state_str = 'Rising' else: rising_or_falling = -1 # Falling edge detected state_str = 'Falling' write_db = threading.Thread( target=write_influxdb_value, args=(self.unique_id, 'edge', rising_or_falling,)) write_db.start() trigger = db_retrieve_table_daemon(Trigger) trigger = trigger.filter( Trigger.trigger_type == 'trigger_edge') trigger = trigger.filter( Trigger.measurement == self.unique_id) trigger = trigger.filter( Trigger.is_activated == True) for each_trigger in trigger.all(): if each_trigger.edge_detected in ['both', state_str.lower()]: now = time.time() timestamp = datetime.datetime.fromtimestamp( now).strftime('%Y-%m-%d %H-%M-%S') message = "{ts}\n[Trigger {cid} ({cname})] " \ "Input {oid} ({name}) {state} edge detected " \ "on pin {pin} (BCM)".format( ts=timestamp, cid=each_trigger.id, cname=each_trigger.name, oid=self.input_id, name=self.input_name, state=state_str, pin=bcm_pin) self.control.trigger_all_actions( each_trigger.unique_id, message=message)
def start_method(self, method_id): """ Instruct a method to start running """ if 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() self.method_start_act = self.method_start_time self.method_start_time = None self.method_end_time = None if method.method_type == 'Duration': if self.method_start_act == 'Ended': with session_scope(MYCODO_DB_PATH) as db_session: mod_conditional = db_session.query(Trigger) mod_conditional = mod_conditional.filter( Trigger.unique_id == self.function_id).first() mod_conditional.is_activated = False db_session.commit() self.stop_controller() self.logger.warning( "Method has ended. " "Activate the Trigger controller to start it again.") elif (self.method_start_act == 'Ready' or self.method_start_act is None): # Method has been instructed to begin now = datetime.datetime.now() self.method_start_time = now if method_data_repeat and method_data_repeat.duration_end: self.method_end_time = now + datetime.timedelta( seconds=float(method_data_repeat.duration_end)) with session_scope(MYCODO_DB_PATH) as db_session: mod_conditional = db_session.query(Trigger) mod_conditional = mod_conditional.filter( Trigger.unique_id == self.function_id).first() mod_conditional.method_start_time = self.method_start_time mod_conditional.method_end_time = self.method_end_time db_session.commit()
def setup_settings(self): """ Define all settings """ cond = db_retrieve_table_daemon( Conditional, unique_id=self.function_id) self.is_activated = cond.is_activated self.smtp_max_count = db_retrieve_table_daemon( SMTP, entry='first').hourly_max self.email_count = 0 self.allowed_to_send_notice = True now = time.time() self.smtp_wait_timer = now + 3600 self.timer_period = None self.conditional_statement = cond.conditional_statement self.period = cond.period self.start_offset = cond.start_offset self.refractory_period = cond.refractory_period self.timer_refractory_period = 0 self.smtp_wait_timer = now + 3600 self.timer_period = now + self.start_offset
def __init__(self, input_dev, testing=False): super(InputModule, self).__init__() self.logger = logging.getLogger("mycodo.inputs.bmp180") if not testing: from Adafruit_BMP import BMP085 self.logger = logging.getLogger( "mycodo.bmp180_{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.bmp = BMP085.BMP085(busnum=self.i2c_bus)
def __init__(self, input_dev, testing=False): super(InputModule, self).__init__() self.logger = logging.getLogger("mycodo.inputs.cozir_co2") if not testing: from cozir import Cozir self.logger = logging.getLogger( "mycodo.cozir_co2_{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.uart_location = input_dev.uart_location self.sensor = Cozir(self.uart_location)
def __init__(self, input_dev, testing=False): super(InputModule, self).__init__() self.logger = logging.getLogger("mycodo.inputs.htu21d") if not testing: import pigpio self.logger = logging.getLogger( "mycodo.htu21d_{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.i2c_address = 0x40 # HTU21D-F Address self.pi = pigpio.pi()
def __init__(self, input_dev, testing=False): super(InputModule, self).__init__() self.logger = logging.getLogger("mycodo.inputs.sht2x") if not testing: from smbus2 import SMBus self.logger = logging.getLogger( "mycodo.sht2x_{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_address = int(str(input_dev.i2c_location), 16) self.i2c_bus = input_dev.i2c_bus self.sht2x = SMBus(self.i2c_bus)
def __init__(self, input_dev, testing=False): super(InputModule, self).__init__() self.logger = logging.getLogger("mycodo.inputs.signal_pwm") if not testing: import pigpio self.logger = logging.getLogger( "mycodo.signal_pwm_{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.weighting = input_dev.weighting self.sample_time = input_dev.sample_time self.pigpio = pigpio
def __init__(self, input_dev, testing=False): super(InputModule, self).__init__() self.logger = logging.getLogger("mycodo.inputs.hdc1000") if not testing: self.logger = logging.getLogger( "mycodo.hdc1000_{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.resolution_temperature = input_dev.resolution self.resolution_humidity = input_dev.resolution_2 self.i2c_bus = input_dev.i2c_bus self.i2c_address = 0x40 # HDC1000-F Address self.HDC1000_fr = io.open("/dev/i2c-" + str(self.i2c_bus), "rb", buffering=0) self.HDC1000_fw = io.open("/dev/i2c-" + str(self.i2c_bus), "wb", buffering=0) # set device address fcntl.ioctl(self.HDC1000_fr, I2C_SLAVE, self.i2c_address) fcntl.ioctl(self.HDC1000_fw, I2C_SLAVE, self.i2c_address) time.sleep(0.015) # 15ms startup time config = HDC1000_CONFIG_ACQUISITION_MODE s = [HDC1000_CONFIGURATION_REGISTER, config >> 8, 0x00] s2 = bytearray(s) self.HDC1000_fw.write(s2) # sending config register bytes time.sleep(0.015) # From the data sheet # Set resolutions if self.resolution_temperature == 11: self.set_temperature_resolution(HDC1000_CONFIG_TEMPERATURE_RESOLUTION_11BIT) elif self.resolution_temperature == 14: self.set_temperature_resolution(HDC1000_CONFIG_TEMPERATURE_RESOLUTION_14BIT) if self.resolution_humidity == 8: self.set_humidity_resolution(HDC1000_CONFIG_HUMIDITY_RESOLUTION_8BIT) elif self.resolution_humidity == 11: self.set_humidity_resolution(HDC1000_CONFIG_HUMIDITY_RESOLUTION_11BIT) elif self.resolution_humidity == 14: self.set_humidity_resolution(HDC1000_CONFIG_HUMIDITY_RESOLUTION_14BIT)
def __init__(self, input_dev, testing=False): super(InputModule, self).__init__() self.logger = logging.getLogger("mycodo.inputs.bh1750") if not testing: from smbus2 import SMBus self.logger = logging.getLogger( "mycodo.bh1750_{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_address = int(str(input_dev.i2c_location), 16) self.resolution = input_dev.resolution self.sensitivity = input_dev.sensitivity self.i2c_bus = SMBus(input_dev.i2c_bus) self.power_down() self.set_sensitivity(sensitivity=self.sensitivity)
def __init__(self, input_dev, testing=False): super(InputModule, self).__init__() self.logger = logging.getLogger("mycodo.inputs.th16") self.ip_address = None if not testing: self.logger = logging.getLogger( "mycodo.th16_{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) if input_dev.custom_options: for each_option in input_dev.custom_options.split(';'): option = each_option.split(',')[0] value = each_option.split(',')[1] if option == 'ip_address': self.ip_address = value.replace(" ", "") # Remove spaces from string
def edge_detected(self, bcm_pin): """ Callback function from GPIO.add_event_detect() for when an edge is detected Write rising (1) or falling (-1) edge to influxdb database Trigger any conditionals that match the rising/falling/both edge :param bcm_pin: BMC pin of rising/falling edge (required parameter) :return: None """ try: import RPi.GPIO as GPIO gpio_state = GPIO.input(int(self.gpio_location)) except: self.logger.error( "RPi.GPIO and Raspberry Pi required for this action") gpio_state = None if gpio_state is not None and time.time() > self.edge_reset_timer: self.edge_reset_timer = time.time() + self.switch_reset_period if (self.switch_edge == 'rising' or (self.switch_edge == 'both' and gpio_state)): rising_or_falling = 1 # Rising edge detected state_str = 'Rising' else: rising_or_falling = -1 # Falling edge detected state_str = 'Falling' write_db = threading.Thread(target=write_influxdb_value, args=( self.unique_id, 'edge', rising_or_falling, )) write_db.start() trigger = db_retrieve_table_daemon(Trigger) trigger = trigger.filter(Trigger.trigger_type == 'trigger_edge') trigger = trigger.filter(Trigger.measurement == self.unique_id) trigger = trigger.filter(Trigger.is_activated == True) for each_trigger in trigger.all(): if each_trigger.edge_detected in ['both', state_str.lower()]: now = time.time() timestamp = datetime.datetime.fromtimestamp(now).strftime( '%Y-%m-%d %H-%M-%S') message = "{ts}\n[Trigger {cid} ({cname})] " \ "Input {oid} ({name}) {state} edge detected " \ "on pin {pin} (BCM)".format( ts=timestamp, cid=each_trigger.id, cname=each_trigger.name, oid=self.unique_id, name=self.input_name, state=state_str, pin=bcm_pin) self.logger.debug("Edge: {}".format(message)) self.control.trigger_all_actions(each_trigger.unique_id, message=message)
def run_function(self): temp_c = None hum_percent = None vpd_pa = None last_measurement_temp = self.get_last_measurement( self.select_measurement_temperature_c_device_id, self.select_measurement_temperature_c_measurement_id, max_age=self.max_measure_age_temperature_c) if last_measurement_temp: device_measurement = get_measurement( self.select_measurement_temperature_c_measurement_id) conversion = db_retrieve_table_daemon( Conversion, unique_id=device_measurement.conversion_id) channel, unit, measurement = return_measurement_info( device_measurement, conversion) temp_c = convert_from_x_to_y_unit(unit, 'C', last_measurement_temp[1]) last_measurement_hum = self.get_last_measurement( self.select_measurement_humidity_device_id, self.select_measurement_humidity_measurement_id, max_age=self.max_measure_age_humidity) if last_measurement_hum: device_measurement = get_measurement( self.select_measurement_humidity_measurement_id) conversion = db_retrieve_table_daemon( Conversion, unique_id=device_measurement.conversion_id) channel, unit, measurement = return_measurement_info( device_measurement, conversion) hum_percent = convert_from_x_to_y_unit(unit, 'percent', last_measurement_hum[1]) if temp_c and hum_percent: measurement_dict = copy.deepcopy(measurements_dict) try: vpd_pa = calculate_vapor_pressure_deficit(temp_c, hum_percent) except TypeError as err: self.logger.error("Error: {msg}".format(msg=err)) if vpd_pa: dev_measurement = self.channels_measurement[0] channel, unit, measurement = return_measurement_info( dev_measurement, self.channels_conversion[0]) vpd_store = convert_from_x_to_y_unit('Pa', unit, vpd_pa) measurement_dict[0] = { 'measurement': measurement, 'unit': unit, 'value': vpd_store } # Add measurement(s) to influxdb if measurement_dict: self.logger.debug( "Adding measurements to InfluxDB with ID {}: {}".format( self.unique_id, measurement_dict)) add_measurements_influxdb(self.unique_id, measurement_dict) else: self.logger.debug( "No measurements to add to InfluxDB with ID {}".format( self.unique_id)) else: self.logger.debug( "Could not acquire both temperature and humidity measurements.")
def initialize_variables(self): self.dict_inputs = parse_input_information() self.sample_rate = db_retrieve_table_daemon( Misc, entry='first').sample_rate_controller_input input_dev = db_retrieve_table_daemon(Input, unique_id=self.unique_id) self.device_measurements = db_retrieve_table_daemon( DeviceMeasurements).filter( DeviceMeasurements.device_id == self.unique_id) self.conversions = db_retrieve_table_daemon(Conversion) self.input_dev = input_dev self.input_name = input_dev.name self.unique_id = input_dev.unique_id self.log_level_debug = input_dev.log_level_debug self.gpio_location = input_dev.gpio_location self.device = input_dev.device self.interface = input_dev.interface self.period = input_dev.period self.start_offset = input_dev.start_offset # Edge detection self.switch_edge = input_dev.switch_edge self.switch_bouncetime = input_dev.switch_bouncetime self.switch_reset_period = input_dev.switch_reset_period # Pre-Output: Activates prior to input measurement self.pre_output_id = input_dev.pre_output_id self.pre_output_duration = input_dev.pre_output_duration self.pre_output_during_measure = input_dev.pre_output_during_measure self.pre_output_setup = False self.last_measurement = 0 self.next_measurement = time.time() + self.start_offset self.get_new_measurement = False self.trigger_cond = False self.measurement_acquired = False self.pre_output_activated = False self.pre_output_locked = False self.pre_output_timer = time.time() self.set_log_level_debug(self.log_level_debug) # Check if Pre-Output ID actually exists output = db_retrieve_table_daemon(Output, entry='all') for each_output in output: if (each_output.unique_id == self.pre_output_id and self.pre_output_duration): self.pre_output_setup = True smtp = db_retrieve_table_daemon(SMTP, entry='first') self.smtp_max_count = smtp.hourly_max self.email_count = 0 self.allowed_to_send_notice = True # Set up input lock self.lock_file = '/var/lock/input_pre_output_{id}'.format( id=self.pre_output_id) # Convert string I2C address to base-16 int if self.interface == 'I2C': self.i2c_address = int(str(self.input_dev.i2c_location), 16) # Set up edge detection of a GPIO pin if self.device == 'EDGE': try: import RPi.GPIO as GPIO if self.switch_edge == 'rising': self.switch_edge_gpio = GPIO.RISING elif self.switch_edge == 'falling': self.switch_edge_gpio = GPIO.FALLING else: self.switch_edge_gpio = GPIO.BOTH except: self.logger.error( "RPi.GPIO and Raspberry Pi required for this action") self.device_recognized = True if self.device in self.dict_inputs: input_loaded = load_module_from_file( self.dict_inputs[self.device]['file_path'], 'inputs') if self.device == 'EDGE': # Edge detection handled internally, no module to load self.measure_input = None else: self.measure_input = input_loaded.InputModule(self.input_dev) else: self.device_recognized = False self.logger.debug( "Device '{device}' not recognized".format(device=self.device)) raise Exception("'{device}' is not a valid device type.".format( device=self.device)) self.edge_reset_timer = time.time() self.input_timer = time.time() self.lastUpdate = None # Set up edge detection if self.device == 'EDGE': try: import RPi.GPIO as GPIO GPIO.setmode(GPIO.BCM) GPIO.setup(int(self.gpio_location), GPIO.IN) GPIO.add_event_detect(int(self.gpio_location), self.switch_edge_gpio, callback=self.edge_detected, bouncetime=self.switch_bouncetime) except: self.logger.error( "RPi.GPIO and Raspberry Pi required for this action") # Set up MQTT listener elif ('listener' in self.dict_inputs[self.device] and self.dict_inputs[self.device]['listener']): input_listener = threading.Thread( target=self.measure_input.listener()) input_listener.daemon = True input_listener.start()
def trigger_function_actions(function_id, message=''): """ Execute the Actions belonging to a particular Function :param function_id: :param message: The message generated from the conditional check :return: """ logger_actions = logging.getLogger( "mycodo.trigger_function_actions_{id}".format( id=function_id.split('-')[0])) # List of all email notification recipients # List is appended with TO email addresses when an email Action is # encountered. An email is sent to all recipients after all actions # have been executed. email_recipients = [] # List of tags to add to a note note_tags = [] attachment_file = None attachment_type = None actions = db_retrieve_table_daemon(Actions) actions = actions.filter(Actions.function_id == function_id).all() for cond_action in actions: (message, note_tags, email_recipients, attachment_file, attachment_type) = trigger_action(cond_action.unique_id, message=message, single_action=False, note_tags=note_tags, email_recipients=email_recipients, attachment_file=attachment_file, attachment_type=attachment_type) # Send email after all conditional actions have been checked # In order to append all action messages to send in the email # send_email_at_end will be None or the TO email address if email_recipients: smtp = db_retrieve_table_daemon(SMTP, entry='first') send_email(smtp.host, smtp.ssl, smtp.port, smtp.user, smtp.passw, smtp.email_from, email_recipients, message, attachment_file, attachment_type) # Create a note with the tags from the unique_ids in the list note_tags if note_tags: list_tags = [] for each_note_tag_id in note_tags: check_tag = db_retrieve_table_daemon(NoteTags, unique_id=each_note_tag_id) if check_tag: list_tags.append(each_note_tag_id) if list_tags: with session_scope(MYCODO_DB_PATH) as db_session: new_note = Notes() new_note.name = 'Action' new_note.tags = ','.join(list_tags) new_note.note = message db_session.add(new_note) logger_actions.debug(message)
def which_controller(unique_id): """Determine which type of controller the unique_id is for""" controller_type = None controller_object = None controller_entry = None if db_retrieve_table_daemon(Conditional, unique_id=unique_id): controller_type = 'Conditional' controller_object = Conditional controller_entry = db_retrieve_table_daemon(Conditional, unique_id=unique_id) elif db_retrieve_table_daemon(Input, unique_id=unique_id): controller_type = 'Input' controller_object = Input controller_entry = db_retrieve_table_daemon(Input, unique_id=unique_id) elif db_retrieve_table_daemon(LCD, unique_id=unique_id): controller_type = 'LCD' controller_object = LCD controller_entry = db_retrieve_table_daemon(LCD, unique_id=unique_id) elif db_retrieve_table_daemon(Math, unique_id=unique_id): controller_type = 'Math' controller_object = Math controller_entry = db_retrieve_table_daemon(Math, unique_id=unique_id) elif db_retrieve_table_daemon(PID, unique_id=unique_id): controller_type = 'PID' controller_object = PID controller_entry = db_retrieve_table_daemon(PID, unique_id=unique_id) elif db_retrieve_table_daemon(Trigger, unique_id=unique_id): controller_type = 'Trigger' controller_object = Trigger controller_entry = db_retrieve_table_daemon(Trigger, unique_id=unique_id) return controller_type, controller_object, controller_entry
def send_anonymous_stats(start_time, debug=False): """ Send anonymous usage statistics Example use: current_stat = return_stat_file_dict(csv_file) add_update_csv(csv_file, 'stat', current_stat['stat'] + 5) """ if debug: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) try: client = InfluxDBClient(STATS_HOST, STATS_PORT, STATS_USER, STATS_PASSWORD, STATS_DATABASE) # Prepare stats before sending uptime = (time.time() - start_time) / 86400.0 # Days add_update_csv(STATS_CSV, 'uptime', uptime) version_num = db_retrieve_table_daemon(AlembicVersion, entry='first') version_send = version_num.version_num if version_num else 'None' add_update_csv(STATS_CSV, 'alembic_version', version_send) outputs = db_retrieve_table_daemon(Output) add_update_csv(STATS_CSV, 'num_relays', outputs.count()) inputs = db_retrieve_table_daemon(Input) add_update_csv(STATS_CSV, 'num_sensors', inputs.count()) add_update_csv(STATS_CSV, 'num_sensors_active', inputs.filter(Input.is_activated.is_(True)).count()) conditionals = db_retrieve_table_daemon(Conditional) add_update_csv(STATS_CSV, 'num_conditionals', conditionals.count()) add_update_csv( STATS_CSV, 'num_conditionals_active', conditionals.filter(Conditional.is_activated.is_(True)).count()) pids = db_retrieve_table_daemon(PID) add_update_csv(STATS_CSV, 'num_pids', pids.count()) add_update_csv(STATS_CSV, 'num_pids_active', pids.filter(PID.is_activated.is_(True)).count()) triggers = db_retrieve_table_daemon(Trigger) add_update_csv(STATS_CSV, 'num_triggers', triggers.count()) add_update_csv(STATS_CSV, 'num_triggers_active', triggers.filter(Trigger.is_activated.is_(True)).count()) functions = db_retrieve_table_daemon(CustomController) add_update_csv(STATS_CSV, 'num_functions', functions.count()) add_update_csv( STATS_CSV, 'num_functions_active', functions.filter(CustomController.is_activated.is_(True)).count()) actions = db_retrieve_table_daemon(Actions) add_update_csv(STATS_CSV, 'num_actions', actions.count()) methods = db_retrieve_table_daemon(Method) add_update_csv(STATS_CSV, 'num_methods', methods.count()) add_update_csv( STATS_CSV, 'num_methods_in_pid', pids.filter(PID.setpoint_tracking_type == 'method').count()) add_update_csv( STATS_CSV, 'num_setpoint_meas_in_pid', pids.filter(PID.setpoint_tracking_type == 'input-math').count()) country = geocoder.ip('me').country if not country: country = 'None' add_update_csv(STATS_CSV, 'country', country) add_update_csv( STATS_CSV, 'ram_use_mb', resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / float(1000)) add_update_csv(STATS_CSV, 'Mycodo_revision', MYCODO_VERSION) add_update_csv( STATS_CSV, 'master_branch', int(os.path.exists(os.path.join(INSTALL_DIRECTORY, '.master')))) # Combine stats into list of dictionaries new_stats_dict = return_stat_file_dict(STATS_CSV) formatted_stat_dict = [] for each_key, each_value in new_stats_dict.items(): if each_key != 'stat': # Do not send header row formatted_stat_dict = add_stat_dict(formatted_stat_dict, new_stats_dict['id'], each_key, each_value) # Send stats to secure, remote influxdb server (only write permission) client.write_points(formatted_stat_dict) logger.debug("Sent anonymous usage statistics") return 0 except requests.ConnectionError: logger.debug("Could not send anonymous usage statistics: Connection " "timed out (expected if there's no internet or the " "server is down)") except InfluxDBServerError as except_msg: logger.error("Statistics: InfluxDB server error: {}".format( except_msg['error'])) except Exception as except_msg: logger.exception( "Could not send anonymous usage statistics: {err}".format( err=except_msg)) return 1
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(self): self.running = True self.logger.info("Activated in {:.1f} ms".format( (timeit.default_timer() - self.thread_startup_timer) * 1000)) self.ready.set() while self.running: # Timer is set to react at a specific hour and minute of the day if self.timer_type == 'time': if (int(self.start_hour) == datetime.datetime.now().hour and int(self.start_minute) == datetime.datetime.now().minute): # Ensure this is triggered only once at this specific time if self.date_timer_not_executed: self.date_timer_not_executed = False message = "At {st}, turn Output {id} {state}".format( st=self.time_start, id=self.output_id, state=self.state) if self.state == 'on' and self.duration_on: message += " for {sec} seconds".format( sec=self.duration_on) else: self.duration_on = 0 self.logger.debug(message) modulate_output = threading.Thread( target=self.control.output_on_off, args=(self.output_id, self.state,), kwargs={'duration': self.duration_on}) modulate_output.start() elif not self.date_timer_not_executed: self.date_timer_not_executed = True # Timer is set to react at a specific time duration of the day elif self.timer_type == 'timespan': if time_between_range(self.time_start, self.time_end): current_output_state = self.control.relay_state(self.output_id) if self.state != current_output_state: message = "Output {output} should be {state}, but is " \ "{cstate}. Turning {state}.".format( output=self.output_id, state=self.state, cstate=current_output_state) modulate_output = threading.Thread( target=self.control.output_on_off, args=(self.output_id, self.state,)) modulate_output.start() self.logger.debug(message) # Timer is a simple on/off duration timer elif self.timer_type == 'duration': if time.time() > self.duration_timer: self.duration_timer = (time.time() + self.duration_on + self.duration_off) self.logger.debug("Turn Output {output} on for {onsec} " "seconds, then off for {offsec} " "seconds".format( output=self.output_id, onsec=self.duration_on, offsec=self.duration_off)) output_on = threading.Thread(target=self.control.relay_on, args=(self.output_id, self.duration_on,)) output_on.start() # Timer is a PWM Method timer elif self.timer_type == 'pwm_method': try: if time.time() > self.pwm_method_timer: if self.method_start_act == 'Ended': self.stop_controller(ended_normally=False, deactivate_timer=True) self.logger.info( "Method has ended. " "Activate the Timer controller to start it again.") else: this_controller = db_retrieve_table_daemon( Timer, device_id=self.timer_id) setpoint, ended = calculate_method_setpoint( self.method_id, Timer, this_controller, Method, MethodData, self.logger) if ended: self.method_start_act = 'Ended' if setpoint > 100: setpoint = 100 elif setpoint < 0: setpoint = 0 self.logger.debug( "Turn Output {output} to a PWM duty cycle of " "{dc:.1f} %".format( output=self.output_id, dc=setpoint)) # Activate pwm with calculated duty cycle self.control.relay_on( self.output_id, duty_cycle=setpoint) self.pwm_method_timer = time.time() + self.method_period except Exception: self.logger.exception(1) time.sleep(0.1) self.control.relay_off(self.output_id) self.running = False self.logger.info("Deactivated in {:.1f} ms".format( (timeit.default_timer() - self.thread_shutdown_timer) * 1000))
def 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.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)) # 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))
def download_data(self): # Clear data previously stored in dictionary self.gadget.loggedDataReadout = {'Temp': {}, 'Humi': {}} # Download stored data starting from self.gadget.newestTimeStampMs self.gadget.readLoggedDataInterval( startMs=self.gadget.newestTimeStampMs) while self.running: if (not self.gadget.waitForNotifications(5) or not self.gadget.isLogReadoutInProgress()): break # Done reading data list_timestamps_temp = [] list_timestamps_humi = [] # Store logged temperature measurement = self.device_measurements.filter( DeviceMeasurements.channel == 0).first() conversion = db_retrieve_table_daemon( Conversion, unique_id=measurement.conversion_id) for each_ts, each_measure in self.gadget.loggedDataReadout['Temp'].items(): list_timestamps_temp.append(each_ts) datetime_ts = datetime.datetime.utcfromtimestamp(each_ts / 1000) if self.is_enabled(0): measurement_single = { 0: { 'measurement': 'temperature', 'unit': 'C', 'value': each_measure } } measurement_single = parse_measurement( conversion, measurement, measurement_single, measurement.channel, measurement_single[0]) write_influxdb_value( self.unique_id, measurement_single[0]['unit'], value=measurement_single[0]['value'], measure=measurement_single[0]['measurement'], channel=0, timestamp=datetime_ts) # Store logged humidity measurement = self.device_measurements.filter( DeviceMeasurements.channel == 1).first() conversion = db_retrieve_table_daemon( Conversion, unique_id=measurement.conversion_id) for each_ts, each_measure in self.gadget.loggedDataReadout['Humi'].items(): list_timestamps_humi.append(each_ts) datetime_ts = datetime.datetime.utcfromtimestamp(each_ts / 1000) if self.is_enabled(1): measurement_single = { 1: { 'measurement': 'humidity', 'unit': 'percent', 'value': each_measure } } measurement_single = parse_measurement( conversion, measurement, measurement_single, measurement.channel, measurement_single[1]) write_influxdb_value( self.unique_id, measurement_single[1]['unit'], value=measurement_single[1]['value'], measure=measurement_single[1]['measurement'], channel=1, timestamp=datetime_ts) # Find common timestamps from both temperature and humidity lists list_timestamps_both = list( set(list_timestamps_temp).intersection(list_timestamps_humi)) for each_ts in list_timestamps_both: datetime_ts = datetime.datetime.utcfromtimestamp(each_ts / 1000) # Calculate and store dew point if (self.is_enabled(3) and self.is_enabled(0) and self.is_enabled(1)): measurement = self.device_measurements.filter( DeviceMeasurements.channel == 3).first() conversion = db_retrieve_table_daemon( Conversion, unique_id=measurement.conversion_id) dewpoint = calculate_dewpoint( self.gadget.loggedDataReadout['Temp'][each_ts], self.gadget.loggedDataReadout['Humi'][each_ts]) measurement_single = { 3: { 'measurement': 'dewpoint', 'unit': 'C', 'value': dewpoint } } measurement_single = parse_measurement( conversion, measurement, measurement_single, measurement.channel, measurement_single[3]) write_influxdb_value( self.unique_id, measurement_single[3]['unit'], value=measurement_single[3]['value'], measure=measurement_single[3]['measurement'], channel=3, timestamp=datetime_ts) # Calculate and store vapor pressure deficit if (self.is_enabled(4) and self.is_enabled(0) and self.is_enabled(1)): measurement = self.device_measurements.filter( DeviceMeasurements.channel == 4).first() conversion = db_retrieve_table_daemon( Conversion, unique_id=measurement.conversion_id) vpd = calculate_vapor_pressure_deficit( self.gadget.loggedDataReadout['Temp'][each_ts], self.gadget.loggedDataReadout['Humi'][each_ts]) measurement_single = { 4: { 'measurement': 'vapor_pressure_deficit', 'unit': 'Pa', 'value': vpd } } measurement_single = parse_measurement( conversion, measurement, measurement_single, measurement.channel, measurement_single[4]) write_influxdb_value( self.unique_id, measurement_single[4]['unit'], value=measurement_single[4]['value'], measure=measurement_single[4]['measurement'], channel=4, timestamp=datetime_ts) # Download successfully finished, set newest timestamp self.gadget.newestTimeStampMs = self.gadget.tmp_newestTimeStampMs
def controller_activate(self, cont_type, cont_id): """ Activate currently-inactive controller :return: 0 for success, 1 for fail, with success or error message :rtype: int, str :param cont_type: Which controller type is to be activated? :type cont_type: str :param cont_id: Unique ID for controller :type cont_id: str """ try: if cont_id in self.controller[cont_type]: if self.controller[cont_type][cont_id].is_running(): message = "Cannot activate {type} controller with ID {id}: " \ "It's already active.".format(type=cont_type, id=cont_id) self.logger.warning(message) return 1, message controller_manage = {} ready = threading.Event() if cont_type == 'LCD': controller_manage['type'] = LCD controller_manage['function'] = LCDController elif cont_type == 'Math': controller_manage['type'] = Math controller_manage['function'] = MathController elif cont_type == 'PID': controller_manage['type'] = PID controller_manage['function'] = PIDController elif cont_type == 'Input': controller_manage['type'] = Input controller_manage['function'] = InputController elif cont_type == 'Timer': controller_manage['type'] = Timer controller_manage['function'] = TimerController else: return 1, "'{type}' not a valid controller type.".format( type=cont_type) # Check if the controller ID actually exists and start it controller = db_retrieve_table_daemon(controller_manage['type'], device_id=cont_id) if controller: self.controller[cont_type][cont_id] = controller_manage[ 'function'](ready, cont_id) self.controller[cont_type][cont_id].daemon = True self.controller[cont_type][cont_id].start() ready.wait() # wait for thread to return ready return 0, "{type} controller with ID {id} " \ "activated.".format(type=cont_type, id=cont_id) else: return 1, "{type} controller with ID {id} not found.".format( type=cont_type, id=cont_id) except Exception as except_msg: message = "Could not activate {type} controller with ID {id}:" \ " {err}".format(type=cont_type, id=cont_id, err=except_msg) self.logger.exception(message) return 1, message
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 __init__(self, ready, math_id): threading.Thread.__init__(self) self.logger = logging.getLogger( "mycodo.math_{id}".format(id=math_id.split('-')[0])) try: self.measurements = None self.running = False self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.pause_loop = False self.verify_pause_loop = True self.control = DaemonControl() self.sample_rate = db_retrieve_table_daemon( Misc, entry='first').sample_rate_controller_math smtp = db_retrieve_table_daemon(SMTP, entry='first') self.smtp_max_count = smtp.hourly_max self.email_count = 0 self.allowed_to_send_notice = True self.math_id = math_id math = db_retrieve_table_daemon(Math, unique_id=self.math_id) self.device_measurements = db_retrieve_table_daemon( DeviceMeasurements).filter( DeviceMeasurements.device_id == self.math_id) # General variables self.unique_id = math.unique_id self.name = math.name self.math_type = math.math_type self.is_activated = math.is_activated self.period = math.period self.start_offset = math.start_offset self.max_measure_age = math.max_measure_age # Inputs to calculate with self.inputs = math.inputs # Difference variables self.difference_reverse_order = math.difference_reverse_order self.difference_absolute = math.difference_absolute # Equation variables self.equation_input = math.equation_input self.equation = math.equation # Redundancy variables self.order_of_use = math.order_of_use # Verification variables self.max_difference = math.max_difference # Humidity variables self.dry_bulb_t_id = math.dry_bulb_t_id self.dry_bulb_t_measure_id = math.dry_bulb_t_measure_id self.wet_bulb_t_id = math.wet_bulb_t_id self.wet_bulb_t_measure_id = math.wet_bulb_t_measure_id self.pressure_pa_id = math.pressure_pa_id self.pressure_pa_measure_id = math.pressure_pa_measure_id # Misc ids self.unique_id_1 = math.unique_id_1 self.unique_measurement_id_1 = math.unique_measurement_id_1 self.unique_id_2 = math.unique_id_2 self.unique_measurement_id_2 = math.unique_measurement_id_2 self.timer = time.time() + self.start_offset except Exception as except_msg: self.logger.exception("Init Error: {err}".format(err=except_msg))
def initialize_variables(self): lcd_dev = db_retrieve_table_daemon(LCD, unique_id=self.unique_id) self.lcd_type = lcd_dev.lcd_type self.lcd_name = lcd_dev.name self.lcd_period = lcd_dev.period self.lcd_x_characters = lcd_dev.x_characters self.lcd_y_lines = lcd_dev.y_lines self.timer = time.time() + self.lcd_period self.backlight_timer = time.time() self.log_level_debug = lcd_dev.log_level_debug self.set_log_level_debug(self.log_level_debug) # Add custom measurement and units to list self.list_inputs = add_custom_measurements( db_retrieve_table_daemon(Measurement, entry='all')) self.list_inputs.update( {'input_time': {'unit': None, 'name': 'Time'}}) self.list_inputs.update( {'pid_time': {'unit': None, 'name': 'Time'}}) self.dict_units = add_custom_units( db_retrieve_table_daemon(Unit, entry='all')) lcd_data = db_retrieve_table_daemon( LCDData).filter(LCDData.lcd_id == lcd_dev.unique_id).all() for each_lcd_display in lcd_data: self.display_sets.append(each_lcd_display.unique_id) self.lcd_string_line[each_lcd_display.unique_id] = {} self.lcd_line[each_lcd_display.unique_id] = {} self.lcd_text[each_lcd_display.unique_id] = {} self.lcd_max_age[each_lcd_display.unique_id] = {} self.lcd_decimal_places[each_lcd_display.unique_id] = {} for i in range(1, self.lcd_y_lines + 1): self.lcd_string_line[each_lcd_display.unique_id][i] = '' self.lcd_line[each_lcd_display.unique_id][i] = {} if i == 1: self.lcd_text[each_lcd_display.unique_id][i] = each_lcd_display.line_1_text self.lcd_max_age[each_lcd_display.unique_id][i] = each_lcd_display.line_1_max_age self.lcd_decimal_places[each_lcd_display.unique_id][i] = each_lcd_display.line_1_decimal_places elif i == 2: self.lcd_text[each_lcd_display.unique_id][i] = each_lcd_display.line_2_text self.lcd_max_age[each_lcd_display.unique_id][i] = each_lcd_display.line_2_max_age self.lcd_decimal_places[each_lcd_display.unique_id][i] = each_lcd_display.line_2_decimal_places elif i == 3: self.lcd_text[each_lcd_display.unique_id][i] = each_lcd_display.line_3_text self.lcd_max_age[each_lcd_display.unique_id][i] = each_lcd_display.line_3_max_age self.lcd_decimal_places[each_lcd_display.unique_id][i] = each_lcd_display.line_3_decimal_places elif i == 4: self.lcd_text[each_lcd_display.unique_id][i] = each_lcd_display.line_4_text self.lcd_max_age[each_lcd_display.unique_id][i] = each_lcd_display.line_4_max_age self.lcd_decimal_places[each_lcd_display.unique_id][i] = each_lcd_display.line_4_decimal_places elif i == 5: self.lcd_text[each_lcd_display.unique_id][i] = each_lcd_display.line_5_text self.lcd_max_age[each_lcd_display.unique_id][i] = each_lcd_display.line_5_max_age self.lcd_decimal_places[each_lcd_display.unique_id][i] = each_lcd_display.line_5_decimal_places elif i == 6: self.lcd_text[each_lcd_display.unique_id][i] = each_lcd_display.line_6_text self.lcd_max_age[each_lcd_display.unique_id][i] = each_lcd_display.line_6_max_age self.lcd_decimal_places[each_lcd_display.unique_id][i] = each_lcd_display.line_6_decimal_places elif i == 7: self.lcd_text[each_lcd_display.unique_id][i] = each_lcd_display.line_7_text self.lcd_max_age[each_lcd_display.unique_id][i] = each_lcd_display.line_7_max_age self.lcd_decimal_places[each_lcd_display.unique_id][i] = each_lcd_display.line_7_decimal_places elif i == 8: self.lcd_text[each_lcd_display.unique_id][i] = each_lcd_display.line_8_text self.lcd_max_age[each_lcd_display.unique_id][i] = each_lcd_display.line_8_max_age self.lcd_decimal_places[each_lcd_display.unique_id][i] = each_lcd_display.line_8_decimal_places if self.lcd_y_lines in [2, 4, 8]: self.setup_lcd_line( each_lcd_display.unique_id, 1, each_lcd_display.line_1_id, each_lcd_display.line_1_measurement) self.setup_lcd_line( each_lcd_display.unique_id, 2, each_lcd_display.line_2_id, each_lcd_display.line_2_measurement) if self.lcd_y_lines in [4, 8]: self.setup_lcd_line( each_lcd_display.unique_id, 3, each_lcd_display.line_3_id, each_lcd_display.line_3_measurement) self.setup_lcd_line( each_lcd_display.unique_id, 4, each_lcd_display.line_4_id, each_lcd_display.line_4_measurement) if self.lcd_y_lines == 8: self.setup_lcd_line( each_lcd_display.unique_id, 5, each_lcd_display.line_5_id, each_lcd_display.line_5_measurement) self.setup_lcd_line( each_lcd_display.unique_id, 6, each_lcd_display.line_6_id, each_lcd_display.line_6_measurement) self.setup_lcd_line( each_lcd_display.unique_id, 7, each_lcd_display.line_7_id, each_lcd_display.line_7_measurement) self.setup_lcd_line( each_lcd_display.unique_id, 8, each_lcd_display.line_8_id, each_lcd_display.line_8_measurement) if self.lcd_type in ['16x2_generic', '20x4_generic']: from mycodo.devices.lcd_generic import LCD_Generic self.lcd_out = LCD_Generic(lcd_dev) self.lcd_init() elif self.lcd_type in ['16x2_grove_lcd_rgb']: from mycodo.devices.lcd_grove_lcd_rgb import LCD_Grove_LCD_RGB self.lcd_out = LCD_Grove_LCD_RGB(lcd_dev) self.lcd_init() elif self.lcd_type in ['128x32_pioled', '128x64_pioled']: from mycodo.devices.lcd_pioled import LCD_Pioled self.lcd_out = LCD_Pioled(lcd_dev) self.lcd_init() elif self.lcd_type in ['128x32_pioled_circuit_python', '128x64_pioled_circuit_python']: from mycodo.devices.lcd_pioled_circuitpython import LCD_Pioled_Circuitpython self.lcd_out = LCD_Pioled_Circuitpython(lcd_dev) self.lcd_init() else: self.logger.error("Unknown LCD type: {}".format(self.lcd_type)) if self.lcd_initialized: line_1 = 'Mycodo {}'.format(MYCODO_VERSION) line_2 = 'Start {}'.format(self.lcd_name) self.lcd_out.lcd_write_lines(line_1, line_2, '', '')
def calculate_math(self): measurement_dict = {} # # Average (multiple channels) # if self.math_type == 'average': device_measurement = self.device_measurements.filter( DeviceMeasurements.channel == 0).first() 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) success, measure = self.get_measurements_from_str(self.inputs) if success: average = float(sum(measure) / float(len(measure))) measurement_dict = { channel: { 'measurement': measurement, 'unit': unit, 'value': average } } elif measure: self.logger.error(measure) else: self.error_not_within_max_age() # # Average (single channel) # elif self.math_type == 'average_single': device_id = self.inputs.split(',')[0] measurement_id = self.inputs.split(',')[1] if measurement_id == 'output': output = db_retrieve_table_daemon(Output, unique_id=device_id) channel = output.channel unit = output.unit measurement = output.measurement else: device_measurement = db_retrieve_table_daemon( DeviceMeasurements, unique_id=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) try: average_measurements = average_past_seconds( device_id, unit, channel, self.max_measure_age, measure=measurement) if average_measurements: measurement_dict = { channel: { 'measurement': measurement, 'unit': unit, 'value': average_measurements } } else: self.error_not_within_max_age() except Exception as msg: self.logger.exception( "average_single Error: {err}".format(err=msg)) # # Sum (multiple channels) # if self.math_type == 'sum': device_measurement = self.device_measurements.filter( DeviceMeasurements.channel == 0).first() 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) success, measure = self.get_measurements_from_str(self.inputs) if success: sum_value = float(sum(measure)) measurement_dict = { channel: { 'measurement': measurement, 'unit': unit, 'value': sum_value } } elif measure: self.logger.error(measure) else: self.error_not_within_max_age() # # Sum (single channel) # elif self.math_type == 'sum_single': device_id = self.inputs.split(',')[0] measurement_id = self.inputs.split(',')[1] if measurement_id == 'output': output = db_retrieve_table_daemon(Output, unique_id=device_id) channel = output.channel unit = output.unit measurement = output.measurement else: device_measurement = db_retrieve_table_daemon( DeviceMeasurements, unique_id=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) try: sum_measurements = sum_past_seconds(device_id, unit, channel, self.max_measure_age, measure=measurement) if sum_measurements: measurement_dict = { channel: { 'measurement': measurement, 'unit': unit, 'value': sum_measurements } } else: self.error_not_within_max_age() except Exception as msg: self.logger.exception( "sum_single Error: {err}".format(err=msg)) # # Difference between two channels # elif self.math_type == 'difference': device_measurement = self.device_measurements.filter( DeviceMeasurements.channel == 0).first() 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) success, measure = self.get_measurements_from_str(self.inputs) if success: if self.difference_reverse_order: difference = measure[1] - measure[0] else: difference = measure[0] - measure[1] if self.difference_absolute: difference = abs(difference) measurement_dict = { channel: { 'measurement': measurement, 'unit': unit, 'value': difference } } elif measure: self.logger.error(measure) else: self.error_not_within_max_age() # # Equation (math performed on measurement) # elif self.math_type == 'equation': device_measurement = self.device_measurements.filter( DeviceMeasurements.channel == 0).first() 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) success, measure = self.get_measurements_from_str( self.equation_input) if success: replaced_str = self.equation.replace('x', str(measure[0])) equation_output = eval(replaced_str) measurement_dict = { channel: { 'measurement': measurement, 'unit': unit, 'value': float(equation_output) } } elif measure: self.logger.error(measure) else: self.error_not_within_max_age() # # Redundancy (Use next measurement if one isn't currently available) # elif self.math_type == 'redundancy': list_order = self.order_of_use.split(';') measurement_success = False for each_id_measurement_id in list_order: device_id = each_id_measurement_id.split(',')[0] measurement_id = each_id_measurement_id.split(',')[1] device_measurement = self.device_measurements.filter( DeviceMeasurements.channel == 0).first() 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) try: success_measure, measure = self.get_measurements_from_id( device_id, measurement_id) if success_measure: measurement_dict = { channel: { 'measurement': measurement, 'unit': unit, 'value': float(measure[1]), 'timestamp': measure[0], } } measurement_success = True break except Exception as msg: self.logger.exception( "redundancy Error: {err}".format(err=msg)) if not measurement_success: self.error_not_within_max_age() # # Statistical analysis on all measurements from a period of time # elif self.math_type == 'statistics': success, measure = self.get_measurements_from_str(self.inputs) if success: # Perform some math stat_mean = float(sum(measure) / float(len(measure))) stat_median = median(measure) stat_minimum = min(measure) stat_maximum = max(measure) stdev_ = stdev(measure) stdev_mean_upper = stat_mean + stdev_ stdev_mean_lower = stat_mean - stdev_ list_measurement = [ stat_mean, stat_median, stat_minimum, stat_maximum, stdev_, stdev_mean_upper, stdev_mean_lower ] for each_measurement in self.device_measurements.all(): conversion = db_retrieve_table_daemon( Conversion, unique_id=each_measurement.conversion_id) channel, unit, measurement = return_measurement_info( each_measurement, conversion) measurement_dict[channel] = { 'measurement': measurement, 'unit': unit, 'value': list_measurement[channel] } elif measure: self.logger.error(measure) else: self.error_not_within_max_age() # # Verification (only use measurement if it's close to another measurement) # elif self.math_type == 'verification': device_measurement = self.device_measurements.filter( DeviceMeasurements.channel == 0).first() 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) success, measure = self.get_measurements_from_str(self.inputs) if (success and max(measure) - min(measure) < self.max_difference): difference = max(measure) - min(measure) measurement_dict = { channel: { 'measurement': measurement, 'unit': unit, 'value': difference } } elif measure: self.logger.error(measure) else: self.error_not_within_max_age() # # Calculate humidity from wet- and dry-bulb temperatures # elif self.math_type == 'humidity': pressure_pa = 101325 critical_error = False if self.pressure_pa_id and self.pressure_pa_measure_id: success_pa, pressure = self.get_measurements_from_id( self.pressure_pa_id, self.pressure_pa_measure_id) if success_pa: pressure_pa = int(pressure[1]) # Pressure must be in Pa, convert if not if db_retrieve_table_daemon( DeviceMeasurements, unique_id=self.pressure_pa_measure_id): measurement = db_retrieve_table_daemon( DeviceMeasurements, unique_id=self.pressure_pa_measure_id) else: self.logger.error( "Could not find pressure measurement") measurement = None critical_error = True if measurement and measurement.unit != 'Pa': for each_conv in db_retrieve_table_daemon(Conversion, entry='all'): if (each_conv.convert_unit_from == measurement.unit and each_conv.convert_unit_to == 'Pa'): pressure_pa = convert_units( each_conv.unique_id, pressure_pa) else: self.logger.error( "Could not find conversion for unit " "{unit} to Pa (Pascals)".format( unit=measurement.unit)) critical_error = True success_dbt, dry_bulb_t = self.get_measurements_from_id( self.dry_bulb_t_id, self.dry_bulb_t_measure_id) success_wbt, wet_bulb_t = self.get_measurements_from_id( self.wet_bulb_t_id, self.wet_bulb_t_measure_id) if success_dbt and success_wbt: dbt_kelvin = float(dry_bulb_t[1]) wbt_kelvin = float(wet_bulb_t[1]) if db_retrieve_table_daemon( DeviceMeasurements, unique_id=self.dry_bulb_t_measure_id): measurement = db_retrieve_table_daemon( DeviceMeasurements, unique_id=self.dry_bulb_t_measure_id) else: self.logger.error("Could not find pressure measurement") measurement = None critical_error = True if measurement and measurement.unit != 'K': conversion_found = False for each_conv in db_retrieve_table_daemon(Conversion, entry='all'): if (each_conv.convert_unit_from == measurement.unit and each_conv.convert_unit_to == 'K'): dbt_kelvin = convert_units(each_conv.unique_id, dbt_kelvin) conversion_found = True if not conversion_found: self.logger.error("Could not find conversion for unit " "{unit} to K (Kelvin)".format( unit=measurement.unit)) critical_error = True if db_retrieve_table_daemon( DeviceMeasurements, unique_id=self.dry_bulb_t_measure_id): measurement = db_retrieve_table_daemon( DeviceMeasurements, unique_id=self.dry_bulb_t_measure_id) else: self.logger.error("Could not find pressure measurement") measurement = None critical_error = True if measurement and measurement.unit != 'K': conversion_found = False for each_conv in db_retrieve_table_daemon(Conversion, entry='all'): if (each_conv.convert_unit_from == measurement.unit and each_conv.convert_unit_to == 'K'): wbt_kelvin = convert_units(each_conv.unique_id, wbt_kelvin) conversion_found = True if not conversion_found: self.logger.error("Could not find conversion for unit " "{unit} to K (Kelvin)".format( unit=measurement.unit)) critical_error = True # Convert temperatures to Kelvin (already done above) # dbt_kelvin = celsius_to_kelvin(dry_bulb_t_c) # wbt_kelvin = celsius_to_kelvin(wet_bulb_t_c) psypi = None try: if not critical_error: psypi = SI.state("DBT", dbt_kelvin, "WBT", wbt_kelvin, pressure_pa) else: self.logger.error( "One or more critical errors prevented the " "humidity from being calculated") except TypeError as err: self.logger.error("TypeError: {msg}".format(msg=err)) if psypi: percent_relative_humidity = psypi[2] * 100 # Ensure percent humidity stays within 0 - 100 % range if percent_relative_humidity > 100: percent_relative_humidity = 100 elif percent_relative_humidity < 0: percent_relative_humidity = 0 # Dry bulb temperature: psypi[0]) # Wet bulb temperature: psypi[5]) specific_enthalpy = float(psypi[1]) humidity = float(percent_relative_humidity) specific_volume = float(psypi[3]) humidity_ratio = float(psypi[4]) list_measurement = [ specific_enthalpy, humidity, specific_volume, humidity_ratio ] for each_measurement in self.device_measurements.all(): conversion = db_retrieve_table_daemon( Conversion, unique_id=each_measurement.conversion_id) channel, unit, measurement = return_measurement_info( each_measurement, conversion) measurement_dict[channel] = { 'measurement': measurement, 'unit': unit, 'value': list_measurement[channel] } else: self.error_not_within_max_age() # # Calculate vapor pressure deficit from temperature and humidity # elif self.math_type == 'vapor_pressure_deficit': vpd_pa = None critical_error = False success_dbt, temperature = self.get_measurements_from_id( self.unique_id_1, self.unique_measurement_id_1) success_wbt, humidity = self.get_measurements_from_id( self.unique_id_2, self.unique_measurement_id_2) if success_dbt and success_wbt: vpd_temperature_celsius = float(temperature[1]) vpd_humidity_percent = float(humidity[1]) if db_retrieve_table_daemon( DeviceMeasurements, unique_id=self.unique_measurement_id_1): measurement = db_retrieve_table_daemon( DeviceMeasurements, unique_id=self.unique_measurement_id_1) else: self.logger.error("Could not find temperature measurement") measurement = None critical_error = True if measurement and measurement.unit != 'C': conversion_found = False for each_conv in db_retrieve_table_daemon(Conversion, entry='all'): if (each_conv.convert_unit_from == measurement.unit and each_conv.convert_unit_to == 'C'): vpd_temperature_celsius = convert_units( each_conv.unique_id, vpd_temperature_celsius) conversion_found = True if not conversion_found: self.logger.error("Could not find conversion for unit " "{unit} to C (Celsius)".format( unit=measurement.unit)) critical_error = True if db_retrieve_table_daemon( DeviceMeasurements, unique_id=self.unique_measurement_id_2): measurement = db_retrieve_table_daemon( DeviceMeasurements, unique_id=self.unique_measurement_id_2) else: self.logger.error("Could not find humidity measurement") measurement = None critical_error = True if measurement and measurement.unit != 'percent': conversion_found = False for each_conv in db_retrieve_table_daemon(Conversion, entry='all'): if (each_conv.convert_unit_from == measurement.unit and each_conv.convert_unit_to == 'percent'): vpd_humidity_percent = convert_units( each_conv.unique_id, vpd_humidity_percent) conversion_found = True if not conversion_found: self.logger.error("Could not find conversion for unit " "{unit} to percent (%)".format( unit=measurement.unit)) critical_error = True try: if not critical_error: vpd_pa = calculate_vapor_pressure_deficit( vpd_temperature_celsius, vpd_humidity_percent) else: self.logger.error( "One or more critical errors prevented the " "vapor pressure deficit from being calculated") except TypeError as err: self.logger.error("TypeError: {msg}".format(msg=err)) if vpd_pa: measure = self.device_measurements.first() conversion = db_retrieve_table_daemon( Conversion, unique_id=measure.conversion_id) channel, unit, measurement = return_measurement_info( measure, conversion) measurement_dict[channel] = { 'measurement': measurement, 'unit': unit, 'value': vpd_pa } else: self.error_not_within_max_age() else: self.logger.error( "Unknown math type: {type}".format(type=self.math_type)) # Finally, add measurements to influxdb add_measurements_influxdb(self.unique_id, measurement_dict)
def setup_output(self): import Adafruit_PCA9685 self.setup_output_variables(OUTPUT_INFORMATION) error = [] if self.pwm_hertz < 40: error.append("PWM Hertz must be a value between 40 and 1600") if error: for each_error in error: self.logger.error(each_error) return try: self.pwm_output = Adafruit_PCA9685.PCA9685( address=int(str(self.output.i2c_location), 16), busnum=self.output.i2c_bus) self.pwm_output.set_pwm_freq(self.pwm_hertz) self.output_setup = True self.logger.debug("Output setup on bus {} at {}".format( self.output.i2c_bus, self.output.i2c_location)) for i in range(16): if self.options_channels['state_startup'][i] == 0: self.logger.debug( "Startup state channel {ch}: off".format(ch=i)) self.output_switch('off', output_channel=i) elif self.options_channels['state_startup'][ i] == 'set_duty_cycle': self.logger.debug( "Startup state channel {ch}: on ({dc:.2f} %)".format( ch=i, dc=self.options_channels['startup_value'][i])) self.output_switch( 'on', output_channel=i, amount=self.options_channels['startup_value'][i]) elif self.options_channels['state_startup'][ i] == 'last_duty_cycle': self.logger.debug( "Startup state channel {ch}: last".format(ch=i)) device_measurement = db_retrieve_table_daemon( DeviceMeasurements).filter( and_( DeviceMeasurements.device_id == self.unique_id, DeviceMeasurements.channel == i)).first() last_measurement = None if device_measurement: channel, unit, measurement = return_measurement_info( device_measurement, None) last_measurement = read_last_influxdb( self.unique_id, unit, channel, measure=measurement, duration_sec=None) if last_measurement: self.logger.debug( "Setting channel {ch} startup duty cycle to last known value of {dc} %" .format(ch=i, dc=last_measurement[1])) self.output_switch('on', amount=last_measurement[1]) else: self.logger.error( "Output channel {} instructed at startup to be set to " "the last known duty cycle, but a last known " "duty cycle could not be found in the measurement " "database".format(i)) else: self.logger.debug( "Startup state channel {ch}: no change".format(ch=i)) except Exception as except_msg: self.logger.exception( "Output was unable to be setup: {err}".format(err=except_msg))
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.log_level_debug = pid.log_level_debug 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] if self.log_level_debug: self.logger.setLevel(logging.DEBUG) else: self.logger.setLevel(logging.INFO) 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 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 output_id = None output_channel_id = 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] if output_id and output_channel_id: daemon_control = DaemonControl() if daemon_control.output_state( output_id, output_channel=output_channel_id) == "on": output_already_on = True else: daemon_control.output_on(output_id, output_channel=output_channel_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 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 elif settings.library == 'http_address': 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) elif settings.library == 'http_address_requests': 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) 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_id and daemon_control and not output_already_on: daemon_control.output_off(output_id, output_channel=output_channel_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))
def __init__(self, ready, pid_id): threading.Thread.__init__(self) self.logger = logging.getLogger("{}_{}".format(__name__, 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.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.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_function(self): temp_wet_k = None temp_dry_k = None pressure_pa = 101325 if (self.select_measurement_pressure_pa_device_id and self.select_measurement_pressure_pa_measurement_id and self.max_measure_age_pressure_pa): device_measurement = get_measurement( self.select_measurement_pressure_pa_measurement_id) conversion = db_retrieve_table_daemon( Conversion, unique_id=device_measurement.conversion_id) channel, unit, measurement = return_measurement_info( device_measurement, conversion) last_measurement_pa = self.get_last_measurement( self.select_measurement_pressure_pa_device_id, self.select_measurement_pressure_pa_measurement_id, max_age=self.max_measure_age_pressure_pa) if last_measurement_pa: pressure_pa = convert_from_x_to_y_unit(unit, 'Pa', last_measurement_pa[1]) last_measurement_wet = self.get_last_measurement( self.select_measurement_temp_wet_c_device_id, self.select_measurement_temp_wet_c_measurement_id, max_age=self.max_measure_age_temp_wet_c) if last_measurement_wet: device_measurement = get_measurement( self.select_measurement_temp_wet_c_measurement_id) conversion = db_retrieve_table_daemon( Conversion, unique_id=device_measurement.conversion_id) channel, unit, measurement = return_measurement_info( device_measurement, conversion) temp_wet_k = convert_from_x_to_y_unit(unit, 'K', last_measurement_wet[1]) last_measurement_dry = self.get_last_measurement( self.select_measurement_temp_dry_c_device_id, self.select_measurement_temp_dry_c_measurement_id, max_age=self.max_measure_age_temp_dry_c) if last_measurement_dry: device_measurement = get_measurement( self.select_measurement_temp_dry_c_measurement_id) conversion = db_retrieve_table_daemon( Conversion, unique_id=device_measurement.conversion_id) channel, unit, measurement = return_measurement_info( device_measurement, conversion) temp_dry_k = convert_from_x_to_y_unit(unit, 'K', last_measurement_dry[1]) if temp_wet_k and temp_dry_k: measurements = copy.deepcopy(measurements_dict) psypi = None try: psypi = SI.state("DBT", temp_dry_k, "WBT", temp_wet_k, pressure_pa) except TypeError as err: self.logger.error("TypeError: {msg}".format(msg=err)) if not psypi: self.logger.error( "Could not calculate humidity from wet/dry bulbs") return percent_relative_humidity = psypi[2] * 100 # Ensure percent humidity stays within 0 - 100 % range if percent_relative_humidity > 100: percent_relative_humidity = 100 elif percent_relative_humidity < 0: percent_relative_humidity = 0 # Dry bulb temperature: psypi[0]) # Wet bulb temperature: psypi[5]) specific_enthalpy = float(psypi[1]) humidity = float(percent_relative_humidity) specific_volume = float(psypi[3]) humidity_ratio = float(psypi[4]) self.logger.debug("Dry Temp: {dtk}, " "Wet Temp: {wtk}, " "Pressure: {pres}," "Humidity: {rh}".format(dtk=temp_dry_k, wtk=temp_wet_k, pres=pressure_pa, rh=humidity)) list_measurement = [ humidity, humidity_ratio, specific_enthalpy, specific_volume ] for each_measurement in self.device_measurements.all(): if each_measurement.is_enabled: conversion = db_retrieve_table_daemon( Conversion, unique_id=each_measurement.conversion_id) channel, unit, measurement = return_measurement_info( each_measurement, conversion) measurements[channel] = { 'measurement': measurement, 'unit': unit, 'value': list_measurement[channel] } # Add measurement(s) to influxdb if measurements: self.logger.debug( "Adding measurements to InfluxDB with ID {}: {}".format( self.unique_id, measurements)) add_measurements_influxdb(self.unique_id, measurements) else: self.logger.debug( "No measurements to add to InfluxDB with ID {}".format( self.unique_id))
def get_new_data(self, past_seconds): # Basic implementation. Future development may use more complex library to access API endpoint = "https://{app}.data.thethingsnetwork.org/api/v2/query/{dev}?last={time}".format( app=self.application_id, dev=self.device_id, time="{}s".format(int(past_seconds))) headers = {"Authorization": "key {k}".format(k=self.app_api_key)} timestamp_format = '%Y-%m-%dT%H:%M:%S.%f' response = requests.get(endpoint, headers=headers) try: response.json() except ValueError: # No data returned self.logger.debug( "Response Error. Response: {}. Likely there is no data to be retrieved on TTN" .format(response.content)) return for each_resp in response.json(): if not self.running: break try: datetime_utc = datetime.datetime.strptime( each_resp['time'][:-7], timestamp_format) except Exception: # Sometimes the original timestamp is in milliseconds # instead of nanoseconds. Therefore, remove 3 less digits # past the decimal and try again to parse. try: datetime_utc = datetime.datetime.strptime( each_resp['time'][:-4], timestamp_format) except Exception as e: self.logger.error( "Could not parse timestamp '{}': {}".format( each_resp['time'], e)) continue # Malformed timestamp encountered. Discard measurement. if (not self.latest_datetime or self.latest_datetime < datetime_utc): self.latest_datetime = datetime_utc measurements = {} for channel in self.channels_measurement: if (self.is_enabled(channel) and self.options_channels['variable_name'][channel] in each_resp and each_resp[self.options_channels['variable_name'] [channel]] is not None): # Original value/unit measurements[channel] = {} measurements[channel][ 'measurement'] = self.channels_measurement[ channel].measurement measurements[channel]['unit'] = self.channels_measurement[ channel].unit measurements[channel]['value'] = each_resp[ self.options_channels['variable_name'][channel]] measurements[channel]['timestamp_utc'] = datetime_utc # Convert value/unit is conversion_id present and valid if self.channels_conversion[channel]: conversion = db_retrieve_table_daemon( Conversion, unique_id=self.channels_measurement[channel]. conversion_id) if conversion: meas = parse_measurement( self.channels_conversion[channel], self.channels_measurement[channel], measurements, channel, measurements[channel], timestamp=datetime_utc) measurements[channel]['measurement'] = meas[ channel]['measurement'] measurements[channel]['unit'] = meas[channel][ 'unit'] measurements[channel]['value'] = meas[channel][ 'value'] if measurements: self.logger.debug( "Adding measurements to influxdb: {}".format(measurements)) add_measurements_influxdb( self.unique_id, measurements, use_same_timestamp=INPUT_INFORMATION[ 'measurements_use_same_timestamp']) else: self.logger.debug("No measurements to add to influxdb.") # set datetime to latest timestamp if self.running: with session_scope(MYCODO_DB_PATH) as new_session: mod_input = new_session.query(Input).filter( Input.unique_id == self.unique_id).first() if not mod_input.datetime or mod_input.datetime < self.latest_datetime: mod_input.datetime = self.latest_datetime new_session.commit()
def generate_output_usage_report(): """ Generate output usage report in a csv file """ logger.debug("Generating output usage report...") try: assure_path_exists(USAGE_REPORTS_PATH) misc = db_retrieve_table_daemon(Misc, entry='first') output = db_retrieve_table_daemon(Output) output_usage = return_output_usage(misc, output.all()) timestamp = time.strftime("%Y-%m-%d_%H-%M") file_name = 'output_usage_report_{ts}.csv'.format(ts=timestamp) report_path_file = os.path.join(USAGE_REPORTS_PATH, file_name) with open(report_path_file, 'wb') as f: w = csv.writer(f) # Header row w.writerow([ 'Relay ID', 'Relay Unique ID', 'Relay Name', 'Type', 'Past Day', 'Past Week', 'Past Month', 'Past Month (from {})'.format(misc.output_usage_dayofmonth), 'Past Year' ]) for key, value in output_usage.items(): if key in ['total_duration', 'total_cost', 'total_kwh']: # Totals rows w.writerow([ '', '', '', key, value['1d'], value['1w'], value['1m'], value['1m_date'], value['1y'] ]) else: # Each output rows each_output = output.filter( Output.unique_id == key).first() w.writerow([ each_output.unique_id, each_output.unique_id, str(each_output.name).encode("utf-8"), 'hours_on', value['1d']['hours_on'], value['1w']['hours_on'], value['1m']['hours_on'], value['1m_date']['hours_on'], value['1y']['hours_on'] ]) w.writerow([ each_output.unique_id, each_output.unique_id, str(each_output.name).encode("utf-8"), 'kwh', value['1d']['kwh'], value['1w']['kwh'], value['1m']['kwh'], value['1m_date']['kwh'], value['1y']['kwh'] ]) w.writerow([ each_output.unique_id, each_output.unique_id, str(each_output.name).encode("utf-8"), 'cost', value['1d']['cost'], value['1w']['cost'], value['1m']['cost'], value['1m_date']['cost'], value['1y']['cost'] ]) set_user_grp(report_path_file, 'mycodo', 'mycodo') except Exception: logger.exception("Energy Usage Report Generation ERROR")
def get_measurement(self): """ Gets the sensor's pH measurement via UART/I2C """ ph = None return_dict = measurements_dict.copy() # Calibrate the pH measurement based on a temperature measurement if (self.calibrate_sensor_measure and ',' in self.calibrate_sensor_measure): self.logger.debug("pH sensor set to calibrate temperature") device_id = self.calibrate_sensor_measure.split(',')[0] measurement_id = self.calibrate_sensor_measure.split(',')[1] device_measurement = self.device_measurements.filter( DeviceMeasurements.unique_id == measurement_id).first() 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) last_measurement = read_last_influxdb( device_id, measurement.unit, measurement.measurement, measurement.channel, self.max_age) if last_measurement: self.logger.debug( "Latest temperature used to calibrate: {temp}".format( temp=last_measurement[1])) atlas_command = AtlasScientificCommand(self.input_dev) ret_value, ret_msg = atlas_command.calibrate( 'temperature', temperature=last_measurement[1]) time.sleep(0.5) self.logger.debug( "Calibration returned: {val}, {msg}".format( val=ret_value, msg=ret_msg)) else: self.logger.error( "Calibration measurement not found within the past " "{} seconds".format(self.max_age)) # Read sensor via FTDI if self.interface == 'FTDI': if self.atlas_sensor_ftdi.setup: lines = self.atlas_sensor_ftdi.query('R') if lines: self.logger.debug( "All Lines: {lines}".format(lines=lines)) # 'check probe' indicates an error reading the sensor if 'check probe' in lines: self.logger.error( '"check probe" returned from sensor') # if a string resembling a float value is returned, this # is out measurement value elif str_is_float(lines[0]): ph = float(lines[0]) self.logger.debug( 'Value[0] is float: {val}'.format(val=ph)) else: # During calibration, the sensor is put into # continuous mode, which causes a return of several # values in one string. If the return value does # not represent a float value, it is likely to be a # string of several values. This parses and returns # the first value. if str_is_float(lines[0].split(b'\r')[0]): ph = lines[0].split(b'\r')[0] # Lastly, this is called if the return value cannot # be determined. Watchthe output in the GUI to see # what it is. else: ph = lines[0] self.logger.error( 'Value[0] is not float or "check probe": ' '{val}'.format(val=ph)) else: self.logger.error('FTDI device is not set up.' 'Check the log for errors.') # Read sensor via UART elif self.interface == 'UART': if self.atlas_sensor_uart.setup: lines = self.atlas_sensor_uart.query('R') if lines: self.logger.debug( "All Lines: {lines}".format(lines=lines)) # 'check probe' indicates an error reading the sensor if 'check probe' in lines: self.logger.error( '"check probe" returned from sensor') # if a string resembling a float value is returned, this # is out measurement value elif str_is_float(lines[0]): ph = float(lines[0]) self.logger.debug( 'Value[0] is float: {val}'.format(val=ph)) else: # During calibration, the sensor is put into # continuous mode, which causes a return of several # values in one string. If the return value does # not represent a float value, it is likely to be a # string of several values. This parses and returns # the first value. if str_is_float(lines[0].split(b'\r')[0]): ph = lines[0].split(b'\r')[0] # Lastly, this is called if the return value cannot # be determined. Watchthe output in the GUI to see # what it is. else: ph = lines[0] self.logger.error( 'Value[0] is not float or "check probe": ' '{val}'.format(val=ph)) else: self.logger.error('UART device is not set up.' 'Check the log for errors.') # Read sensor via I2C elif self.interface == 'I2C': if self.atlas_sensor_i2c.setup: ph_status, ph_str = self.atlas_sensor_i2c.query('R') if ph_status == 'error': self.logger.error( "Sensor read unsuccessful: {err}".format( err=ph_str)) elif ph_status == 'success': ph = float(ph_str) else: self.logger.error( 'I2C device is not set up. Check the log for errors.') return_dict[0]['value'] = ph return return_dict
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 __init__(self, ready, timer_id): threading.Thread.__init__(self) self.logger = logging.getLogger( "mycodo.timer_{id}".format(id=timer_id)) self.thread_startup_timer = timeit.default_timer() self.thread_shutdown_timer = 0 self.ready = ready self.timer_id = timer_id self.control = DaemonControl() timer = db_retrieve_table_daemon(Timer, device_id=self.timer_id) self.timer_type = timer.timer_type self.output_unique_id = timer.relay_id self.method_id = timer.method_id self.method_period = timer.method_period self.state = timer.state self.time_start = timer.time_start self.time_end = timer.time_end self.duration_on = timer.duration_on self.duration_off = timer.duration_off self.output_id = db_retrieve_table_daemon( Output, unique_id=self.output_unique_id).id # Time of day split into hour and minute if self.time_start: time_split = self.time_start.split(":") self.start_hour = time_split[0] self.start_minute = time_split[1] else: self.start_hour = None self.start_minute = None if self.time_end: time_split = self.time_end.split(":") self.end_hour = time_split[0] self.end_minute = time_split[1] else: self.end_hour = None self.end_minute = None self.duration_timer = time.time() self.pwm_method_timer = time.time() self.date_timer_not_executed = True self.running = False if self.method_id: method = db_retrieve_table_daemon(Method, device_id=self.method_id) method_data = db_retrieve_table_daemon(MethodData) method_data = method_data.filter(MethodData.method_id == self.method_id) method_data_repeat = method_data.filter(MethodData.duration_sec == 0).first() self.method_type = method.method_type self.method_start_act = timer.method_start_time self.method_start_time = None self.method_end_time = None if self.method_type == 'Duration': if self.method_start_act == 'Ended': self.stop_controller(ended_normally=False, deactivate_timer=True) self.logger.warning( "Method has ended. " "Activate the Timer controller to start it again.") elif self.method_start_act == 'Ready' or self.method_start_act is None: # Method has been instructed to begin now = datetime.datetime.now() self.method_start_time = now if method_data_repeat and method_data_repeat.duration_end: self.method_end_time = now + datetime.timedelta( seconds=float(method_data_repeat.duration_end)) with session_scope(MYCODO_DB_PATH) as db_session: mod_timer = db_session.query(Timer) mod_timer = mod_timer.filter(Timer.id == self.timer_id).first() mod_timer.method_start_time = self.method_start_time mod_timer.method_end_time = self.method_end_time db_session.commit() else: # Method neither instructed to begin or not to # Likely there was a daemon restart ot power failure # Resume method with saved start_time self.method_start_time = datetime.datetime.strptime( str(timer.method_start_time), '%Y-%m-%d %H:%M:%S.%f') if method_data_repeat and method_data_repeat.duration_end: self.method_end_time = datetime.datetime.strptime( str(timer.method_end_time), '%Y-%m-%d %H:%M:%S.%f') if self.method_end_time > datetime.datetime.now(): self.logger.warning( "Resuming method {id}: started {start}, " "ends {end}".format( id=self.method_id, start=self.method_start_time, end=self.method_end_time)) else: self.method_start_act = 'Ended' else: self.method_start_act = 'Ended'
def get_new_data(self, past_seconds): # Basic implementation. Future development may use more complex library to access API endpoint = "https://{app}.data.thethingsnetwork.org/api/v2/query/{dev}?last={time}".format( app=self.application_id, dev=self.device_id, time="{}s".format(int(past_seconds))) headers = {"Authorization": "key {k}".format(k=self.app_api_key)} timestamp_format = '%Y-%m-%dT%H:%M:%S.%f' response = requests.get(endpoint, headers=headers) try: responses = response.json() except ValueError: # No data returned self.logger.debug("Response Error. Response: {}".format( response.content)) return for each_resp in response.json(): if not self.running: break ts_formatted_correctly = False try: datetime_utc = datetime.datetime.strptime( each_resp['time'][:-7], timestamp_format) ts_formatted_correctly = True except: # Sometimes the original timestamp is in milliseconds # instead of nanoseconds. Therefore, remove 3 less digits # past the decimal and try again to parse. try: datetime_utc = datetime.datetime.strptime( each_resp['time'][:-4], timestamp_format) ts_formatted_correctly = True except: self.logger.error("Could not parse timestamp: {}".format( each_resp['time'])) if not ts_formatted_correctly: # Malformed timestamp encountered. Discard measurement. continue if (not self.latest_datetime or self.latest_datetime < datetime_utc): self.latest_datetime = datetime_utc measurements = {} for each_meas in self.device_measurements.all(): if (self.is_enabled(each_meas.channel) and each_meas.name in each_resp and each_resp[each_meas.name] is not None): # Original value/unit measurements[each_meas.channel] = {} measurements[each_meas.channel]['measurement'] = each_meas.measurement measurements[each_meas.channel]['unit'] = each_meas.unit measurements[each_meas.channel]['value'] = each_resp[each_meas.name] measurements[each_meas.channel]['timestamp'] = datetime_utc # Convert value/unit is conversion_id present and valid if each_meas.conversion_id: conversion = db_retrieve_table_daemon( Conversion, unique_id=each_meas.conversion_id) if conversion: meas = parse_measurement( conversion, each_meas, measurements, each_meas.channel, measurements[each_meas.channel]) measurements[each_meas.channel]['measurement'] = meas[each_meas.channel]['measurement'] measurements[each_meas.channel]['unit'] = meas[each_meas.channel]['unit'] measurements[each_meas.channel]['value'] = meas[each_meas.channel]['value'] measurements[each_meas.channel]['timestamp'] = datetime_utc add_measurements_influxdb(self.unique_id, measurements) # set datetime to latest timestamp if self.running: with session_scope(MYCODO_DB_PATH) as new_session: mod_input = new_session.query(Input).filter( Input.unique_id == self.unique_id).first() if not mod_input.datetime or mod_input.datetime < self.latest_datetime: mod_input.datetime = self.latest_datetime new_session.commit()
def initialize_variables(self): input_dev = db_retrieve_table_daemon(Input, unique_id=self.unique_id) self.log_level_debug = input_dev.log_level_debug self.set_log_level_debug(self.log_level_debug) self.dict_inputs = parse_input_information() self.sample_rate = db_retrieve_table_daemon( Misc, entry='first').sample_rate_controller_input self.device_measurements = db_retrieve_table_daemon( DeviceMeasurements).filter( DeviceMeasurements.device_id == self.unique_id) self.conversions = db_retrieve_table_daemon(Conversion) self.input_dev = input_dev self.input_name = input_dev.name self.unique_id = input_dev.unique_id self.gpio_location = input_dev.gpio_location self.device = input_dev.device self.interface = input_dev.interface self.period = input_dev.period self.start_offset = input_dev.start_offset # Pre-Output (activates output prior to and/or during input measurement) self.pre_output_setup = False if self.input_dev.pre_output_id and "," in self.input_dev.pre_output_id: try: self.pre_output_id = input_dev.pre_output_id.split(",")[0] self.pre_output_channel_id = input_dev.pre_output_id.split( ",")[1] self.pre_output_duration = input_dev.pre_output_duration self.pre_output_during_measure = input_dev.pre_output_during_measure self.pre_output_activated = False self.pre_output_timer = time.time() self.pre_output_lock_file = '/var/lock/input_pre_output_{id}_{ch}'.format( id=self.pre_output_id, ch=self.pre_output_channel_id) # Check if Pre Output and channel IDs exists output = db_retrieve_table_daemon(Output, unique_id=self.pre_output_id) output_channel = db_retrieve_table_daemon( OutputChannel, unique_id=self.pre_output_channel_id) if output and output_channel and self.pre_output_duration: self.pre_output_channel = output_channel.channel self.logger.debug("Pre output successfully set up") self.pre_output_setup = True except: self.logger.exception("Could not set up pre-output") self.last_measurement = 0 self.next_measurement = time.time() + self.start_offset self.get_new_measurement = False self.trigger_cond = False self.measurement_acquired = False smtp = db_retrieve_table_daemon(SMTP, entry='first') self.smtp_max_count = smtp.hourly_max self.email_count = 0 self.allowed_to_send_notice = True # Convert string I2C address to base-16 int if self.interface == 'I2C' and self.input_dev.i2c_location: self.i2c_address = int(str(self.input_dev.i2c_location), 16) self.device_recognized = True if self.device in self.dict_inputs: input_loaded = load_module_from_file( self.dict_inputs[self.device]['file_path'], 'inputs') self.measure_input = input_loaded.InputModule(self.input_dev) else: self.device_recognized = False self.logger.debug( "Device '{device}' not recognized".format(device=self.device)) raise Exception("'{device}' is not a valid device type.".format( device=self.device)) self.input_timer = time.time() self.lastUpdate = None # Set up listener (e.g. MQTT, EDGE) if ('listener' in self.dict_inputs[self.device] and self.dict_inputs[self.device]['listener']): self.logger.debug( "Detected as listener. Starting listener thread.") input_listener = threading.Thread( target=self.measure_input.listener()) input_listener.daemon = True input_listener.start()
def setup_output(self): import pigpio self.pigpio = pigpio error = [] if self.options_channels['pin'][0] is None: error.append("Pin must be set") if self.options_channels['pwm_hertz'][0] <= 0: error.append("PWM Hertz must be a positive value") if error: for each_error in error: self.logger.error(each_error) return try: self.pwm_output = self.pigpio.pi() if not self.pwm_output.connected: self.logger.error("Could not connect to pigpiod") self.pwm_output = None return if self.options_channels['pwm_library'][0] == 'pigpio_hardware': self.pwm_output.hardware_PWM( self.options_channels['pin'][0], self.options_channels['pwm_hertz'][0], 0) elif self.options_channels['pwm_library'][0] == 'pigpio_any': self.pwm_output.set_PWM_frequency( self.options_channels['pin'][0], self.options_channels['pwm_hertz'][0]) self.pwm_output.set_PWM_dutycycle( self.options_channels['pin'][0], 0) self.output_setup = True self.logger.info("Output setup on pin {}".format( self.options_channels['pin'][0])) if self.options_channels['state_startup'][0] == 0: self.output_switch('off') elif self.options_channels['state_startup'][0] == 'set_duty_cycle': self.output_switch( 'on', amount=self.options_channels['startup_value'][0]) elif self.options_channels['state_startup'][ 0] == 'last_duty_cycle': device_measurement = db_retrieve_table_daemon( DeviceMeasurements).filter( and_(DeviceMeasurements.device_id == self.unique_id, DeviceMeasurements.channel == 0)).first() last_measurement = None if device_measurement: channel, unit, measurement = return_measurement_info( device_measurement, None) last_measurement = read_last_influxdb(self.unique_id, unit, channel, measure=measurement, duration_sec=None) if last_measurement: self.logger.info( "Setting startup duty cycle to last known value of {dc} %" .format(dc=last_measurement[1])) self.output_switch('on', amount=last_measurement[1]) else: self.logger.error( "Output instructed at startup to be set to " "the last known duty cycle, but a last known " "duty cycle could not be found in the measurement " "database") except Exception as except_msg: self.logger.exception( "Output was unable to be setup on pin {pin}: {err}".format( pin=self.options_channels['pin'][0], err=except_msg))
def output_on_off(self, output_id, state, duration=0.0, min_off=0.0, duty_cycle=0.0, trigger_conditionals=True): """ Turn a output on or off The GPIO may be either HIGH or LOW to activate a output. This trigger state will be referenced to determine if the GPIO needs to be high or low to turn the output on or off. Conditionals will be checked for each action requested of a output, and if true, those conditional actions will be executed. For example: 'If output 1 turns on, turn output 3 off' :param output_id: ID for output :type output_id: int :param state: What state is desired? 'on' or 'off' :type state: str :param duration: If state is 'on', a duration can be set to turn the output off after :type duration: float :param min_off: Don't turn on if not off for at least this duration (0 = disabled) :type min_off: float :param duty_cycle: Duty cycle of PWM output :type duty_cycle: float :param trigger_conditionals: Whether to trigger conditionals to act or not :type trigger_conditionals: bool """ # Check if output exists output_id = int(output_id) if output_id not in self.output_id: self.logger.warning("Cannot turn {state} Output with ID {id}. " "It doesn't exist".format(state=state, id=output_id)) return 1 # Signaled to turn output on if state == 'on': # Check if pin is valid if (self.output_type[output_id] in ['pwm', 'wired', 'wireless_433MHz_pi_switch'] and self.output_pin[output_id] is None): self.logger.warning( "Invalid pin for output {id} ({name}): {pin}.".format( id=self.output_id[output_id], name=self.output_name[output_id], pin=self.output_pin[output_id])) return 1 # Check if max amperage will be exceeded if self.output_type[output_id] in [ 'command', 'wired', 'wireless_433MHz_pi_switch' ]: current_amps = self.current_amp_load() max_amps = db_retrieve_table_daemon(Misc, entry='first').max_amps if current_amps + self.output_amps[output_id] > max_amps: self.logger.warning( "Cannot turn output {} ({}) On. If this output turns on, " "there will be {} amps being drawn, which exceeds the " "maximum set draw of {} amps.".format( self.output_id[output_id], self.output_name[output_id], current_amps, max_amps)) return 1 # If the output is used in a PID, a minimum off duration is set, # and if the off duration has surpassed that amount of time (i.e. # has it been off for longer then the minimum off duration?). current_time = datetime.datetime.now() if (min_off and not self.is_on(output_id) and current_time > self.output_on_until[output_id]): off_seconds = ( current_time - self.output_on_until[output_id]).total_seconds() if off_seconds < min_off: self.logger.debug( "Output {id} ({name}) instructed to turn on by PID, " "however the minimum off period of {min_off_sec} " "seconds has not been reached yet (it has only been " "off for {off_sec} seconds).".format( id=self.output_id[output_id], name=self.output_name[output_id], min_off_sec=min_off, off_sec=off_seconds)) return 1 # Turn output on for a duration if (self.output_type[output_id] in ['command', 'wired', 'wireless_433MHz_pi_switch'] and duration != 0): time_now = datetime.datetime.now() # Output is already on for a duration if self.is_on( output_id) and self.output_on_duration[output_id]: if self.output_on_until[output_id] > time_now: remaining_time = (self.output_on_until[output_id] - time_now).total_seconds() else: remaining_time = 0 time_on = abs( self.output_last_duration[output_id]) - remaining_time self.logger.debug( "Output {rid} ({rname}) is already on for a duration " "of {ron:.2f} seconds (with {rremain:.2f} seconds " "remaining). Recording the amount of time the output " "has been on ({rbeenon:.2f} sec) and updating the on " "duration to {rnewon:.2f} seconds.".format( rid=self.output_id[output_id], rname=self.output_name[output_id], ron=abs(self.output_last_duration[output_id]), rremain=remaining_time, rbeenon=time_on, rnewon=abs(duration))) self.output_on_until[output_id] = ( time_now + datetime.timedelta(seconds=abs(duration))) self.output_last_duration[output_id] = duration # Write the duration the output was ON to the # database at the timestamp it turned ON if time_on > 0: # Make sure the recorded value is recorded negative # if instructed to do so if self.output_last_duration[output_id] < 0: duration_on = float(-time_on) else: duration_on = float(time_on) timestamp = ( datetime.datetime.utcnow() - datetime.timedelta(seconds=abs(duration_on))) write_db = threading.Thread( target=write_influxdb_value, args=( self.output_unique_id[output_id], 'duration_sec', duration_on, timestamp, )) write_db.start() return 0 # Output is on, but not for a duration elif self.is_on(output_id) and not self.output_on_duration: self.output_on_duration[output_id] = True self.output_on_until[output_id] = ( time_now + datetime.timedelta(seconds=abs(duration))) self.output_last_duration[output_id] = duration self.logger.debug( "Output {id} ({name}) is currently on without a " "duration. Turning into a duration of {dur:.1f} " "seconds.".format(id=self.output_id[output_id], name=self.output_name[output_id], dur=abs(duration))) return 0 # Output is not already on else: self.output_on_duration[output_id] = True self.output_last_duration[output_id] = duration self.logger.debug("Output {id} ({name}) on for {dur:.1f} " "seconds.".format( id=self.output_id[output_id], name=self.output_name[output_id], dur=abs(duration))) self.output_switch(output_id, 'on') self.output_on_until[output_id] = ( datetime.datetime.now() + datetime.timedelta(seconds=abs(duration))) # Just turn output on elif self.output_type[output_id] in [ 'command', 'wired', 'wireless_433MHz_pi_switch' ]: if self.is_on(output_id): self.logger.debug( "Output {id} ({name}) is already on.".format( id=self.output_id[output_id], name=self.output_name[output_id])) return 1 else: # Record the time the output was turned on in order to # calculate and log the total duration is was on, when # it eventually turns off. self.output_time_turned_on[ output_id] = datetime.datetime.now() self.logger.debug( "Output {id} ({name}) ON at {timeon}.".format( id=self.output_id[output_id], name=self.output_name[output_id], timeon=self.output_time_turned_on[output_id])) self.output_switch(output_id, 'on') # PWM command output elif self.output_type[output_id] == 'command_pwm': if duty_cycle: self.pwm_time_turned_on[output_id] = datetime.datetime.now( ) else: self.pwm_time_turned_on[output_id] = None self.logger.debug( "PWM command {id} ({name}) ON with a duty cycle of {dc:.2f}%" .format(id=self.output_id[output_id], name=self.output_name[output_id], dc=abs(duty_cycle))) self.output_switch(output_id, 'on', duty_cycle=duty_cycle) # Write the duty cycle of the PWM to the database write_db = threading.Thread( target=write_influxdb_value, args=( self.output_unique_id[output_id], 'duty_cycle', duty_cycle, )) write_db.start() # PWM output elif self.output_type[output_id] == 'pwm': # Record the time the PWM was turned on if self.pwm_hertz[output_id] <= 0: self.logger.warning("PWM Hertz must be a positive value") return 1 self.pwm_time_turned_on[output_id] = datetime.datetime.now() self.logger.debug( "PWM {id} ({name}) ON with a duty cycle of {dc:.2f}% at {hertz} Hz" .format(id=self.output_id[output_id], name=self.output_name[output_id], dc=abs(duty_cycle), hertz=self.pwm_hertz[output_id])) self.output_switch(output_id, 'on', duty_cycle=duty_cycle) # Write the duty cycle of the PWM to the database write_db = threading.Thread( target=write_influxdb_value, args=( self.output_unique_id[output_id], 'duty_cycle', duty_cycle, )) write_db.start() # Signaled to turn output off elif state == 'off': if not self._is_setup(output_id): return if (self.output_type[output_id] in ['pwm', 'wired', 'wireless_433MHz_pi_switch'] and self.output_pin[output_id] is None): return self.output_switch(output_id, 'off') self.logger.debug("Output {id} ({name}) turned off.".format( id=self.output_id[output_id], name=self.output_name[output_id])) # Write PWM duty cycle to database if (self.output_type[output_id] in ['pwm', 'command_pwm'] and self.pwm_time_turned_on[output_id] is not None): # Write the duration the PWM was ON to the database # at the timestamp it turned ON duration_on = ( datetime.datetime.now() - self.pwm_time_turned_on[output_id]).total_seconds() self.pwm_time_turned_on[output_id] = None timestamp = (datetime.datetime.utcnow() - datetime.timedelta(seconds=duration_on)) write_db = threading.Thread( target=write_influxdb_value, args=( self.output_unique_id[output_id], 'duty_cycle', duty_cycle, timestamp, )) write_db.start() # Write output duration on to database elif (self.output_time_turned_on[output_id] is not None or self.output_on_duration[output_id]): duration_sec = None timestamp = None if self.output_on_duration[output_id]: remaining_time = 0 time_now = datetime.datetime.now() if self.output_on_until[output_id] > time_now: remaining_time = (self.output_on_until[output_id] - time_now).total_seconds() duration_sec = (abs(self.output_last_duration[output_id]) - remaining_time) timestamp = (datetime.datetime.utcnow() - datetime.timedelta(seconds=duration_sec)) # Store negative duration if a negative duration is received if self.output_last_duration[output_id] < 0: duration_sec = -duration_sec self.output_on_duration[output_id] = False self.output_on_until[output_id] = datetime.datetime.now() if self.output_time_turned_on[output_id] is not None: # Write the duration the output was ON to the database # at the timestamp it turned ON duration_sec = ( datetime.datetime.now() - self.output_time_turned_on[output_id]).total_seconds() timestamp = (datetime.datetime.utcnow() - datetime.timedelta(seconds=duration_sec)) self.output_time_turned_on[output_id] = None write_db = threading.Thread( target=write_influxdb_value, args=( self.output_unique_id[output_id], 'duration_sec', duration_sec, timestamp, )) write_db.start() if trigger_conditionals: self.check_conditionals(output_id, state=state, on_duration=duration, duty_cycle=duty_cycle)
def trigger_action(cond_action_id, message='', note_tags=None, email_recipients=None, attachment_file=None, attachment_type=None, single_action=False): """ Trigger individual action If single_action == False, message, note_tags, email_recipients, attachment_file, and attachment_type are returned and may be passed back to this function in order to append to those lists. :param cond_action_id: unique_id of action :param message: message string to append to that will be sent back :param note_tags: list of note tags to use if creating a note :param email_recipients: list of email addresses to notify if sending an email :param attachment_file: string location of a file to attach to an email :param attachment_type: string type of email attachment :param single_action: True if only one action is being triggered, False if only one of multiple actions :return: message or (message, note_tags, email_recipients, attachment_file, attachment_type) """ cond_action = db_retrieve_table_daemon(Actions, unique_id=cond_action_id) if not cond_action: message += 'Error: Action with ID {} not found!'.format(cond_action_id) if single_action: return message else: return message, note_tags, email_recipients, attachment_file, attachment_type logger_actions = logging.getLogger("mycodo.trigger_action_{id}".format( id=cond_action.unique_id.split('-')[0])) message += "\n[Action {id}]:".format( id=cond_action.unique_id.split('-')[0], action_type=cond_action.action_type) try: control = DaemonControl() smtp_table = db_retrieve_table_daemon(SMTP, entry='first') smtp_max_count = smtp_table.hourly_max smtp_wait_timer = smtp_table.smtp_wait_timer email_count = smtp_table.email_count # Pause if cond_action.action_type == 'pause_actions': message += " [{id}] Pause actions for {sec} seconds.".format( id=cond_action.id, sec=cond_action.pause_duration) time.sleep(cond_action.pause_duration) # Actuate output (duration) if (cond_action.action_type == 'output' and cond_action.do_unique_id and cond_action.do_output_state in ['on', 'off']): this_output = db_retrieve_table_daemon( Output, unique_id=cond_action.do_unique_id, entry='first') message += " Turn output {unique_id} ({id}, {name}) {state}".format( unique_id=cond_action.do_unique_id, id=this_output.id, name=this_output.name, state=cond_action.do_output_state) if (cond_action.do_output_state == 'on' and cond_action.do_output_duration): message += " for {sec} seconds".format( sec=cond_action.do_output_duration) message += "." output_on_off = threading.Thread( target=control.output_on_off, args=( cond_action.do_unique_id, cond_action.do_output_state, ), kwargs={'duration': cond_action.do_output_duration}) output_on_off.start() # Actuate output (PWM) if (cond_action.action_type == 'output_pwm' and cond_action.do_unique_id and 0 <= cond_action.do_output_pwm <= 100): this_output = db_retrieve_table_daemon( Output, unique_id=cond_action.do_unique_id, entry='first') message += " Turn output {unique_id} ({id}, {name}) duty cycle to {duty_cycle}%.".format( unique_id=cond_action.do_unique_id, id=this_output.id, name=this_output.name, duty_cycle=cond_action.do_output_pwm) output_on = threading.Thread( target=control.output_on, args=(cond_action.do_unique_id, ), kwargs={'duty_cycle': cond_action.do_output_pwm}) output_on.start() # Execute command in shell if cond_action.action_type == 'command': # Replace string variables with actual values command_str = cond_action.do_action_string # TODO: Maybe get this working again with the new measurement system # # Replace measurement variables # if last_measurement: # command_str = command_str.replace( # "((measure_{var}))".format( # var=device_measurement), str(last_measurement)) # if device and device.period: # command_str = command_str.replace( # "((measure_period))", str(device.period)) # if input_dev: # command_str = command_str.replace( # "((measure_location))", str(input_dev.location)) # if input_dev and device_measurement == input_dev.measurements: # command_str = command_str.replace( # "((measure_linux_command))", str(last_measurement)) # # # Replace output variables # if output: # if output.pin: # command_str = command_str.replace( # "((output_pin))", str(output.pin)) # if output_state: # command_str = command_str.replace( # "((output_action))", str(output_state)) # if on_duration: # command_str = command_str.replace( # "((output_duration))", str(on_duration)) # if duty_cycle: # command_str = command_str.replace( # "((output_pwm))", str(duty_cycle)) # # # Replace edge variables # if edge: # command_str = command_str.replace( # "((edge_state))", str(edge)) message += " Execute '{com}' ".format(com=command_str) _, _, cmd_status = cmd_output(command_str) message += "(return status: {stat}).".format(stat=cmd_status) # Create Note if cond_action.action_type == 'create_note': tag_name = db_retrieve_table_daemon( NoteTags, unique_id=cond_action.do_action_string).name message += " Create note with tag '{}'.".format(tag_name) if single_action and cond_action.do_action_string: list_tags = [] check_tag = db_retrieve_table_daemon( NoteTags, unique_id=cond_action.do_action_string) if check_tag: list_tags.append(cond_action.do_action_string) if list_tags: with session_scope(MYCODO_DB_PATH) as db_session: new_note = Notes() new_note.name = 'Action' new_note.tags = ','.join(list_tags) new_note.note = message db_session.add(new_note) else: note_tags.append(cond_action.do_action_string) # Capture photo # If emailing later, store location of photo as attachment_file if cond_action.action_type in ['photo', 'photo_email']: this_camera = db_retrieve_table_daemon( Camera, unique_id=cond_action.do_unique_id, entry='first') message += " Capturing photo with camera {unique_id} ({id}, {name}).".format( unique_id=cond_action.do_unique_id, id=this_camera.id, name=this_camera.name) camera_still = db_retrieve_table_daemon( Camera, unique_id=cond_action.do_unique_id) attachment_path_file = camera_record('photo', camera_still.unique_id) attachment_file = os.path.join(attachment_path_file[0], attachment_path_file[1]) # Capture video if cond_action.action_type in ['video', 'video_email']: this_camera = db_retrieve_table_daemon( Camera, unique_id=cond_action.do_unique_id, entry='first') message += " Capturing video with camera {unique_id} ({id}, {name}).".format( unique_id=cond_action.do_unique_id, id=this_camera.id, name=this_camera.name) camera_stream = db_retrieve_table_daemon( Camera, unique_id=cond_action.do_unique_id) attachment_path_file = camera_record( 'video', camera_stream.unique_id, duration_sec=cond_action.do_camera_duration) attachment_file = os.path.join(attachment_path_file[0], attachment_path_file[1]) # Activate Controller if cond_action.action_type == 'activate_controller': (controller_type, controller_object, controller_entry) = which_controller(cond_action.do_unique_id) message += " Activate Controller {unique_id} ({id}, {name}).".format( unique_id=cond_action.do_unique_id, id=controller_entry.id, name=controller_entry.name) if controller_entry.is_activated: message += " Notice: Controller is already active!" else: with session_scope(MYCODO_DB_PATH) as new_session: mod_cont = new_session.query(controller_object).filter( controller_object.unique_id == cond_action.do_unique_id).first() mod_cont.is_activated = True new_session.commit() activate_controller = threading.Thread( target=control.controller_activate, args=( controller_type, cond_action.do_unique_id, )) activate_controller.start() # Deactivate Controller if cond_action.action_type == 'deactivate_controller': (controller_type, controller_object, controller_entry) = which_controller(cond_action.do_unique_id) message += " Deactivate Controller {unique_id} ({id}, {name}).".format( unique_id=cond_action.do_unique_id, id=controller_entry.id, name=controller_entry.name) if not controller_entry.is_activated: message += " Notice: Controller is already inactive!" else: with session_scope(MYCODO_DB_PATH) as new_session: mod_cont = new_session.query(controller_object).filter( controller_object.unique_id == cond_action.do_unique_id).first() mod_cont.is_activated = False new_session.commit() deactivate_controller = threading.Thread( target=control.controller_deactivate, args=( controller_type, cond_action.do_unique_id, )) deactivate_controller.start() # Resume PID controller if cond_action.action_type == 'resume_pid': pid = db_retrieve_table_daemon(PID, unique_id=cond_action.do_unique_id, entry='first') message += " Resume PID {unique_id} ({id}, {name}).".format( unique_id=cond_action.do_unique_id, id=pid.id, name=pid.name) if not pid.is_paused: message += " Notice: PID is not paused!" elif pid.is_activated: with session_scope(MYCODO_DB_PATH) as new_session: mod_pid = new_session.query(PID).filter( PID.unique_id == cond_action.do_unique_id).first() mod_pid.is_paused = False new_session.commit() resume_pid = threading.Thread( target=control.pid_resume, args=(cond_action.do_unique_id, )) resume_pid.start() # Pause PID controller if cond_action.action_type == 'pause_pid': pid = db_retrieve_table_daemon(PID, unique_id=cond_action.do_unique_id, entry='first') message += " Pause PID {unique_id} ({id}, {name}).".format( unique_id=cond_action.do_unique_id, id=pid.id, name=pid.name) if pid.is_paused: message += " Notice: PID is already paused!" elif pid.is_activated: with session_scope(MYCODO_DB_PATH) as new_session: mod_pid = new_session.query(PID).filter( PID.unique_id == cond_action.do_unique_id).first() mod_pid.is_paused = True new_session.commit() pause_pid = threading.Thread(target=control.pid_pause, args=(cond_action.do_unique_id, )) pause_pid.start() # Set PID Setpoint if cond_action.action_type == 'setpoint_pid': pid = db_retrieve_table_daemon(PID, unique_id=cond_action.do_unique_id, entry='first') message += " Set Setpoint of PID {unique_id} ({id}, {name}).".format( unique_id=cond_action.do_unique_id, id=pid.id, name=pid.name) if pid.is_activated: setpoint_pid = threading.Thread( target=control.pid_set, args=( pid.unique_id, 'setpoint', float(cond_action.do_action_string), )) setpoint_pid.start() else: with session_scope(MYCODO_DB_PATH) as new_session: mod_pid = new_session.query(PID).filter( PID.unique_id == cond_action.do_unique_id).first() mod_pid.setpoint = cond_action.do_action_string new_session.commit() # Set PID Method and start method from beginning if cond_action.action_type == 'method_pid': pid = db_retrieve_table_daemon(PID, unique_id=cond_action.do_unique_id, entry='first') message += " Set Method of PID {unique_id} ({id}, {name}).".format( unique_id=cond_action.do_unique_id, id=pid.id, name=pid.name) # Instruct method to start with session_scope(MYCODO_DB_PATH) as new_session: mod_pid = new_session.query(PID).filter( PID.unique_id == cond_action.do_unique_id).first() mod_pid.method_start_time = 'Ready' new_session.commit() pid = db_retrieve_table_daemon(PID, unique_id=cond_action.do_unique_id, entry='first') if pid.is_activated: method_pid = threading.Thread(target=control.pid_set, args=( pid.unique_id, 'method', cond_action.do_action_string, )) method_pid.start() else: with session_scope(MYCODO_DB_PATH) as new_session: mod_pid = new_session.query(PID).filter( PID.unique_id == cond_action.do_unique_id).first() mod_pid.method_id = cond_action.do_action_string new_session.commit() # Email the Conditional message. Optionally capture a photo or # video and attach to the email. if cond_action.action_type in ['email', 'photo_email', 'video_email']: if (email_count >= smtp_max_count and time.time() < smtp_wait_timer): allowed_to_send_notice = False else: if time.time() > smtp_wait_timer: with session_scope(MYCODO_DB_PATH) as new_session: mod_smtp = new_session.query(SMTP).first() mod_smtp.email_count = 0 mod_smtp.smtp_wait_timer = time.time() + 3600 new_session.commit() allowed_to_send_notice = True with session_scope(MYCODO_DB_PATH) as new_session: mod_smtp = new_session.query(SMTP).first() mod_smtp.email_count += 1 new_session.commit() # If the emails per hour limit has not been exceeded if allowed_to_send_notice: message += " Notify {email}.".format( email=cond_action.do_action_string) # attachment_type != False indicates to # attach a photo or video if cond_action.action_type == 'photo_email': message += " Photo attached to email." attachment_type = 'still' elif cond_action.action_type == 'video_email': message += " Video attached to email." attachment_type = 'video' if single_action and cond_action.do_action_string: smtp = db_retrieve_table_daemon(SMTP, entry='first') send_email(smtp.host, smtp.ssl, smtp.port, smtp.user, smtp.passw, smtp.email_from, [cond_action.do_action_string], message, attachment_file, attachment_type) else: email_recipients.append(cond_action.do_action_string) else: logger_actions.error( "Wait {sec:.0f} seconds to email again.".format( sec=smtp_wait_timer - time.time())) if cond_action.action_type == 'flash_lcd_on': lcd = db_retrieve_table_daemon(LCD, unique_id=cond_action.do_unique_id) message += " LCD {unique_id} ({id}, {name}) Flash On.".format( unique_id=cond_action.do_unique_id, id=lcd.id, name=lcd.name) start_flashing = threading.Thread(target=control.lcd_flash, args=( cond_action.do_unique_id, True, )) start_flashing.start() if cond_action.action_type == 'flash_lcd_off': lcd = db_retrieve_table_daemon(LCD, unique_id=cond_action.do_unique_id) message += " LCD {unique_id} ({id}, {name}) Flash Off.".format( unique_id=cond_action.do_unique_id, id=lcd.id, name=lcd.name) start_flashing = threading.Thread(target=control.lcd_flash, args=( cond_action.do_unique_id, False, )) start_flashing.start() if cond_action.action_type == 'lcd_backlight_off': lcd = db_retrieve_table_daemon(LCD, unique_id=cond_action.do_unique_id) message += " LCD {unique_id} ({id}, {name}) Backlight Off.".format( unique_id=cond_action.do_unique_id, id=lcd.id, name=lcd.name) start_flashing = threading.Thread(target=control.lcd_backlight, args=( cond_action.do_unique_id, False, )) start_flashing.start() if cond_action.action_type == 'lcd_backlight_on': lcd = db_retrieve_table_daemon(LCD, unique_id=cond_action.do_unique_id) message += " LCD {unique_id} ({id}, {name}) Backlight On.".format( unique_id=cond_action.do_unique_id, id=lcd.id, name=lcd.name) start_flashing = threading.Thread(target=control.lcd_backlight, args=( cond_action.do_unique_id, True, )) start_flashing.start() except Exception: logger_actions.exception("Error triggering action:") message += " Error while executing action! See Daemon log for details." if single_action: return message else: return message, note_tags, email_recipients, attachment_file, attachment_type