From 6d6d3dd296978d367df80193a9fc842cd79a662d Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Fri, 13 Mar 2020 10:43:39 +0000 Subject: [PATCH 01/17] Prep for v0.0.2 --- library/CHANGELOG.txt | 7 +++++++ library/enviroplus/__init__.py | 2 +- library/setup.cfg | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/library/CHANGELOG.txt b/library/CHANGELOG.txt index 0f98d12..65825f2 100644 --- a/library/CHANGELOG.txt +++ b/library/CHANGELOG.txt @@ -1,3 +1,10 @@ +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..d18f409 100644 --- a/library/enviroplus/__init__.py +++ b/library/enviroplus/__init__.py @@ -1 +1 @@ -__version__ = '0.0.1' +__version__ = '0.0.2' diff --git a/library/setup.cfg b/library/setup.cfg index 362646d..c8a3ace 100644 --- a/library/setup.cfg +++ b/library/setup.cfg @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- [metadata] name = enviroplus -version = 0.0.1 +version = 0.0.2 author = Philip Howard author_email = phil@pimoroni.com description = Enviro pHAT Plus environmental monitoring add-on for Raspberry Pi" From f8d88341a5c4dbc710ada549f5d7a9dc272c4040 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Fri, 13 Mar 2020 10:44:13 +0000 Subject: [PATCH 02/17] Update README for 0.0.2 --- library/README.rst | 84 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 13 deletions(-) diff --git a/library/README.rst b/library/README.rst index f90e072..6bf510d 100644 --- a/library/README.rst +++ b/library/README.rst @@ -1,29 +1,87 @@ -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.2 +----- + +* Add support for extra ADC channel in Gas +* Handle breaking change in new ltr559 library +* Add Noise functionality + 0.0.1 ----- From 97ee1d88e84df79d82c3ca20eb89378c65197f9a Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 16 Mar 2020 11:37:40 +0000 Subject: [PATCH 03/17] Fix noise_floor bug --- library/CHANGELOG.txt | 5 +++++ library/README.rst | 5 +++++ library/enviroplus/__init__.py | 2 +- library/enviroplus/noise.py | 2 +- library/setup.cfg | 14 +++++++++----- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/library/CHANGELOG.txt b/library/CHANGELOG.txt index 65825f2..81d4136 100644 --- a/library/CHANGELOG.txt +++ b/library/CHANGELOG.txt @@ -1,3 +1,8 @@ +0.0.3 +----- + +* Fix "self.noise_floor" bug in get_noise_profile + 0.0.2 ----- diff --git a/library/README.rst b/library/README.rst index 6bf510d..bd74b9d 100644 --- a/library/README.rst +++ b/library/README.rst @@ -75,6 +75,11 @@ Help & Support .. |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 ----- diff --git a/library/enviroplus/__init__.py b/library/enviroplus/__init__.py index d18f409..ffcc925 100644 --- a/library/enviroplus/__init__.py +++ b/library/enviroplus/__init__.py @@ -1 +1 @@ -__version__ = '0.0.2' +__version__ = '0.0.3' diff --git a/library/enviroplus/noise.py b/library/enviroplus/noise.py index 2e7472d..6830bd0 100644 --- a/library/enviroplus/noise.py +++ b/library/enviroplus/noise.py @@ -73,7 +73,7 @@ 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 diff --git a/library/setup.cfg b/library/setup.cfg index c8a3ace..83f89fa 100644 --- a/library/setup.cfg +++ b/library/setup.cfg @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- [metadata] name = enviroplus -version = 0.0.2 +version = 0.0.3 author = Philip Howard author_email = phil@pimoroni.com description = Enviro pHAT Plus environmental monitoring add-on for Raspberry Pi" @@ -33,10 +33,10 @@ install_requires = ltr559 st7735 ads1015 - fonts - font-roboto - astral - pytz + fonts + font-roboto + astral + pytz sounddevice [flake8] @@ -56,12 +56,16 @@ py2deps = python-numpy python-smbus python-pil + python-spidev + python-rpi.gpio libportaudio2 py3deps = python3-pip python3-numpy python3-smbus python3-pil + python3-spidev + python3-rpi.gpio libportaudio2 configtxt = dtoverlay=pi3-miniuart-bt From be4d0fc9022240b5047654523aab877cbaa8a73d Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 17 Mar 2020 11:37:53 +0000 Subject: [PATCH 04/17] Test tweaks and linting I've re-written the tests to use conftest.py to set up and tear down mock modules via fixtures. I have also linted the examples, removing redundant linebreaks, commenting out unused variables and attempting to simplify long lines. --- examples/all-in-one-no-pm.py | 15 +- examples/all-in-one.py | 16 +- examples/combined.py | 344 ++++++++++++++++---------------- examples/luftdaten.py | 51 ++--- examples/noise-amps-at-freqs.py | 10 +- examples/noise-profile.py | 10 +- examples/weather-and-light.py | 24 +-- library/tests/conftest.py | 56 ++++++ library/tests/test_setup.py | 49 +---- 9 files changed, 297 insertions(+), 278 deletions(-) create mode 100644 library/tests/conftest.py 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..6dda607 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) diff --git a/examples/combined.py b/examples/combined.py index c2fd397..4b8fbdd 100755 --- a/examples/combined.py +++ b/examples/combined.py @@ -2,7 +2,6 @@ import time import colorsys -import os import sys import ST7735 try: @@ -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.warn("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.warn("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.warn("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 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() + + # 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/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..bccf7cc 100755 --- a/examples/weather-and-light.py +++ b/examples/weather-and-light.py @@ -109,19 +109,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 +151,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 +216,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 +244,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 +385,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 +396,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 +406,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 +418,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/library/tests/conftest.py b/library/tests/conftest.py new file mode 100644 index 0000000..8a3ffb4 --- /dev/null +++ b/library/tests/conftest.py @@ -0,0 +1,56 @@ +"""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=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'] + diff --git a/library/tests/test_setup.py b/library/tests/test_setup.py index 7c25d94..95080b6 100644 --- a/library/tests/test_setup.py +++ b/library/tests/test_setup.py @@ -1,32 +1,15 @@ 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 +26,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 +35,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,12 +44,7 @@ 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 @@ -84,12 +52,7 @@ def test_gas_read_adc_default_gain(): assert gas.read_adc() == 0.255 -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 From e9c93677beeb6c6416d8f15aa2bd29ce3cd4b726 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 17 Mar 2020 12:31:33 +0000 Subject: [PATCH 05/17] Test noise, fix gas --- library/tests/conftest.py | 17 ++++++++ library/tests/test_noise.py | 78 +++++++++++++++++++++++++++++++++++++ library/tests/test_setup.py | 34 +++++++++++++++- 3 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 library/tests/test_noise.py diff --git a/library/tests/conftest.py b/library/tests/conftest.py index 8a3ffb4..b026172 100644 --- a/library/tests/conftest.py +++ b/library/tests/conftest.py @@ -54,3 +54,20 @@ def 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..c93f8cc --- /dev/null +++ b/library/tests/test_noise.py @@ -0,0 +1,78 @@ +import sys +import mock +import pytest + + +def force_reimport(module): + """Force the module under test to be re-imported. + + Because pytest runs all tests within the same scope (this makes me cry) + we have to do some manual housekeeping to avoid tests polluting each other. + + Since conftest.py already does some sys.modules mangling I see no reason not to + do the same thing here. + """ + if "." in module: + steps = module.split(".") + else: + steps = [module] + + for i in range(len(steps)): + module = ".".join(steps[0:i + 1]) + try: + del sys.modules[module] + except KeyError: + pass + + +def test_noise_setup(sounddevice, numpy): + force_reimport('enviroplus.noise') + 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): + # Ippity zippidy what is this farce + # a curious function that makes my tests pass? + force_reimport('enviroplus.noise') + 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): + # Ippity zippidy what is this farce + # a curious function that makes my tests pass? + force_reimport('enviroplus.noise') + from enviroplus.noise import Noise + + 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') + + +def test_get_amplitude_at_frequency_range(sounddevice, numpy): + # Ippity zippidy what is this farce + # a curious function that makes my tests pass? + force_reimport('enviroplus.noise') + 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 95080b6..6b6658c 100644 --- a/library/tests/test_setup.py +++ b/library/tests/test_setup.py @@ -2,6 +2,28 @@ import mock +def force_reimport(module): + """Force the module under test to be re-imported. + + Because pytest runs all tests within the same scope (this makes me cry) + we have to do some manual housekeeping to avoid tests polluting each other. + + Since conftest.py already does some sys.modules mangling I see no reason not to + do the same thing here. + """ + if "." in module: + steps = module.split(".") + else: + steps = [module] + + for i in range(len(steps)): + module = ".".join(steps[0:i + 1]) + try: + del sys.modules[module] + except KeyError: + pass + + def test_gas_setup(GPIO, smbus): from enviroplus import gas gas._is_setup = False @@ -49,7 +71,8 @@ def test_gas_read_adc_default_gain(GPIO, smbus): 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(GPIO, smbus): @@ -59,3 +82,12 @@ def test_gas_read_adc_str(GPIO, smbus): gas.enable_adc(True) gas.set_adc_gain(2.048) assert 'ADC' in str(gas.read_all()) + + +def test_gas_cleanup(GPIO, smbus): + force_reimport('enviroplus.gas') + from enviroplus import gas + + gas.cleanup() + + GPIO.output.assert_called_with(gas.MICS6814_HEATER_PIN, 0) From 4cc6c622eb92bbc223db977e37bd351f84ef2ee5 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 24 Mar 2020 11:23:30 +0000 Subject: [PATCH 06/17] Move package and requires to setup.cfg --- library/setup.cfg | 1 + library/setup.py | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/library/setup.cfg b/library/setup.cfg index 83f89fa..dddc8af 100644 --- a/library/setup.cfg +++ b/library/setup.cfg @@ -27,6 +27,7 @@ classifiers = Topic :: System :: Hardware [options] +packages = enviroplus install_requires = pimoroni-bme280 pms5003 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() From e72e5682757a38fb94317d68758b874c61d55dd1 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 29 Apr 2020 11:56:42 +0100 Subject: [PATCH 07/17] Drop Python 2.6 --- library/setup.cfg | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/library/setup.cfg b/library/setup.cfg index dddc8af..af306be 100644 --- a/library/setup.cfg +++ b/library/setup.cfg @@ -4,7 +4,7 @@ name = enviroplus 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 From 5a376ddbb3ec3399eb6c5872f4ed2ac88cd75afc Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 29 Apr 2020 12:50:37 +0100 Subject: [PATCH 08/17] Catch #61 with tests and fix --- library/enviroplus/noise.py | 2 +- library/tests/conftest.py | 17 +++++++++++++++++ library/tests/test_noise.py | 38 ++++--------------------------------- library/tests/test_setup.py | 27 -------------------------- 4 files changed, 22 insertions(+), 62 deletions(-) diff --git a/library/enviroplus/noise.py b/library/enviroplus/noise.py index 6830bd0..7b6d5e2 100644 --- a/library/enviroplus/noise.py +++ b/library/enviroplus/noise.py @@ -76,7 +76,7 @@ def get_noise_profile(self, 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/tests/conftest.py b/library/tests/conftest.py index b026172..8a5c54c 100644 --- a/library/tests/conftest.py +++ b/library/tests/conftest.py @@ -14,6 +14,23 @@ def __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.""" diff --git a/library/tests/test_noise.py b/library/tests/test_noise.py index c93f8cc..3778c16 100644 --- a/library/tests/test_noise.py +++ b/library/tests/test_noise.py @@ -1,32 +1,7 @@ -import sys -import mock import pytest -def force_reimport(module): - """Force the module under test to be re-imported. - - Because pytest runs all tests within the same scope (this makes me cry) - we have to do some manual housekeeping to avoid tests polluting each other. - - Since conftest.py already does some sys.modules mangling I see no reason not to - do the same thing here. - """ - if "." in module: - steps = module.split(".") - else: - steps = [module] - - for i in range(len(steps)): - module = ".".join(steps[0:i + 1]) - try: - del sys.modules[module] - except KeyError: - pass - - def test_noise_setup(sounddevice, numpy): - force_reimport('enviroplus.noise') from enviroplus.noise import Noise noise = Noise(sample_rate=16000, duration=0.1) @@ -34,9 +9,6 @@ def test_noise_setup(sounddevice, numpy): def test_noise_get_amplitudes_at_frequency_ranges(sounddevice, numpy): - # Ippity zippidy what is this farce - # a curious function that makes my tests pass? - force_reimport('enviroplus.noise') from enviroplus.noise import Noise noise = Noise(sample_rate=16000, duration=0.1) @@ -49,11 +21,10 @@ def test_noise_get_amplitudes_at_frequency_ranges(sounddevice, numpy): def test_noise_get_noise_profile(sounddevice, numpy): - # Ippity zippidy what is this farce - # a curious function that makes my tests pass? - force_reimport('enviroplus.noise') 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, @@ -63,11 +34,10 @@ def test_noise_get_noise_profile(sounddevice, numpy): 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): - # Ippity zippidy what is this farce - # a curious function that makes my tests pass? - force_reimport('enviroplus.noise') from enviroplus.noise import Noise noise = Noise(sample_rate=16000, duration=0.1) diff --git a/library/tests/test_setup.py b/library/tests/test_setup.py index 6b6658c..2aa7b49 100644 --- a/library/tests/test_setup.py +++ b/library/tests/test_setup.py @@ -1,29 +1,3 @@ -import sys -import mock - - -def force_reimport(module): - """Force the module under test to be re-imported. - - Because pytest runs all tests within the same scope (this makes me cry) - we have to do some manual housekeeping to avoid tests polluting each other. - - Since conftest.py already does some sys.modules mangling I see no reason not to - do the same thing here. - """ - if "." in module: - steps = module.split(".") - else: - steps = [module] - - for i in range(len(steps)): - module = ".".join(steps[0:i + 1]) - try: - del sys.modules[module] - except KeyError: - pass - - def test_gas_setup(GPIO, smbus): from enviroplus import gas gas._is_setup = False @@ -85,7 +59,6 @@ def test_gas_read_adc_str(GPIO, smbus): def test_gas_cleanup(GPIO, smbus): - force_reimport('enviroplus.gas') from enviroplus import gas gas.cleanup() From 0c5c9465f1a8cecddbeb654b6dbacaedc4ba0e39 Mon Sep 17 00:00:00 2001 From: mendhak Date: Fri, 8 May 2020 20:38:27 +0100 Subject: [PATCH 09/17] Include python cffi in setup dependencies --- library/setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/setup.cfg b/library/setup.cfg index af306be..d2909c1 100644 --- a/library/setup.cfg +++ b/library/setup.cfg @@ -56,6 +56,7 @@ py2deps = python-numpy python-smbus python-pil + python-cffi python-spidev python-rpi.gpio libportaudio2 @@ -64,6 +65,7 @@ py3deps = python3-numpy python3-smbus python3-pil + python3-cffi python3-spidev python3-rpi.gpio libportaudio2 From 230698ad4272177850319668510c217fb8e699ab Mon Sep 17 00:00:00 2001 From: Sumit Kumar Maitra Date: Thu, 4 Jun 2020 00:45:47 +0100 Subject: [PATCH 10/17] Added supported board images and example --- Enviro-Plus-pHAT.jpg | Bin 0 -> 47407 bytes Enviro-mini-pHAT.jpg | Bin 0 -> 46494 bytes README.md | 8 +- examples/all-in-one-enviro-mini.py | 166 +++++++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 Enviro-Plus-pHAT.jpg create mode 100644 Enviro-mini-pHAT.jpg create mode 100755 examples/all-in-one-enviro-mini.py diff --git a/Enviro-Plus-pHAT.jpg b/Enviro-Plus-pHAT.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f0947a093c5d7447072352e3835964f54189b6ed GIT binary patch literal 47407 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 Kt`Fd^x&H!!qS0~y literal 0 HcmV?d00001 diff --git a/Enviro-mini-pHAT.jpg b/Enviro-mini-pHAT.jpg new file mode 100644 index 0000000000000000000000000000000000000000..120469f420abe79fe9db2d997afd5e776449340d GIT binary patch literal 46494 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 ZtGD^ym>Wfzkxm#%?`!F?kp1`be*sDckKh0R literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 5fb9fe6..843de1f 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) ``` 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) From 6a89566fe3e2227d60780c65ef3bb023ea0e1362 Mon Sep 17 00:00:00 2001 From: Peter McDonald Date: Wed, 17 Jun 2020 23:44:38 +0100 Subject: [PATCH 11/17] Correcting examples --- examples/all-in-one.py | 6 +++--- examples/combined.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/all-in-one.py b/examples/all-in-one.py index 6dda607..f7933a9 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -198,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) @@ -209,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) @@ -220,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 4b8fbdd..b417ccf 100755 --- a/examples/combined.py +++ b/examples/combined.py @@ -12,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 @@ -276,7 +276,7 @@ def main(): 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) @@ -287,7 +287,7 @@ def main(): 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) @@ -298,7 +298,7 @@ def main(): 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) @@ -331,8 +331,8 @@ def main(): pms_data = None try: pms_data = pms5003.read() - except pmsReadTimeoutError: - logging.warn("Failed to read PMS5003") + 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))) From f5335ba6681d5268d306ab438571bb369d682c29 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 18 Jun 2020 09:35:06 +0100 Subject: [PATCH 12/17] Adds mqtt example (#68) Adds mqtt example by @robmarkcole - see also: https://github.com/robmarkcole/rpi-enviro-mqtt --- examples/mqtt-all.py | 212 +++++++++++++++++++++++++++++++++++++++++++ library/setup.cfg | 1 + 2 files changed, 213 insertions(+) create mode 100755 examples/mqtt-all.py diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py new file mode 100755 index 0000000..26f46bb --- /dev/null +++ b/examples/mqtt-all.py @@ -0,0 +1,212 @@ +""" +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 +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" + +# mqtt callbacks +def on_connect(client, userdata, flags, rc): + print(f"CONNACK received with code {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 PMS5003 and return as dict +def read_values(bme280, pms5003): + # 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()) + 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) + 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 + + +# 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 = 16 + 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) + id = get_serial_number() + message = "{}\nWi-Fi: {}\nmqtt-broker: {}".format(id, 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" + ) + args = parser.parse_args() + + print( + """mqtt-all.py - Reads temperature, pressure, humidity, + PM2.5, and PM10 from Enviro plus and sends data over mqtt. + + broker: {} + port: {} + topic: {} + + Press Ctrl+C to exit! + + """.format( + args.broker, args.port, args.topic + ) + ) + + mqtt_client = mqtt.Client() + 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() + + # Create PMS5003 instance + pms5003 = PMS5003() + + # Raspberry Pi ID + device_serial_number = get_serial_number() + id = "raspi-" + device_serial_number + + # Display Raspberry Pi serial and Wi-Fi status + print("Raspberry Pi serial: {}".format(get_serial_number())) + print("Wi-Fi: {}\n".format("connected" if check_wifi() else "disconnected")) + print("MQTT broker IP: {}".format(args.broker)) + + time_since_update = 0 + update_time = time.time() + + # Main loop to read data, display, and send over mqtt + mqtt_client.loop_start() + while True: + try: + time_since_update = time.time() - update_time + values = read_values(bme280, pms5003) + values["serial"] = device_serial_number + print(values) + mqtt_client.publish(args.topic, json.dumps(values)) + if time_since_update > 145: + update_time = time.time() + display_status(disp, args.broker) + except Exception as e: + print(e) + + +if __name__ == "__main__": + main() diff --git a/library/setup.cfg b/library/setup.cfg index d2909c1..c59250c 100644 --- a/library/setup.cfg +++ b/library/setup.cfg @@ -38,6 +38,7 @@ install_requires = astral pytz sounddevice + paho-mqtt [flake8] exclude = From f984ea196d3c96763d5048aac9b5656a404a9d65 Mon Sep 17 00:00:00 2001 From: Philip Howard Date: Thu, 18 Jun 2020 09:40:51 +0100 Subject: [PATCH 13/17] Added user projects section to README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 843de1f..bcb1aa6 100644 --- a/README.md +++ b/README.md @@ -54,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 From a1a12d7adabe5fd7dfb5710596fb44cb17dcf062 Mon Sep 17 00:00:00 2001 From: Robin Cole Date: Sat, 11 Jul 2020 11:22:04 +0100 Subject: [PATCH 14/17] Update mqtt-all.py --- examples/mqtt-all.py | 106 +++++++++++++++++++++++++++---------------- 1 file changed, 66 insertions(+), 40 deletions(-) diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index 26f46bb..0eff47a 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -9,7 +9,7 @@ import ST7735 import time from bme280 import BME280 -from pms5003 import PMS5003, ReadTimeoutError +from pms5003 import PMS5003, ReadTimeoutError, SerialTimeoutError from enviroplus import gas try: @@ -37,10 +37,10 @@ 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): - print(f"CONNACK received with code {rc}") if rc == 0: print("connected OK") else: @@ -51,11 +51,10 @@ def on_publish(client, userdata, mid): print("mid: " + str(mid)) -# Read values from BME280 and PMS5003 and return as dict -def read_values(bme280, pms5003): +# 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 @@ -65,6 +64,17 @@ def read_values(bme280, pms5003): 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) @@ -76,17 +86,14 @@ def read_values(bme280, pms5003): 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) - 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 # Get CPU temperature to use for compensation def get_cpu_temperature(): - process = Popen(["vcgencmd", "measure_temp"], stdout=PIPE, universal_newlines=True) + process = Popen( + ["vcgencmd", "measure_temp"], stdout=PIPE, universal_newlines=True + ) output, _error = process.communicate() return float(output[output.index("=") + 1 : output.rindex("'")]) @@ -113,14 +120,16 @@ def display_status(disp, mqtt_broker): WIDTH = disp.width HEIGHT = disp.height # Text settings - font_size = 16 + 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) - id = get_serial_number() - message = "{}\nWi-Fi: {}\nmqtt-broker: {}".format(id, wifi_status, mqtt_broker) + 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) @@ -132,34 +141,50 @@ def display_status(disp, mqtt_broker): def main(): - parser = argparse.ArgumentParser(description="Publish enviroplus values over mqtt") + parser = argparse.ArgumentParser( + description="Publish enviroplus values over mqtt" + ) parser.add_argument( - "--broker", default=DEFAULT_MQTT_BROKER_IP, type=str, help="mqtt broker IP", + "--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", + "--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( - """mqtt-all.py - Reads temperature, pressure, humidity, - PM2.5, and PM10 from Enviro plus and sends data over mqtt. + f"""mqtt-all.py - Reads Enviro plus data and sends over mqtt. - broker: {} - port: {} - topic: {} + broker: {args.broker} + client_id: {device_id} + port: {args.port} + topic: {args.topic} Press Ctrl+C to exit! - """.format( - args.broker, args.port, args.topic - ) + """ ) - mqtt_client = mqtt.Client() + 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) @@ -177,33 +202,34 @@ def main(): # Initialize display disp.begin() - # Create PMS5003 instance - pms5003 = PMS5003() - - # Raspberry Pi ID - device_serial_number = get_serial_number() - id = "raspi-" + device_serial_number + # 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("Raspberry Pi serial: {}".format(get_serial_number())) + 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)) - time_since_update = 0 - update_time = time.time() - # Main loop to read data, display, and send over mqtt mqtt_client.loop_start() while True: try: - time_since_update = time.time() - update_time - values = read_values(bme280, pms5003) + 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)) - if time_since_update > 145: - update_time = time.time() display_status(disp, args.broker) + time.sleep(args.interval) except Exception as e: print(e) From c161c52b91df55e59441551353551c5b539121ad Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 30 Jul 2020 10:47:04 +0100 Subject: [PATCH 15/17] Fix combined.py indentation for Python 3.x --- examples/combined.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/combined.py b/examples/combined.py index b417ccf..6c4ab6f 100755 --- a/examples/combined.py +++ b/examples/combined.py @@ -26,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! @@ -172,7 +172,7 @@ def display_everything(): variable = variables[i] data_value = values[variable][-1] unit = units[i] - x = x_offset + ((WIDTH / column_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] From 3e4b64c3fcb517c2224341ac0ca8ca60a0b48685 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 30 Jul 2020 11:53:10 +0100 Subject: [PATCH 16/17] Experimental fix to communicate Py version reqs for #78 --- examples/weather-and-light.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/weather-and-light.py b/examples/weather-and-light.py index bccf7cc..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 From ddb2f5d8a7a50ce5524b0013742c35eb4dba2fb5 Mon Sep 17 00:00:00 2001 From: Jaroslav Lichtblau Date: Wed, 5 Aug 2020 12:18:26 +0200 Subject: [PATCH 17/17] Minute instead month in backup file name Fix for the DATESTAMP variable, to show proper file name. --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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