Skip to content

Commit

Permalink
First working version with digital sensors
Browse files Browse the repository at this point in the history
  • Loading branch information
anxuae committed May 26, 2018
1 parent d23e00e commit e9025a5
Show file tree
Hide file tree
Showing 9 changed files with 109 additions and 51 deletions.
17 changes: 13 additions & 4 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,17 @@ Default configuration
duration = 60
[SENSOR]
# ADS1115 channels used to read the humidity level (4 max)
analog_pins = (1, 2, 3, 4)
# Physical GPIO DO-OUT pin use to power on/off the sensors
power_pin = 12
# Physical GPIO pins to detect threshold exceeded (4 max)
digital_pins = (11, 13, 15, 16)
# True if need to power on the sensors continuously (accelerate corrosion of resistive sensors)
always_powered = False
# Physical GPIO DO-IN pins to detect threshold exceeded
digital_pins = (11, 15, 31)
# ADS1115 channels used to read the humidity level
analog_pins = (1, 2, 3)
# Sensor physical range measured with the ADS1115 (from dry to wet)
analog_range = (0, 970)
2 changes: 1 addition & 1 deletion pih2o/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

"""An automatic plant watering application in pure Python for the Raspberry Pi."""

__version__ = "0.0.0"
__version__ = "0.0.1"

from pih2o.h2o import create_app # For WSGI loading
2 changes: 1 addition & 1 deletion pih2o/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def __init__(self, app):

def get(self, pin=None):
if pin is None:
return [sensor.pin for sensor in self.app.sensors()], 200
return [sensor.pin for sensor in self.app.sensors], 200
else:
return self.app.read_sensors(pin)[0].json(), 200

Expand Down
15 changes: 10 additions & 5 deletions pih2o/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ def safe_eval(value):
),
("SENSOR",
odict((
("power_pins", ((12, 16, 32), "Physical GPIO DO-OUT pins use to power on/off the sensors")),
("analog_pins", ((1, 2, 3), "ADS1115 channels used to read the humidity level")),
("power_pin", (12, "Physical GPIO DO-OUT pin use to power on/off the sensors")),
("always_powered", (False, "True if need to power on the sensors continuously (accelerate corrosion of resistive sensors)")),
("digital_pins", ((11, 15, 31), "Physical GPIO DO-IN pins to detect threshold exceeded")),
("analog_pins", ((1, 2, 3), "ADS1115 channels used to read the humidity level")),
("analog_range", ((0, 970), "Sensor physical range measured with the ADS1115 (from dry to wet)")),
))
),
))
Expand Down Expand Up @@ -79,19 +81,22 @@ class PiConfigParser(ConfigParser):
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:///' + osp.join(osp.dirname(self.filename), 'pih2o.db')
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + self.db_filename

if not osp.isfile(self.filename) or clear:
LOGGER.info("Generate the configuration file in '%s'", self.filename)
dirname = osp.dirname(self.filename)
if not osp.isdir(dirname):
os.makedirs(dirname)
generate_default_config(self.filename)
if osp.isfile(SQLALCHEMY_DATABASE_URI):
os.remove(SQLALCHEMY_DATABASE_URI)

if osp.isfile(self.db_filename):
LOGGER.info("Dropping all measurements from database '%s'", self.db_filename)
os.remove(self.db_filename)

self.reload()

Expand Down
42 changes: 27 additions & 15 deletions pih2o/controls/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""Sensors management.
"""

import random
import time
import threading
from RPi import GPIO
import Adafruit_ADS1x15
Expand All @@ -19,26 +19,36 @@ class HumiditySensor(object):

stype = None

def __init__(self, pin, power_pin, power_auto=True):
def __init__(self, pin, power_pin=0, analog_range=None):
self.pin = pin
self.power_pin = power_pin
self.power_auto = power_auto
self.power_pin = power_pin # 0 means do not manage the power
self.analog_range = analog_range or []
self._lock = threading.Lock()

GPIO.setup(self.power_pin, GPIO.OUT)
if not power_auto:
GPIO.setup(self.pin, GPIO.IN)
if self.power_pin:
GPIO.setup(self.power_pin, GPIO.OUT)

def power_on(self):
"""Power on the sensor.
"""
if self.power_pin and GPIO.input(self.power_pin) == GPIO.LOW:
GPIO.output(self.power_pin, GPIO.HIGH)
time.sleep(5) # Make sure the sensor is powered and ready

def get_value(self):
"""Return the sensor value.
The sensor is automatically powered on if necessary.
"""
with self._lock:
if self.power_auto:
GPIO.output(self.power_pin, GPIO.HIGH)
value = self._read()
if self.power_auto:
GPIO.output(self.power_pin, GPIO.LOW)
return value
self.power_on()
return self._read()

def power_off(self):
"""Power off the sensor.
"""
if self.power_pin and GPIO.input(self.power_pin) == GPIO.HIGH:
GPIO.output(self.power_pin, GPIO.LOW)

def _read(self):
raise NotImplementedError
Expand All @@ -51,14 +61,16 @@ class DigitalHumiditySensor(HumiditySensor):
def _read(self):
"""Return True if the sensor has detected a low humidity level.
"""
return random.choice([True, False])
return GPIO.input(self.pin) == GPIO.HIGH


class AnalogHumiditySensor(HumiditySensor):

stype = 'analog'

def _read(self):
"""Return the humidity level measured by the sensor.
"""Return the humidity level (in %) measured by the sensor.
"""
return random.randint(2, 99)
# Choose gain 1 because the sensor range is +/-4.096V
value = adc.read_adc(self.pin, gain=1)
return (value - min(self.analog_range)) * 100. / (max(self.analog_range) - min(self.analog_range))
56 changes: 32 additions & 24 deletions pih2o/h2o.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""Pih2o main module.
"""

import os
import sys
import time
import atexit
Expand All @@ -20,7 +21,7 @@
from pih2o import models
from pih2o.api import ApiConfig, ApiPump, ApiSensors, ApiMeasurements
from pih2o.utils import LOGGER
from pih2o.config import PiConfigParser
from pih2o.config import PiConfigParser, SQLALCHEMY_DATABASE_URI
from pih2o.controls.pump import Pump
from pih2o.controls.sensor import AnalogHumiditySensor, DigitalHumiditySensor

Expand Down Expand Up @@ -75,8 +76,7 @@ def say_hello():
# The HW connection of the controls
GPIO.setmode(GPIO.BOARD) # GPIO in physical pins mode
self.pump = None
self.analog_sensors = []
self.digital_sensors = []
self.sensors = []
self._pump_timer = None

self.init_controls()
Expand All @@ -92,17 +92,22 @@ def init_controls(self):
"""
self.pump = Pump(self.config.getint("PUMP", "pin"))

