diff --git a/Enviro-Plus-pHAT.jpg b/Enviro-Plus-pHAT.jpg new file mode 100644 index 0000000..f0947a0 Binary files /dev/null and b/Enviro-Plus-pHAT.jpg differ diff --git a/Enviro-mini-pHAT.jpg b/Enviro-mini-pHAT.jpg new file mode 100644 index 0000000..120469f Binary files /dev/null and b/Enviro-mini-pHAT.jpg differ diff --git a/README.md b/README.md index 5fb9fe6..bcb1aa6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ -# Enviro+ +# Enviro+ Designed for environmental monitoring, Enviro+ lets you measure air quality (pollutant gases and particulates), temperature, pressure, humidity, light, and noise level. Learn more - https://shop.pimoroni.com/products/enviro-plus + [![Build Status](https://travis-ci.com/pimoroni/enviroplus-python.svg?branch=master)](https://travis-ci.com/pimoroni/enviroplus-python) [![Coverage Status](https://coveralls.io/repos/github/pimoroni/enviroplus-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/enviroplus-python?branch=master) [![PyPi Package](https://img.shields.io/pypi/v/enviroplus.svg)](https://pypi.python.org/pypi/enviroplus) @@ -11,6 +12,11 @@ Designed for environmental monitoring, Enviro+ lets you measure air quality (pol You're best using the "One-line" install method if you want all of the UART serial configuration for the PMS5003 particulate matter sensor to run automatically. +**Note** The code in this repository supports both the Enviro+ and Enviro Mini boards. _The Enviro Mini board does not have the Gas sensor or the breakout for the PM sensor._ + +![Enviro Plus pHAT](./Enviro-Plus-pHAT.jpg) +![Enviro Mini pHAT](./Enviro-mini-pHAT.jpg) + ## One-line (Installs from GitHub) ``` @@ -48,6 +54,12 @@ And install additional dependencies: sudo apt install python-numpy python-smbus python-pil python-setuptools ``` +## Alternate Software & User Projects + +* enviro monitor - https://github.com/roscoe81/enviro-monitor +* mqtt-all - https://github.com/robmarkcole/rpi-enviro-mqtt - now upstream: [see examples/mqtt-all.py](examples/mqtt-all.py) +* adafruit_io.py - https://github.com/dedSyn4ps3/enviroplus-python/blob/master/examples/adafruit_io.py - uses Adafruit Blinka and BME280 libraries to publish to Adafruit IO + ## Help & Support * GPIO Pinout - https://pinout.xyz/pinout/enviro_plus diff --git a/examples/all-in-one-enviro-mini.py b/examples/all-in-one-enviro-mini.py new file mode 100755 index 0000000..d7a001f --- /dev/null +++ b/examples/all-in-one-enviro-mini.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 + +import time +import colorsys +import os +import sys +import ST7735 +try: + # Transitional fix for breaking change in LTR559 + from ltr559 import LTR559 + ltr559 = LTR559() +except ImportError: + import ltr559 + +from bme280 import BME280 +from enviroplus import gas +from subprocess import PIPE, Popen +from PIL import Image +from PIL import ImageDraw +from PIL import ImageFont +from fonts.ttf import RobotoMedium as UserFont +import logging + +logging.basicConfig( + format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + level=logging.INFO, + datefmt='%Y-%m-%d %H:%M:%S') + +logging.info("""all-in-one.py - Displays readings from all of Enviro plus' sensors +Press Ctrl+C to exit! +""") + +# BME280 temperature/pressure/humidity sensor +bme280 = BME280() + +# Create ST7735 LCD display class +st7735 = ST7735.ST7735( + port=0, + cs=1, + dc=9, + backlight=12, + rotation=270, + spi_speed_hz=10000000 +) + +# Initialize display +st7735.begin() + +WIDTH = st7735.width +HEIGHT = st7735.height + +# Set up canvas and font +img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0)) +draw = ImageDraw.Draw(img) +path = os.path.dirname(os.path.realpath(__file__)) +font_size = 20 +font = ImageFont.truetype(UserFont, font_size) + +message = "" + +# The position of the top bar +top_pos = 25 + + +# Displays data and text on the 0.96" LCD +def display_text(variable, data, unit): + # Maintain length of list + values[variable] = values[variable][1:] + [data] + # Scale the values for the variable between 0 and 1 + vmin = min(values[variable]) + vmax = max(values[variable]) + colours = [(v - vmin + 1) / (vmax - vmin + 1) for v in values[variable]] + # Format the variable name and value + message = "{}: {:.1f} {}".format(variable[:4], data, unit) + logging.info(message) + draw.rectangle((0, 0, WIDTH, HEIGHT), (255, 255, 255)) + for i in range(len(colours)): + # Convert the values to colours from red to blue + colour = (1.0 - colours[i]) * 0.6 + r, g, b = [int(x * 255.0) for x in colorsys.hsv_to_rgb(colour, 1.0, 1.0)] + # Draw a 1-pixel wide rectangle of colour + draw.rectangle((i, top_pos, i + 1, HEIGHT), (r, g, b)) + # Draw a line graph in black + line_y = HEIGHT - (top_pos + (colours[i] * (HEIGHT - top_pos))) + top_pos + draw.rectangle((i, line_y, i + 1, line_y + 1), (0, 0, 0)) + # Write the text at the top in black + draw.text((0, 0), message, font=font, fill=(0, 0, 0)) + st7735.display(img) + + +# Get the temperature of the CPU for compensation +def get_cpu_temperature(): + process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE, universal_newlines=True) + output, _error = process.communicate() + return float(output[output.index('=') + 1:output.rindex("'")]) + + +# Tuning factor for compensation. Decrease this number to adjust the +# temperature down, and increase to adjust up +factor = 2.25 + +cpu_temps = [get_cpu_temperature()] * 5 + +delay = 0.5 # Debounce the proximity tap +mode = 0 # The starting mode +last_page = 0 +light = 1 + +# Create a values dict to store the data +variables = ["temperature", + "pressure", + "humidity", + "light"] + +values = {} + +for v in variables: + values[v] = [1] * WIDTH + +# The main loop +try: + while True: + proximity = ltr559.get_proximity() + + # If the proximity crosses the threshold, toggle the mode + if proximity > 1500 and time.time() - last_page > delay: + mode += 1 + mode %= len(variables) + last_page = time.time() + + # One mode for each variable + if mode == 0: + # variable = "temperature" + unit = "C" + cpu_temp = get_cpu_temperature() + # Smooth out with some averaging to decrease jitter + cpu_temps = cpu_temps[1:] + [cpu_temp] + avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps)) + raw_temp = bme280.get_temperature() + data = raw_temp - ((avg_cpu_temp - raw_temp) / factor) + display_text(variables[mode], data, unit) + + if mode == 1: + # variable = "pressure" + unit = "hPa" + data = bme280.get_pressure() + display_text(variables[mode], data, unit) + + if mode == 2: + # variable = "humidity" + unit = "%" + data = bme280.get_humidity() + display_text(variables[mode], data, unit) + + if mode == 3: + # variable = "light" + unit = "Lux" + if proximity < 10: + data = ltr559.get_lux() + else: + data = 1 + display_text(variables[mode], data, unit) + +# Exit cleanly +except KeyboardInterrupt: + sys.exit(0) diff --git a/examples/all-in-one-no-pm.py b/examples/all-in-one-no-pm.py index d9b1069..de8ab06 100755 --- a/examples/all-in-one-no-pm.py +++ b/examples/all-in-one-no-pm.py @@ -67,8 +67,9 @@ def display_text(variable, data, unit): # Maintain length of list values[variable] = values[variable][1:] + [data] # Scale the values for the variable between 0 and 1 - colours = [(v - min(values[variable]) + 1) / (max(values[variable]) - - min(values[variable]) + 1) for v in values[variable]] + vmin = min(values[variable]) + vmax = max(values[variable]) + colours = [(v - vmin + 1) / (vmax - vmin + 1) for v in values[variable]] # Format the variable name and value message = "{}: {:.1f} {}".format(variable[:4], data, unit) logging.info(message) @@ -76,14 +77,12 @@ def display_text(variable, data, unit): for i in range(len(colours)): # Convert the values to colours from red to blue colour = (1.0 - colours[i]) * 0.6 - r, g, b = [int(x * 255.0) for x in colorsys.hsv_to_rgb(colour, - 1.0, 1.0)] + r, g, b = [int(x * 255.0) for x in colorsys.hsv_to_rgb(colour, 1.0, 1.0)] # Draw a 1-pixel wide rectangle of colour - draw.rectangle((i, top_pos, i+1, HEIGHT), (r, g, b)) + draw.rectangle((i, top_pos, i + 1, HEIGHT), (r, g, b)) # Draw a line graph in black - line_y = HEIGHT - (top_pos + (colours[i] * (HEIGHT - top_pos)))\ - + top_pos - draw.rectangle((i, line_y, i+1, line_y+1), (0, 0, 0)) + line_y = HEIGHT - (top_pos + (colours[i] * (HEIGHT - top_pos))) + top_pos + draw.rectangle((i, line_y, i + 1, line_y + 1), (0, 0, 0)) # Write the text at the top in black draw.text((0, 0), message, font=font, fill=(0, 0, 0)) st7735.display(img) diff --git a/examples/all-in-one.py b/examples/all-in-one.py index c0423e6..f7933a9 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -2,7 +2,6 @@ import time import colorsys -import os import sys import ST7735 try: @@ -72,8 +71,9 @@ def display_text(variable, data, unit): # Maintain length of list values[variable] = values[variable][1:] + [data] # Scale the values for the variable between 0 and 1 - colours = [(v - min(values[variable]) + 1) / (max(values[variable]) - - min(values[variable]) + 1) for v in values[variable]] + vmin = min(values[variable]) + vmax = max(values[variable]) + colours = [(v - vmin + 1) / (vmax - vmin + 1) for v in values[variable]] # Format the variable name and value message = "{}: {:.1f} {}".format(variable[:4], data, unit) logging.info(message) @@ -81,14 +81,12 @@ def display_text(variable, data, unit): for i in range(len(colours)): # Convert the values to colours from red to blue colour = (1.0 - colours[i]) * 0.6 - r, g, b = [int(x * 255.0) for x in colorsys.hsv_to_rgb(colour, - 1.0, 1.0)] + r, g, b = [int(x * 255.0) for x in colorsys.hsv_to_rgb(colour, 1.0, 1.0)] # Draw a 1-pixel wide rectangle of colour - draw.rectangle((i, top_pos, i+1, HEIGHT), (r, g, b)) + draw.rectangle((i, top_pos, i + 1, HEIGHT), (r, g, b)) # Draw a line graph in black - line_y = HEIGHT - (top_pos + (colours[i] * (HEIGHT - top_pos)))\ - + top_pos - draw.rectangle((i, line_y, i+1, line_y+1), (0, 0, 0)) + line_y = HEIGHT - (top_pos + (colours[i] * (HEIGHT - top_pos))) + top_pos + draw.rectangle((i, line_y, i + 1, line_y + 1), (0, 0, 0)) # Write the text at the top in black draw.text((0, 0), message, font=font, fill=(0, 0, 0)) st7735.display(img) @@ -200,7 +198,7 @@ def get_cpu_temperature(): try: data = pms5003.read() except pmsReadTimeoutError: - logging.warn("Failed to read PMS5003") + logging.warning("Failed to read PMS5003") else: data = float(data.pm_ug_per_m3(1.0)) display_text(variables[mode], data, unit) @@ -211,7 +209,7 @@ def get_cpu_temperature(): try: data = pms5003.read() except pmsReadTimeoutError: - logging.warn("Failed to read PMS5003") + logging.warning("Failed to read PMS5003") else: data = float(data.pm_ug_per_m3(2.5)) display_text(variables[mode], data, unit) @@ -222,7 +220,7 @@ def get_cpu_temperature(): try: data = pms5003.read() except pmsReadTimeoutError: - logging.warn("Failed to read PMS5003") + logging.warning("Failed to read PMS5003") else: data = float(data.pm_ug_per_m3(10)) display_text(variables[mode], data, unit) diff --git a/examples/combined.py b/examples/combined.py index c2fd397..6c4ab6f 100755 --- a/examples/combined.py +++ b/examples/combined.py @@ -2,7 +2,6 @@ import time import colorsys -import os import sys import ST7735 try: @@ -13,7 +12,7 @@ import ltr559 from bme280 import BME280 -from pms5003 import PMS5003, ReadTimeoutError as pmsReadTimeoutError +from pms5003 import PMS5003, ReadTimeoutError as pmsReadTimeoutError, SerialTimeoutError from enviroplus import gas from subprocess import PIPE, Popen from PIL import Image @@ -27,7 +26,7 @@ level=logging.INFO, datefmt='%Y-%m-%d %H:%M:%S') -logging.info("""all-in-one.py - Displays readings from all of Enviro plus' sensors +logging.info("""combined.py - Displays readings from all of Enviro plus' sensors Press Ctrl+C to exit! @@ -107,23 +106,23 @@ # with NO WARRANTY. The authors of this example code claim # NO RESPONSIBILITY if reliance on the following values or this # code in general leads to ANY DAMAGES or DEATH. -limits = [[4,18,28,35], - [250,650,1013.25,1015], - [20,30,60,70], - [-1,-1,30000,100000], - [-1,-1,40,50], - [-1,-1,450,550], - [-1,-1,200,300], - [-1,-1,50,100], - [-1,-1,50,100], - [-1,-1,50,100]] +limits = [[4, 18, 28, 35], + [250, 650, 1013.25, 1015], + [20, 30, 60, 70], + [-1, -1, 30000, 100000], + [-1, -1, 40, 50], + [-1, -1, 450, 550], + [-1, -1, 200, 300], + [-1, -1, 50, 100], + [-1, -1, 50, 100], + [-1, -1, 50, 100]] # RGB palette for values on the combined screen -palette = [(0,0,255), # Dangerously Low - (0,255,255), # Low - (0,255,0), # Normal - (255,255,0), # High - (255,0,0)] # Dangerously High +palette = [(0, 0, 255), # Dangerously Low + (0, 255, 255), # Low + (0, 255, 0), # Normal + (255, 255, 0), # High + (255, 0, 0)] # Dangerously High values = {} @@ -133,8 +132,9 @@ def display_text(variable, data, unit): # Maintain length of list values[variable] = values[variable][1:] + [data] # Scale the values for the variable between 0 and 1 - colours = [(v - min(values[variable]) + 1) / (max(values[variable]) - - min(values[variable]) + 1) for v in values[variable]] + vmin = min(values[variable]) + vmax = max(values[variable]) + colours = [(v - vmin + 1) / (vmax - vmin + 1) for v in values[variable]] # Format the variable name and value message = "{}: {:.1f} {}".format(variable[:4], data, unit) logging.info(message) @@ -142,18 +142,17 @@ def display_text(variable, data, unit): for i in range(len(colours)): # Convert the values to colours from red to blue colour = (1.0 - colours[i]) * 0.6 - r, g, b = [int(x * 255.0) for x in colorsys.hsv_to_rgb(colour, - 1.0, 1.0)] + r, g, b = [int(x * 255.0) for x in colorsys.hsv_to_rgb(colour, 1.0, 1.0)] # Draw a 1-pixel wide rectangle of colour - draw.rectangle((i, top_pos, i+1, HEIGHT), (r, g, b)) + draw.rectangle((i, top_pos, i + 1, HEIGHT), (r, g, b)) # Draw a line graph in black - line_y = HEIGHT - (top_pos + (colours[i] * (HEIGHT - top_pos)))\ - + top_pos - draw.rectangle((i, line_y, i+1, line_y+1), (0, 0, 0)) + line_y = HEIGHT - (top_pos + (colours[i] * (HEIGHT - top_pos))) + top_pos + draw.rectangle((i, line_y, i + 1, line_y + 1), (0, 0, 0)) # Write the text at the top in black draw.text((0, 0), message, font=font, fill=(0, 0, 0)) st7735.display(img) + # Saves the data to be used in the graphs later and prints to the log def save_data(idx, data): variable = variables[idx] @@ -168,24 +167,23 @@ def save_data(idx, data): def display_everything(): draw.rectangle((0, 0, WIDTH, HEIGHT), (0, 0, 0)) column_count = 2 - row_count = (len(variables)/column_count) + row_count = (len(variables) / column_count) for i in range(len(variables)): variable = variables[i] data_value = values[variable][-1] unit = units[i] - x = x_offset + ((WIDTH/column_count) * (i / row_count)) - y = y_offset + ((HEIGHT/row_count) * (i % row_count)) + x = x_offset + ((WIDTH // column_count) * (i // row_count)) + y = y_offset + ((HEIGHT / row_count) * (i % row_count)) message = "{}: {:.1f} {}".format(variable[:4], data_value, unit) lim = limits[i] rgb = palette[0] for j in range(len(lim)): if data_value > lim[j]: - rgb = palette[j+1] + rgb = palette[j + 1] draw.text((x, y), message, font=smallfont, fill=rgb) st7735.display(img) - # Get the temperature of the CPU for compensation def get_cpu_temperature(): process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE, universal_newlines=True) @@ -193,156 +191,158 @@ def get_cpu_temperature(): return float(output[output.index('=') + 1:output.rindex("'")]) -# Tuning factor for compensation. Decrease this number to adjust the -# temperature down, and increase to adjust up -factor = 2.25 - -cpu_temps = [get_cpu_temperature()] * 5 - -delay = 0.5 # Debounce the proximity tap -mode = 10 # The starting mode -last_page = 0 -light = 1 +def main(): + # Tuning factor for compensation. Decrease this number to adjust the + # temperature down, and increase to adjust up + factor = 2.25 + + cpu_temps = [get_cpu_temperature()] * 5 + + delay = 0.5 # Debounce the proximity tap + mode = 10 # The starting mode + last_page = 0 + + for v in variables: + values[v] = [1] * WIDTH + + # The main loop + try: + while True: + proximity = ltr559.get_proximity() + + # If the proximity crosses the threshold, toggle the mode + if proximity > 1500 and time.time() - last_page > delay: + mode += 1 + mode %= (len(variables) + 1) + last_page = time.time() + + # One mode for each variable + if mode == 0: + # variable = "temperature" + unit = "C" + cpu_temp = get_cpu_temperature() + # Smooth out with some averaging to decrease jitter + cpu_temps = cpu_temps[1:] + [cpu_temp] + avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps)) + raw_temp = bme280.get_temperature() + data = raw_temp - ((avg_cpu_temp - raw_temp) / factor) + display_text(variables[mode], data, unit) -for v in variables: - values[v] = [1] * WIDTH + if mode == 1: + # variable = "pressure" + unit = "hPa" + data = bme280.get_pressure() + display_text(variables[mode], data, unit) -# The main loop -try: - while True: - proximity = ltr559.get_proximity() - - # If the proximity crosses the threshold, toggle the mode - if proximity > 1500 and time.time() - last_page > delay: - mode += 1 - mode %= (len(variables)+1) - last_page = time.time() - - # One mode for each variable - if mode == 0: - # variable = "temperature" - unit = "C" - cpu_temp = get_cpu_temperature() - # Smooth out with some averaging to decrease jitter - cpu_temps = cpu_temps[1:] + [cpu_temp] - avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps)) - raw_temp = bme280.get_temperature() - data = raw_temp - ((avg_cpu_temp - raw_temp) / factor) - display_text(variables[mode], data, unit) - - if mode == 1: - # variable = "pressure" - unit = "hPa" - data = bme280.get_pressure() - display_text(variables[mode], data, unit) - - if mode == 2: - # variable = "humidity" - unit = "%" - data = bme280.get_humidity() - display_text(variables[mode], data, unit) - - if mode == 3: - # variable = "light" - unit = "Lux" - if proximity < 10: - data = ltr559.get_lux() - else: - data = 1 - display_text(variables[mode], data, unit) - - if mode == 4: - # variable = "oxidised" - unit = "kO" - data = gas.read_all() - data = data.oxidising / 1000 - display_text(variables[mode], data, unit) - - if mode == 5: - # variable = "reduced" - unit = "kO" - data = gas.read_all() - data = data.reducing / 1000 - display_text(variables[mode], data, unit) - - if mode == 6: - # variable = "nh3" - unit = "kO" - data = gas.read_all() - data = data.nh3 / 1000 - display_text(variables[mode], data, unit) - - if mode == 7: - # variable = "pm1" - unit = "ug/m3" - try: - data = pms5003.read() - except pmsReadTimeoutError: - logging.warn("Failed to read PMS5003") - else: - data = float(data.pm_ug_per_m3(1.0)) + if mode == 2: + # variable = "humidity" + unit = "%" + data = bme280.get_humidity() display_text(variables[mode], data, unit) - if mode == 8: - # variable = "pm25" - unit = "ug/m3" - try: - data = pms5003.read() - except pmsReadTimeoutError: - logging.warn("Failed to read PMS5003") - else: - data = float(data.pm_ug_per_m3(2.5)) + if mode == 3: + # variable = "light" + unit = "Lux" + if proximity < 10: + data = ltr559.get_lux() + else: + data = 1 display_text(variables[mode], data, unit) - if mode == 9: - # variable = "pm10" - unit = "ug/m3" - try: - data = pms5003.read() - except pmsReadTimeoutError: - logging.warn("Failed to read PMS5003") - else: - data = float(data.pm_ug_per_m3(10)) + if mode == 4: + # variable = "oxidised" + unit = "kO" + data = gas.read_all() + data = data.oxidising / 1000 display_text(variables[mode], data, unit) - if mode == 10: - # Everything on one screen - cpu_temp = get_cpu_temperature() - # Smooth out with some averaging to decrease jitter - cpu_temps = cpu_temps[1:] + [cpu_temp] - avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps)) - raw_temp = bme280.get_temperature() - raw_data = raw_temp - ((avg_cpu_temp - raw_temp) / factor) - save_data(0, raw_data) - display_everything() - raw_data = bme280.get_pressure() - save_data(1, raw_data) - display_everything() - raw_data = bme280.get_humidity() - save_data(2, raw_data) - if proximity < 10: - raw_data = ltr559.get_lux() - else: - raw_data = 1 - save_data(3, raw_data) - display_everything() - gas_data = gas.read_all() - save_data(4, gas_data.oxidising / 1000) - save_data(5, gas_data.reducing / 1000) - save_data(6, gas_data.nh3 / 1000) - display_everything() - pms_data = None - try: - pms_data = pms5003.read() - except pmsReadTimeoutError: - logging.warn("Failed to read PMS5003") - else: - save_data(7, float(pms_data.pm_ug_per_m3(1.0))) - save_data(8, float(pms_data.pm_ug_per_m3(2.5))) - save_data(9, float(pms_data.pm_ug_per_m3(10))) - display_everything() + if mode == 5: + # variable = "reduced" + unit = "kO" + data = gas.read_all() + data = data.reducing / 1000 + display_text(variables[mode], data, unit) + if mode == 6: + # variable = "nh3" + unit = "kO" + data = gas.read_all() + data = data.nh3 / 1000 + display_text(variables[mode], data, unit) -# Exit cleanly -except KeyboardInterrupt: - sys.exit(0) + if mode == 7: + # variable = "pm1" + unit = "ug/m3" + try: + data = pms5003.read() + except pmsReadTimeoutError: + logging.warning("Failed to read PMS5003") + else: + data = float(data.pm_ug_per_m3(1.0)) + display_text(variables[mode], data, unit) + + if mode == 8: + # variable = "pm25" + unit = "ug/m3" + try: + data = pms5003.read() + except pmsReadTimeoutError: + logging.warning("Failed to read PMS5003") + else: + data = float(data.pm_ug_per_m3(2.5)) + display_text(variables[mode], data, unit) + + if mode == 9: + # variable = "pm10" + unit = "ug/m3" + try: + data = pms5003.read() + except pmsReadTimeoutError: + logging.warning("Failed to read PMS5003") + else: + data = float(data.pm_ug_per_m3(10)) + display_text(variables[mode], data, unit) + if mode == 10: + # Everything on one screen + cpu_temp = get_cpu_temperature() + # Smooth out with some averaging to decrease jitter + cpu_temps = cpu_temps[1:] + [cpu_temp] + avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps)) + raw_temp = bme280.get_temperature() + raw_data = raw_temp - ((avg_cpu_temp - raw_temp) / factor) + save_data(0, raw_data) + display_everything() + raw_data = bme280.get_pressure() + save_data(1, raw_data) + display_everything() + raw_data = bme280.get_humidity() + save_data(2, raw_data) + if proximity < 10: + raw_data = ltr559.get_lux() + else: + raw_data = 1 + save_data(3, raw_data) + display_everything() + gas_data = gas.read_all() + save_data(4, gas_data.oxidising / 1000) + save_data(5, gas_data.reducing / 1000) + save_data(6, gas_data.nh3 / 1000) + display_everything() + pms_data = None + try: + pms_data = pms5003.read() + except (SerialTimeoutError, pmsReadTimeoutError): + logging.warning("Failed to read PMS5003") + else: + save_data(7, float(pms_data.pm_ug_per_m3(1.0))) + save_data(8, float(pms_data.pm_ug_per_m3(2.5))) + save_data(9, float(pms_data.pm_ug_per_m3(10))) + display_everything() + + # Exit cleanly + except KeyboardInterrupt: + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/examples/luftdaten.py b/examples/luftdaten.py index d2d6562..84f1117 100755 --- a/examples/luftdaten.py +++ b/examples/luftdaten.py @@ -115,32 +115,35 @@ def send_to_luftdaten(values, id): pm_values = dict(i for i in values.items() if i[0].startswith("P")) temp_values = dict(i for i in values.items() if not i[0].startswith("P")) - resp_1 = requests.post("https://api.luftdaten.info/v1/push-sensor-data/", - json={ - "software_version": "enviro-plus 0.0.1", - "sensordatavalues": [{"value_type": key, "value": val} for - key, val in pm_values.items()] - }, - headers={ - "X-PIN": "1", - "X-Sensor": id, - "Content-Type": "application/json", - "cache-control": "no-cache" - } + pm_values_json = [{"value_type": key, "value": val} for key, val in pm_values.items()] + temp_values_json = [{"value_type": key, "value": val} for key, val in temp_values.items()] + + resp_1 = requests.post( + "https://api.luftdaten.info/v1/push-sensor-data/", + json={ + "software_version": "enviro-plus 0.0.1", + "sensordatavalues": pm_values_json + }, + headers={ + "X-PIN": "1", + "X-Sensor": id, + "Content-Type": "application/json", + "cache-control": "no-cache" + } ) - resp_2 = requests.post("https://api.luftdaten.info/v1/push-sensor-data/", - json={ - "software_version": "enviro-plus 0.0.1", - "sensordatavalues": [{"value_type": key, "value": val} for - key, val in temp_values.items()] - }, - headers={ - "X-PIN": "11", - "X-Sensor": id, - "Content-Type": "application/json", - "cache-control": "no-cache" - } + resp_2 = requests.post( + "https://api.luftdaten.info/v1/push-sensor-data/", + json={ + "software_version": "enviro-plus 0.0.1", + "sensordatavalues": temp_values_json + }, + headers={ + "X-PIN": "11", + "X-Sensor": id, + "Content-Type": "application/json", + "cache-control": "no-cache" + } ) if resp_1.ok and resp_2.ok: diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py new file mode 100755 index 0000000..0eff47a --- /dev/null +++ b/examples/mqtt-all.py @@ -0,0 +1,238 @@ +""" +Run mqtt broker on localhost: sudo apt-get install mosquitto mosquitto-clients + +Example run: python3 mqtt-all.py --broker 192.168.1.164 --topic enviro +""" +#!/usr/bin/env python3 + +import argparse +import ST7735 +import time +from bme280 import BME280 +from pms5003 import PMS5003, ReadTimeoutError, SerialTimeoutError +from enviroplus import gas + +try: + # Transitional fix for breaking change in LTR559 + from ltr559 import LTR559 + + ltr559 = LTR559() +except ImportError: + import ltr559 + +from subprocess import PIPE, Popen, check_output +from PIL import Image, ImageDraw, ImageFont +from fonts.ttf import RobotoMedium as UserFont +import json + +import paho.mqtt.client as mqtt +import paho.mqtt.publish as publish + +try: + from smbus2 import SMBus +except ImportError: + from smbus import SMBus + + +DEFAULT_MQTT_BROKER_IP = "localhost" +DEFAULT_MQTT_BROKER_PORT = 1883 +DEFAULT_MQTT_TOPIC = "enviroplus" +DEFAULT_READ_INTERVAL = 5 + +# mqtt callbacks +def on_connect(client, userdata, flags, rc): + if rc == 0: + print("connected OK") + else: + print("Bad connection Returned code=", rc) + + +def on_publish(client, userdata, mid): + print("mid: " + str(mid)) + + +# Read values from BME280 and return as dict +def read_bme280(bme280): + # Compensation factor for temperature + comp_factor = 2.25 + values = {} + cpu_temp = get_cpu_temperature() + raw_temp = bme280.get_temperature() # float + comp_temp = raw_temp - ((cpu_temp - raw_temp) / comp_factor) + values["temperature"] = int(comp_temp) + values["pressure"] = round( + int(bme280.get_pressure() * 100), -1 + ) # round to nearest 10 + values["humidity"] = int(bme280.get_humidity()) + data = gas.read_all() + values["oxidised"] = int(data.oxidising / 1000) + values["reduced"] = int(data.reducing / 1000) + values["nh3"] = int(data.nh3 / 1000) + values["lux"] = int(ltr559.get_lux()) + return values + + +# Read values PMS5003 and return as dict +def read_pms5003(pms5003): + values = {} + try: + pm_values = pms5003.read() # int + values["pm1"] = pm_values.pm_ug_per_m3(1) + values["pm25"] = pm_values.pm_ug_per_m3(2.5) + values["pm10"] = pm_values.pm_ug_per_m3(10) + except ReadTimeoutError: + pms5003.reset() + pm_values = pms5003.read() + values["pm1"] = pm_values.pm_ug_per_m3(1) + values["pm25"] = pm_values.pm_ug_per_m3(2.5) + values["pm10"] = pm_values.pm_ug_per_m3(10) + return values + + +# Get CPU temperature to use for compensation +def get_cpu_temperature(): + process = Popen( + ["vcgencmd", "measure_temp"], stdout=PIPE, universal_newlines=True + ) + output, _error = process.communicate() + return float(output[output.index("=") + 1 : output.rindex("'")]) + + +# Get Raspberry Pi serial number to use as ID +def get_serial_number(): + with open("/proc/cpuinfo", "r") as f: + for line in f: + if line[0:6] == "Serial": + return line.split(":")[1].strip() + + +# Check for Wi-Fi connection +def check_wifi(): + if check_output(["hostname", "-I"]): + return True + else: + return False + + +# Display Raspberry Pi serial and Wi-Fi status on LCD +def display_status(disp, mqtt_broker): + # Width and height to calculate text position + WIDTH = disp.width + HEIGHT = disp.height + # Text settings + font_size = 12 + font = ImageFont.truetype(UserFont, font_size) + + wifi_status = "connected" if check_wifi() else "disconnected" + text_colour = (255, 255, 255) + back_colour = (0, 170, 170) if check_wifi() else (85, 15, 15) + device_serial_number = get_serial_number() + message = "{}\nWi-Fi: {}\nmqtt-broker: {}".format( + device_serial_number, wifi_status, mqtt_broker + ) + img = Image.new("RGB", (WIDTH, HEIGHT), color=(0, 0, 0)) + draw = ImageDraw.Draw(img) + size_x, size_y = draw.textsize(message, font) + x = (WIDTH - size_x) / 2 + y = (HEIGHT / 2) - (size_y / 2) + draw.rectangle((0, 0, 160, 80), back_colour) + draw.text((x, y), message, font=font, fill=text_colour) + disp.display(img) + + +def main(): + parser = argparse.ArgumentParser( + description="Publish enviroplus values over mqtt" + ) + parser.add_argument( + "--broker", + default=DEFAULT_MQTT_BROKER_IP, + type=str, + help="mqtt broker IP", + ) + parser.add_argument( + "--port", + default=DEFAULT_MQTT_BROKER_PORT, + type=int, + help="mqtt broker port", + ) + parser.add_argument( + "--topic", default=DEFAULT_MQTT_TOPIC, type=str, help="mqtt topic" + ) + parser.add_argument( + "--interval", + default=DEFAULT_READ_INTERVAL, + type=int, + help="the read interval in seconds", + ) + args = parser.parse_args() + + # Raspberry Pi ID + device_serial_number = get_serial_number() + device_id = "raspi-" + device_serial_number + + print( + f"""mqtt-all.py - Reads Enviro plus data and sends over mqtt. + + broker: {args.broker} + client_id: {device_id} + port: {args.port} + topic: {args.topic} + + Press Ctrl+C to exit! + + """ + ) + + mqtt_client = mqtt.Client(client_id=device_id) + mqtt_client.on_connect = on_connect + mqtt_client.on_publish = on_publish + mqtt_client.connect(args.broker, port=args.port) + + bus = SMBus(1) + + # Create BME280 instance + bme280 = BME280(i2c_dev=bus) + + # Create LCD instance + disp = ST7735.ST7735( + port=0, cs=1, dc=9, backlight=12, rotation=270, spi_speed_hz=10000000 + ) + + # Initialize display + disp.begin() + + # Try to create PMS5003 instance + HAS_PMS = False + try: + pms5003 = PMS5003() + pm_values = pms5003.read() + HAS_PMS = True + print("PMS5003 sensor is connected") + except SerialTimeoutError: + print("No PMS5003 sensor connected") + + # Display Raspberry Pi serial and Wi-Fi status + print("RPi serial: {}".format(device_serial_number)) + print("Wi-Fi: {}\n".format("connected" if check_wifi() else "disconnected")) + print("MQTT broker IP: {}".format(args.broker)) + + # Main loop to read data, display, and send over mqtt + mqtt_client.loop_start() + while True: + try: + values = read_bme280(bme280) + if HAS_PMS: + pms_values = read_pms5003(pms5003) + values.update(pms_values) + values["serial"] = device_serial_number + print(values) + mqtt_client.publish(args.topic, json.dumps(values)) + display_status(disp, args.broker) + time.sleep(args.interval) + except Exception as e: + print(e) + + +if __name__ == "__main__": + main() diff --git a/examples/noise-amps-at-freqs.py b/examples/noise-amps-at-freqs.py index 8b1ddd5..4c14c58 100755 --- a/examples/noise-amps-at-freqs.py +++ b/examples/noise-amps-at-freqs.py @@ -15,11 +15,11 @@ noise = Noise() disp = ST7735.ST7735( - port=0, - cs=ST7735.BG_SPI_CS_FRONT, - dc=9, - backlight=12, - rotation=90) + port=0, + cs=ST7735.BG_SPI_CS_FRONT, + dc=9, + backlight=12, + rotation=90) disp.begin() diff --git a/examples/noise-profile.py b/examples/noise-profile.py index 1afdff5..4084439 100755 --- a/examples/noise-profile.py +++ b/examples/noise-profile.py @@ -13,11 +13,11 @@ noise = Noise() disp = ST7735.ST7735( - port=0, - cs=ST7735.BG_SPI_CS_FRONT, - dc=9, - backlight=12, - rotation=90) + port=0, + cs=ST7735.BG_SPI_CS_FRONT, + dc=9, + backlight=12, + rotation=90) disp.begin() diff --git a/examples/weather-and-light.py b/examples/weather-and-light.py index a26ec7b..cd8ae96 100755 --- a/examples/weather-and-light.py +++ b/examples/weather-and-light.py @@ -1,4 +1,7 @@ #!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +f"Sorry! This program requires Python >= 3.6 😅" import os import time @@ -109,19 +112,19 @@ def sun_moon_time(city_name, time_zone): if sunrise_today < local_dt < sunset_today: day = True period = sunset_today - sunrise_today - mid = sunrise_today + (period / 2) + # mid = sunrise_today + (period / 2) progress = local_dt - sunrise_today elif local_dt > sunset_today: day = False period = sunrise_tomorrow - sunset_today - mid = sunset_today + (period / 2) + # mid = sunset_today + (period / 2) progress = local_dt - sunset_today else: day = False period = sunrise_today - sunset_yesterday - mid = sunset_yesterday + (period / 2) + # mid = sunset_yesterday + (period / 2) progress = local_dt - sunset_yesterday # Convert time deltas to seconds @@ -151,7 +154,7 @@ def draw_background(progress, period, day): # New image for background colour img = Image.new('RGBA', (WIDTH, HEIGHT), color=background) - draw = ImageDraw.Draw(img) + # draw = ImageDraw.Draw(img) # New image for sun/moon overlay overlay = Image.new('RGBA', (WIDTH, HEIGHT), color=(0, 0, 0, 0)) @@ -216,12 +219,12 @@ def analyse_pressure(pressure, t): slope = line[0][0] intercept = line[0][1] variance = numpy.var(pressure_vals) - residuals = numpy.var([(slope * x + intercept - y) for x, y in zip(time_vals, pressure_vals)]) + residuals = numpy.var([(slope * x + intercept - y) for x, y in zip(time_vals, pressure_vals)]) r_squared = 1 - residuals / variance # Calculate change in pressure per hour change_per_hour = slope * 60 * 60 - variance_per_hour = variance * 60 * 60 + # variance_per_hour = variance * 60 * 60 mean_pressure = numpy.mean(pressure_vals) @@ -244,10 +247,10 @@ def analyse_pressure(pressure, t): change_per_hour = 0 trend = "-" -# time.sleep(interval) - + # time.sleep(interval) return (mean_pressure, change_per_hour, trend) + def describe_pressure(pressure): """Convert pressure into barometer-type description.""" if pressure < 970: @@ -385,7 +388,7 @@ def describe_light(light): else: range_string = "------" img = overlay_text(img, (68, 18 + spacing), range_string, font_sm, align_right=True, rectangle=True) - temp_icon = Image.open(path + "/icons/temperature.png") + temp_icon = Image.open(f"{path}/icons/temperature.png") img.paste(temp_icon, (margin, 18), mask=temp_icon) # Humidity @@ -396,7 +399,7 @@ def describe_light(light): spacing = font_lg.getsize(humidity_string)[1] + 1 humidity_desc = describe_humidity(corr_humidity).upper() img = overlay_text(img, (68, 48 + spacing), humidity_desc, font_sm, align_right=True, rectangle=True) - humidity_icon = Image.open(path + "/icons/humidity-" + humidity_desc.lower() + ".png") + humidity_icon = Image.open(f"{path}/icons/humidity-{humidity_desc.lower()}.png") img.paste(humidity_icon, (margin, 48), mask=humidity_icon) # Light @@ -406,7 +409,7 @@ def describe_light(light): spacing = font_lg.getsize(light_string.replace(",", ""))[1] + 1 light_desc = describe_light(light).upper() img = overlay_text(img, (WIDTH - margin - 1, 18 + spacing), light_desc, font_sm, align_right=True, rectangle=True) - light_icon = Image.open(path + "/icons/bulb-" + light_desc.lower() + ".png") + light_icon = Image.open(f"{path}/icons/bulb-{light_desc.lower()}.png") img.paste(humidity_icon, (80, 18), mask=light_icon) # Pressure @@ -418,7 +421,7 @@ def describe_light(light): pressure_desc = describe_pressure(mean_pressure).upper() spacing = font_lg.getsize(pressure_string.replace(",", ""))[1] + 1 img = overlay_text(img, (WIDTH - margin - 1, 48 + spacing), pressure_desc, font_sm, align_right=True, rectangle=True) - pressure_icon = Image.open(path + "/icons/weather-" + pressure_desc.lower() + ".png") + pressure_icon = Image.open(f"{path}/icons/weather-{pressure_desc.lower()}.png") img.paste(pressure_icon, (80, 48), mask=pressure_icon) # Display image diff --git a/install.sh b/install.sh index 3e5b898..6837697 100755 --- a/install.sh +++ b/install.sh @@ -1,7 +1,7 @@ #!/bin/bash CONFIG=/boot/config.txt -DATESTAMP=`date "+%Y-%M-%d-%H-%M-%S"` +DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` CONFIG_BACKUP=false APT_HAS_UPDATED=false USER_HOME=/home/$SUDO_USER diff --git a/library/CHANGELOG.txt b/library/CHANGELOG.txt index 0f98d12..81d4136 100644 --- a/library/CHANGELOG.txt +++ b/library/CHANGELOG.txt @@ -1,3 +1,15 @@ +0.0.3 +----- + +* Fix "self.noise_floor" bug in get_noise_profile + +0.0.2 +----- + +* Add support for extra ADC channel in Gas +* Handle breaking change in new ltr559 library +* Add Noise functionality + 0.0.1 ----- diff --git a/library/README.rst b/library/README.rst index f90e072..bd74b9d 100644 --- a/library/README.rst +++ b/library/README.rst @@ -1,29 +1,92 @@ -Enviro+ pHAT -============ +Enviro+ +======= -`Build Status `__ -`Coverage -Status `__ -`PyPi Package `__ `Python -Versions `__ +Designed for environmental monitoring, Enviro+ lets you measure air +quality (pollutant gases and particulates), temperature, pressure, +humidity, light, and noise level. Learn more - +https://shop.pimoroni.com/products/enviro-plus + +|Build Status| |Coverage Status| |PyPi Package| |Python Versions| Installing ========== -Stable library from PyPi: +You're best using the "One-line" install method if you want all of the +UART serial configuration for the PMS5003 particulate matter sensor to +run automatically. + +One-line (Installs from GitHub) +------------------------------- + +:: -- Just run ``sudo pip install enviroplus`` + curl -sSL https://get.pimoroni.com/enviroplus | bash -(**Note** that you’re best using the git clone / install.sh method below -if you want all of the UART serial configuration for the PMS5003 -particulate matter sensor to run automatically) +**Note** report issues with one-line installer here: +https://github.com/pimoroni/get -Latest/development library from GitHub: +Or... Install and configure dependencies from GitHub: +----------------------------------------------------- - ``git clone https://github.com/pimoroni/enviroplus-python`` - ``cd enviroplus-python`` - ``sudo ./install.sh`` +**Note** Raspbian Lite users may first need to install git: +``sudo apt install git`` + +Or... Install from PyPi and configure manually: +----------------------------------------------- + +- Run ``sudo pip install enviroplus`` + +**Note** this wont perform any of the required configuration changes on +your Pi, you may additionally need to: + +- Enable i2c: ``raspi-config nonint do_i2c 0`` +- Enable SPI: ``raspi-config nonint do_spi 0`` + +And if you're using a PMS5003 sensor you will need to: + +- Enable serial: + ``raspi-config nonint set_config_var enable_uart 1 /boot/config.txt`` +- Disable serial terminal: ``sudo raspi-config nonint do_serial 1`` +- Add ``dtoverlay=pi3-miniuart-bt`` to your ``/boot/config.txt`` + +And install additional dependencies: + +:: + + sudo apt install python-numpy python-smbus python-pil python-setuptools + +Help & Support +-------------- + +- GPIO Pinout - https://pinout.xyz/pinout/enviro\_plus +- Support forums - http://forums.pimoroni.com/c/support +- Discord - https://discord.gg/hr93ByC + +.. |Build Status| image:: https://travis-ci.com/pimoroni/enviroplus-python.svg?branch=master + :target: https://travis-ci.com/pimoroni/enviroplus-python +.. |Coverage Status| image:: https://coveralls.io/repos/github/pimoroni/enviroplus-python/badge.svg?branch=master + :target: https://coveralls.io/github/pimoroni/enviroplus-python?branch=master +.. |PyPi Package| image:: https://img.shields.io/pypi/v/enviroplus.svg + :target: https://pypi.python.org/pypi/enviroplus +.. |Python Versions| image:: https://img.shields.io/pypi/pyversions/enviroplus.svg + :target: https://pypi.python.org/pypi/enviroplus + +0.0.3 +----- + +* Fix "self.noise_floor" bug in get_noise_profile + +0.0.2 +----- + +* Add support for extra ADC channel in Gas +* Handle breaking change in new ltr559 library +* Add Noise functionality + 0.0.1 ----- diff --git a/library/enviroplus/__init__.py b/library/enviroplus/__init__.py index b8023d8..ffcc925 100644 --- a/library/enviroplus/__init__.py +++ b/library/enviroplus/__init__.py @@ -1 +1 @@ -__version__ = '0.0.1' +__version__ = '0.0.3' diff --git a/library/enviroplus/noise.py b/library/enviroplus/noise.py index 2e7472d..7b6d5e2 100644 --- a/library/enviroplus/noise.py +++ b/library/enviroplus/noise.py @@ -73,10 +73,10 @@ def get_noise_profile(self, high_start = mid_start + int(sample_count * mid) noise_ceiling = high_start + int(sample_count * high) - amp_low = numpy.mean(magnitude[self.noise_floor:mid_start]) + amp_low = numpy.mean(magnitude[noise_floor:mid_start]) amp_mid = numpy.mean(magnitude[mid_start:high_start]) amp_high = numpy.mean(magnitude[high_start:noise_ceiling]) - amp_total = (low + mid + high) / 3.0 + amp_total = (amp_low + amp_mid + amp_high) / 3.0 return amp_low, amp_mid, amp_high, amp_total diff --git a/library/setup.cfg b/library/setup.cfg index 362646d..c59250c 100644 --- a/library/setup.cfg +++ b/library/setup.cfg @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- [metadata] name = enviroplus -version = 0.0.1 +version = 0.0.3 author = Philip Howard author_email = phil@pimoroni.com -description = Enviro pHAT Plus environmental monitoring add-on for Raspberry Pi" +description = Enviro pHAT Plus environmental monitoring add-on for Raspberry Pi long_description = file: README.rst keywords = Raspberry Pi url = https://www.pimoroni.com @@ -19,7 +19,6 @@ classifiers = Operating System :: POSIX :: Linux License :: OSI Approved :: MIT License Intended Audience :: Developers - Programming Language :: Python :: 2.6 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Topic :: Software Development @@ -27,17 +26,19 @@ classifiers = Topic :: System :: Hardware [options] +packages = enviroplus install_requires = pimoroni-bme280 pms5003 ltr559 st7735 ads1015 - fonts - font-roboto - astral - pytz + fonts + font-roboto + astral + pytz sounddevice + paho-mqtt [flake8] exclude = @@ -56,12 +57,18 @@ py2deps = python-numpy python-smbus python-pil + python-cffi + python-spidev + python-rpi.gpio libportaudio2 py3deps = python3-pip python3-numpy python3-smbus python3-pil + python3-cffi + python3-spidev + python3-rpi.gpio libportaudio2 configtxt = dtoverlay=pi3-miniuart-bt diff --git a/library/setup.py b/library/setup.py index 784db51..40d6dbc 100755 --- a/library/setup.py +++ b/library/setup.py @@ -30,7 +30,4 @@ if parse_version(__version__) < minimum_version: raise RuntimeError("Package setuptools must be at least version {}".format(minimum_version)) -setup( - packages=['enviroplus'], - install_requires=['setuptools>={}'.format(minimum_version), 'pimoroni-bme280', 'pms5003', 'ltr559', 'st7735', 'ads1015', 'fonts', 'font-roboto', 'astral', 'pytz'] -) +setup() diff --git a/library/tests/conftest.py b/library/tests/conftest.py new file mode 100644 index 0000000..8a5c54c --- /dev/null +++ b/library/tests/conftest.py @@ -0,0 +1,90 @@ +"""Test configuration. +These allow the mocking of various Python modules +that might otherwise have runtime side-effects. +""" +import sys +import mock +import pytest +from i2cdevice import MockSMBus + + +class SMBusFakeDevice(MockSMBus): + def __init__(self, i2c_bus): + MockSMBus.__init__(self, i2c_bus) + self.regs[0x00:0x01] = 0x0f, 0x00 + + +@pytest.fixture(scope='function', autouse=True) +def cleanup(): + yield None + try: + del sys.modules['enviroplus'] + except KeyError: + pass + try: + del sys.modules['enviroplus.noise'] + except KeyError: + pass + try: + del sys.modules['enviroplus.gas'] + except KeyError: + pass + + +@pytest.fixture(scope='function', autouse=False) +def GPIO(): + """Mock RPi.GPIO module.""" + GPIO = mock.MagicMock() + # Fudge for Python < 37 (possibly earlier) + sys.modules['RPi'] = mock.Mock() + sys.modules['RPi'].GPIO = GPIO + sys.modules['RPi.GPIO'] = GPIO + yield GPIO + del sys.modules['RPi'] + del sys.modules['RPi.GPIO'] + + +@pytest.fixture(scope='function', autouse=False) +def spidev(): + """Mock spidev module.""" + spidev = mock.MagicMock() + sys.modules['spidev'] = spidev + yield spidev + del sys.modules['spidev'] + + +@pytest.fixture(scope='function', autouse=False) +def smbus(): + """Mock smbus module.""" + smbus = mock.MagicMock() + smbus.SMBus = SMBusFakeDevice + sys.modules['smbus'] = smbus + yield smbus + del sys.modules['smbus'] + + +@pytest.fixture(scope='function', autouse=False) +def atexit(): + """Mock atexit module.""" + atexit = mock.MagicMock() + sys.modules['atexit'] = atexit + yield atexit + del sys.modules['atexit'] + + +@pytest.fixture(scope='function', autouse=False) +def sounddevice(): + """Mock sounddevice module.""" + sounddevice = mock.MagicMock() + sys.modules['sounddevice'] = sounddevice + yield sounddevice + del sys.modules['sounddevice'] + + +@pytest.fixture(scope='function', autouse=False) +def numpy(): + """Mock numpy module.""" + numpy = mock.MagicMock() + sys.modules['numpy'] = numpy + yield numpy + del sys.modules['numpy'] diff --git a/library/tests/test_noise.py b/library/tests/test_noise.py new file mode 100644 index 0000000..3778c16 --- /dev/null +++ b/library/tests/test_noise.py @@ -0,0 +1,48 @@ +import pytest + + +def test_noise_setup(sounddevice, numpy): + from enviroplus.noise import Noise + + noise = Noise(sample_rate=16000, duration=0.1) + del noise + + +def test_noise_get_amplitudes_at_frequency_ranges(sounddevice, numpy): + from enviroplus.noise import Noise + + noise = Noise(sample_rate=16000, duration=0.1) + noise.get_amplitudes_at_frequency_ranges([ + (100, 500), + (501, 1000) + ]) + + sounddevice.rec.assert_called_with(0.1 * 16000, samplerate=16000, blocking=True, channels=1, dtype='float64') + + +def test_noise_get_noise_profile(sounddevice, numpy): + from enviroplus.noise import Noise + + numpy.mean.return_value = 10.0 + + noise = Noise(sample_rate=16000, duration=0.1) + amp_low, amp_mid, amp_high, amp_total = noise.get_noise_profile( + noise_floor=100, + low=0.12, + mid=0.36, + high=None) + + sounddevice.rec.assert_called_with(0.1 * 16000, samplerate=16000, blocking=True, channels=1, dtype='float64') + + assert amp_total == 10.0 + + +def test_get_amplitude_at_frequency_range(sounddevice, numpy): + from enviroplus.noise import Noise + + noise = Noise(sample_rate=16000, duration=0.1) + + noise.get_amplitude_at_frequency_range(0, 8000) + + with pytest.raises(ValueError): + noise.get_amplitude_at_frequency_range(0, 16000) diff --git a/library/tests/test_setup.py b/library/tests/test_setup.py index 7c25d94..2aa7b49 100644 --- a/library/tests/test_setup.py +++ b/library/tests/test_setup.py @@ -1,32 +1,11 @@ -import sys -import mock -from i2cdevice import MockSMBus - - -class SMBusFakeDevice(MockSMBus): - def __init__(self, i2c_bus): - MockSMBus.__init__(self, i2c_bus) - self.regs[0x00:0x01] = 0x0f, 0x00 - - -def test_gas_setup(): - sys.modules['RPi'] = mock.Mock() - sys.modules['RPi.GPIO'] = mock.Mock() - smbus = mock.Mock() - smbus.SMBus = SMBusFakeDevice - sys.modules['smbus'] = smbus +def test_gas_setup(GPIO, smbus): from enviroplus import gas gas._is_setup = False gas.setup() gas.setup() -def test_gas_read_all(): - sys.modules['RPi'] = mock.Mock() - sys.modules['RPi.GPIO'] = mock.Mock() - smbus = mock.Mock() - smbus.SMBus = SMBusFakeDevice - sys.modules['smbus'] = smbus +def test_gas_read_all(GPIO, smbus): from enviroplus import gas gas._is_setup = False result = gas.read_all() @@ -43,12 +22,7 @@ def test_gas_read_all(): assert "Oxidising" in str(result) -def test_gas_read_each(): - sys.modules['RPi'] = mock.Mock() - sys.modules['RPi.GPIO'] = mock.Mock() - smbus = mock.Mock() - smbus.SMBus = SMBusFakeDevice - sys.modules['smbus'] = smbus +def test_gas_read_each(GPIO, smbus): from enviroplus import gas gas._is_setup = False @@ -57,12 +31,7 @@ def test_gas_read_each(): assert int(gas.read_nh3()) == 16813 -def test_gas_read_adc(): - sys.modules['RPi'] = mock.Mock() - sys.modules['RPi.GPIO'] = mock.Mock() - smbus = mock.Mock() - smbus.SMBus = SMBusFakeDevice - sys.modules['smbus'] = smbus +def test_gas_read_adc(GPIO, smbus): from enviroplus import gas gas._is_setup = False @@ -71,28 +40,27 @@ def test_gas_read_adc(): assert gas.read_adc() == 0.255 -def test_gas_read_adc_default_gain(): - sys.modules['RPi'] = mock.Mock() - sys.modules['RPi.GPIO'] = mock.Mock() - smbus = mock.Mock() - smbus.SMBus = SMBusFakeDevice - sys.modules['smbus'] = smbus +def test_gas_read_adc_default_gain(GPIO, smbus): from enviroplus import gas gas._is_setup = False gas.enable_adc(True) - assert gas.read_adc() == 0.255 + gas.set_adc_gain(gas.MICS6814_GAIN) + assert gas.read_adc() == 0.765 -def test_gas_read_adc_str(): - sys.modules['RPi'] = mock.Mock() - sys.modules['RPi.GPIO'] = mock.Mock() - smbus = mock.Mock() - smbus.SMBus = SMBusFakeDevice - sys.modules['smbus'] = smbus +def test_gas_read_adc_str(GPIO, smbus): from enviroplus import gas gas._is_setup = False gas.enable_adc(True) gas.set_adc_gain(2.048) assert 'ADC' in str(gas.read_all()) + + +def test_gas_cleanup(GPIO, smbus): + from enviroplus import gas + + gas.cleanup() + + GPIO.output.assert_called_with(gas.MICS6814_HEATER_PIN, 0)