def gpio_state_unique_id(unique_id): """Return the GPIO state, for dashboard output """ output = Output.query.filter( Output.unique_id == unique_id).first() daemon_control = DaemonControl() GPIO.setmode(GPIO.BCM) GPIO.setwarnings(False) if output.output_type == 'wired' and output.pin and -1 < output.pin < 40: GPIO.setup(output.pin, GPIO.OUT) if GPIO.input(output.pin) == output.trigger: state = 'on' else: state = 'off' elif (output.output_type in ['command', 'command_pwm', 'python', 'python_pwm', 'atlas_ezo_pmp'] or (output.output_type in ['pwm', 'wireless_rpi_rf'] and output.pin and -1 < output.pin < 40)): state = daemon_control.output_state(output.unique_id) else: state = None return jsonify(state)
def gpio_state(): """Return the GPIO state, for output page status""" output = Output.query.all() daemon_control = DaemonControl() state = {} GPIO.setmode(GPIO.BCM) GPIO.setwarnings(False) for each_output in output: if each_output.output_type == 'wired' and each_output.pin and -1 < each_output.pin < 40: GPIO.setup(each_output.pin, GPIO.OUT) if GPIO.input(each_output.pin) == each_output.trigger: state[each_output.unique_id] = 'on' else: state[each_output.unique_id] = 'off' elif (each_output.output_type in ['command', 'command_pwm', 'python', 'python_pwm', 'atlas_ezo_pmp'] or (each_output.output_type in ['pwm', 'wireless_rpi_rf'] and each_output.pin and -1 < each_output.pin < 40)): state[each_output.unique_id] = daemon_control.output_state(each_output.unique_id) else: state[each_output.unique_id] = None return jsonify(state)
def gpio_state_unique_id(unique_id, channel_id): """Return the GPIO state, for dashboard output """ output = Output.query.filter(Output.unique_id == unique_id).first() channel = OutputChannel.query.filter( OutputChannel.unique_id == channel_id).first() daemon_control = DaemonControl() state = daemon_control.output_state(unique_id, channel.channel) return jsonify(state)
def get_condition_measurement(sql_condition): """ Returns condition measurements for Conditional controllers :param sql_condition: str containing comma-separated device ID and measurement ID :return: measurement: float measurement, gpio_state: int 0 or 1, output_state: 'on', 'off', or int duty cycle """ # Check Measurement Conditions if sql_condition.condition_type == 'measurement': 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 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 # Return GPIO state 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 # Return output state elif sql_condition.condition_type == 'output_state': output = Output.query.filter( Output.unique_id == sql_condition.output_id).first() if output: control = DaemonControl() return control.output_state(output.unique_id)
def gpio_state_unique_id(unique_id): """Return the GPIO state, for dashboard output """ output = Output.query.filter(Output.unique_id == unique_id).first() daemon_control = DaemonControl() GPIO.setmode(GPIO.BCM) GPIO.setwarnings(False) if output.output_type == 'wired' and output.pin and -1 < output.pin < 40: GPIO.setup(output.pin, GPIO.OUT) if GPIO.input(output.pin) == output.trigger: state = 'on' else: state = 'off' elif (output.output_type in ['command', 'command_pwm'] or (output.output_type in ['pwm', 'wireless_433MHz_pi_switch'] and output.pin and -1 < output.pin < 40)): state = daemon_control.output_state(output.id) else: state = None return jsonify(state)
def output_sec_on(output_id, past_seconds, output_channel=0): """ Return the number of seconds a output has been ON in the past number of seconds """ # Get the number of seconds ON stored in the database output = db_retrieve_table_daemon(Output, unique_id=output_id) client = InfluxDBClient(INFLUXDB_HOST, INFLUXDB_PORT, INFLUXDB_USER, INFLUXDB_PASSWORD, INFLUXDB_DATABASE, timeout=5) if not output_id: return None # Get the number of seconds not stored in the database (if currently on) output_time_on = 0 try: control = DaemonControl() if control.output_state(output_id, output_channel=output_channel) == 'on': output_time_on = control.output_sec_currently_on( output_id, output_channel=output_channel) except Exception: logger.exception("output_sec_on()") query = query_string('s', output.unique_id, measure='duration_time', channel=output_channel, value='SUM', past_sec=past_seconds) query_output = client.query(query) sec_recorded_on = 0 if query_output: sec_recorded_on = query_output.raw['series'][0]['values'][0][1] sec_currently_on = 0 if output_time_on: sec_currently_on = min(output_time_on, past_seconds) return sec_recorded_on + sec_currently_on
def get(self, unique_id): """Show the settings and status for an output""" if not utils_general.user_has_permission('edit_controllers'): abort(403) try: dict_data = get_from_db(OutputSchema, Output, unique_id=unique_id) measure_schema = DeviceMeasurementsSchema() list_data = return_list_of_dictionaries( measure_schema.dump( DeviceMeasurements.query.filter_by( device_id=unique_id).all(), many=True)) control = DaemonControl() output_state = control.output_state(unique_id) return {'output settings': dict_data, 'output device measurements': list_data, 'output state': output_state}, 200 except Exception: abort(500, message='An exception occurred', error=traceback.format_exc())
def gpio_state(): """Return the GPIO state, for output page status""" output = Output.query.all() daemon_control = DaemonControl() state = {} GPIO.setmode(GPIO.BCM) GPIO.setwarnings(False) for each_output in output: if each_output.output_type == 'wired' and each_output.pin and -1 < each_output.pin < 40: GPIO.setup(each_output.pin, GPIO.OUT) if GPIO.input(each_output.pin) == each_output.trigger: state[each_output.unique_id] = 'on' else: state[each_output.unique_id] = 'off' elif (each_output.output_type in ['command', 'command_pwm'] or (each_output.output_type in ['pwm', 'wireless_433MHz_pi_switch'] and each_output.pin and -1 < each_output.pin < 40)): state[each_output.unique_id] = daemon_control.output_state( each_output.unique_id) else: state[each_output.unique_id] = None return jsonify(state)
class InputModule(AbstractInput): """ A sensor support class that measures the DHT22's humidity and temperature and calculates the dew point An adaptation of DHT22 code from https://github.com/joan2937/pigpio The sensor is also known as the AM2302. The sensor can be powered from the Pi 3.3-volt or 5-volt rail. Powering from the 3.3-volt rail is simpler and safer. You may need to power from 5 if the sensor is connected via a long cable. For 3.3-volt operation connect pin 1 to 3.3 volts and pin 4 to ground. Connect pin 2 to a gpio. For 5-volt operation connect pin 1 to the 5 volts and pin 4 to ground. The following pin 2 connection works for me. Use at YOUR OWN RISK. 5V--5K_resistor--+--10K_resistor--Ground | DHT22 pin 2 -----+ | gpio ------------+ """ def __init__(self, input_dev, testing=False): """ Instantiate with the Pi and gpio to which the DHT22 output pin is connected. Optionally a gpio used to power the sensor may be specified. This gpio will be set high to power the sensor. If the sensor locks it will be power cycled to restart the readings. Taking readings more often than about once every two seconds will eventually cause the DHT22 to hang. A 3 second interval seems OK. """ super(InputModule, self).__init__(input_dev, testing=testing, name=__name__) 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.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_input() def get_measurement(self): """ Gets the humidity and temperature """ self.return_dict = measurements_dict.copy() if not self.pi.connected: # Check if pigpiod is running self.logger.error('Could not connect to pigpiod. ' 'Ensure it is running and try again.') return None, None, None # Ensure if the power pin turns off, it is turned back on if (self.power_output_id and db_retrieve_table_daemon(Output, unique_id=self.power_output_id) and self.control.output_state(self.power_output_id) == 'off'): self.logger.error( 'Sensor power output {rel} detected as being off. ' 'Turning on.'.format(rel=self.power_output_id)) self.start_input() time.sleep(2) # Try twice to get measurement. This prevents an anomaly where # the first measurement fails if the sensor has just been powered # for the first time. for _ in range(4): self.measure_sensor() if self.temp_dew_point is not None: if self.is_enabled(0): self.value_set(0, self.temp_temperature) if self.is_enabled(1): self.value_set(1, self.temp_humidity) if (self.is_enabled(2) and self.is_enabled(0) and self.is_enabled(1)): self.value_set(2, self.temp_dew_point) if (self.is_enabled(3) and self.is_enabled(0) and self.is_enabled(1)): self.value_set(3, self.temp_vpd) return self.return_dict # success - no errors time.sleep(2) # Measurement failure, power cycle the sensor (if enabled) # Then try two more times to get a measurement if self.power_output_id is not None and self.running: self.stop_input() time.sleep(3) self.start_input() for _ in range(2): self.measure_sensor() if self.temp_dew_point is not None: if self.is_enabled(0): self.value_set(0, self.temp_temperature) if self.is_enabled(1): self.value_set(1, self.temp_humidity) if (self.is_enabled(2) and self.is_enabled(0) and self.is_enabled(1)): self.value_set(2, self.temp_dew_point) if (self.is_enabled(3) and self.is_enabled(0) and self.is_enabled(1)): self.value_set(3, self.temp_vpd) return self.return_dict # success - no errors time.sleep(2) self.logger.debug("Could not acquire a measurement") return None def measure_sensor(self): self.temp_temperature = None self.temp_humidity = None self.temp_dew_point = None self.temp_vpd = None initialized = False try: self.close() time.sleep(0.2) self.setup() time.sleep(0.2) initialized = True except Exception as except_msg: self.logger.error( "Could not initialize sensor. Check if it's connected " "properly and pigpiod is running. Error: {msg}".format( msg=except_msg)) if initialized: try: self.pi.write(self.gpio, self.pigpio.LOW) time.sleep(0.017) # 17 ms self.pi.set_mode(self.gpio, self.pigpio.INPUT) self.pi.set_watchdog(self.gpio, 200) time.sleep(0.2) if (self.temp_humidity is not None and self.temp_temperature is not None): self.temp_dew_point = calculate_dewpoint( self.temp_temperature, self.temp_humidity) self.temp_vpd = calculate_vapor_pressure_deficit( self.temp_temperature, self.temp_humidity) except Exception as e: self.logger.exception( "Exception when taking a reading: {err}".format( err=e)) finally: self.close() def setup(self): """ Clears the internal gpio pull-up/down resistor. Kills any watchdogs. Setup callbacks """ self.no_response = 0 self.tov = None self.high_tick = 0 self.bit = 40 self.either_edge_cb = None self.pi.set_pull_up_down(self.gpio, self.pigpio.PUD_OFF) self.pi.set_watchdog(self.gpio, 0) # Kill any watchdogs self.register_callbacks() def register_callbacks(self): """ Monitors RISING_EDGE changes using callback """ self.either_edge_cb = self.pi.callback(self.gpio, self.pigpio.EITHER_EDGE, self.either_edge_callback) def either_edge_callback(self, gpio, level, tick): """ Either Edge callbacks, called each time the gpio edge changes. Accumulate the 40 data bits from the DHT22 sensor. Format into 5 bytes, humidity high, humidity low, temperature high, temperature low, checksum. """ level_handlers = { self.pigpio.FALLING_EDGE: self._edge_fall, self.pigpio.RISING_EDGE: self._edge_rise, self.pigpio.EITHER_EDGE: self._edge_either } handler = level_handlers[level] diff = self.pigpio.tickDiff(self.high_tick, tick) handler(tick, diff) def _edge_rise(self, tick, diff): """ Handle Rise signal """ # Edge length determines if bit is 1 or 0. if diff >= 50: val = 1 if diff >= 200: # Bad bit? self.CS = 256 # Force bad checksum. else: val = 0 if self.bit >= 40: # Message complete. self.bit = 40 elif self.bit >= 32: # In checksum byte. self.CS = (self.CS << 1) + val if self.bit == 39: # 40th bit received. self.pi.set_watchdog(self.gpio, 0) self.no_response = 0 total = self.hH + self.hL + self.tH + self.tL if (total & 255) == self.CS: # Is checksum ok? self.temp_humidity = ((self.hH << 8) + self.hL) * 0.1 if self.tH & 128: # Negative temperature. mult = -0.1 self.tH &= 127 else: mult = 0.1 self.temp_temperature = ((self.tH << 8) + self.tL) * mult self.tov = time.time() else: self.bad_CS += 1 elif self.bit >= 24: # in temp low byte self.tL = (self.tL << 1) + val elif self.bit >= 16: # in temp high byte self.tH = (self.tH << 1) + val elif self.bit >= 8: # in humidity low byte self.hL = (self.hL << 1) + val elif self.bit >= 0: # in humidity high byte self.hH = (self.hH << 1) + val self.bit += 1 def _edge_fall(self, tick, diff): """ Handle Fall signal """ # Edge length determines if bit is 1 or 0. self.high_tick = tick if diff <= 250000: return self.bit = -2 self.hH = 0 self.hL = 0 self.tH = 0 self.tL = 0 self.CS = 0 def _edge_either(self, tick, diff): """ Handle Either signal or Timeout """ self.pi.set_watchdog(self.gpio, 0) if self.bit < 8: # Too few data bits received. self.bad_MM += 1 # Bump missing message count. self.no_response += 1 if self.no_response > self.MAX_NO_RESPONSE: self.no_response = 0 self.bad_SR += 1 # Bump sensor reset count. if self.power_output_id is not None: self.logger.error( "Invalid data, power cycling sensor.") self.stop_input() time.sleep(2) self.start_input() elif self.bit < 39: # Short message received. self.bad_SM += 1 # Bump short message count. self.no_response = 0 else: # Full message received. self.no_response = 0 def staleness(self): """ Return time since measurement made """ if self.tov is not None: return time.time() - self.tov else: return -999 def bad_checksum(self): """ Return count of messages received with bad checksums """ return self.bad_CS def short_message(self): """ Return count of short messages """ return self.bad_SM def missing_message(self): """ Return count of missing messages """ return self.bad_MM def sensor_resets(self): """ Return count of power cycles because of sensor hangs """ return self.bad_SR def close(self): """ Stop reading sensor, remove callbacks """ self.pi.set_watchdog(self.gpio, 0) if self.either_edge_cb: self.either_edge_cb.cancel() self.either_edge_cb = None def start_input(self): """ Turn the sensor on """ if self.power_output_id: self.logger.info("Turning on sensor") self.control.output_on(self.power_output_id, 0) time.sleep(2) self.powered = True def stop_input(self): """ Turn the sensor off """ if self.power_output_id: self.logger.info("Turning off sensor") self.control.output_off(self.power_output_id) self.powered = False
class CustomModule(AbstractController, threading.Thread): """ Class to operate custom controller """ def __init__(self, ready, unique_id, testing=False): threading.Thread.__init__(self) super(CustomModule, self).__init__(ready, unique_id=unique_id, name=__name__) self.unique_id = unique_id self.log_level_debug = None self.control_variable = None self.timestamp = None self.timer = None self.control = DaemonControl() self.outputIsOn = False # Initialize custom options self.measurement_device_id = None self.measurement_measurement_id = None self.output_device_id = None self.output_measurement_id = None self.output_channel_id = None self.setpoint = None self.hysteresis = None self.direction = None self.output_channel = None self.update_period = None # Set custom options custom_function = db_retrieve_table_daemon(CustomController, unique_id=unique_id) self.setup_custom_options(FUNCTION_INFORMATION['custom_options'], custom_function) self.output_channel = self.get_output_channel_from_channel_id( self.output_channel_id) self.initialize_variables() def initialize_variables(self): controller = db_retrieve_table_daemon(CustomController, unique_id=self.unique_id) self.log_level_debug = controller.log_level_debug self.set_log_level_debug(self.log_level_debug) self.timestamp = time.time() def run(self): try: if self.output_channel is None: self.logger.error( "Cannot start bang-bang controller: Could not find output channel." ) self.deactivate_self() return self.logger.info("Activated in {:.1f} ms".format( (timeit.default_timer() - self.thread_startup_timer) * 1000)) self.ready.set() self.running = True self.timer = time.time() self.logger.info( "Bang-Bang controller started with options: " "Measurement Device: {}, Measurement: {}, Output: {}, " "Output_Channel: {}, Setpoint: {}, Hysteresis: {}, " "Direction: {}, Period: {}".format( self.measurement_device_id, self.measurement_measurement_id, self.output_device_id, self.output_channel, self.setpoint, self.hysteresis, self.direction, self.update_period)) # Start a loop while self.running: self.loop() time.sleep(self.update_period) except: self.logger.exception("Run Error") finally: self.run_finally() self.running = False if self.thread_shutdown_timer: self.logger.info("Deactivated in {:.1f} ms".format( (timeit.default_timer() - self.thread_shutdown_timer) * 1000)) else: self.logger.error("Deactivated unexpectedly") def loop(self): last_measurement = self.get_last_measurement( self.measurement_device_id, self.measurement_measurement_id)[1] outputState = self.control.output_state(self.output_device_id, self.output_channel) self.logger.info("Input: {}, output: {}, target: {}, hyst: {}".format( last_measurement, outputState, self.setpoint, self.hysteresis)) if self.direction == 'raise': if outputState == 'on': # looking to turn output off if last_measurement > (self.setpoint + self.hysteresis): self.control.output_off( self.output_device_id, output_channel=self.output_channel, ) else: # looking to turn output on if last_measurement < (self.setpoint - self.hysteresis): self.control.output_on(self.output_device_id, output_channel=self.output_channel) elif self.direction == 'lower': if outputState == 'on': # looking to turn output off if last_measurement < (self.setpoint - self.hysteresis): self.control.output_off( self.output_device_id, output_channel=self.output_channel, ) else: # looking to turn output on if last_measurement > (self.setpoint + self.hysteresis): self.control.output_on(self.output_device_id, output_channel=self.output_channel) else: self.logger.info("Unknown controller direction: {}".format( self.direction)) def deactivate_self(self): self.logger.info("Deactivating bang-bang controller") with session_scope(MYCODO_DB_PATH) as new_session: mod_cont = new_session.query(CustomController).filter( CustomController.unique_id == self.unique_id).first() mod_cont.is_activated = False new_session.commit() deactivate_controller = threading.Thread( target=self.control.controller_deactivate, args=(self.unique_id, )) deactivate_controller.start() def pre_stop(self): self.control.output_off(self.output_device_id, self.output_channel)
class InputModule(AbstractInput): """ A sensor support class that measures the AM2315's humidity and temperature and calculates the dew point """ def __init__(self, input_dev, testing=False): super(InputModule, self).__init__() self.setup_logger(testing=testing, name=__name__, input_dev=input_dev) self.powered = False self.am = None if not testing: from mycodo.mycodo_client import DaemonControl self.device_measurements = db_retrieve_table_daemon( DeviceMeasurements).filter( DeviceMeasurements.device_id == input_dev.unique_id) self.i2c_bus = input_dev.i2c_bus self.power_output_id = input_dev.power_output_id self.control = DaemonControl() self.start_sensor() self.am = AM2315(self.i2c_bus) def get_measurement(self): """ Gets the humidity and temperature """ self.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): self.set_value(0, temperature) if self.is_enabled(1): self.set_value(1, humidity) if (self.is_enabled(2) and self.is_enabled(0) and self.is_enabled(1)): self.set_value( 2, calculate_dewpoint(self.get_value(0), self.get_value(1))) if (self.is_enabled(3) and self.is_enabled(0) and self.is_enabled(1)): self.set_value( 3, calculate_vapor_pressure_deficit(self.get_value(0), self.get_value(1))) return self.return_dict else: self.logger.debug("Could not acquire a measurement") def return_measurements(self): # Retry measurement if CRC fails for num_measure in range(3): humidity, temperature = self.am.data() if humidity is None: self.logger.debug( "Measurement {num} returned failed CRC".format( num=num_measure)) pass else: dew_pt = calculate_dewpoint(temperature, humidity) return dew_pt, humidity, temperature time.sleep(2) self.logger.error("All measurements returned failed CRC") return None, None, None def start_sensor(self): """ Turn the sensor on """ if self.power_output_id: self.logger.info("Turning on sensor") self.control.output_on(self.power_output_id, 0) time.sleep(2) self.powered = True def stop_sensor(self): """ Turn the sensor off """ if self.power_output_id: self.logger.info("Turning off sensor") self.control.output_off(self.power_output_id) self.powered = False
class InputModule(AbstractInput): """ A sensor support class that measures the DHT11's humidity and temperature and calculates the dew point The DHT11 class is a stripped version of the DHT22 sensor code by joan2937. You can find the initial implementation here: - https://github.com/srounet/pigpio/tree/master/EXAMPLES/Python/DHT22_AM2302_SENSOR """ def __init__(self, input_dev, testing=False): """ :param gpio: gpio pin number :type gpio: int :param power: Power pin number :type power: int Instantiate with the Pi and gpio to which the DHT11 output pin is connected. Optionally a gpio used to power the sensor may be specified. This gpio will be set high to power the sensor. """ super(InputModule, self).__init__(input_dev, testing=testing, name=__name__) self.pi = None self.pigpio = None self.control = None self.temp_temperature = 0 self.temp_humidity = 0 self.temp_dew_point = None self.temp_vpd = None self.power_output_id = None self.powered = False if not testing: self.initialize_input() def initialize_input(self): import pigpio from mycodo.mycodo_client import DaemonControl self.gpio = int(self.input_dev.gpio_location) self.power_output_id = self.input_dev.power_output_id self.control = DaemonControl() self.pigpio = pigpio self.pi = self.pigpio.pi() self.high_tick = None self.bit = None self.either_edge_cb = None self.start_input() def get_measurement(self): """ Gets the humidity and temperature """ if not self.pi.connected: # Check if pigpiod is running self.logger.error("Could not connect to pigpiod. Ensure it is running and try again.") return None self.return_dict = copy.deepcopy(measurements_dict) import pigpio self.pigpio = pigpio # Ensure if the power pin turns off, it is turned back on if (self.power_output_id and db_retrieve_table_daemon(Output, unique_id=self.power_output_id) and self.control.output_state(self.power_output_id) == 'off'): self.logger.error( 'Sensor power output {rel} detected as being off. Turning on.'.format(rel=self.power_output_id)) self.start_input() time.sleep(2) # Try twice to get measurement. This prevents an anomaly where # the first measurement fails if the sensor has just been powered # for the first time. for _ in range(2): self.measure_sensor() if self.temp_dew_point is not None: self.value_set(0, self.temp_temperature) self.value_set(1, self.temp_humidity) self.value_set(2, self.temp_dew_point) self.value_set(3, self.temp_vpd) return self.return_dict # success - no errors time.sleep(2) # Measurement failure, power cycle the sensor (if enabled) # Then try two more times to get a measurement if self.power_output_id is not None and self.running: self.stop_input() time.sleep(2) self.start_input() for _ in range(2): self.measure_sensor() if self.temp_dew_point is not None: self.value_set(0, self.temp_temperature) self.value_set(1, self.temp_humidity) self.value_set(2, self.temp_dew_point) self.value_set(3, self.temp_vpd) return self.return_dict # success - no errors time.sleep(2) self.logger.error("Could not acquire a measurement") return None def measure_sensor(self): self.temp_temperature = 0 self.temp_humidity = 0 self.temp_dew_point = None self.temp_vpd = None try: try: self.setup() except Exception as except_msg: self.logger.error( 'Could not initialize sensor. Check if gpiod is running. Error: {msg}'.format(msg=except_msg)) self.pi.write(self.gpio, self.pigpio.LOW) time.sleep(0.017) # 17 ms self.pi.set_mode(self.gpio, self.pigpio.INPUT) self.pi.set_watchdog(self.gpio, 200) time.sleep(0.2) if self.temp_humidity != 0: self.temp_dew_point = calculate_dewpoint(self.temp_temperature, self.temp_humidity) self.temp_vpd = calculate_vapor_pressure_deficit(self.temp_temperature, self.temp_humidity) except Exception as e: self.logger.error("Exception raised when taking a reading: {err}".format(err=e)) finally: self.close() return (self.temp_dew_point, self.temp_humidity, self.temp_temperature) def setup(self): """ Clears the internal gpio pull-up/down resistor. Kills any watchdogs. Setup callbacks """ self.high_tick = 0 self.bit = 40 self.either_edge_cb = None self.pi.set_pull_up_down(self.gpio, self.pigpio.PUD_OFF) self.pi.set_watchdog(self.gpio, 0) self.register_callbacks() def register_callbacks(self): """ Monitors RISING_EDGE changes using callback """ self.either_edge_cb = self.pi.callback( self.gpio, self.pigpio.EITHER_EDGE, self.either_edge_callback) def either_edge_callback(self, gpio, level, tick): """ Either Edge callbacks, called each time the gpio edge changes. Accumulate the 40 data bits from the DHT11 sensor. """ level_handlers = { self.pigpio.FALLING_EDGE: self._edge_fall, self.pigpio.RISING_EDGE: self._edge_rise, self.pigpio.EITHER_EDGE: self._edge_either } handler = level_handlers[level] diff = self.pigpio.tickDiff(self.high_tick, tick) handler(tick, diff) def _edge_rise(self, tick, diff): """ Handle Rise signal """ val = 0 if diff >= 50: val = 1 if diff >= 200: # Bad bit? self.checksum = 256 # Force bad checksum if self.bit >= 40: # Message complete self.bit = 40 elif self.bit >= 32: # In checksum byte self.checksum = (self.checksum << 1) + val if self.bit == 39: # 40th bit received self.pi.set_watchdog(self.gpio, 0) total = self.temp_humidity + self.temp_temperature # is checksum ok ? if not (total & 255) == self.checksum: # For some reason the port from python 2 to python 3 causes # this bad checksum error to happen during every read # TODO: Investigate how to properly check the checksum in python 3 self.logger.debug("Exception raised when taking a reading: Bad Checksum.") elif 16 <= self.bit < 24: # in temperature byte self.temp_temperature = (self.temp_temperature << 1) + val elif 0 <= self.bit < 8: # in humidity byte self.temp_humidity = (self.temp_humidity << 1) + val self.bit += 1 def _edge_fall(self, tick, diff): """ Handle Fall signal """ self.high_tick = tick if diff <= 250000: return self.bit = -2 self.checksum = 0 self.temp_temperature = 0 self.temp_humidity = 0 def _edge_either(self, tick, diff): """ Handle Either signal """ self.pi.set_watchdog(self.gpio, 0) def close(self): """ Stop reading sensor, remove callbacks """ self.pi.set_watchdog(self.gpio, 0) if self.either_edge_cb: self.either_edge_cb.cancel() self.either_edge_cb = None def start_input(self): """ Power the sensor """ if self.power_output_id: self.logger.info("Turning on sensor") self.control.output_on( self.power_output_id, 0) time.sleep(2) self.powered = True def stop_input(self): """ Depower the sensor """ if self.power_output_id: self.logger.info("Turning off sensor") self.control.output_off(self.power_output_id) self.powered = False
def get_condition_value(condition_id): """ Returns condition measurements for Conditional controllers :param condition_id: Conditional condition ID :return: measurement: multiple types """ sql_condition = db_retrieve_table_daemon(ConditionalConditions).filter( ConditionalConditions.unique_id == condition_id).first() # Check Measurement Conditions if sql_condition.condition_type in ['measurement', 'measurement_past_average', 'measurement_past_sum']: 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 if sql_condition.condition_type == 'measurement': return_measurement = get_last_measurement( device_id, unit, measurement, channel, max_age) elif sql_condition.condition_type == 'measurement_past_average': measurement_list = [] measurements_str = get_past_measurements( device_id, unit, measurement, channel, max_age) for each_set in measurements_str.split(';'): measurement_list.append(float(each_set.split(',')[1])) return_measurement = sum(measurement_list) / len(measurement_list) elif sql_condition.condition_type == 'measurement_past_sum': measurement_list = [] measurements_str = get_past_measurements( device_id, unit, measurement, channel, max_age) for each_set in measurements_str.split(';'): measurement_list.append(float(each_set.split(',')[1])) return_measurement = sum(measurement_list) else: return return return_measurement # Return GPIO state elif sql_condition.condition_type == 'gpio_state': try: import RPi.GPIO as GPIO GPIO.setmode(GPIO.BCM) GPIO.setup(int(sql_condition.gpio_pin), GPIO.IN) gpio_state = GPIO.input(int(sql_condition.gpio_pin)) except Exception as e: gpio_state = None logger.error("Exception reading the GPIO pin: {}".format(e)) return gpio_state # Return output state elif sql_condition.condition_type == 'output_state': control = DaemonControl() return control.output_state(sql_condition.output_id) # Return the duration the output is currently on for elif sql_condition.condition_type == 'output_duration_on': control = DaemonControl() return control.output_sec_currently_on(sql_condition.output_id) # Return controller active state elif sql_condition.condition_type == 'controller_status': controller_type, _, _ = which_controller(sql_condition.controller_id) control = DaemonControl() return control.controller_is_active(sql_condition.controller_id)
class AM2315Sensor(AbstractInput): """ A sensor support class that measures the AM2315's humidity and temperature and calculates the dew point """ def __init__(self, input_dev, testing=False): super(AM2315Sensor, self).__init__() self.logger = logging.getLogger('mycodo.inputs.am2315') self._dew_point = None self._humidity = None self._temperature = None self.powered = False self.am = None if not testing: from mycodo.mycodo_client import DaemonControl self.logger = logging.getLogger( 'mycodo.inputs.am2315_{id}'.format(id=input_dev.id)) self.i2c_bus = input_dev.i2c_bus self.power_output_id = input_dev.power_output_id self.convert_to_unit = input_dev.convert_to_unit self.control = DaemonControl() self.start_sensor() self.am = AM2315(self.i2c_bus) def __repr__(self): """ Representation of object """ return "<{cls}(dewpoint={dpt})(humidity={hum})(temperature={temp})>".format( cls=type(self).__name__, dpt="{0:.2f}".format(self._dew_point), hum="{0:.2f}".format(self._humidity), temp="{0:.2f}".format(self._temperature)) def __str__(self): """ Return measurement information """ return "Dew Point: {dpt}, Humidity: {hum}, Temperature: {temp}".format( dpt="{0:.2f}".format(self._dew_point), hum="{0:.2f}".format(self._humidity), temp="{0:.2f}".format(self._temperature)) def __iter__(self): # must return an iterator """ AM2315Sensor iterates through live measurement readings """ return self def next(self): """ Get next measurement reading """ if self.read(): # raised an error raise StopIteration # required return dict(dewpoint=float('{0:.2f}'.format(self._dew_point)), humidity=float('{0:.2f}'.format(self._humidity)), temperature=float('{0:.2f}'.format(self._temperature))) @property def dew_point(self): """ AM2315 dew point in Celsius """ if self._dew_point is None: # update if needed self.read() return self._dew_point @property def humidity(self): """ AM2315 relative humidity in percent """ if self._humidity is None: # update if needed self.read() return self._humidity @property def temperature(self): """ AM2315 temperature in Celsius """ if self._temperature is None: # update if needed self.read() return self._temperature def get_measurement(self): """ Gets the humidity and temperature """ self._dew_point = None self._humidity = None self._temperature = None # Ensure if the power pin turns off, it is turned back on if (self.power_output_id and db_retrieve_table_daemon( Output, unique_id=self.power_output_id) and self.control.output_state(self.power_output_id) == 'off'): self.logger.error( 'Sensor power output {rel} detected as being off. ' 'Turning on.'.format(rel=self.power_output_id)) self.start_sensor() time.sleep(2) # Try twice to get measurement. This prevents an anomaly where # the first measurement fails if the sensor has just been powered # for the first time. for _ in range(2): dew_point, humidity, temperature = self.return_measurements() if dew_point is not None: dew_point = convert_units('dewpoint', 'celsius', self.convert_to_unit, dew_point) temperature = convert_units('temperature', 'celsius', self.convert_to_unit, temperature) return dew_point, humidity, temperature # success - no errors time.sleep(2) # Measurement failure, power cycle the sensor (if enabled) # Then try two more times to get a measurement if self.power_output_id: self.stop_sensor() time.sleep(2) self.start_sensor() for _ in range(2): dew_point, humidity, temperature = self.return_measurements() if dew_point is not None: dew_point = convert_units('dewpoint', 'celsius', self.convert_to_unit, dew_point) temperature = convert_units('temperature', 'celsius', self.convert_to_unit, temperature) return dew_point, humidity, temperature # success time.sleep(2) self.logger.debug("Could not acquire a measurement") return None, None, None def return_measurements(self): # Retry measurement if CRC fails for num_measure in range(3): humidity, temperature = self.am.data() if humidity is None: self.logger.debug( "Measurement {num} returned failed CRC".format( num=num_measure)) pass else: dew_pt = dewpoint(temperature, humidity) return dew_pt, humidity, temperature time.sleep(2) self.logger.error("All measurements returned failed CRC") return None, None, None def read(self): """ Takes a reading from the AM2315 and updates the self.dew_point, self._humidity, and self._temperature values :returns: None on success or 1 on error """ try: (self._dew_point, self._humidity, self._temperature) = self.get_measurement() if self._dew_point is not None: return # success - no errors except Exception as e: self.logger.exception( "{cls} raised an exception when taking a reading: " "{err}".format(cls=type(self).__name__, err=e)) return 1 def start_sensor(self): """ Turn the sensor on """ if self.power_output_id: self.logger.info("Turning on sensor") self.control.output_on(self.power_output_id, 0) time.sleep(2) self.powered = True def stop_sensor(self): """ Turn the sensor off """ if self.power_output_id: self.logger.info("Turning off sensor") self.control.output_off(self.power_output_id) self.powered = False
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 output_channel = None if settings.output_id and ',' in settings.output_id: output_id = settings.output_id.split(",")[0] output_channel_id = settings.output_id.split(",")[1] output_channel = db_retrieve_table_daemon(OutputChannel, unique_id=output_channel_id) if output_id and output_channel: daemon_control = DaemonControl() if daemon_control.output_state( output_id, output_channel=output_channel.channel) == "on": output_already_on = True else: daemon_control.output_on(output_id, output_channel=output_channel.channel) # Pause while the output remains on for the specified duration. # Used for instance to allow fluorescent lights to fully turn on before # capturing an image. if settings.output_duration: time.sleep(settings.output_duration) if settings.library == 'picamera': 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 and daemon_control and not output_already_on: daemon_control.output_off(output_id, output_channel=output_channel.channel) try: set_user_grp(path_file, 'mycodo', 'mycodo') return save_path, filename except Exception as e: logger.exception( "Exception raised in 'camera_record' when setting user grp: " "{err}".format(err=e))
class InputModule(AbstractInput): """ A sensor support class that measures the AM2315's humidity and temperature and calculates the dew point """ def __init__(self, input_dev, testing=False): super(InputModule, self).__init__() self.logger = logging.getLogger('mycodo.inputs.am2315') self.powered = False self.am = None if not testing: from mycodo.mycodo_client import DaemonControl self.logger = logging.getLogger( 'mycodo.am2315_{id}'.format(id=input_dev.unique_id.split('-')[0])) self.device_measurements = db_retrieve_table_daemon( DeviceMeasurements).filter( DeviceMeasurements.device_id == input_dev.unique_id) self.i2c_bus = input_dev.i2c_bus self.power_output_id = input_dev.power_output_id self.control = DaemonControl() self.start_sensor() self.am = AM2315(self.i2c_bus) def get_measurement(self): """ Gets the humidity and temperature """ return_dict = measurements_dict.copy() temperature = None humidity = None dew_point = None measurements_success = False # Ensure if the power pin turns off, it is turned back on if (self.power_output_id and db_retrieve_table_daemon(Output, unique_id=self.power_output_id) and self.control.output_state(self.power_output_id) == 'off'): self.logger.error( 'Sensor power output {rel} detected as being off. ' 'Turning on.'.format(rel=self.power_output_id)) self.start_sensor() time.sleep(2) # Try twice to get measurement. This prevents an anomaly where # the first measurement fails if the sensor has just been powered # for the first time. for _ in range(2): dew_point, humidity, temperature = self.return_measurements() if dew_point is not None: measurements_success = True break time.sleep(2) # Measurement failure, power cycle the sensor (if enabled) # Then try two more times to get a measurement if self.power_output_id and not measurements_success: self.stop_sensor() time.sleep(2) self.start_sensor() for _ in range(2): dew_point, humidity, temperature = self.return_measurements() if dew_point is not None: measurements_success = True break time.sleep(2) if measurements_success: if self.is_enabled(0): return_dict[0]['value'] = temperature if self.is_enabled(1): return_dict[1]['value'] = humidity if (self.is_enabled(2) and self.is_enabled(0) and self.is_enabled(1)): return_dict[2]['value'] = calculate_dewpoint( return_dict[0]['value'], return_dict[1]['value']) if (self.is_enabled(3) and self.is_enabled(0) and self.is_enabled(1)): return_dict[3]['value'] = calculate_vapor_pressure_deficit( return_dict[0]['value'], return_dict[1]['value']) return return_dict else: self.logger.debug("Could not acquire a measurement") def return_measurements(self): # Retry measurement if CRC fails for num_measure in range(3): humidity, temperature = self.am.data() if humidity is None: self.logger.debug( "Measurement {num} returned failed CRC".format( num=num_measure)) pass else: dew_pt = calculate_dewpoint(temperature, humidity) return dew_pt, humidity, temperature time.sleep(2) self.logger.error("All measurements returned failed CRC") return None, None, None def start_sensor(self): """ Turn the sensor on """ if self.power_output_id: self.logger.info("Turning on sensor") self.control.output_on(self.power_output_id, 0) time.sleep(2) self.powered = True def stop_sensor(self): """ Turn the sensor off """ if self.power_output_id: self.logger.info("Turning off sensor") self.control.output_off(self.power_output_id) self.powered = False
class CustomModule(AbstractFunction): """ Class to operate custom controller """ def __init__(self, function, testing=False): super(CustomModule, self).__init__(function, testing=testing, name=__name__) self.control_variable = None self.timestamp = None self.control = DaemonControl() self.timer_loop = time.time() # Initialize custom options self.measurement_device_id = None self.measurement_measurement_id = None self.output_raise_device_id = None self.output_raise_measurement_id = None self.output_raise_channel_id = None self.output_lower_device_id = None self.output_lower_measurement_id = None self.output_lower_channel_id = None self.setpoint = None self.hysteresis = None self.direction = None self.output_raise_channel = None self.output_lower_channel = None self.update_period = None # Set custom options custom_function = db_retrieve_table_daemon(CustomController, unique_id=self.unique_id) self.setup_custom_options(FUNCTION_INFORMATION['custom_options'], custom_function) if not testing: self.initialize_variables() def initialize_variables(self): self.timestamp = time.time() self.output_raise_channel = self.get_output_channel_from_channel_id( self.output_raise_channel_id) self.output_lower_channel = self.get_output_channel_from_channel_id( self.output_lower_channel_id) self.logger.info( "Bang-Bang controller started with options: " "Measurement Device: {}, Measurement: {}," "Output Raise: {}, Output_Raise_Channel: {}," "Output Lower: {}, Output_Lower_Channel: {}," "Setpoint: {}, Hysteresis: {}, " "Direction: {}, Period: {}".format( self.measurement_device_id, self.measurement_measurement_id, self.output_raise_device_id, self.output_raise_channel, self.output_lower_device_id, self.output_lower_channel, self.setpoint, self.hysteresis, self.direction, self.update_period)) def loop(self): if self.timer_loop > time.time(): return while self.timer_loop < time.time(): self.timer_loop += self.update_period if ((self.direction == 'raise' and self.output_raise_channel is None) or (self.direction == 'lower' and self.output_lower_channel is None) or self.direction == 'both' and None in [self.output_raise_channel, self.output_lower_channel]): self.logger.error( "Cannot start bang-bang controller: Check output channel(s).") return last_measurement = self.get_last_measurement( self.measurement_device_id, self.measurement_measurement_id)[1] output_raise_state = self.control.output_state( self.output_raise_device_id, self.output_raise_channel) output_lower_state = self.control.output_state( self.output_lower_device_id, self.output_raise_channel) self.logger.info( "Input: {}, output_raise: {}, output_lower: {}, target: {}, hyst: {}" .format(last_measurement, output_raise_state, output_lower_state, self.setpoint, self.hysteresis)) if self.direction == 'raise': if last_measurement > (self.setpoint + self.hysteresis): if output_raise_state == 'on': self.control.output_off( self.output_raise_device_id, output_channel=self.output_raise_channel) elif last_measurement < (self.setpoint - self.hysteresis): self.control.output_on( self.output_raise_device_id, output_channel=self.output_raise_channel) elif self.direction == 'lower': if last_measurement < (self.setpoint - self.hysteresis): if output_lower_state == 'on': self.control.output_off( self.output_lower_device_id, output_channel=self.output_lower_channel) elif last_measurement > (self.setpoint + self.hysteresis): self.control.output_on( self.output_lower_device_id, output_channel=self.output_lower_channel) elif self.direction == 'both': if (last_measurement > (self.setpoint - self.hysteresis) or last_measurement < (self.setpoint + self.hysteresis)): if output_raise_state == 'on': self.control.output_off( self.output_raise_device_id, output_channel=self.output_raise_channel) if output_lower_state == 'on': self.control.output_off( self.output_lower_device_id, output_channel=self.output_lower_channel) elif last_measurement > (self.setpoint + self.hysteresis): self.control.output_on( self.output_lower_device_id, output_channel=self.output_lower_channel) elif last_measurement < (self.setpoint - self.hysteresis): self.control.output_on( self.output_raise_device_id, output_channel=self.output_raise_channel) else: self.logger.info("Unknown controller direction: '{}'".format( self.direction)) def stop_function(self): self.control.output_off(self.output_raise_device_id, self.output_raise_channel) self.control.output_off(self.output_lower_device_id, self.output_lower_channel)
class InputModule(AbstractInput): """ A sensor support class that measures the DHT22's humidity and temperature and calculates the dew point An adaptation of DHT22 code from https://github.com/joan2937/pigpio The sensor is also known as the AM2302. The sensor can be powered from the Pi 3.3-volt or 5-volt rail. Powering from the 3.3-volt rail is simpler and safer. You may need to power from 5 if the sensor is connected via a long cable. For 3.3-volt operation connect pin 1 to 3.3 volts and pin 4 to ground. Connect pin 2 to a gpio. For 5-volt operation connect pin 1 to the 5 volts and pin 4 to ground. The following pin 2 connection works for me. Use at YOUR OWN RISK. 5V--5K_resistor--+--10K_resistor--Ground | DHT22 pin 2 -----+ | gpio ------------+ """ def __init__(self, input_dev, testing=False): """ Instantiate with the Pi and gpio to which the DHT22 output pin is connected. Optionally a gpio used to power the sensor may be specified. This gpio will be set high to power the sensor. If the sensor locks it will be power cycled to restart the readings. Taking readings more often than about once every two seconds will eventually cause the DHT22 to hang. A 3 second interval seems OK. """ super(InputModule, self).__init__() self.logger = logging.getLogger('mycodo.inputs.dht22') self.temp_temperature = None self.temp_humidity = None self.temp_dew_point = None self.temp_vpd = None self.power_output_id = None self.powered = False self.pi = None if not testing: import pigpio from mycodo.mycodo_client import DaemonControl self.logger = logging.getLogger( 'mycodo.dht22_{id}'.format(id=input_dev.unique_id.split('-')[0])) self.device_measurements = db_retrieve_table_daemon( DeviceMeasurements).filter( DeviceMeasurements.device_id == input_dev.unique_id) self.power_output_id = input_dev.power_output_id self.control = DaemonControl() self.pigpio = pigpio self.pi = self.pigpio.pi() self.gpio = int(input_dev.gpio_location) self.bad_CS = 0 # Bad checksum count self.bad_SM = 0 # Short message count self.bad_MM = 0 # Missing message count self.bad_SR = 0 # Sensor reset count # Power cycle if timeout > MAX_NO_RESPONSE self.MAX_NO_RESPONSE = 3 self.no_response = None self.tov = None self.high_tick = None self.bit = None self.either_edge_cb = None self.start_sensor() def get_measurement(self): """ Gets the humidity and temperature """ return_dict = measurements_dict.copy() if not self.pi.connected: # Check if pigpiod is running self.logger.error('Could not connect to pigpiod. ' 'Ensure it is running and try again.') return None, None, None # Ensure if the power pin turns off, it is turned back on if (self.power_output_id and db_retrieve_table_daemon(Output, unique_id=self.power_output_id) and self.control.output_state(self.power_output_id) == 'off'): self.logger.error( 'Sensor power output {rel} detected as being off. ' 'Turning on.'.format(rel=self.power_output_id)) self.start_sensor() time.sleep(2) # Try twice to get measurement. This prevents an anomaly where # the first measurement fails if the sensor has just been powered # for the first time. for _ in range(4): self.measure_sensor() if self.temp_dew_point is not None: if self.is_enabled(0): return_dict[0]['value'] = self.temp_temperature if self.is_enabled(1): return_dict[1]['value'] = self.temp_humidity if (self.is_enabled(2) and self.is_enabled(0) and self.is_enabled(1)): return_dict[2]['value'] = self.temp_dew_point if (self.is_enabled(3) and self.is_enabled(0) and self.is_enabled(1)): return_dict[3]['value'] = self.temp_vpd return return_dict # success - no errors time.sleep(2) # Measurement failure, power cycle the sensor (if enabled) # Then try two more times to get a measurement if self.power_output_id is not None and self.running: self.stop_sensor() time.sleep(3) self.start_sensor() for _ in range(2): self.measure_sensor() if self.temp_dew_point is not None: if self.is_enabled(0): return_dict[0]['value'] = self.temp_temperature if self.is_enabled(1): return_dict[1]['value'] = self.temp_humidity if (self.is_enabled(2) and self.is_enabled(0) and self.is_enabled(1)): return_dict[2]['value'] = self.temp_dew_point if (self.is_enabled(3) and self.is_enabled(0) and self.is_enabled(1)): return_dict[3]['value'] = self.temp_vpd return return_dict # success - no errors time.sleep(2) self.logger.debug("Could not acquire a measurement") return None def measure_sensor(self): self.temp_temperature = None self.temp_humidity = None self.temp_dew_point = None self.temp_vpd = None initialized = False try: self.close() time.sleep(0.2) self.setup() time.sleep(0.2) initialized = True except Exception as except_msg: self.logger.error( "Could not initialize sensor. Check if it's connected " "properly and pigpiod is running. Error: {msg}".format( msg=except_msg)) if initialized: try: self.pi.write(self.gpio, self.pigpio.LOW) time.sleep(0.017) # 17 ms self.pi.set_mode(self.gpio, self.pigpio.INPUT) self.pi.set_watchdog(self.gpio, 200) time.sleep(0.2) if (self.temp_humidity is not None and self.temp_temperature is not None): self.temp_dew_point = calculate_dewpoint( self.temp_temperature, self.temp_humidity) self.temp_vpd = calculate_vapor_pressure_deficit( self.temp_temperature, self.temp_humidity) except Exception as e: self.logger.exception( "Exception when taking a reading: {err}".format( err=e)) finally: self.close() def setup(self): """ Clears the internal gpio pull-up/down resistor. Kills any watchdogs. Setup callbacks """ self.no_response = 0 self.tov = None self.high_tick = 0 self.bit = 40 self.either_edge_cb = None self.pi.set_pull_up_down(self.gpio, self.pigpio.PUD_OFF) self.pi.set_watchdog(self.gpio, 0) # Kill any watchdogs self.register_callbacks() def register_callbacks(self): """ Monitors RISING_EDGE changes using callback """ self.either_edge_cb = self.pi.callback(self.gpio, self.pigpio.EITHER_EDGE, self.either_edge_callback) def either_edge_callback(self, gpio, level, tick): """ Either Edge callbacks, called each time the gpio edge changes. Accumulate the 40 data bits from the DHT22 sensor. Format into 5 bytes, humidity high, humidity low, temperature high, temperature low, checksum. """ level_handlers = { self.pigpio.FALLING_EDGE: self._edge_fall, self.pigpio.RISING_EDGE: self._edge_rise, self.pigpio.EITHER_EDGE: self._edge_either } handler = level_handlers[level] diff = self.pigpio.tickDiff(self.high_tick, tick) handler(tick, diff) def _edge_rise(self, tick, diff): """ Handle Rise signal """ # Edge length determines if bit is 1 or 0. if diff >= 50: val = 1 if diff >= 200: # Bad bit? self.CS = 256 # Force bad checksum. else: val = 0 if self.bit >= 40: # Message complete. self.bit = 40 elif self.bit >= 32: # In checksum byte. self.CS = (self.CS << 1) + val if self.bit == 39: # 40th bit received. self.pi.set_watchdog(self.gpio, 0) self.no_response = 0 total = self.hH + self.hL + self.tH + self.tL if (total & 255) == self.CS: # Is checksum ok? self.temp_humidity = ((self.hH << 8) + self.hL) * 0.1 if self.tH & 128: # Negative temperature. mult = -0.1 self.tH &= 127 else: mult = 0.1 self.temp_temperature = ((self.tH << 8) + self.tL) * mult self.tov = time.time() else: self.bad_CS += 1 elif self.bit >= 24: # in temp low byte self.tL = (self.tL << 1) + val elif self.bit >= 16: # in temp high byte self.tH = (self.tH << 1) + val elif self.bit >= 8: # in humidity low byte self.hL = (self.hL << 1) + val elif self.bit >= 0: # in humidity high byte self.hH = (self.hH << 1) + val self.bit += 1 def _edge_fall(self, tick, diff): """ Handle Fall signal """ # Edge length determines if bit is 1 or 0. self.high_tick = tick if diff <= 250000: return self.bit = -2 self.hH = 0 self.hL = 0 self.tH = 0 self.tL = 0 self.CS = 0 def _edge_either(self, tick, diff): """ Handle Either signal or Timeout """ self.pi.set_watchdog(self.gpio, 0) if self.bit < 8: # Too few data bits received. self.bad_MM += 1 # Bump missing message count. self.no_response += 1 if self.no_response > self.MAX_NO_RESPONSE: self.no_response = 0 self.bad_SR += 1 # Bump sensor reset count. if self.power_output_id is not None: self.logger.error( "Invalid data, power cycling sensor.") self.stop_sensor() time.sleep(2) self.start_sensor() elif self.bit < 39: # Short message received. self.bad_SM += 1 # Bump short message count. self.no_response = 0 else: # Full message received. self.no_response = 0 def staleness(self): """ Return time since measurement made """ if self.tov is not None: return time.time() - self.tov else: return -999 def bad_checksum(self): """ Return count of messages received with bad checksums """ return self.bad_CS def short_message(self): """ Return count of short messages """ return self.bad_SM def missing_message(self): """ Return count of missing messages """ return self.bad_MM def sensor_resets(self): """ Return count of power cycles because of sensor hangs """ return self.bad_SR def close(self): """ Stop reading sensor, remove callbacks """ self.pi.set_watchdog(self.gpio, 0) if self.either_edge_cb: self.either_edge_cb.cancel() self.either_edge_cb = None def start_sensor(self): """ Turn the sensor on """ if self.power_output_id: self.logger.info("Turning on sensor") self.control.output_on(self.power_output_id, 0) time.sleep(2) self.powered = True def stop_sensor(self): """ Turn the sensor off """ if self.power_output_id: self.logger.info("Turning off sensor") self.control.output_off(self.power_output_id) self.powered = False
def get_condition_value(condition_id): """ Returns condition measurements for Conditional controllers :param condition_id: Conditional condition ID :return: measurement: multiple types """ sql_condition = db_retrieve_table_daemon(ConditionalConditions).filter( ConditionalConditions.unique_id == condition_id).first() # Check Measurement Conditions if sql_condition.condition_type == 'measurement': 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 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 # Return GPIO state 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 Exception as e: gpio_state = None logger.error("Exception reading the GPIO pin: {}".format(e)) return gpio_state # Return output state elif sql_condition.condition_type == 'output_state': control = DaemonControl() return control.output_state(sql_condition.output_id) # Return the duration the output is currently on for elif sql_condition.condition_type == 'output_duration_on': control = DaemonControl() return control.output_sec_currently_on(sql_condition.output_id) # Return controller active state elif sql_condition.condition_type == 'controller_status': controller_type, _, _ = which_controller(sql_condition.controller_id) control = DaemonControl() return control.controller_is_active(controller_type, sql_condition.controller_id)
class CustomModule(AbstractFunction): """ Class to operate custom controller """ def __init__(self, function, testing=False): super().__init__(function, testing=testing, name=__name__) self.control_variable = None self.timestamp = None self.control = DaemonControl() self.outputIsOn = False self.timer_loop = time.time() # Initialize custom options self.measurement_device_id = None self.measurement_measurement_id = None self.output_device_id = None self.output_measurement_id = None self.output_channel_id = None self.setpoint = None self.hysteresis = None self.direction = None self.output_channel = None self.update_period = None self.duty_cycle_increase = None self.duty_cycle_maintain = None self.duty_cycle_decrease = None self.duty_cycle_shutdown = None # Set custom options custom_function = db_retrieve_table_daemon(CustomController, unique_id=self.unique_id) self.setup_custom_options(FUNCTION_INFORMATION['custom_options'], custom_function) if not testing: self.try_initialize() def initialize(self): self.timestamp = time.time() self.output_channel = self.get_output_channel_from_channel_id( self.output_channel_id) self.logger.info( "Bang-Bang controller started with options: " "Measurement Device: {}, Measurement: {}, Output: {}, " "Output_Channel: {}, Setpoint: {}, Hysteresis: {}, " "Direction: {}, Increase: {}%, Maintain: {}%, Decrease: {}%, " "Shutdown: {}%, Period: {}".format( self.measurement_device_id, self.measurement_measurement_id, self.output_device_id, self.output_channel, self.setpoint, self.hysteresis, self.direction, self.duty_cycle_increase, self.duty_cycle_maintain, self.duty_cycle_decrease, self.duty_cycle_shutdown, self.update_period)) def loop(self): if self.timer_loop > time.time(): return while self.timer_loop < time.time(): self.timer_loop += self.update_period if self.output_channel is None: self.logger.error( "Cannot run bang-bang controller: Check output channel.") return last_measurement = self.get_last_measurement( self.measurement_device_id, self.measurement_measurement_id)[1] outputState = self.control.output_state(self.output_device_id, self.output_channel) self.logger.info("Input: {}, output: {}, target: {}, hyst: {}".format( last_measurement, outputState, self.setpoint, self.hysteresis)) if self.direction == 'raise': if last_measurement < (self.setpoint - self.hysteresis): self.control.output_on(self.output_device_id, output_type='pwm', amount=self.duty_cycle_increase, output_channel=self.output_channel) else: self.control.output_on(self.output_device_id, output_type='pwm', amount=self.duty_cycle_maintain, output_channel=self.output_channel) elif self.direction == 'lower': if last_measurement > (self.setpoint + self.hysteresis): self.control.output_on(self.output_device_id, output_type='pwm', amount=self.duty_cycle_decrease, output_channel=self.output_channel) else: self.control.output_on(self.output_device_id, output_type='pwm', amount=self.duty_cycle_maintain, output_channel=self.output_channel) elif self.direction == 'both': if last_measurement < (self.setpoint - self.hysteresis): self.control.output_on(self.output_device_id, output_type='pwm', amount=self.duty_cycle_increase, output_channel=self.output_channel) elif last_measurement > (self.setpoint + self.hysteresis): self.control.output_on(self.output_device_id, output_type='pwm', amount=self.duty_cycle_decrease, output_channel=self.output_channel) else: self.control.output_on(self.output_device_id, output_type='pwm', amount=self.duty_cycle_maintain, output_channel=self.output_channel) else: self.logger.info("Unknown controller direction: '{}'".format( self.direction)) def stop_function(self): self.control.output_on(self.output_device_id, output_type='pwm', amount=self.duty_cycle_shutdown, output_channel=self.output_channel)
class CustomModule(AbstractFunction): """ Class to operate custom controller """ def __init__(self, function, testing=False): super(CustomModule, self).__init__(function, testing=testing, name=__name__) self.control_variable = None self.timestamp = None self.control = DaemonControl() self.outputIsOn = False self.timer_loop = time.time() # Initialize custom options self.measurement_device_id = None self.measurement_measurement_id = None self.output_device_id = None self.output_measurement_id = None self.output_channel_id = None self.setpoint = None self.hysteresis = None self.direction = None self.output_channel = None self.update_period = None # Set custom options custom_function = db_retrieve_table_daemon(CustomController, unique_id=self.unique_id) self.setup_custom_options(FUNCTION_INFORMATION['custom_options'], custom_function) self.output_channel = self.get_output_channel_from_channel_id( self.output_channel_id) if not testing: self.initialize_variables() def initialize_variables(self): self.timestamp = time.time() self.logger.info( "Bang-Bang controller started with options: " "Measurement Device: {}, Measurement: {}, Output: {}, " "Output_Channel: {}, Setpoint: {}, Hysteresis: {}, " "Direction: {}, Period: {}".format( self.measurement_device_id, self.measurement_measurement_id, self.output_device_id, self.output_channel, self.setpoint, self.hysteresis, self.direction, self.update_period)) def loop(self): if self.output_channel is None: self.logger.error( "Cannot start bang-bang controller: Could not find output channel." ) self.deactivate_self() return if self.timer_loop > time.time(): return while self.timer_loop < time.time(): self.timer_loop += self.update_period last_measurement = self.get_last_measurement( self.measurement_device_id, self.measurement_measurement_id)[1] outputState = self.control.output_state(self.output_device_id, self.output_channel) self.logger.info("Input: {}, output: {}, target: {}, hyst: {}".format( last_measurement, outputState, self.setpoint, self.hysteresis)) if self.direction == 'raise': if outputState == 'on': # looking to turn output off if last_measurement > (self.setpoint + self.hysteresis): self.control.output_off(self.output_device_id, output_channel=self.output_channel) else: # looking to turn output on if last_measurement < (self.setpoint - self.hysteresis): self.control.output_on(self.output_device_id, output_channel=self.output_channel) elif self.direction == 'lower': if outputState == 'on': # looking to turn output off if last_measurement < (self.setpoint - self.hysteresis): self.control.output_off(self.output_device_id, output_channel=self.output_channel) else: # looking to turn output on if last_measurement > (self.setpoint + self.hysteresis): self.control.output_on(self.output_device_id, output_channel=self.output_channel) else: self.logger.info("Unknown controller direction: {}".format( self.direction)) def deactivate_self(self): self.logger.info("Deactivating bang-bang controller") with session_scope(MYCODO_DB_PATH) as new_session: mod_cont = new_session.query(CustomController).filter( CustomController.unique_id == self.unique_id).first() mod_cont.is_activated = False new_session.commit() deactivate_controller = threading.Thread( target=self.control.controller_deactivate, args=(self.unique_id, )) deactivate_controller.start() def stop_function(self): self.control.output_off(self.output_device_id, self.output_channel)
class InputModule(AbstractInput): """ A sensor support class that measures the DHT11's humidity and temperature and calculates the dew point The DHT11 class is a stripped version of the DHT22 sensor code by joan2937. You can find the initial implementation here: - https://github.com/srounet/pigpio/tree/master/EXAMPLES/Python/DHT22_AM2302_SENSOR """ def __init__(self, input_dev, testing=False): """ :param gpio: gpio pin number :type gpio: int :param power: Power pin number :type power: int Instantiate with the Pi and gpio to which the DHT11 output pin is connected. Optionally a gpio used to power the sensor may be specified. This gpio will be set high to power the sensor. """ super(InputModule, self).__init__() self.logger = logging.getLogger('mycodo.inputs.dht11') self._dew_point = None self._humidity = None self._temperature = None self.temp_temperature = 0 self.temp_humidity = 0 self.temp_dew_point = None self.power_output_id = None self.powered = False if not testing: import pigpio from mycodo.mycodo_client import DaemonControl self.logger = logging.getLogger('mycodo.dht11_{id}'.format( id=input_dev.unique_id.split('-')[0])) self.convert_to_unit = input_dev.convert_to_unit self.gpio = int(input_dev.gpio_location) self.power_output_id = input_dev.power_output_id self.control = DaemonControl() self.pigpio = pigpio self.pi = self.pigpio.pi() self.high_tick = None self.bit = None self.either_edge_cb = None self.start_sensor() def __repr__(self): """ Representation of object """ return "<{cls}(dewpoint={dpt})(humidity={hum})(temperature={temp})>".format( cls=type(self).__name__, dpt="{0:.2f}".format(self._dew_point), hum="{0:.2f}".format(float(self._humidity)), temp="{0:.2f}".format(float(self._temperature))) def __str__(self): """ Return measurement information """ return "Dew Point: {dpt}, Humidity: {hum}, Temperature: {temp}".format( dpt="{0:.2f}".format(self._dew_point), hum="{0:.2f}".format(float(self._humidity)), temp="{0:.2f}".format(float(self._temperature))) def __iter__(self): # must return an iterator """ DHT11Sensor iterates through live measurement readings """ return self def next(self): """ Get next measurement reading """ if self.read(): # raised an error raise StopIteration # required return dict(dewpoint=float('{0:.2f}'.format(self._dew_point)), humidity=float("{0:.2f}".format(float(self._humidity))), temperature=float("{0:.2f}".format(float( self._temperature)))) @property def dew_point(self): """ DHT11 dew point in Celsius """ if self._dew_point is None: # update if needed self.read() return self._dew_point @property def humidity(self): """ DHT11 relative humidity in percent """ if self._humidity is None: # update if needed self.read() return self._humidity @property def temperature(self): """ DHT11 temperature in Celsius """ if self._temperature is None: # update if needed self.read() return self._temperature def get_measurement(self): """ Gets the humidity and temperature """ self._dew_point = None self._humidity = None self._temperature = None if not self.pi.connected: # Check if pigpiod is running self.logger.error("Could not connect to pigpiod." "Ensure it is running and try again.") return None, None, None import pigpio self.pigpio = pigpio # Ensure if the power pin turns off, it is turned back on if (self.power_output_id and db_retrieve_table_daemon( Output, unique_id=self.power_output_id) and self.control.output_state(self.power_output_id) == 'off'): self.logger.error( 'Sensor power output {rel} detected as being off. ' 'Turning on.'.format(rel=self.power_output_id)) self.start_sensor() time.sleep(2) # Try twice to get measurement. This prevents an anomaly where # the first measurement fails if the sensor has just been powered # for the first time. for _ in range(2): self.measure_sensor() if self.temp_dew_point is not None: self.temp_dew_point = convert_units('dewpoint', 'C', self.convert_to_unit, self.temp_dew_point) self.temp_temperature = convert_units('temperature', 'C', self.convert_to_unit, self.temp_temperature) self.temp_humidity = convert_units('humidity', 'percent', self.convert_to_unit, self.temp_humidity) return (self.temp_dew_point, self.temp_humidity, self.temp_temperature) # success - no errors time.sleep(2) # Measurement failure, power cycle the sensor (if enabled) # Then try two more times to get a measurement if self.power_output_id is not None and self.running: self.stop_sensor() time.sleep(2) self.start_sensor() for _ in range(2): self.measure_sensor() if self.temp_dew_point is not None: self.temp_dew_point = convert_units( 'dewpoint', 'C', self.convert_to_unit, self.temp_dew_point) self.temp_temperature = convert_units( 'temperature', 'C', self.convert_to_unit, self.temp_temperature) self.temp_humidity = convert_units('humidity', 'percent', self.convert_to_unit, self.temp_humidity) return (self.temp_dew_point, self.temp_humidity, self.temp_temperature) # success - no errors time.sleep(2) self.logger.error("Could not acquire a measurement") return None, None, None def read(self): """ Takes a reading from the DHT11 and updates the self.dew_point, self._humidity, and self._temperature values :returns: None on success or 1 on error """ try: (self._dew_point, self._humidity, self._temperature) = self.get_measurement() if self._dew_point is not None: return # success - no errors except Exception as e: self.logger.exception( "{cls} raised an exception when taking a reading: " "{err}".format(cls=type(self).__name__, err=e)) return 1 def measure_sensor(self): self.temp_temperature = 0 self.temp_humidity = 0 self.temp_dew_point = None try: try: self.setup() except Exception as except_msg: self.logger.error( 'Could not initialize sensor. Check if gpiod is running. ' 'Error: {msg}'.format(msg=except_msg)) self.pi.write(self.gpio, self.pigpio.LOW) time.sleep(0.017) # 17 ms self.pi.set_mode(self.gpio, self.pigpio.INPUT) self.pi.set_watchdog(self.gpio, 200) time.sleep(0.2) if self.temp_humidity != 0: self.temp_dew_point = calculate_dewpoint( self.temp_temperature, self.temp_humidity) except Exception as e: self.logger.error( "Exception raised when taking a reading: {err}".format(err=e)) finally: self.close() return (self.temp_dew_point, self.temp_humidity, self.temp_temperature) def setup(self): """ Clears the internal gpio pull-up/down resistor. Kills any watchdogs. Setup callbacks """ self._humidity = 0 self._temperature = 0 self.high_tick = 0 self.bit = 40 self.either_edge_cb = None self.pi.set_pull_up_down(self.gpio, self.pigpio.PUD_OFF) self.pi.set_watchdog(self.gpio, 0) self.register_callbacks() def register_callbacks(self): """ Monitors RISING_EDGE changes using callback """ self.either_edge_cb = self.pi.callback(self.gpio, self.pigpio.EITHER_EDGE, self.either_edge_callback) def either_edge_callback(self, gpio, level, tick): """ Either Edge callbacks, called each time the gpio edge changes. Accumulate the 40 data bits from the DHT11 sensor. """ level_handlers = { self.pigpio.FALLING_EDGE: self._edge_fall, self.pigpio.RISING_EDGE: self._edge_rise, self.pigpio.EITHER_EDGE: self._edge_either } handler = level_handlers[level] diff = self.pigpio.tickDiff(self.high_tick, tick) handler(tick, diff) def _edge_rise(self, tick, diff): """ Handle Rise signal """ val = 0 if diff >= 50: val = 1 if diff >= 200: # Bad bit? self.checksum = 256 # Force bad checksum if self.bit >= 40: # Message complete self.bit = 40 elif self.bit >= 32: # In checksum byte self.checksum = (self.checksum << 1) + val if self.bit == 39: # 40th bit received self.pi.set_watchdog(self.gpio, 0) total = self.temp_humidity + self.temp_temperature # is checksum ok ? if not (total & 255) == self.checksum: # For some reason the port from python 2 to python 3 causes # this bad checksum error to happen during every read # TODO: Investigate how to properly check the checksum in python 3 self.logger.debug( "Exception raised when taking a reading: " "Bad Checksum.") elif 16 <= self.bit < 24: # in temperature byte self.temp_temperature = (self.temp_temperature << 1) + val elif 0 <= self.bit < 8: # in humidity byte self.temp_humidity = (self.temp_humidity << 1) + val self.bit += 1 def _edge_fall(self, tick, diff): """ Handle Fall signal """ self.high_tick = tick if diff <= 250000: return self.bit = -2 self.checksum = 0 self.temp_temperature = 0 self.temp_humidity = 0 def _edge_either(self, tick, diff): """ Handle Either signal """ self.pi.set_watchdog(self.gpio, 0) def close(self): """ Stop reading sensor, remove callbacks """ self.pi.set_watchdog(self.gpio, 0) if self.either_edge_cb: self.either_edge_cb.cancel() self.either_edge_cb = None def start_sensor(self): """ Power the sensor """ if self.power_output_id: self.logger.info("Turning on sensor") self.control.output_on(self.power_output_id, 0) time.sleep(2) self.powered = True def stop_sensor(self): """ Depower the sensor """ if self.power_output_id: self.logger.info("Turning off sensor") self.control.output_off(self.power_output_id) self.powered = False