if self.config.gettyped("SENSOR", "analog_pins"):
read_pins = self.config.gettyped("SENSOR", "analog_pins")
if read_pins:
# Use analog sensors
for pin in self.config.gettyped("SENSOR", "analog_pins"):
self.analog_sensors.append(AnalogHumiditySensor(pin))
elif self.config.gettyped("SENSOR", "digital_pins"):
# Use digital sensors
for pin in self.config.gettyped("SENSOR", "digital_pins"):
self.digital_sensors.append(DigitalHumiditySensor(pin))
sensor_class = AnalogHumiditySensor
else:
# Use digital sensors
read_pins = self.config.gettyped("SENSOR", "digital_pins")
sensor_class = DigitalHumiditySensor

if not read_pins:
raise ValueError("Neither analog nor digital sensor defined in the configuration")

for pin in read_pins:
self.sensors.append(sensor_class(pin, self.config.gettyped("SENSOR", "power_pin"),
self.config.gettyped('SENSOR', 'analog_range')))

def is_running(self):
"""Return True if the watering daemon is running.
"""
Expand All @@ -125,40 +130,41 @@ def start_watering(self, duration=None):
self._pump_timer.daemon = True
self._pump_timer.start()

def sensors(self):
"""Return the available sensors."""
return self.analog_sensors or self.digital_sensors

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():
if not self.sensors:
raise EnvironmentError("The sensors are not initialized")

data = []
threshold = self.config.getfloat('GENERAL', 'humidity_threshold')

for sensor in self.sensors():
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 < threshold
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, humidity, triggered)
sensor.pin, measure.humidity, measure.triggered)

data.append(measure)

if not self.config.getboolean("SENSOR", "always_powered"):
for sensor in self.sensors:
sensor.power_off()

data.append(models.Measurement(**{'sensor': sensor.pin,
'humidity': humidity,
'triggered': triggered,
'record_time': datetime.now()}))
return data

def start_daemon(self):
Expand Down Expand Up @@ -227,6 +233,8 @@ def shutdown_daemon(self):
# To be sure and avoid floor flooding :)
self.pump.stop()

GPIO.cleanup()


def create_app(cfgfile="~/.config/pih2o/pih2o.cfg"):
"""Application factory.
Expand Down
16 changes: 16 additions & 0 deletions tests/mocks/Adafruit_ADS1x15.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-

"""
Mocks for tests on other HW than Raspberry Pi.
"""

import random


class ADS1115(object):

def __init__(self):
pass

def read_adc(self, pin, gain=1):
return random.randrange(0, 900, 1)
8 changes: 8 additions & 0 deletions tests/mocks/RPi.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
Mocks for tests on other HW than Raspberry Pi.
"""

import random


class GPIO(object):

Expand All @@ -23,6 +25,12 @@ def setmode(cls, mode):
def setup(cls, pin, direction, **kwargs):
print("Mock: setup GPIO pin {} to {}".format(pin, direction))

@classmethod
def input(cls, pin):
status = random.choice([True, False])
print("Mock: input GPIO pin {} = {}".format(pin, status))
return status

@classmethod
def output(cls, pin, status):
print("Mock: output GPIO pin {} to {}".format(pin, status))
Expand Down
2 changes: 1 addition & 1 deletion tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def test_start_pump_with_duration(client):
def test_get_sensors_list(client):
resp = client.get('/pih2o/api/v1/sensors')
assert resp.status_code == 200
assert json.loads(resp.data) == [1, 2, 3, 4]
assert json.loads(resp.data) == [1, 2, 3]


def test_read_one_sensor(client):
Expand Down

0 comments on commit e9025a5

Please sign in to comment.