def stop(self): """ Stop the pump. """ LOGGER.info("Stoping pump (physical pin %s)", self.pin) GPIO.output(self.pin, GPIO.LOW) self._running.clear()
def create_app(cfgfile="~/.config/pih2o/pih2o.cfg"): """Entry point to use with a WSGI server (e.g. gunicorn). """ parser = argparse.ArgumentParser(usage="%(prog)s [options]", description=pih2o.__doc__) parser.add_argument('--version', action='version', version=pih2o.__version__, help=u"show program's version number and exit") parser.add_argument("--config", action='store_true', help=u"edit the current configuration") parser.add_argument("--reset", action='store_true', help=u"restore the default configuration") parser.add_argument("--log", default=None, help=u"save console output to the given file") group = parser.add_mutually_exclusive_group() group.add_argument("-v", "--verbose", dest='logging', action='store_const', const=logging.DEBUG, help=u"report more information about operations", default=logging.INFO) group.add_argument("-q", "--quiet", dest='logging', action='store_const', const=logging.WARNING, help=u"report only errors and warnings", default=logging.INFO) options, _args = parser.parse_known_args() logging.basicConfig(filename=options.log, filemode='w', format='[ %(levelname)-8s] %(name)-18s: %(message)s', level=options.logging) config = PiConfigParser(cfgfile, options.reset) if options.config: LOGGER.info("Editing the automatic plant watering configuration...") config.open_editor() sys.exit(0) elif not options.reset: LOGGER.info("Starting the automatic plant watering application...") app = PiApplication(config) app.start_daemon() return app.flask_app # Return the WSGI application else: sys.exit(0)
def start_daemon(self): """Start the watering daemon main loop. """ if self.is_running(): raise EnvironmentError("Watering daemon is already running") self._stop.clear() self._thread = threading.Thread(target=self.main_loop) self._thread.daemon = True self._thread.start() LOGGER.debug("Watering daemon started")
def start(self): """ Start the pump. """ if self.is_running(): # Avoid starting several times to prevent concurent access raise IOError("Watering is already started") LOGGER.info("Starting pump (physical pin %s)", self.pin) GPIO.output(self.pin, GPIO.HIGH) self._running.set()
def _read(self): """Return the humidity level (in %) measured by the sensor. """ # Choose gain 1 because the sensor range is +/-4.096V value = self.adc.read_adc(self.pin, gain=1) if value < min(self.analog_range) or value > max(self.analog_range): LOGGER.warning( "Sensor %s value '%s' is outside the defined range %s", self.pin, value, self.analog_range) return 100 - (value - min(self.analog_range)) * 100. / ( max(self.analog_range) - min(self.analog_range))
def save(self, default=False): """Save the current or default values into the configuration file. """ LOGGER.info("Generate the configuration file in '%s'", self.filename) with open(self.filename, 'w') as fp: for section, options in DEFAULT.items(): fp.write("[{}]\n".format(section)) for name, value in options.items(): if default: val = value[0] else: val = self.get(section, name) fp.write("# {}\n{} = {}\n\n".format(value[1], name, val))
def open_editor(self): """Open a text editor to edit the configuration file. """ for editor in self.editors: try: process = subprocess.Popen([editor, self.filename]) process.communicate() self.reload() return except OSError as e: if e.errno != os.errno.ENOENT: # Something else went wrong while trying to run the editor raise LOGGER.critical("Can't find installed text editor among %s", self.editors)
def shutdown_daemon(self): """Quit the watering daemon. """ if self._pump_timer: self._pump_timer.cancel() if self.is_running(): self._stop.set() self._thread.join() LOGGER.debug("Watering daemon stopped") # To be sure and avoid floor flooding :) self.pump.stop() GPIO.cleanup()
def main_loop(self): """Watering daemon loop. """ cron_pattern = self.config.get('GENERAL', 'record_interval') if not croniter.is_valid(cron_pattern): raise ValueError("Invalid cron pattern '{}'".format(cron_pattern)) cron = croniter(cron_pattern) next_record = cron.get_next() while not self._stop.is_set(): if self._stop.wait(next_record - time.time()): break # Stop requested # Calculate next wakeup time next_record = cron.get_next() # Take a new measurement triggered_sensors = [] untriggered_sensors = [] with self.flask_app.app_context(): for measure in self.read_sensors(): models.db.session.add(measure) if measure.triggered: LOGGER.info("Sensor on physical pin '%s' is triggered", measure.sensor) triggered_sensors.append(measure) else: untriggered_sensors.append(measure) models.db.session.commit() # Start the watering if necessary (do nothing if already running) strategy = self.config.get('GENERAL', 'watering_strategy') if (strategy == 'majority' and float(len(triggered_sensors)) >= float(len(untriggered_sensors))) or \ (strategy == 'first' and triggered_sensors) or \ (strategy == 'last' and not untriggered_sensors): if self.pump.is_running(): LOGGER.warning( "Skipping watering because pump is already running") else: self.pump.start() self._stop.wait(self.config.getint("PUMP", "duration")) self.pump.stop()
def enable_autostart(self, enable=True): """Auto-start pih2o at the Raspberry Pi startup. """ filename = osp.expanduser('~/.config/autostart/pih2o.desktop') dirname = osp.dirname(filename) if enable and not osp.isfile(filename): if not osp.isdir(dirname): os.makedirs(dirname) LOGGER.info("Generate the auto-startup file in '%s'", dirname) with open(filename, 'w') as fp: fp.write("[Desktop Entry]\n") fp.write("Name=pih2o\n") fp.write("Exec=pih2o\n") fp.write("Type=application\n") elif not enable and osp.isfile(filename): LOGGER.info("Remove the auto-startup file in '%s'", dirname) os.remove(filename)
def read_sensors(self, sensor_pin=None): """Read values from one or all sensors. :param sensor_id: pin of the sensor :type sensor_id: int """ if not self.sensors: raise EnvironmentError("The sensors are not initialized") data = [] for sensor in self.sensors: if sensor_pin is not None and sensor.pin != sensor_pin: continue if sensor.stype == 'analog': humidity = sensor.get_value() triggered = humidity <= self.config.getfloat( "GENERAL", "humidity_threshold") else: humidity = 0 triggered = sensor.get_value() measure = models.Measurement( **{ 'sensor': sensor.pin, 'humidity': humidity, 'triggered': triggered, 'record_time': datetime.now() }) LOGGER.debug( "New measurement: sensor=%s, humidity=%s, triggered=%s", sensor.pin, measure.humidity, measure.triggered) data.append(measure) for sensor in self.sensors: sensor.power_off() return data
def __init__(self, config): self._thread = None self._stop = threading.Event() self.config = config LOGGER.debug("Initializing flask instance") self.flask_app = flask.Flask(pih2o.__name__) self.flask_app.config.from_object('pih2o.config') got_request_exception.connect(self.log_exception, self.flask_app) @self.flask_app.route('/pih2o') def say_hello(): return flask.jsonify({ "name": pih2o.__name__, "version": pih2o.__version__, "running": self.is_running() }) LOGGER.debug("Initializing the database for measurements") models.db.init_app(self.flask_app) with self.flask_app.app_context(): models.db.create_all() LOGGER.debug("Initializing the RESTful API") self.api = Api(self.flask_app, catch_all_404s=True) root = '/pih2o/api/v1' self.api.add_resource(ApiConfig, root + '/config', root + '/config/<string:section>', root + '/config/<string:section>/<string:key>', endpoint='config', resource_class_args=(config, )) self.api.add_resource(ApiPump, root + '/pump', root + '/pump/<int:duration>', endpoint='pump', resource_class_args=(self, )) self.api.add_resource(ApiSensors, root + '/sensors', root + '/sensors/<int:pin>', endpoint='sensors', resource_class_args=(self, )) self.api.add_resource(ApiMeasurements, root + '/measurements', endpoint='measurements', resource_class_args=(models.db, )) # The HW connection of the controls GPIO.setmode(GPIO.BOARD) # GPIO in physical pins mode self.pump = None self.sensors = [] self._pump_timer = None self.init_controls() atexit.register(self.shutdown_daemon)
def __init__(self, filename, clear=False): ConfigParser.__init__(self) self.filename = osp.abspath(osp.expanduser(filename)) self.db_filename = osp.join(osp.dirname(self.filename), 'pih2o.db') # Update Flask configuration global SQLALCHEMY_DATABASE_URI SQLALCHEMY_DATABASE_URI = 'sqlite:///' + self.db_filename if not osp.isfile(self.filename) or clear: dirname = osp.dirname(self.filename) if not osp.isdir(dirname): os.makedirs(dirname) self.save(True) if osp.isfile(self.db_filename): resp = input( "You really want to erase the current database? (y/N)") if resp in ('y', 'yes', 'Y', 'YES'): LOGGER.info("Dropping all measurements from database '%s'", self.db_filename) os.remove(self.db_filename) self.reload()