diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..07620e3 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,41 @@ +name: Build + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + name: Python ${{ matrix.python }} + runs-on: ubuntu-latest + strategy: + matrix: + python: ['3.9', '3.10', '3.11'] + + env: + RELEASE_FILE: ${{ github.event.repository.name }}-${{ github.event.release.tag_name || github.sha }}-py${{ matrix.python }} + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + + - name: Install Dependencies + run: | + make dev-deps + + - name: Build Packages + run: | + make build + + - name: Upload Packages + uses: actions/upload-artifact@v4 + with: + name: ${{ env.RELEASE_FILE }} + path: dist/ diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml new file mode 100644 index 0000000..ac672a5 --- /dev/null +++ b/.github/workflows/qa.yml @@ -0,0 +1,39 @@ +name: QA + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + name: linting & spelling + runs-on: ubuntu-latest + env: + TERM: xterm-256color + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python '3,11' + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install Dependencies + run: | + make dev-deps + + - name: Run Quality Assurance + run: | + make qa + + - name: Run Code Checks + run: | + make check + + - name: Run Bash Code Checks + run: | + make shellcheck diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e8e6358..6f8cff7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,37 +1,41 @@ -name: Python Tests +name: Tests on: pull_request: push: branches: - - master + - main jobs: test: + name: Python ${{ matrix.python }} runs-on: ubuntu-latest strategy: matrix: python: ['3.9', '3.10', '3.11'] steps: - - uses: actions/checkout@v2 + - name: Checkout Code + uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} + - name: Install Dependencies run: | - python -m pip install --upgrade setuptools tox + make dev-deps + - name: Run Tests - working-directory: library run: | - tox -e py + make pytest + - name: Coverage + if: ${{ matrix.python == '3.9' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - working-directory: library run: | python -m pip install coveralls coveralls --service=github - if: ${{ matrix.python == '3.11' }} diff --git a/library/CHANGELOG.txt b/CHANGELOG.md similarity index 79% rename from library/CHANGELOG.txt rename to CHANGELOG.md index 21f175a..d28efc6 100644 --- a/library/CHANGELOG.txt +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +1.0.1 +----------- + +* README.md: Fix images + +1.0.0 +----- + +* BREAKING: Port to gpiod/gpiodevice for Pi 5/Bookworm. + 0.0.6 ----- diff --git a/library/MANIFEST.in b/MANIFEST.in similarity index 100% rename from library/MANIFEST.in rename to MANIFEST.in diff --git a/Makefile b/Makefile index 12afc90..34f4a7d 100644 --- a/Makefile +++ b/Makefile @@ -1,69 +1,63 @@ -LIBRARY_VERSION=$(shell grep version library/setup.cfg | awk -F" = " '{print $$2}') -LIBRARY_NAME=$(shell grep name library/setup.cfg | awk -F" = " '{print $$2}') +LIBRARY_NAME := $(shell hatch project metadata name 2> /dev/null) +LIBRARY_VERSION := $(shell hatch version 2> /dev/null) -.PHONY: usage install uninstall +.PHONY: usage install uninstall check pytest qa build-deps check tag wheel sdist clean dist testdeploy deploy usage: +ifdef LIBRARY_NAME @echo "Library: ${LIBRARY_NAME}" @echo "Version: ${LIBRARY_VERSION}\n" +else + @echo "WARNING: You should 'make dev-deps'\n" +endif @echo "Usage: make , where target is one of:\n" - @echo "install: install the library locally from source" - @echo "uninstall: uninstall the local library" - @echo "check: peform basic integrity checks on the codebase" - @echo "python-readme: generate library/README.rst from README.md" - @echo "python-wheels: build python .whl files for distribution" - @echo "python-sdist: build python source distribution" - @echo "python-clean: clean python build and dist directories" - @echo "python-dist: build all python distribution files" - @echo "python-testdeploy: build all and deploy to test PyPi" - @echo "tag: tag the repository with the current version" + @echo "install: install the library locally from source" + @echo "uninstall: uninstall the local library" + @echo "dev-deps: install Python dev dependencies" + @echo "check: perform basic integrity checks on the codebase" + @echo "qa: run linting and package QA" + @echo "pytest: run Python test fixtures" + @echo "clean: clean Python build and dist directories" + @echo "build: build Python distribution files" + @echo "testdeploy: build and upload to test PyPi" + @echo "deploy: build and upload to PyPi" + @echo "tag: tag the repository with the current version\n" install: - ./install.sh + ./install.sh --unstable uninstall: ./uninstall.sh -check: - @echo "Checking for trailing whitespace" - @! grep -IUrn --color "[[:blank:]]$$" --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO - @echo "Checking for DOS line-endings" - @! grep -IUrn --color " " --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile - @echo "Checking library/CHANGELOG.txt" - @cat library/CHANGELOG.txt | grep ^${LIBRARY_VERSION} - @echo "Checking library/${LIBRARY_NAME}/__init__.py" - @cat library/${LIBRARY_NAME}/__init__.py | grep "^__version__ = '${LIBRARY_VERSION}'" - -tag: - git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" +dev-deps: + python3 -m pip install -r requirements-dev.txt + sudo apt install dos2unix shellcheck -python-readme: library/README.md +check: + @bash check.sh -python-license: library/LICENSE.txt +shellcheck: + shellcheck *.sh -library/README.md: README.md library/CHANGELOG.txt - cp README.md library/README.md - printf "\n# Changelog\n" >> library/README.md - cat library/CHANGELOG.txt >> library/README.md +qa: + tox -e qa -library/LICENSE.txt: LICENSE - cp LICENSE library/LICENSE.txt +pytest: + tox -e py -python-wheels: python-readme python-license - cd library; python3 setup.py bdist_wheel +nopost: + @bash check.sh --nopost -python-sdist: python-readme python-license - cd library; python3 setup.py sdist +tag: + git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" -python-clean: - -rm -r library/dist - -rm -r library/build - -rm -r library/*.egg-info +build: check + @hatch build -python-dist: python-clean python-wheels python-sdist - ls library/dist +clean: + -rm -r dist -python-testdeploy: python-dist - twine upload --repository-url https://test.pypi.org/legacy/ library/dist/* +testdeploy: build + twine upload --repository testpypi dist/* -python-deploy: check python-dist - twine upload library/dist/* +deploy: nopost build + twine upload dist/* diff --git a/README.md b/README.md index 72e0597..45436ff 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ You are best using the "One-line" install method if you want all of the UART ser **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) +![Enviro Plus pHAT](https://raw.githubusercontent.com/pimoroni/enviroplus-python/main/Enviro-Plus-pHAT.jpg) +![Enviro Mini pHAT](https://raw.githubusercontent.com/pimoroni/enviroplus-python/main/Enviro-mini-pHAT.jpg) :warning: This library now supports Python 3 only, Python 2 is EOL - https://www.python.org/doc/sunset-python-2/ diff --git a/check-install.py b/check-install.py deleted file mode 100755 index ea5eecd..0000000 --- a/check-install.py +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -f"Sorry! This program requires Python >= 3.6 😅. Run with \"python3 check-install.py\"" - -CONFIG_FILE = "/boot/config.txt" - -print("""Checking Enviro+ install, please wait...""") - -errors = 0 -check_apt = False - -try: - import apt - check_apt = True -except ImportErorr: - print("⚠️ Could not import \"apt\". Unable to verify system dependencies.") - - -apt_deps = { - "python3", - "python3-pip", - "python3-numpy", - "python3-smbus", - "python3-pil", - "python3-cffi", - "python3-spidev", - "python3-rpi.gpio", - "libportaudio2" -} - -deps = { - "bme280": None, - "pms5003": None, - "ltr559": None, - "ST7735": None, - "ads1015": "0.0.7", - "fonts": None, - "font_roboto": None, - "astral": None, - "pytz": None, - "sounddevice": None, - "paho.mqtt": None -} - -config = { - "dtparam=i2c_arm=on", - "dtparam=spi=on", - "dtoverlay=adau7002-simple", - "dtoverlay=pi3-miniuart-bt", - "enable_uart=1" -} - -if check_apt: - print("\nSystem dependencies...") - print(" Retrieving cache...") - cache = apt.Cache() - - for dep in apt_deps: - installed = False - print(f" Checking for {dep}".ljust(35), end="") - try: - installed = cache[dep].is_installed - except KeyError: - pass - - if installed: - print("✅") - else: - print("⚠️ Missing!") - errors += 1 - -print("\nPython dependencies...") - -for dep, version in deps.items(): - print(f" Checking for {dep}".ljust(35), end="") - try: - __import__(dep) - print("✅") - except ImportError: - print("⚠️ Missing!") - errors += 1 - -print("\nSystem config...") - -config_txt = open(CONFIG_FILE, "r").read().split("\n") - -def check_config(line): - global errors - print(f" Checking for {line} in {CONFIG_FILE}: ", end="") - for cline in config_txt: - if cline.startswith(line): - print("✅") - return - print("⚠️ Missing!") - errors += 1 - -for line in config: - check_config(line) - -if errors > 0: - print("\n⚠️ Config errors were found! Something might be awry.") -else: - print("\n✅ Looks good from here!") - -print("\nHave you?") -print(" • Rebooted after installing") -print(" • Made sure to run examples with \"python3\"") -print(" • Checked for any errors when running \"sudo ./install.sh\"") diff --git a/check.sh b/check.sh new file mode 100755 index 0000000..38dfc3a --- /dev/null +++ b/check.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +# This script handles some basic QA checks on the source + +NOPOST=$1 +LIBRARY_NAME=$(hatch project metadata name) +LIBRARY_VERSION=$(hatch version | awk -F "." '{print $1"."$2"."$3}') +POST_VERSION=$(hatch version | awk -F "." '{print substr($4,0,length($4))}') +TERM=${TERM:="xterm-256color"} + +success() { + echo -e "$(tput setaf 2)$1$(tput sgr0)" +} + +inform() { + echo -e "$(tput setaf 6)$1$(tput sgr0)" +} + +warning() { + echo -e "$(tput setaf 1)$1$(tput sgr0)" +} + +while [[ $# -gt 0 ]]; do + K="$1" + case $K in + -p|--nopost) + NOPOST=true + shift + ;; + *) + if [[ $1 == -* ]]; then + printf "Unrecognised option: %s\n" "$1"; + exit 1 + fi + POSITIONAL_ARGS+=("$1") + shift + esac +done + +inform "Checking $LIBRARY_NAME $LIBRARY_VERSION\n" + +inform "Checking for trailing whitespace..." +if grep -IUrn --color "[[:blank:]]$" --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO; then + warning "Trailing whitespace found!" + exit 1 +else + success "No trailing whitespace found." +fi +printf "\n" + +inform "Checking for DOS line-endings..." +if grep -lIUrn --color $'\r' --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile; then + warning "DOS line-endings found!" + exit 1 +else + success "No DOS line-endings found." +fi +printf "\n" + +inform "Checking CHANGELOG.md..." +if ! grep "^${LIBRARY_VERSION}" CHANGELOG.md > /dev/null 2>&1; then + warning "Changes missing for version ${LIBRARY_VERSION}! Please update CHANGELOG.md." + exit 1 +else + success "Changes found for version ${LIBRARY_VERSION}." +fi +printf "\n" + +inform "Checking for git tag ${LIBRARY_VERSION}..." +if ! git tag -l | grep -E "${LIBRARY_VERSION}$"; then + warning "Missing git tag for version ${LIBRARY_VERSION}" +fi +printf "\n" + +if [[ $NOPOST ]]; then + inform "Checking for .postN on library version..." + if [[ "$POST_VERSION" != "" ]]; then + warning "Found .$POST_VERSION on library version." + inform "Please only use these for testpypi releases." + exit 1 + else + success "OK" + fi +fi diff --git a/enviroplus/__init__.py b/enviroplus/__init__.py new file mode 100644 index 0000000..5c4105c --- /dev/null +++ b/enviroplus/__init__.py @@ -0,0 +1 @@ +__version__ = "1.0.1" diff --git a/library/enviroplus/gas.py b/enviroplus/gas.py similarity index 71% rename from library/enviroplus/gas.py rename to enviroplus/gas.py index 54c240f..3583428 100644 --- a/library/enviroplus/gas.py +++ b/enviroplus/gas.py @@ -1,22 +1,28 @@ """Read the MICS6814 via an ads1015 ADC""" -import time import atexit +import time + import ads1015 -import RPi.GPIO as GPIO +import gpiod +import gpiodevice +from gpiod.line import Direction, Value -MICS6814_HEATER_PIN = 24 MICS6814_GAIN = 6.144 +OUTH = gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE) + + ads1015.I2C_ADDRESS_DEFAULT = ads1015.I2C_ADDRESS_ALTERNATE _is_setup = False _is_available = False _adc_enabled = False _adc_gain = 6.148 +_heater = None class Mics6814Reading(object): - __slots__ = 'oxidising', 'reducing', 'nh3', 'adc' + __slots__ = "oxidising", "reducing", "nh3", "adc" def __init__(self, ox, red, nh3, adc=None): self.oxidising = ox @@ -25,24 +31,20 @@ def __init__(self, ox, red, nh3, adc=None): self.adc = adc def __repr__(self): - fmt = """Oxidising: {ox:05.02f} Ohms -Reducing: {red:05.02f} Ohms -NH3: {nh3:05.02f} Ohms""" + fmt = f"""Oxidising: {self.oxidising:05.02f} Ohms +Reducing: {self.reducing:05.02f} Ohms +NH3: {self.nh3:05.02f} Ohms""" if self.adc is not None: - fmt += """ -ADC: {adc:05.02f} Volts + fmt += f""" +ADC: {self.adc:05.02f} Volts """ - return fmt.format( - ox=self.oxidising, - red=self.reducing, - nh3=self.nh3, - adc=self.adc) + return fmt __str__ = __repr__ def setup(): - global adc, adc_type, _is_setup, _is_available + global adc, adc_type, _is_setup, _is_available, _heater if _is_setup: return _is_setup = True @@ -55,17 +57,15 @@ def setup(): _is_available = False return - adc.set_mode('single') + adc.set_mode("single") adc.set_programmable_gain(MICS6814_GAIN) - if adc_type == 'ADS1115': + if adc_type == "ADS1115": adc.set_sample_rate(128) else: adc.set_sample_rate(1600) - GPIO.setwarnings(False) - GPIO.setmode(GPIO.BCM) - GPIO.setup(MICS6814_HEATER_PIN, GPIO.OUT) - GPIO.output(MICS6814_HEATER_PIN, 1) + _heater = gpiodevice.get_pin("GPIO24") + atexit.register(cleanup) @@ -87,19 +87,22 @@ def set_adc_gain(value): def cleanup(): - GPIO.output(MICS6814_HEATER_PIN, 0) + if _heater is None: + return + lines, offset = _heater + lines.set_value(offset, Value.INACTIVE) def read_all(): - """Return gas resistence for oxidising, reducing and NH3""" + """Return gas resistance for oxidising, reducing and NH3""" setup() if not _is_available: raise RuntimeError("Gas sensor not connected.") - ox = adc.get_voltage('in0/gnd') - red = adc.get_voltage('in1/gnd') - nh3 = adc.get_voltage('in2/gnd') + ox = adc.get_voltage("in0/gnd") + red = adc.get_voltage("in1/gnd") + nh3 = adc.get_voltage("in2/gnd") try: ox = (ox * 56000) / (3.3 - ox) @@ -120,11 +123,11 @@ def read_all(): if _adc_enabled: if _adc_gain == MICS6814_GAIN: - analog = adc.get_voltage('ref/gnd') + analog = adc.get_voltage("ref/gnd") else: adc.set_programmable_gain(_adc_gain) time.sleep(0.05) - analog = adc.get_voltage('ref/gnd') + analog = adc.get_voltage("ref/gnd") adc.set_programmable_gain(MICS6814_GAIN) return Mics6814Reading(ox, red, nh3, analog) diff --git a/library/enviroplus/noise.py b/enviroplus/noise.py similarity index 85% rename from library/enviroplus/noise.py rename to enviroplus/noise.py index 0d413bd..261c3ab 100644 --- a/library/enviroplus/noise.py +++ b/enviroplus/noise.py @@ -1,11 +1,9 @@ -import sounddevice import numpy +import sounddevice -class Noise(): - def __init__(self, - sample_rate=16000, - duration=0.5): +class Noise: + def __init__(self, sample_rate=16000, duration=0.5): """Noise measurement. :param sample_rate: Sample rate in Hz @@ -39,18 +37,14 @@ def get_amplitude_at_frequency_range(self, start, end): """ n = self.sample_rate // 2 if start > n or end > n: - raise ValueError("Maxmimum frequency is {}".format(n)) + raise ValueError(f"Maximum frequency is {n}") recording = self._record() magnitude = numpy.abs(numpy.fft.rfft(recording[:, 0], n=self.sample_rate)) return numpy.mean(magnitude[start:end]) - def get_noise_profile(self, - noise_floor=100, - low=0.12, - mid=0.36, - high=None): - """Returns a noise charateristic profile. + def get_noise_profile(self, noise_floor=100, low=0.12, mid=0.36, high=None): + """Returns a noise characteristic profile. Bins all frequencies into 3 weighted groups expressed as a percentage of the total frequency range. @@ -83,9 +77,9 @@ def get_noise_profile(self, def _record(self): return sounddevice.rec( int(self.duration * self.sample_rate), - device='adau7002', + device="adau7002", samplerate=self.sample_rate, blocking=True, channels=1, - dtype='float64' + dtype="float64" ) diff --git a/examples/adc.py b/examples/adc.py index a345d23..983aec3 100755 --- a/examples/adc.py +++ b/examples/adc.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 +import logging import time + from enviroplus import gas -import logging logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") logging.info("""adc.py - Print readings from the MICS6814 Gas sensor. diff --git a/examples/all-in-one-enviro-mini.py b/examples/all-in-one-enviro-mini.py index d7a001f..4aea5c4 100755 --- a/examples/all-in-one-enviro-mini.py +++ b/examples/all-in-one-enviro-mini.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 -import time import colorsys import os import sys -import ST7735 +import time + +import st7735 + try: # Transitional fix for breaking change in LTR559 from ltr559 import LTR559 @@ -12,21 +14,19 @@ except ImportError: import ltr559 -from bme280 import BME280 -from enviroplus import gas +import logging from subprocess import PIPE, Popen -from PIL import Image -from PIL import ImageDraw -from PIL import ImageFont + +from bme280 import BME280 from fonts.ttf import RobotoMedium as UserFont -import logging +from PIL import Image, ImageDraw, ImageFont logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") -logging.info("""all-in-one.py - Displays readings from all of Enviro plus' sensors +logging.info("""all-in-one.py - Displays readings from all of Enviro plus" sensors Press Ctrl+C to exit! """) @@ -34,11 +34,11 @@ bme280 = BME280() # Create ST7735 LCD display class -st7735 = ST7735.ST7735( +st7735 = st7735.ST7735( port=0, cs=1, - dc=9, - backlight=12, + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) @@ -50,7 +50,7 @@ HEIGHT = st7735.height # Set up canvas and font -img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0)) +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 @@ -71,7 +71,7 @@ def display_text(variable, data, unit): 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) + message = f"{variable[:4]}: {data:.1f} {unit}" logging.info(message) draw.rectangle((0, 0, WIDTH, HEIGHT), (255, 255, 255)) for i in range(len(colours)): @@ -90,9 +90,9 @@ def display_text(variable, data, unit): # Get the temperature of the CPU 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("'")]) + return float(output[output.index("=") + 1:output.rindex("'")]) # Tuning factor for compensation. Decrease this number to adjust the @@ -131,7 +131,7 @@ def get_cpu_temperature(): # One mode for each variable if mode == 0: # variable = "temperature" - unit = "C" + unit = "°C" cpu_temp = get_cpu_temperature() # Smooth out with some averaging to decrease jitter cpu_temps = cpu_temps[1:] + [cpu_temp] diff --git a/examples/all-in-one-no-pm.py b/examples/all-in-one-no-pm.py index ec1ed1f..c017205 100755 --- a/examples/all-in-one-no-pm.py +++ b/examples/all-in-one-no-pm.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 -import time import colorsys import os import sys -import ST7735 +import time + +import st7735 + try: # Transitional fix for breaking change in LTR559 from ltr559 import LTR559 @@ -12,18 +14,18 @@ except ImportError: import ltr559 +import logging + from bme280 import BME280 -from enviroplus import gas -from PIL import Image -from PIL import ImageDraw -from PIL import ImageFont from fonts.ttf import RobotoMedium as UserFont -import logging +from PIL import Image, ImageDraw, ImageFont + +from enviroplus import gas logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + 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! @@ -33,11 +35,11 @@ bme280 = BME280() # Create ST7735 LCD display class -st7735 = ST7735.ST7735( +st7735 = st7735.ST7735( port=0, cs=1, - dc=9, - backlight=12, + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) @@ -49,7 +51,7 @@ HEIGHT = st7735.height # Set up canvas and font -img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0)) +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 @@ -70,7 +72,7 @@ def display_text(variable, data, unit): 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) + message = f"{variable[:4]}: {data:.1f} {unit}" logging.info(message) draw.rectangle((0, 0, WIDTH, HEIGHT), (255, 255, 255)) for i in range(len(colours)): @@ -134,7 +136,7 @@ def get_cpu_temperature(): # One mode for each variable if mode == 0: # variable = "temperature" - unit = "C" + unit = "°C" cpu_temp = get_cpu_temperature() # Smooth out with some averaging to decrease jitter cpu_temps = cpu_temps[1:] + [cpu_temp] diff --git a/examples/all-in-one.py b/examples/all-in-one.py index ac53999..b558f48 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 -import time import colorsys import sys -import ST7735 +import time + +import st7735 + try: # Transitional fix for breaking change in LTR559 from ltr559 import LTR559 @@ -11,19 +13,20 @@ except ImportError: import ltr559 +import logging + from bme280 import BME280 -from pms5003 import PMS5003, ReadTimeoutError as pmsReadTimeoutError -from enviroplus import gas -from PIL import Image -from PIL import ImageDraw -from PIL import ImageFont from fonts.ttf import RobotoMedium as UserFont -import logging +from PIL import Image, ImageDraw, ImageFont +from pms5003 import PMS5003 +from pms5003 import ReadTimeoutError as pmsReadTimeoutError + +from enviroplus import gas logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") logging.info("""all-in-one.py - Displays readings from all of Enviro plus' sensors @@ -38,11 +41,11 @@ pms5003 = PMS5003() # Create ST7735 LCD display class -st7735 = ST7735.ST7735( +st7735 = st7735.ST7735( port=0, cs=1, - dc=9, - backlight=12, + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) @@ -54,7 +57,7 @@ HEIGHT = st7735.height # Set up canvas and font -img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0)) +img = Image.new("RGB", (WIDTH, HEIGHT), color=(0, 0, 0)) draw = ImageDraw.Draw(img) font_size = 20 font = ImageFont.truetype(UserFont, font_size) @@ -74,7 +77,7 @@ def display_text(variable, data, unit): 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) + message = f"{variable[:4]}: {data:.1f} {unit}" logging.info(message) draw.rectangle((0, 0, WIDTH, HEIGHT), (255, 255, 255)) for i in range(len(colours)): @@ -141,7 +144,7 @@ def get_cpu_temperature(): # One mode for each variable if mode == 0: # variable = "temperature" - unit = "C" + unit = "°C" cpu_temp = get_cpu_temperature() # Smooth out with some averaging to decrease jitter cpu_temps = cpu_temps[1:] + [cpu_temp] diff --git a/examples/combined.py b/examples/combined.py index 556bcb0..79e5d62 100755 --- a/examples/combined.py +++ b/examples/combined.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 -import time import colorsys import sys -import ST7735 +import time + +import st7735 + try: # Transitional fix for breaking change in LTR559 from ltr559 import LTR559 @@ -11,20 +13,22 @@ except ImportError: import ltr559 -from bme280 import BME280 -from pms5003 import PMS5003, ReadTimeoutError as pmsReadTimeoutError, SerialTimeoutError -from enviroplus import gas +import logging from subprocess import PIPE, Popen -from PIL import Image -from PIL import ImageDraw -from PIL import ImageFont + +from bme280 import BME280 from fonts.ttf import RobotoMedium as UserFont -import logging +from PIL import Image, ImageDraw, ImageFont +from pms5003 import PMS5003 +from pms5003 import ReadTimeoutError as pmsReadTimeoutError +from pms5003 import SerialTimeoutError + +from enviroplus import gas logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") logging.info("""combined.py - Displays readings from all of Enviro plus' sensors @@ -40,11 +44,11 @@ time.sleep(1.0) # Create ST7735 LCD display class -st7735 = ST7735.ST7735( +st7735 = st7735.ST7735( port=0, cs=1, - dc=9, - backlight=12, + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) @@ -56,7 +60,7 @@ HEIGHT = st7735.height # Set up canvas and font -img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0)) +img = Image.new("RGB", (WIDTH, HEIGHT), color=(0, 0, 0)) draw = ImageDraw.Draw(img) font_size_small = 10 font_size_large = 20 @@ -136,7 +140,7 @@ def display_text(variable, data, unit): 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) + message = f"{variable[:4]}: {data:.1f} {unit}" logging.info(message) draw.rectangle((0, 0, WIDTH, HEIGHT), (255, 255, 255)) for i in range(len(colours)): @@ -159,7 +163,7 @@ def save_data(idx, data): # Maintain length of list values[variable] = values[variable][1:] + [data] unit = units[idx] - message = "{}: {:.1f} {}".format(variable[:4], data, unit) + message = f"{variable[:4]}: {data:.1f} {unit}" logging.info(message) @@ -174,7 +178,7 @@ def display_everything(): unit = units[i] 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) + message = f"{variable[:4]}: {data_value:.1f} {unit}" lim = limits[i] rgb = palette[0] for j in range(len(lim)): @@ -186,9 +190,9 @@ def display_everything(): # Get the temperature of the CPU 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("'")]) + return float(output[output.index("=") + 1:output.rindex("'")]) def main(): @@ -219,7 +223,7 @@ def main(): # One mode for each variable if mode == 0: # variable = "temperature" - unit = "C" + unit = "°C" cpu_temp = get_cpu_temperature() # Smooth out with some averaging to decrease jitter cpu_temps = cpu_temps[1:] + [cpu_temp] diff --git a/examples/compensated-temperature.py b/examples/compensated-temperature.py index b648f57..fb692f0 100755 --- a/examples/compensated-temperature.py +++ b/examples/compensated-temperature.py @@ -1,19 +1,15 @@ #!/usr/bin/env python3 +import logging import time -from bme280 import BME280 - -try: - from smbus2 import SMBus -except ImportError: - from smbus import SMBus -import logging +from bme280 import BME280 +from smbus2 import SMBus logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") logging.info("""compensated-temperature.py - Use the CPU temperature to compensate temperature readings from the BME280 sensor. @@ -49,5 +45,5 @@ def get_cpu_temperature(): avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps)) raw_temp = bme280.get_temperature() comp_temp = raw_temp - ((avg_cpu_temp - raw_temp) / factor) - logging.info("Compensated temperature: {:05.2f} *C".format(comp_temp)) + logging.info(f"Compensated temperature: {comp_temp:05.2f} °C") time.sleep(1.0) diff --git a/examples/gas.py b/examples/gas.py index 5d72cb9..c5fce5f 100755 --- a/examples/gas.py +++ b/examples/gas.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 +import logging import time + from enviroplus import gas -import logging logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") logging.info("""gas.py - Print readings from the MICS6814 Gas sensor. diff --git a/examples/lcd.py b/examples/lcd.py index 10413b9..97c2be4 100755 --- a/examples/lcd.py +++ b/examples/lcd.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 -import ST7735 -from PIL import Image, ImageDraw, ImageFont -from fonts.ttf import RobotoMedium as UserFont import logging +import st7735 +from fonts.ttf import RobotoMedium as UserFont +from PIL import Image, ImageDraw, ImageFont + logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") logging.info("""lcd.py - Hello, World! example on the 0.96" LCD. @@ -17,11 +18,11 @@ """) # Create LCD class instance. -disp = ST7735.ST7735( +disp = st7735.ST7735( port=0, cs=1, - dc=9, - backlight=12, + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) @@ -34,7 +35,7 @@ HEIGHT = disp.height # New canvas to draw on. -img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0)) +img = Image.new("RGB", (WIDTH, HEIGHT), color=(0, 0, 0)) draw = ImageDraw.Draw(img) # Text settings. @@ -44,7 +45,10 @@ back_colour = (0, 170, 170) message = "Hello, World!" -size_x, size_y = draw.textsize(message, font) + +x1, y1, x2, y2 = font.getbbox(message) +size_x = x2 - x1 +size_y = y2 - y1 # Calculate text position x = (WIDTH - size_x) / 2 diff --git a/examples/light.py b/examples/light.py index db61e6a..70414db 100755 --- a/examples/light.py +++ b/examples/light.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 -import time import logging +import time + try: # Transitional fix for breaking change in LTR559 from ltr559 import LTR559 @@ -11,9 +12,9 @@ logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") logging.info("""light.py - Print readings from the LTR559 Light & Proximity sensor. @@ -25,9 +26,9 @@ while True: lux = ltr559.get_lux() prox = ltr559.get_proximity() - logging.info("""Light: {:05.02f} Lux -Proximity: {:05.02f} -""".format(lux, prox)) + logging.info(f"""Light: {lux:05.02f} Lux +Proximity: {prox:05.02f} +""") time.sleep(1.0) except KeyboardInterrupt: pass diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index 5e564af..ecab4a3 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -6,11 +6,13 @@ """ import argparse -import ST7735 -import time import ssl +import time + +import st7735 from bme280 import BME280 from pms5003 import PMS5003, ReadTimeoutError, SerialTimeoutError + from enviroplus import gas try: @@ -21,13 +23,12 @@ 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 +from subprocess import PIPE, Popen, check_output import paho.mqtt.client as mqtt -import paho.mqtt.publish as publish +from fonts.ttf import RobotoMedium as UserFont +from PIL import Image, ImageDraw, ImageFont try: from smbus2 import SMBus @@ -132,12 +133,12 @@ def display_status(disp, mqtt_broker): 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 - ) + message = f"{device_serial_number}\nWi-Fi: {wifi_status}\nmqtt-broker: {mqtt_broker}" img = Image.new("RGB", (WIDTH, HEIGHT), color=(0, 0, 0)) draw = ImageDraw.Draw(img) - size_x, size_y = draw.textsize(message, font) + x1, y1, x2, y2 = font.getbbox(message) + size_x = x2 - x1 + size_y = y2 - y1 x = (WIDTH - size_x) / 2 y = (HEIGHT / 2) - (size_y / 2) draw.rectangle((0, 0, 160, 80), back_colour) @@ -173,7 +174,7 @@ def main(): parser.add_argument( "--tls", default=DEFAULT_TLS_MODE, - action='store_true', + action="store_true", help="enable TLS" ) parser.add_argument( @@ -230,8 +231,13 @@ def main(): 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 + disp = st7735.ST7735( + port=0, + cs=1, + dc="GPIO9", + backlight="GPIO12", + rotation=270, + spi_speed_hz=10000000 ) # Initialize display @@ -248,9 +254,10 @@ def main(): 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)) + print(f"RPi serial: {device_serial_number}") + wifi_status = "connected" if check_wifi() else "disconnected" + print(f"Wi-Fi: {wifi_status}\n") + print(f"MQTT broker IP: {args.broker}") # Set an initial update time update_time = time.time() diff --git a/examples/noise-amps-at-freqs.py b/examples/noise-amps-at-freqs.py index 4c14c58..957511b 100755 --- a/examples/noise-amps-at-freqs.py +++ b/examples/noise-amps-at-freqs.py @@ -1,5 +1,6 @@ -import ST7735 +import st7735 from PIL import Image, ImageDraw + from enviroplus.noise import Noise print("""noise-amps-at-freqs.py - Measure amplitude from specific frequency bins @@ -14,16 +15,18 @@ noise = Noise() -disp = ST7735.ST7735( +disp = st7735.ST7735( port=0, - cs=ST7735.BG_SPI_CS_FRONT, - dc=9, - backlight=12, - rotation=90) + cs=1, + dc="GPIO9", + backlight="GPIO12", + rotation=270, + spi_speed_hz=10000000 +) disp.begin() -img = Image.new('RGB', (disp.width, disp.height), color=(0, 0, 0)) +img = Image.new("RGB", (disp.width, disp.height), color=(0, 0, 0)) draw = ImageDraw.Draw(img) diff --git a/examples/noise-profile.py b/examples/noise-profile.py index 4084439..be6a185 100755 --- a/examples/noise-profile.py +++ b/examples/noise-profile.py @@ -1,5 +1,6 @@ -import ST7735 +import st7735 from PIL import Image, ImageDraw + from enviroplus.noise import Noise print("""noise-profile.py - Get a simple noise profile. @@ -12,16 +13,18 @@ noise = Noise() -disp = ST7735.ST7735( +disp = st7735.ST7735( port=0, - cs=ST7735.BG_SPI_CS_FRONT, - dc=9, - backlight=12, - rotation=90) + cs=1, + dc="GPIO9", + backlight="GPIO12", + rotation=270, + spi_speed_hz=10000000 +) disp.begin() -img = Image.new('RGB', (disp.width, disp.height), color=(0, 0, 0)) +img = Image.new("RGB", (disp.width, disp.height), color=(0, 0, 0)) draw = ImageDraw.Draw(img) diff --git a/examples/particulates.py b/examples/particulates.py index 04a4950..6ecaf70 100755 --- a/examples/particulates.py +++ b/examples/particulates.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 +import logging import time + from pms5003 import PMS5003, ReadTimeoutError -import logging logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") logging.info("""particulates.py - Print readings from the PMS5003 particulate sensor. diff --git a/examples/luftdaten.py b/examples/sensorcommunity.py similarity index 62% rename from examples/luftdaten.py rename to examples/sensorcommunity.py index a78909f..cdd3a48 100755 --- a/examples/luftdaten.py +++ b/examples/sensorcommunity.py @@ -1,38 +1,35 @@ #!/usr/bin/env python3 -import requests -import ST7735 +import logging import time +from subprocess import check_output + +import requests +import st7735 from bme280 import BME280 -from pms5003 import PMS5003, ReadTimeoutError, ChecksumMismatchError -from subprocess import PIPE, Popen, check_output -from PIL import Image, ImageDraw, ImageFont from fonts.ttf import RobotoMedium as UserFont - -try: - from smbus2 import SMBus -except ImportError: - from smbus import SMBus -import logging +from PIL import Image, ImageDraw, ImageFont +from pms5003 import PMS5003, ChecksumMismatchError, ReadTimeoutError +from smbus2 import SMBus logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") -logging.info("""luftdaten.py - Reads temperature, pressure, humidity, -#PM2.5, and PM10 from Enviro plus and sends data to Luftdaten, -#the citizen science air quality project. +logging.info("""sensorcommunity.py - Reads temperature, pressure, humidity, +PM2.5, and PM10 from Enviro plus and sends data to Sensor.Community, +the citizen science air quality project. -#Note: you'll need to register with Luftdaten at: -#https://meine.luftdaten.info/ and enter your Raspberry Pi -#serial number that's displayed on the Enviro plus LCD along -#with the other details before the data appears on the -#Luftdaten map. +Note: you'll need to register with Sensor.Community at: +https://devices.sensor.community/ and enter your Raspberry Pi +serial number that's displayed on the Enviro plus LCD along +with the other details before the data appears on the +Sensor.Community map. -#Press Ctrl+C to exit! +Press Ctrl+C to exit! -#""") +""") bus = SMBus(1) @@ -40,11 +37,11 @@ bme280 = BME280(i2c_dev=bus) # Create LCD instance -disp = ST7735.ST7735( +disp = st7735.ST7735( port=0, cs=1, - dc=9, - backlight=12, + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) @@ -62,15 +59,15 @@ def read_values(): cpu_temp = get_cpu_temperature() raw_temp = bme280.get_temperature() comp_temp = raw_temp - ((cpu_temp - raw_temp) / comp_factor) - values["temperature"] = "{:.2f}".format(comp_temp) - values["pressure"] = "{:.2f}".format(bme280.get_pressure() * 100) - values["humidity"] = "{:.2f}".format(bme280.get_humidity()) + values["temperature"] = f"{comp_temp:.2f}" + values["pressure"] = f"{bme280.get_pressure() * 100:.2f}" + values["humidity"] = f"{bme280.get_humidity():.2f}" try: pm_values = pms5003.read() values["P2"] = str(pm_values.pm_ug_per_m3(2.5)) values["P1"] = str(pm_values.pm_ug_per_m3(10)) except(ReadTimeoutError, ChecksumMismatchError): - logging.info("Failed to read PMS5003. Reseting and retrying.") + logging.info("Failed to read PMS5003. Resetting and retrying.") pms5003.reset() pm_values = pms5003.read() values["P2"] = str(pm_values.pm_ug_per_m3(2.5)) @@ -88,15 +85,15 @@ def get_cpu_temperature(): # Get Raspberry Pi serial number to use as ID def get_serial_number(): - with open('/proc/cpuinfo', 'r') as f: + with open("/proc/cpuinfo", "r") as f: for line in f: - if line[0:6] == 'Serial': + if line.startswith("Serial"): return line.split(":")[1].strip() # Check for Wi-Fi connection def check_wifi(): - if check_output(['hostname', '-I']): + if check_output(["hostname", "-I"]): return True else: return False @@ -108,10 +105,12 @@ def display_status(): text_colour = (255, 255, 255) back_colour = (0, 170, 170) if check_wifi() else (85, 15, 15) id = get_serial_number() - message = "{}\nWi-Fi: {}".format(id, wifi_status) - img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0)) + message = f"{id}\nWi-Fi: {wifi_status}" + img = Image.new("RGB", (WIDTH, HEIGHT), color=(0, 0, 0)) draw = ImageDraw.Draw(img) - size_x, size_y = draw.textsize(message, font) + x1, y1, x2, y2 = font.getbbox(message) + size_x = x2 - x1 + size_y = y2 - y1 x = (WIDTH - size_x) / 2 y = (HEIGHT / 2) - (size_y / 2) draw.rectangle((0, 0, 160, 80), back_colour) @@ -119,7 +118,7 @@ def display_status(): disp.display(img) -def send_to_luftdaten(values, id): +def send_to_sensorcommunity(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")) @@ -133,7 +132,7 @@ def send_to_luftdaten(values, id): resp_pm = requests.post( "https://api.sensor.community/v1/push-sensor-data/", json={ - "software_version": "enviro-plus 0.0.1", + "software_version": "enviro-plus 1.0.0", "sensordatavalues": pm_values_json }, headers={ @@ -145,17 +144,17 @@ def send_to_luftdaten(values, id): timeout=5 ) except requests.exceptions.ConnectionError as e: - logging.warning('Sensor.Community (Luftdaten) PM Connection Error: {}'.format(e)) + logging.warning(f"Sensor.Community PM Connection Error: {e}") except requests.exceptions.Timeout as e: - logging.warning('Sensor.Community (Luftdaten) PM Timeout Error: {}'.format(e)) + logging.warning(f"Sensor.Community PM Timeout Error: {e}") except requests.exceptions.RequestException as e: - logging.warning('Sensor.Community (Luftdaten) PM Request Error: {}'.format(e)) + logging.warning(f"Sensor.Community PM Request Error: {e}") try: resp_bmp = requests.post( "https://api.sensor.community/v1/push-sensor-data/", json={ - "software_version": "enviro-plus 0.0.1", + "software_version": "enviro-plus 1.0.0", "sensordatavalues": temp_values_json }, headers={ @@ -167,17 +166,17 @@ def send_to_luftdaten(values, id): timeout=5 ) except requests.exceptions.ConnectionError as e: - logging.warning('Sensor.Community (Luftdaten) Climate Connection Error: {}'.format(e)) + logging.warning(f"Sensor.Community Climate Connection Error: {e}") except requests.exceptions.Timeout as e: - logging.warning('Sensor.Community (Luftdaten) Climate Timeout Error: {}'.format(e)) + logging.warning(f"Sensor.Community Climate Timeout Error: {e}") except requests.exceptions.RequestException as e: - logging.warning('Sensor.Community (Luftdaten) Climate Request Error: {}'.format(e)) + logging.warning(f"Sensor.Community Climate Request Error: {e}") if resp_pm is not None and resp_bmp is not None: if resp_pm.ok and resp_bmp.ok: return True else: - logging.warning('Luftdaten Error. PM: {}, Climate: {}'.format(resp_pm.reason, resp_bmp.reason)) + logging.warning(f"Sensor.Community Error. PM: {resp_pm.reason}, Climate: {resp_bmp.reason}") return False else: return False @@ -186,7 +185,7 @@ def send_to_luftdaten(values, id): # Compensation factor for temperature comp_factor = 2.25 -# Raspberry Pi ID to send to Luftdaten +# Raspberry Pi ID to send to Sensor.Community id = "raspi-" + get_serial_number() # Width and height to calculate text position @@ -198,13 +197,14 @@ def send_to_luftdaten(values, id): font = ImageFont.truetype(UserFont, font_size) # Log Raspberry Pi serial and Wi-Fi status -logging.info("Raspberry Pi serial: {}".format(get_serial_number())) -logging.info("Wi-Fi: {}\n".format("connected" if check_wifi() else "disconnected")) +logging.info(f"Raspberry Pi serial: {get_serial_number()}") +wifi_status = "connected" if check_wifi() else "disconnected" +logging.info(f"Wi-Fi: {wifi_status}\n") time_since_update = 0 update_time = time.time() -# Main loop to read data, display, and send to Luftdaten +# Main loop to read data, display, and send to Sensor.Community while True: try: values = read_values() @@ -212,10 +212,10 @@ def send_to_luftdaten(values, id): if time_since_update > 145: logging.info(values) update_time = time.time() - if send_to_luftdaten(values, id): - logging.info("Luftdaten Response: OK") + if send_to_sensorcommunity(values, id): + logging.info("Sensor.Community Response: OK") else: - logging.warning("Luftdaten Response: Failed") + logging.warning("Sensor.Community Response: Failed") display_status() except Exception as e: - logging.warning('Main Loop Exception: {}'.format(e)) + logging.warning(f"Main Loop Exception: {e}") diff --git a/examples/luftdaten_combined.py b/examples/sensorcommunity_combined.py similarity index 86% rename from examples/luftdaten_combined.py rename to examples/sensorcommunity_combined.py index 1d920db..a7aeb7b 100644 --- a/examples/luftdaten_combined.py +++ b/examples/sensorcommunity_combined.py @@ -1,20 +1,18 @@ +import colorsys import logging -import sys -import requests -import ST7735 import time -import colorsys -from bme280 import BME280 -from pms5003 import PMS5003, ReadTimeoutError from subprocess import PIPE, Popen, check_output -from PIL import Image, ImageDraw, ImageFont + +import requests +import st7735 +from bme280 import BME280 from fonts.ttf import RobotoMedium as UserFont +from PIL import Image, ImageDraw, ImageFont +from pms5003 import PMS5003, ReadTimeoutError +from smbus2 import SMBus + from enviroplus import gas -try: - from smbus2 import SMBus -except ImportError: - from smbus import SMBus try: # Transitional fix for breaking change in LTR559 from ltr559 import LTR559 @@ -22,18 +20,18 @@ except ImportError: import ltr559 -print("""luftdaten_combined.py - This combines the functionality of luftdaten.py and combined.py +print("""sensorcommunity_combined.py - This combines the functionality of sensorcommunity.py and combined.py ================================================================================================ -Luftdaten INFO +Sensor.Community INFO Reads temperature, pressure, humidity, -PM2.5, and PM10 from Enviro plus and sends data to Luftdaten, +PM2.5, and PM10 from Enviro plus and sends data to Sensor.Community, the citizen science air quality project. -Note: you'll need to register with Luftdaten at: -https://meine.luftdaten.info/ and enter your Raspberry Pi +Note: you'll need to register with Sensor.Community at: +https://devices.sensor.community/ and enter your Raspberry Pi serial number that's displayed on the Enviro plus LCD along with the other details before the data appears on the -Luftdaten map. +Sensor.Community map. Press Ctrl+C to exit! @@ -47,9 +45,9 @@ """) logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") logging.info(""" """) bus = SMBus(1) @@ -119,9 +117,9 @@ # Read values from BME280 and PMS5003 and return as dict def read_values(comp_temp, mod_press, raw_humid, raw_pm25, raw_pm10): values = {} - values["temperature"] = "{:.2f}".format(comp_temp) - values["pressure"] = "{:.2f}".format(mod_press) - values["humidity"] = "{:.2f}".format(raw_humid) + values["temperature"] = f"{comp_temp:.2f}" + values["pressure"] = f"{mod_press:.2f}" + values["humidity"] = f"{raw_humid:.2f}" values["P2"] = str(raw_pm25) values["P1"] = str(raw_pm10) return values @@ -129,34 +127,34 @@ def read_values(comp_temp, mod_press, raw_humid, raw_pm25, raw_pm10): # Get CPU temperature to use for compensation def get_cpu_temperature(): - process = Popen(['vcgencmd', 'measure_temp'], + process = Popen(["vcgencmd", "measure_temp"], stdout=PIPE, universal_newlines=True) output, _error = process.communicate() - return float(output[output.index('=') + 1:output.rindex("'")]) + 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: + with open("/proc/cpuinfo", "r") as f: for line in f: - if line[0:6] == 'Serial': + if line.startswith("Serial"): return line.split(":")[1].strip() # Check for Wi-Fi connection def check_wifi(): - if check_output(['hostname', '-I']): + if check_output(["hostname", "-I"]): return True else: return False # Create ST7735 LCD display class -st7735 = ST7735.ST7735( +st7735 = st7735.ST7735( port=0, cs=1, - dc=9, - backlight=12, + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) @@ -168,7 +166,7 @@ def check_wifi(): HEIGHT = st7735.height # Set up canvas and font -img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0)) +img = Image.new("RGB", (WIDTH, HEIGHT), color=(0, 0, 0)) draw = ImageDraw.Draw(img) font_size_small = 10 font_size_large = 20 @@ -189,7 +187,7 @@ def save_data(idx, data): # Maintain length of list values_lcd[variable] = values_lcd[variable][1:] + [data] unit = units[idx] - message = "{}: {:.1f} {}".format(variable[:4], data, unit) + message = f"{variable[:4]}: {data:.1f} {unit}" logging.info(message) @@ -203,7 +201,7 @@ def display_text(variable, data, unit): colours = [(v - vmin + 1) / (vmax - vmin + 1) for v in values_lcd[variable]] # Format the variable name and value - message = "{}: {:.1f} {}".format(variable[:4], data, unit) + message = f"{variable[:4]}: {data:.1f} {unit}" logging.info(message) draw.rectangle((0, 0, WIDTH, HEIGHT), (255, 255, 255)) for i in range(len(colours)): @@ -234,7 +232,7 @@ def display_everything(): unit = units[i] 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) + message = f"{variable[:4]}: {data_value:.1f} {unit}" lim = limits[i] rgb = palette[0] for j in range(len(lim)): @@ -244,7 +242,7 @@ def display_everything(): st7735.display(img) -def send_to_luftdaten(values, id): +def send_to_sensorcommunity(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")) @@ -254,9 +252,9 @@ def send_to_luftdaten(values, id): for key, val in temp_values.items()] resp_1 = requests.post( - "https://api.luftdaten.info/v1/push-sensor-data/", + "https://api.sensor.community/v1/push-sensor-data/", json={ - "software_version": "enviro-plus 0.0.1", + "software_version": "enviro-plus 1.0.0", "sensordatavalues": pm_values_json }, headers={ @@ -268,9 +266,9 @@ def send_to_luftdaten(values, id): ) resp_2 = requests.post( - "https://api.luftdaten.info/v1/push-sensor-data/", + "https://api.sensor.community/v1/push-sensor-data/", json={ - "software_version": "enviro-plus 0.0.1", + "software_version": "enviro-plus 1.0.0", "sensordatavalues": temp_values_json }, headers={ @@ -290,7 +288,7 @@ def send_to_luftdaten(values, id): # Compensation factor for temperature comp_factor = 1 -# Raspberry Pi ID to send to Luftdaten +# Raspberry Pi ID to send to Sensor.Community id = "raspi-" + get_serial_number() @@ -311,14 +309,15 @@ def send_to_luftdaten(values, id): cpu_temps = [get_cpu_temperature()] * 5 # 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(f"Raspberry Pi serial: {get_serial_number()}") +wifi_status = "connected" if check_wifi() else "disconnected" +print(f"Wi-Fi: {wifi_status}\n") time_since_update = 0 update_time = time.time() cpu_temps_len = float(len(cpu_temps)) -# Main loop to read data, display, and send to Luftdaten +# Main loop to read data, display, and send to Sensor.Community while True: try: curtime = time.time() @@ -347,9 +346,10 @@ def send_to_luftdaten(values, id): if time_since_update > 145: values = read_values(comp_temp, raw_press*100, raw_humid, raw_pm25, raw_pm10) - resp = send_to_luftdaten(values, id) + resp = send_to_sensorcommunity(values, id) update_time = curtime - print("Response: {}\n".format("ok" if resp else "failed")) + status = "ok" if resp else "failed" + print(f"Response: {status}\n") # Now comes the combined.py functionality: # If the proximity crosses the threshold, toggle the mode diff --git a/examples/weather-and-light.py b/examples/weather-and-light.py index cd8ae96..04f1bd8 100755 --- a/examples/weather-and-light.py +++ b/examples/weather-and-light.py @@ -1,29 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -f"Sorry! This program requires Python >= 3.6 😅" - +import colorsys import os import time -import numpy -import colorsys -from PIL import Image, ImageDraw, ImageFont, ImageFilter -from fonts.ttf import RobotoMedium as UserFont - -import ST7735 -from bme280 import BME280 -from ltr559 import LTR559 +from datetime import datetime, timedelta +import numpy import pytz -from pytz import timezone +import st7735 from astral.geocoder import database, lookup from astral.sun import sun -from datetime import datetime, timedelta - -try: - from smbus2 import SMBus -except ImportError: - from smbus import SMBus +from bme280 import BME280 +from fonts.ttf import RobotoMedium as UserFont +from ltr559 import LTR559 +from PIL import Image, ImageDraw, ImageFilter, ImageFont +from smbus2 import SMBus def calculate_y_pos(x, centre): @@ -48,7 +40,7 @@ def circle_coordinates(x, y, radius): def map_colour(x, centre, start_hue, end_hue, day): """Given an x coordinate and a centre point, a start and end hue (in degrees), and a Boolean for day or night (day is True, night False), calculate a colour - hue representing the 'colour' of that time of day.""" + hue representing the "colour" of that time of day.""" start_hue = start_hue / 360 # Rescale to between 0 and 1 end_hue = end_hue / 360 @@ -85,7 +77,7 @@ def x_from_sun_moon_time(progress, period, x_range): def sun_moon_time(city_name, time_zone): """Calculate the progress through the current sun/moon period (i.e day or - night) from the last sunrise or sunset, given a datetime object 't'.""" + night) from the last sunrise or sunset, given a datetime object "t".""" city = lookup(city_name, database()) @@ -141,11 +133,11 @@ def draw_background(progress, period, day): # x-coordinate for sun/moon x = x_from_sun_moon_time(progress, period, WIDTH) - # If it's day, then move right to left + # If it"s day, then move right to left if day: x = WIDTH - x - # Calculate position on sun/moon's curve + # Calculate position on sun/moon"s curve centre = WIDTH / 2 y = calculate_y_pos(x, centre) @@ -153,11 +145,11 @@ def draw_background(progress, period, day): background = map_colour(x, 80, mid_hue, day_hue, day) # New image for background colour - img = Image.new('RGBA', (WIDTH, HEIGHT), color=background) + img = Image.new("RGBA", (WIDTH, HEIGHT), color=background) # draw = ImageDraw.Draw(img) # New image for sun/moon overlay - overlay = Image.new('RGBA', (WIDTH, HEIGHT), color=(0, 0, 0, 0)) + overlay = Image.new("RGBA", (WIDTH, HEIGHT), color=(0, 0, 0, 0)) overlay_draw = ImageDraw.Draw(overlay) # Draw the sun/moon @@ -170,9 +162,14 @@ def draw_background(progress, period, day): return composite +def text_size(font, text): + x1, y1, x2, y2 = font.getbbox(text) + return x2 - x1, y2 - y1 + + def overlay_text(img, position, text, font, align_right=False, rectangle=False): draw = ImageDraw.Draw(img) - w, h = font.getsize(text) + w, h = text_size(font, text) if align_right: x, y = position x -= w @@ -183,7 +180,7 @@ def overlay_text(img, position, text, font, align_right=False, rectangle=False): position = (x, y) border = 1 rect = (x - border, y, x + w, y + h + border) - rect_img = Image.new('RGBA', (WIDTH, HEIGHT), color=(0, 0, 0, 0)) + rect_img = Image.new("RGBA", (WIDTH, HEIGHT), color=(0, 0, 0, 0)) rect_draw = ImageDraw.Draw(rect_img) rect_draw.rectangle(rect, (255, 255, 255)) rect_draw.text(position, text, font=font, fill=(0, 0, 0, 0)) @@ -291,11 +288,11 @@ def describe_light(light): # Initialise the LCD -disp = ST7735.ST7735( +disp = st7735.ST7735( port=0, cs=1, - dc=9, - backlight=12, + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) @@ -356,7 +353,7 @@ def describe_light(light): # Time. time_elapsed = time.time() - start_time - date_string = local_dt.strftime("%d %b %y").lstrip('0') + date_string = local_dt.strftime("%d %b %y").lstrip("0") time_string = local_dt.strftime("%H:%M") img = overlay_text(background, (0 + margin, 0 + margin), time_string, font_lg) img = overlay_text(img, (WIDTH - margin, 0 + margin), date_string, font_lg, align_right=True) @@ -382,7 +379,8 @@ def describe_light(light): temp_string = f"{corr_temperature:.0f}°C" img = overlay_text(img, (68, 18), temp_string, font_lg, align_right=True) - spacing = font_lg.getsize(temp_string)[1] + 1 + _, text_height = text_size(font_lg, temp_string) + spacing = text_height + 1 if min_temp is not None and max_temp is not None: range_string = f"{min_temp:.0f}-{max_temp:.0f}" else: @@ -396,7 +394,8 @@ def describe_light(light): corr_humidity = correct_humidity(humidity, temperature, corr_temperature) humidity_string = f"{corr_humidity:.0f}%" img = overlay_text(img, (68, 48), humidity_string, font_lg, align_right=True) - spacing = font_lg.getsize(humidity_string)[1] + 1 + _, text_height = text_size(font_lg, humidity_string) + spacing = text_height + 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(f"{path}/icons/humidity-{humidity_desc.lower()}.png") @@ -406,7 +405,8 @@ def describe_light(light): light = ltr559.get_lux() light_string = f"{int(light):,}" img = overlay_text(img, (WIDTH - margin, 18), light_string, font_lg, align_right=True) - spacing = font_lg.getsize(light_string.replace(",", ""))[1] + 1 + _, text_height = text_size(font_lg, light_string.replace(",", "")) + spacing = text_height + 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(f"{path}/icons/bulb-{light_desc.lower()}.png") @@ -419,7 +419,8 @@ def describe_light(light): pressure_string = f"{int(mean_pressure):,} {trend}" img = overlay_text(img, (WIDTH - margin, 48), pressure_string, font_lg, align_right=True) pressure_desc = describe_pressure(mean_pressure).upper() - spacing = font_lg.getsize(pressure_string.replace(",", ""))[1] + 1 + _, text_height = text_size(font_lg, pressure_string.replace(",", "")) + spacing = text_height + 1 img = overlay_text(img, (WIDTH - margin - 1, 48 + spacing), pressure_desc, font_sm, align_right=True, rectangle=True) pressure_icon = Image.open(f"{path}/icons/weather-{pressure_desc.lower()}.png") img.paste(pressure_icon, (80, 48), mask=pressure_icon) diff --git a/examples/weather.py b/examples/weather.py index 66f18e0..0b671d3 100755 --- a/examples/weather.py +++ b/examples/weather.py @@ -1,19 +1,15 @@ #!/usr/bin/env python3 +import logging import time -from bme280 import BME280 - -try: - from smbus2 import SMBus -except ImportError: - from smbus import SMBus -import logging +from bme280 import BME280 +from smbus2 import SMBus logging.basicConfig( - format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', + format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.INFO, - datefmt='%Y-%m-%d %H:%M:%S') + datefmt="%Y-%m-%d %H:%M:%S") logging.info("""weather.py - Print readings from the BME280 weather sensor. @@ -28,8 +24,8 @@ temperature = bme280.get_temperature() pressure = bme280.get_pressure() humidity = bme280.get_humidity() - logging.info("""Temperature: {:05.2f} *C -Pressure: {:05.2f} hPa -Relative humidity: {:05.2f} % -""".format(temperature, pressure, humidity)) + logging.info(f"""Temperature: {temperature:05.2f} °C +Pressure: {pressure:05.2f} hPa +Relative humidity: {humidity:05.2f} % +""") time.sleep(1) diff --git a/install-bullseye.sh b/install-bullseye.sh deleted file mode 100755 index 9780175..0000000 --- a/install-bullseye.sh +++ /dev/null @@ -1,254 +0,0 @@ -#!/bin/bash -CONFIG=/boot/config.txt -DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` -CONFIG_BACKUP=false -APT_HAS_UPDATED=false -USER_HOME=/home/$SUDO_USER -RESOURCES_TOP_DIR=$USER_HOME/Pimoroni -WD=`pwd` -USAGE="sudo $0 (--unstable)" -POSITIONAL_ARGS=() -UNSTABLE=false -PYTHON="/usr/bin/python3" -CODENAME=`lsb_release -sc` - -distro_check() { - if [[ $CODENAME != "bullseye" ]]; then - printf "This installer is for Raspberry Pi OS: Bullseye only, current distro: $CODENAME\n" - exit 1 - fi -} - -user_check() { - if [ $(id -u) -ne 0 ]; then - printf "Script must be run as root. Try 'sudo $0'\n" - exit 1 - fi -} - -confirm() { - if [ "$FORCE" == '-y' ]; then - true - else - read -r -p "$1 [y/N] " response < /dev/tty - if [[ $response =~ ^(yes|y|Y)$ ]]; then - true - else - false - fi - fi -} - -prompt() { - read -r -p "$1 [y/N] " response < /dev/tty - if [[ $response =~ ^(yes|y|Y)$ ]]; then - true - else - false - fi -} - -success() { - echo -e "$(tput setaf 2)$1$(tput sgr0)" -} - -inform() { - echo -e "$(tput setaf 6)$1$(tput sgr0)" -} - -warning() { - echo -e "$(tput setaf 1)$1$(tput sgr0)" -} - -function do_config_backup { - if [ ! $CONFIG_BACKUP == true ]; then - CONFIG_BACKUP=true - FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" - inform "Backing up $CONFIG to /boot/$FILENAME\n" - cp $CONFIG /boot/$FILENAME - mkdir -p $RESOURCES_TOP_DIR/config-backups/ - cp $CONFIG $RESOURCES_TOP_DIR/config-backups/$FILENAME - if [ -f "$UNINSTALLER" ]; then - echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG" >> $UNINSTALLER - fi - fi -} - -function apt_pkg_install { - PACKAGES=() - PACKAGES_IN=("$@") - for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do - PACKAGE="${PACKAGES_IN[$i]}" - if [ "$PACKAGE" == "" ]; then continue; fi - printf "Checking for $PACKAGE\n" - dpkg -L $PACKAGE > /dev/null 2>&1 - if [ "$?" == "1" ]; then - PACKAGES+=("$PACKAGE") - fi - done - PACKAGES="${PACKAGES[@]}" - if ! [ "$PACKAGES" == "" ]; then - echo "Installing missing packages: $PACKAGES" - if [ ! $APT_HAS_UPDATED ]; then - apt update - APT_HAS_UPDATED=true - fi - apt install -y $PACKAGES - if [ -f "$UNINSTALLER" ]; then - echo "apt uninstall -y $PACKAGES" - fi - fi -} - -while [[ $# -gt 0 ]]; do - K="$1" - case $K in - -u|--unstable) - UNSTABLE=true - shift - ;; - -p|--python) - PYTHON=$2 - shift - shift - ;; - *) - if [[ $1 == -* ]]; then - printf "Unrecognised option: $1\n"; - printf "Usage: $USAGE\n"; - exit 1 - fi - POSITIONAL_ARGS+=("$1") - shift - esac -done - -distro_check -user_check - -if [ ! -f "$PYTHON" ]; then - printf "Python path $PYTHON not found!\n" - exit 1 -fi - -PYTHON_VER=`$PYTHON --version` - -inform "Installing. Please wait..." - -$PYTHON -m pip install --upgrade configparser - -CONFIG_VARS=`$PYTHON - < $UNINSTALLER -printf "It's recommended you run these steps manually.\n" -printf "If you want to run the full script, open it in\n" -printf "an editor and remove 'exit 1' from below.\n" -exit 1 -EOF - -printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Installer\n\n" - -if $UNSTABLE; then - warning "Installing unstable library from source.\n\n" -else - printf "Installing stable library from pypi.\n\n" -fi - -cd library - -printf "Installing for $PYTHON_VER...\n" -apt_pkg_install "${PY3_DEPS[@]}" -if $UNSTABLE; then - $PYTHON setup.py install > /dev/null -else - $PYTHON -m pip install --upgrade $LIBRARY_NAME -fi -if [ $? -eq 0 ]; then - success "Done!\n" - echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> $UNINSTALLER -fi - -cd $WD - -for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do - CMD="${SETUP_CMDS[$i]}" - # Attempt to catch anything that touches /boot/config.txt and trigger a backup - if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG"* ]] || [[ "$CMD" == *"\$CONFIG"* ]]; then - do_config_backup - fi - eval $CMD -done - -for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do - CONFIG_LINE="${CONFIG_TXT[$i]}" - if ! [ "$CONFIG_LINE" == "" ]; then - do_config_backup - inform "Adding $CONFIG_LINE to $CONFIG\n" - sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG - if ! grep -q "^$CONFIG_LINE" $CONFIG; then - printf "$CONFIG_LINE\n" >> $CONFIG - fi - fi -done - -if [ -d "examples" ]; then - if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then - inform "Copying examples to $RESOURCES_DIR" - cp -r examples/ $RESOURCES_DIR - echo "rm -r $RESOURCES_DIR" >> $UNINSTALLER - success "Done!" - fi -fi - -printf "\n" - -if [ -f "/usr/bin/pydoc" ]; then - printf "Generating documentation.\n" - pydoc -w $LIBRARY_NAME > /dev/null - if [ -f "$LIBRARY_NAME.html" ]; then - cp $LIBRARY_NAME.html $RESOURCES_DIR/docs.html - rm -f $LIBRARY_NAME.html - inform "Documentation saved to $RESOURCES_DIR/docs.html" - success "Done!" - else - warning "Error: Failed to generate documentation." - fi -fi - -success "\nAll done!" -warning "If this is your first time installing you should --reboot-- for hardware changes to take effect.\n" -warning "This library is installed for python 3 *only* make sure to use \"python3\" when running examples.\n" diff --git a/install.sh b/install.sh index 66c9e4d..495919b 100755 --- a/install.sh +++ b/install.sh @@ -1,31 +1,29 @@ #!/bin/bash - -CONFIG=/boot/config.txt -DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` +LIBRARY_NAME=$(grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}') +CONFIG_FILE=config.txt +CONFIG_DIR="/boot/firmware" +DATESTAMP=$(date "+%Y-%m-%d-%H-%M-%S") CONFIG_BACKUP=false APT_HAS_UPDATED=false -USER_HOME=/home/$SUDO_USER -RESOURCES_TOP_DIR=$USER_HOME/Pimoroni -WD=`pwd` -USAGE="sudo $0 (--unstable)" +RESOURCES_TOP_DIR="$HOME/Pimoroni" +VENV_BASH_SNIPPET="$RESOURCES_TOP_DIR/auto_venv.sh" +VENV_DIR="$HOME/.virtualenvs/pimoroni" +USAGE="./install.sh (--unstable)" POSITIONAL_ARGS=() +FORCE=false UNSTABLE=false -CODENAME=`lsb_release -sc` +PYTHON="python" +CMD_ERRORS=false -if [[ $CODENAME == "bullseye" ]]; then - bash ./install-bullseye.sh $@ - exit $? -fi user_check() { - if [ $(id -u) -ne 0 ]; then - printf "Script must be run as root. Try 'sudo $0'\n" - exit 1 + if [ "$(id -u)" -eq 0 ]; then + fatal "Script should not be run as root. Try './install.sh'\n" fi } confirm() { - if [ "$FORCE" == '-y' ]; then + if $FORCE; then true else read -r -p "$1 [y/N] " response < /dev/tty @@ -37,15 +35,6 @@ confirm() { fi } -prompt() { - read -r -p "$1 [y/N] " response < /dev/tty - if [[ $response =~ ^(yes|y|Y)$ ]]; then - true - else - false - fi -} - success() { echo -e "$(tput setaf 2)$1$(tput sgr0)" } @@ -55,49 +44,133 @@ inform() { } warning() { - echo -e "$(tput setaf 1)$1$(tput sgr0)" + echo -e "$(tput setaf 1)⚠ WARNING:$(tput sgr0) $1" +} + +fatal() { + echo -e "$(tput setaf 1)⚠ FATAL:$(tput sgr0) $1" + exit 1 +} + +find_config() { + if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then + CONFIG_DIR="/boot" + if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then + fatal "Could not find $CONFIG_FILE!" + fi + else + if [ -f "/boot/$CONFIG_FILE" ] && [ ! -L "/boot/$CONFIG_FILE" ]; then + warning "Oops! It looks like /boot/$CONFIG_FILE is not a link to $CONFIG_DIR/$CONFIG_FILE" + warning "You might want to fix this!" + fi + fi + inform "Using $CONFIG_FILE in $CONFIG_DIR" +} + +venv_bash_snippet() { + inform "Checking for $VENV_BASH_SNIPPET\n" + if [ ! -f "$VENV_BASH_SNIPPET" ]; then + inform "Creating $VENV_BASH_SNIPPET\n" + mkdir -p "$RESOURCES_TOP_DIR" + cat << EOF > "$VENV_BASH_SNIPPET" +# Add "source $VENV_BASH_SNIPPET" to your ~/.bashrc to activate +# the Pimoroni virtual environment automagically! +VENV_DIR="$VENV_DIR" +if [ ! -f \$VENV_DIR/bin/activate ]; then + printf "Creating user Python environment in \$VENV_DIR, please wait...\n" + mkdir -p \$VENV_DIR + python3 -m venv --system-site-packages \$VENV_DIR +fi +printf " ↓ ↓ ↓ ↓ Hello, we've activated a Python venv for you. To exit, type \"deactivate\".\n" +source \$VENV_DIR/bin/activate +EOF + fi +} + +venv_check() { + PYTHON_BIN=$(which "$PYTHON") + if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then + printf "This script should be run in a virtual Python environment.\n" + if confirm "Would you like us to create and/or use a default one?"; then + printf "\n" + if [ ! -f "$VENV_DIR/bin/activate" ]; then + inform "Creating a new virtual Python environment in $VENV_DIR, please wait...\n" + mkdir -p "$VENV_DIR" + /usr/bin/python3 -m venv "$VENV_DIR" --system-site-packages + venv_bash_snippet + # shellcheck disable=SC1091 + source "$VENV_DIR/bin/activate" + else + inform "Activating existing virtual Python environment in $VENV_DIR\n" + printf "source \"%s/bin/activate\"\n" "$VENV_DIR" + # shellcheck disable=SC1091 + source "$VENV_DIR/bin/activate" + fi + else + printf "\n" + fatal "Please create and/or activate a virtual Python environment and try again!\n" + fi + fi + printf "\n" +} + +check_for_error() { + if [ $? -ne 0 ]; then + CMD_ERRORS=true + warning "^^^ 😬 previous command did not exit cleanly!" + fi } function do_config_backup { if [ ! $CONFIG_BACKUP == true ]; then CONFIG_BACKUP=true FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" - inform "Backing up $CONFIG to /boot/$FILENAME\n" - cp $CONFIG /boot/$FILENAME - mkdir -p $RESOURCES_TOP_DIR/config-backups/ - cp $CONFIG $RESOURCES_TOP_DIR/config-backups/$FILENAME + inform "Backing up $CONFIG_DIR/$CONFIG_FILE to $CONFIG_DIR/$FILENAME\n" + sudo cp "$CONFIG_DIR/$CONFIG_FILE" "$CONFIG_DIR/$FILENAME" + mkdir -p "$RESOURCES_TOP_DIR/config-backups/" + cp $CONFIG_DIR/$CONFIG_FILE "$RESOURCES_TOP_DIR/config-backups/$FILENAME" if [ -f "$UNINSTALLER" ]; then - echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG" >> $UNINSTALLER + echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG_DIR/$CONFIG_FILE" >> "$UNINSTALLER" fi fi } function apt_pkg_install { - PACKAGES=() + PACKAGES_NEEDED=() PACKAGES_IN=("$@") + # Check the list of packages and only run update/install if we need to for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do PACKAGE="${PACKAGES_IN[$i]}" if [ "$PACKAGE" == "" ]; then continue; fi - printf "Checking for $PACKAGE\n" - dpkg -L $PACKAGE > /dev/null 2>&1 + printf "Checking for %s\n" "$PACKAGE" + dpkg -L "$PACKAGE" > /dev/null 2>&1 if [ "$?" == "1" ]; then - PACKAGES+=("$PACKAGE") + PACKAGES_NEEDED+=("$PACKAGE") fi done - PACKAGES="${PACKAGES[@]}" + PACKAGES="${PACKAGES_NEEDED[*]}" if ! [ "$PACKAGES" == "" ]; then - echo "Installing missing packages: $PACKAGES" + printf "\n" + inform "Installing missing packages: $PACKAGES" if [ ! $APT_HAS_UPDATED ]; then - apt update + sudo apt update APT_HAS_UPDATED=true fi - apt install -y $PACKAGES + # shellcheck disable=SC2086 + sudo apt install -y $PACKAGES + check_for_error if [ -f "$UNINSTALLER" ]; then - echo "apt uninstall -y $PACKAGES" + echo "apt uninstall -y $PACKAGES" >> "$UNINSTALLER" fi fi } +function pip_pkg_install { + # A null Keyring prevents pip stalling in the background + PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring $PYTHON -m pip install --upgrade "$@" + check_for_error +} + while [[ $# -gt 0 ]]; do K="$1" case $K in @@ -105,10 +178,19 @@ while [[ $# -gt 0 ]]; do UNSTABLE=true shift ;; + -f|--force) + FORCE=true + shift + ;; + -p|--python) + PYTHON=$2 + shift + shift + ;; *) if [[ $1 == -* ]]; then - printf "Unrecognised option: $1\n"; - printf "Usage: $USAGE\n"; + printf "Unrecognised option: %s\n" "$1"; + printf "Usage: %s\n" "$USAGE"; exit 1 fi POSITIONAL_ARGS+=("$1") @@ -116,113 +198,178 @@ while [[ $# -gt 0 ]]; do esac done +printf "Installing %s...\n\n" "$LIBRARY_NAME" + user_check +venv_check -apt_pkg_install python-configparser - -CONFIG_VARS=`python - < $UNINSTALLER +mkdir -p "$RESOURCES_DIR" + +# Create a stub uninstaller file, we'll try to add the inverse of every +# install command run to here, though it's not complete. +cat << EOF > "$UNINSTALLER" printf "It's recommended you run these steps manually.\n" printf "If you want to run the full script, open it in\n" printf "an editor and remove 'exit 1' from below.\n" exit 1 +source $VIRTUAL_ENV/bin/activate EOF -printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Installer\n\n" +printf "\n" + +inform "Installing for $PYTHON_VER...\n" + +# Install apt packages from pyproject.toml / tool.pimoroni.apt_packages +apt_pkg_install "${APT_PACKAGES[@]}" + +printf "\n" if $UNSTABLE; then - warning "Installing unstable library from source.\n\n" + warning "Installing unstable library from source.\n" + pip_pkg_install . else - printf "Installing stable library from pypi.\n\n" + inform "Installing stable library from pypi.\n" + pip_pkg_install "$LIBRARY_NAME" fi -cd library - -if [ -f "/usr/bin/python3" ]; then - printf "Installing for Python 3..\n" - apt_pkg_install "${PY3_DEPS[@]}" - if $UNSTABLE; then - python3 setup.py install > /dev/null - else - pip3 install --upgrade $LIBRARY_NAME - fi - if [ $? -eq 0 ]; then - success "Done!\n" - echo "pip3 uninstall $LIBRARY_NAME" >> $UNINSTALLER - fi -else - printf "/usr/bin/python3 not found. Unable to install!\n" - exit 1 +# shellcheck disable=SC2181 # One of two commands run, depending on --unstable flag +if [ $? -eq 0 ]; then + success "Done!\n" + echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> "$UNINSTALLER" fi -cd $WD +find_config + +printf "\n" + +# Run the setup commands from pyproject.toml / tool.pimoroni.commands +inform "Running setup commands...\n" for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do CMD="${SETUP_CMDS[$i]}" - # Attempt to catch anything that touches /boot/config.txt and trigger a backup - if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG"* ]] || [[ "$CMD" == *"\$CONFIG"* ]]; then + # Attempt to catch anything that touches config.txt and trigger a backup + if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG_DIR/$CONFIG_FILE"* ]] || [[ "$CMD" == *"\$CONFIG_DIR/\$CONFIG_FILE"* ]]; then do_config_backup fi - eval $CMD + if [[ ! "$CMD" == printf* ]]; then + printf "Running: \"%s\"\n" "$CMD" + fi + eval "$CMD" + check_for_error done +printf "\n" + +# Add the config.txt entries from pyproject.toml / tool.pimoroni.configtxt + for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do CONFIG_LINE="${CONFIG_TXT[$i]}" if ! [ "$CONFIG_LINE" == "" ]; then do_config_backup - inform "Adding $CONFIG_LINE to $CONFIG\n" - sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG - if ! grep -q "^$CONFIG_LINE" $CONFIG; then - printf "$CONFIG_LINE\n" >> $CONFIG + inform "Adding $CONFIG_LINE to $CONFIG_DIR/$CONFIG_FILE" + sudo sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG_DIR/$CONFIG_FILE + if ! grep -q "^$CONFIG_LINE" $CONFIG_DIR/$CONFIG_FILE; then + printf "%s \n" "$CONFIG_LINE" | sudo tee --append $CONFIG_DIR/$CONFIG_FILE fi fi done +printf "\n" + +# Just a straight copy of the examples/ dir into ~/Pimoroni/board/examples + if [ -d "examples" ]; then if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then inform "Copying examples to $RESOURCES_DIR" - cp -r examples/ $RESOURCES_DIR - echo "rm -r $RESOURCES_DIR" >> $UNINSTALLER + cp -r examples/ "$RESOURCES_DIR" + echo "rm -r $RESOURCES_DIR" >> "$UNINSTALLER" + success "Done!" + fi +fi + +printf "\n" + +# Use pdoc to generate basic documentation from the installed module + +if confirm "Would you like to generate documentation?"; then + inform "Installing pdoc. Please wait..." + pip_pkg_install pdoc + inform "Generating documentation.\n" + if $PYTHON -m pdoc "$LIBRARY_NAME" -o "$RESOURCES_DIR/docs" > /dev/null; then + inform "Documentation saved to $RESOURCES_DIR/docs" success "Done!" + else + warning "Error: Failed to generate documentation." fi fi -success "\nAll done!" -warning "If this is your first time installing you should --reboot-- for hardware changes to take effect.\n" -warning "This library is installed for Python 3 *only* make sure to use \"python3\" when running examples.\n" -inform "Find uninstall steps in $UNINSTALLER\n" +printf "\n" + +if [ "$CMD_ERRORS" = true ]; then + warning "One or more setup commands appear to have failed." + printf "This might prevent things from working properly.\n" + printf "Make sure your OS is up to date and try re-running this installer.\n" + printf "If things still don't work, report this or find help at %s.\n\n" "$GITHUB_URL" +else + success "\nAll done!" +fi + +printf "If this is your first time installing you should reboot for hardware changes to take effect.\n" +printf "Find uninstall steps in %s\n\n" "$UNINSTALLER" + +if [ "$CMD_ERRORS" = true ]; then + exit 1 +else + exit 0 +fi diff --git a/library/LICENSE.txt b/library/LICENSE.txt deleted file mode 100644 index aed751a..0000000 --- a/library/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2018 Pimoroni Ltd. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/library/README.md b/library/README.md deleted file mode 100644 index 1498814..0000000 --- a/library/README.md +++ /dev/null @@ -1,107 +0,0 @@ -# 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) -[![Python Versions](https://img.shields.io/pypi/pyversions/enviroplus.svg)](https://pypi.python.org/pypi/enviroplus) - -# Installing - -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) - -:warning: This library now supports Python 3 only, Python 2 is EOL - https://www.python.org/doc/sunset-python-2/ - -## One-line (Installs from GitHub) - -``` -curl -sSL https://get.pimoroni.com/enviroplus | bash -``` - -**Note** report issues with one-line installer here: https://github.com/pimoroni/get - -## 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 python3 -m 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 python3-numpy python3-smbus python3-pil python3-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 -* enviroplus_exporter - https://github.com/tijmenvandenbrink/enviroplus_exporter - Prometheus exporter (with added support for Luftdaten and InfluxDB Cloud) -* homekit-enviroplus - https://github.com/sighmon/homekit-enviroplus - An Apple HomeKit accessory for the Pimoroni Enviro+ -* go-enviroplus - https://github.com/rubiojr/go-enviroplus - Go modules to read Enviro+ sensors - -## Help & Support - -* GPIO Pinout - https://pinout.xyz/pinout/enviro_plus -* Support forums - http://forums.pimoroni.com/c/support -* Discord - https://discord.gg/hr93ByC - -# Changelog -0.0.6 ------ - -* Fix noise by specifying adau7002 device - -0.0.5 ------ - -* Drop Python 2.x support -* Add "available()" method for gas sensor - -0.0.4 ------ - -* Add support for ads1015 >= v0.0.7 (ADS1115 ADCs) -* Packaging tweaks - -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 ------ - -* Initial Release diff --git a/library/enviroplus/__init__.py b/library/enviroplus/__init__.py deleted file mode 100644 index fa9c4ec..0000000 --- a/library/enviroplus/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = '0.0.6' diff --git a/library/setup.cfg b/library/setup.cfg deleted file mode 100644 index 68d9d3f..0000000 --- a/library/setup.cfg +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- -[metadata] -name = enviroplus -version = 0.0.6 -author = Philip Howard -author_email = phil@pimoroni.com -description = Enviro pHAT Plus environmental monitoring add-on for Raspberry Pi -long_description = file: README.md -long_description_content_type = text/markdown -keywords = Raspberry Pi -url = https://www.pimoroni.com -project_urls = - GitHub=https://www.github.com/pimoroni/enviroplus-python -license = MIT -# This includes the license file(s) in the wheel. -# https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file -license_files = LICENSE.txt -classifiers = - Development Status :: 4 - Beta - Operating System :: POSIX :: Linux - License :: OSI Approved :: MIT License - Intended Audience :: Developers - Programming Language :: Python :: 3 - Topic :: Software Development - Topic :: Software Development :: Libraries - Topic :: System :: Hardware - -[options] -python_requires = >= 3.6 -packages = enviroplus -install_requires = - pimoroni-bme280 - pms5003 - ltr559 - st7735 - ads1015 >= 0.0.7 - fonts - font-roboto - astral - pytz - sounddevice - paho-mqtt - -[flake8] -exclude = - .tox, - .eggs, - .git, - __pycache__, - build, - dist -ignore = - E501 - -[pimoroni] -py2deps = -py3deps = - python3 - python3-pip - python3-numpy - python3-smbus - python3-pil - python3-cffi - python3-spidev - python3-rpi.gpio - libportaudio2 -configtxt = - dtoverlay=pi3-miniuart-bt - dtoverlay=adau7002-simple -commands = - printf "Setting up i2c and SPI..\n" - raspi-config nonint do_spi 0 - raspi-config nonint do_i2c 0 - printf "Setting up serial for PMS5003..\n" - raspi-config nonint do_serial 1 # Disable serial terminal over /dev/ttyAMA0 - raspi-config nonint set_config_var enable_uart 1 $CONFIG # Enable serial port diff --git a/library/setup.py b/library/setup.py deleted file mode 100755 index 40d6dbc..0000000 --- a/library/setup.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2016 Pimoroni - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -from setuptools import setup, __version__ -from pkg_resources import parse_version - -minimum_version = parse_version('30.4.0') - -if parse_version(__version__) < minimum_version: - raise RuntimeError("Package setuptools must be at least version {}".format(minimum_version)) - -setup() diff --git a/library/tests/conftest.py b/library/tests/conftest.py deleted file mode 100644 index b3fa376..0000000 --- a/library/tests/conftest.py +++ /dev/null @@ -1,115 +0,0 @@ -"""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 - - -class SMBusFakeDeviceNoTimeout(MockSMBus): - def __init__(self, i2c_bus): - MockSMBus.__init__(self, i2c_bus) - self.regs[0x00:0x01] = 0x0f, 0x80 - - -@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 smbus_notimeout(): - """Mock smbus module.""" - smbus = mock.MagicMock() - smbus.SMBus = SMBusFakeDeviceNoTimeout - sys.modules['smbus'] = smbus - yield smbus - del sys.modules['smbus'] - - -@pytest.fixture(scope='function', autouse=False) -def mocksmbus(): - """Mock smbus module.""" - smbus = mock.MagicMock() - 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/tox.ini b/library/tox.ini deleted file mode 100644 index 1b75786..0000000 --- a/library/tox.ini +++ /dev/null @@ -1,24 +0,0 @@ -[tox] -envlist = py,qa -skip_missing_interpreters = True - -[testenv] -commands = - python setup.py install - coverage run -m pytest -v -r wsx - coverage report -deps = - mock - pytest>=3.1 - pytest-cov - -[testenv:qa] -commands = - check-manifest --ignore tox.ini,tests*,.coveragerc - python setup.py check -m -r -s - flake8 --ignore E501 - rstcheck README.rst -deps = - check-manifest - flake8 - rstcheck diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d97b417 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,145 @@ +[build-system] +requires = ["hatchling", "hatch-fancy-pypi-readme"] +build-backend = "hatchling.build" + +[project] +name = "enviroplus" +dynamic = ["version", "readme"] +description = "Enviro pHAT Plus environmental monitoring add-on for Raspberry Pi" +license = {file = "LICENSE"} +requires-python = ">= 3.7" +authors = [ + { name = "Philip Howard", email = "phil@pimoroni.com" }, +] +maintainers = [ + { name = "Philip Howard", email = "phil@pimoroni.com" }, +] +keywords = [ + "Pi", + "Raspberry", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Software Development", + "Topic :: Software Development :: Libraries", + "Topic :: System :: Hardware", +] +dependencies = [ + "gpiod >= 2.1.3", + "gpiodevice >= 0.0.3", + "pimoroni-bme280 >= 1.0.0", + "pms5003 >= 1.0.0", + "ltr559 >= 1.0.0", + "st7735 >= 1.0.0", + "ads1015 >= 1.0.0", + "fonts", + "font-roboto", + "astral", + "pytz", + "sounddevice", + "paho-mqtt" +] + +[project.urls] +GitHub = "https://www.github.com/pimoroni/enviroplus-python" +Homepage = "https://www.pimoroni.com" + +[tool.hatch.version] +path = "enviroplus/__init__.py" + +[tool.hatch.build] +include = [ + "enviroplus", + "README.md", + "CHANGELOG.md", + "LICENSE" +] + +[tool.hatch.build.targets.sdist] +include = [ + "*" +] +exclude = [ + ".*", + "dist" +] + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/markdown" +fragments = [ + { path = "README.md" }, + { text = "\n" }, + { path = "CHANGELOG.md" } +] + +[tool.ruff] +exclude = [ + '.tox', + '.egg', + '.git', + '__pycache__', + 'build', + 'dist' +] +line-length = 200 + +[tool.codespell] +skip = """ +./.tox,\ +./.egg,\ +./.git,\ +./__pycache__,\ +./build,\ +./dist.\ +""" + +[tool.isort] +line_length = 200 + +[tool.black] +line-length = 200 + +[tool.check-manifest] +ignore = [ + '.stickler.yml', + 'boilerplate.md', + 'check.sh', + 'install.sh', + 'uninstall.sh', + 'Makefile', + 'tox.ini', + 'tests/*', + 'examples/*', + '.coveragerc', + 'requirements-dev.txt' +] + +[tool.pimoroni] +apt_packages = [ + "python3", + "python3-pip", + "python3-cffi", + "libportaudio2" +] +configtxt = [ + "dtoverlay=pi3-miniuart-bt", + "dtoverlay=adau7002-simple" +] +commands = [ + "printf \"Setting up i2c and SPI..\\n\"", + "sudo raspi-config nonint do_spi 0", + "sudo raspi-config nonint do_i2c 0", + "printf \"Setting up serial for PMS5003..\\n\"", + "sudo raspi-config nonint do_serial_cons 1", + "sudo raspi-config nonint do_serial_hw 1" +] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..525b042 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,9 @@ +check-manifest +ruff +codespell +isort +twine +hatch +hatch-fancy-pypi-readme +tox +pdoc diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..2023743 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,117 @@ +"""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 + + +class SMBusFakeDeviceNoTimeout(MockSMBus): + def __init__(self, i2c_bus): + MockSMBus.__init__(self, i2c_bus) + self.regs[0x00:0x01] = 0x0f, 0x80 + + +@pytest.fixture(scope="function", autouse=True) +def cleanup(): + yield None + modules = "enviroplus", "enviroplus.noise", "enviroplus.gas", "ads1015", "i2cdevice" + for module in modules: + try: + del sys.modules[module] + except KeyError: + pass + + +@pytest.fixture(scope="function", autouse=False) +def gpiod(): + sys.modules["gpiod"] = mock.Mock() + sys.modules["gpiod.line"] = mock.Mock() + yield sys.modules["gpiod"] + del sys.modules["gpiod.line"] + del sys.modules["gpiod"] + + +@pytest.fixture(scope="function", autouse=False) +def gpiodevice(): + gpiodevice = mock.Mock() + gpiodevice.get_pins_for_platform.return_value = [(mock.Mock(), 0)] + gpiodevice.get_pin.return_value = (mock.Mock(), 0) + + sys.modules["gpiodevice"] = gpiodevice + yield gpiodevice + del sys.modules["gpiodevice"] + + +@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 smbus2 module.""" + smbus = mock.MagicMock() + smbus.SMBus = SMBusFakeDevice + sys.modules["smbus2"] = smbus + yield smbus + del sys.modules["smbus2"] + + +@pytest.fixture(scope="function", autouse=False) +def smbus_notimeout(): + """Mock smbus2 module.""" + smbus = mock.MagicMock() + smbus.SMBus = SMBusFakeDeviceNoTimeout + sys.modules["smbus2"] = smbus + yield smbus + del sys.modules["smbus2"] + + +@pytest.fixture(scope="function", autouse=False) +def mocksmbus(): + """Mock smbus2 module.""" + smbus = mock.MagicMock() + sys.modules["smbus2"] = smbus + yield smbus + del sys.modules["smbus2"] + + +@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/tests/test_noise.py similarity index 80% rename from library/tests/test_noise.py rename to tests/test_noise.py index 4949a3d..a5eb7da 100644 --- a/library/tests/test_noise.py +++ b/tests/test_noise.py @@ -17,7 +17,7 @@ def test_noise_get_amplitudes_at_frequency_ranges(sounddevice, numpy): (501, 1000) ]) - sounddevice.rec.assert_called_with(0.1 * 16000, device='adau7002', samplerate=16000, blocking=True, channels=1, dtype='float64') + sounddevice.rec.assert_called_with(0.1 * 16000, device="adau7002", samplerate=16000, blocking=True, channels=1, dtype="float64") def test_noise_get_noise_profile(sounddevice, numpy): @@ -32,7 +32,7 @@ def test_noise_get_noise_profile(sounddevice, numpy): mid=0.36, high=None) - sounddevice.rec.assert_called_with(0.1 * 16000, device='adau7002', samplerate=16000, blocking=True, channels=1, dtype='float64') + sounddevice.rec.assert_called_with(0.1 * 16000, device="adau7002", samplerate=16000, blocking=True, channels=1, dtype="float64") assert amp_total == 10.0 diff --git a/library/tests/test_setup.py b/tests/test_setup.py similarity index 62% rename from library/tests/test_setup.py rename to tests/test_setup.py index 40bf80d..fa7fb93 100644 --- a/library/tests/test_setup.py +++ b/tests/test_setup.py @@ -1,47 +1,47 @@ import pytest -def test_gas_setup(GPIO, smbus): +def test_gas_setup(gpiod, gpiodevice, smbus): from enviroplus import gas gas._is_setup = False gas.setup() gas.setup() -def test_gas_unavailable(GPIO, mocksmbus): +def test_gas_unavailable(gpiod, gpiodevice, mocksmbus): from enviroplus import gas - mocksmbus.SMBus(1).read_i2c_block_data.side_effect = IOError("Oh noes!") + mocksmbus.SMBus(1).read_i2c_block_data.side_effect = IOError("Oh no!") gas._is_setup = False - assert gas.available() == False + assert gas.available() is False with pytest.raises(RuntimeError): gas.read_all() -def test_gas_available(GPIO, smbus_notimeout): +def test_gas_available(gpiod, gpiodevice, smbus_notimeout): from enviroplus import gas gas._is_setup = False - assert gas.available() == True + assert gas.available() is True -def test_gas_read_all(GPIO, smbus): +def test_gas_read_all(gpiod, gpiodevice, smbus): from enviroplus import gas gas._is_setup = False result = gas.read_all() - assert type(result.oxidising) == float + assert isinstance(result.oxidising, float) assert int(result.oxidising) == 16641 - assert type(result.reducing) == float + assert isinstance(result.reducing, float) assert int(result.reducing) == 16727 - assert type(result.nh3) == float + assert isinstance(result.nh3, float) assert int(result.nh3) == 16813 assert "Oxidising" in str(result) -def test_gas_read_each(GPIO, smbus): +def test_gas_read_each(gpiod, gpiodevice, smbus): from enviroplus import gas gas._is_setup = False @@ -50,7 +50,7 @@ def test_gas_read_each(GPIO, smbus): assert int(gas.read_nh3()) == 16813 -def test_gas_read_adc(GPIO, smbus): +def test_gas_read_adc(gpiod, gpiodevice, smbus): from enviroplus import gas gas._is_setup = False @@ -59,7 +59,7 @@ def test_gas_read_adc(GPIO, smbus): assert gas.read_adc() == 0.255 -def test_gas_read_adc_default_gain(GPIO, smbus): +def test_gas_read_adc_default_gain(gpiod, gpiodevice, smbus): from enviroplus import gas gas._is_setup = False @@ -68,18 +68,19 @@ def test_gas_read_adc_default_gain(GPIO, smbus): assert gas.read_adc() == 0.765 -def test_gas_read_adc_str(GPIO, smbus): +def test_gas_read_adc_str(gpiod, gpiodevice, 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()) + assert "ADC" in str(gas.read_all()) -def test_gas_cleanup(GPIO, smbus): +def test_gas_cleanup(gpiod, gpiodevice, smbus): from enviroplus import gas gas.cleanup() - GPIO.output.assert_called_with(gas.MICS6814_HEATER_PIN, 0) + gas.setup() + gas.cleanup() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..44c8654 --- /dev/null +++ b/tox.ini @@ -0,0 +1,34 @@ +[tox] +envlist = py,qa +skip_missing_interpreters = True +isolated_build = true +minversion = 4.0.0 + +[testenv] +commands = + coverage run -m pytest -v -r wsx + coverage report +deps = + mock + pytest>=3.1 + pytest-cov + build + +[testenv:qa] +commands = + check-manifest + python -m build --no-isolation + python -m twine check dist/* + isort --check . + ruff . + codespell . +deps = + check-manifest + ruff + codespell + isort + twine + build + hatch + hatch-fancy-pypi-readme + diff --git a/uninstall.sh b/uninstall.sh index e317444..3314b7f 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,33 +1,72 @@ #!/bin/bash -LIBRARY_VERSION=`cat library/setup.cfg | grep version | awk -F" = " '{print $2}'` -LIBRARY_NAME=`cat library/setup.cfg | grep name | awk -F" = " '{print $2}'` +FORCE=false +LIBRARY_NAME=$(grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}') +RESOURCES_DIR=$HOME/Pimoroni/$LIBRARY_NAME +PYTHON="python" -printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Uninstaller\n\n" -if [ $(id -u) -ne 0 ]; then - printf "Script must be run as root. Try 'sudo ./uninstall.sh'\n" - exit 1 -fi +venv_check() { + PYTHON_BIN=$(which $PYTHON) + if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then + printf "This script should be run in a virtual Python environment.\n" + exit 1 + fi +} -cd library +user_check() { + if [ "$(id -u)" -eq 0 ]; then + printf "Script should not be run as root. Try './uninstall.sh'\n" + exit 1 + fi +} -printf "Unnstalling for Python 2..\n" -pip uninstall $LIBRARY_NAME +confirm() { + if $FORCE; then + true + else + read -r -p "$1 [y/N] " response < /dev/tty + if [[ $response =~ ^(yes|y|Y)$ ]]; then + true + else + false + fi + fi +} -if [ -f "/usr/bin/pip3" ]; then - printf "Uninstalling for Python 3..\n" - pip3 uninstall $LIBRARY_NAME -fi +prompt() { + read -r -p "$1 [y/N] " response < /dev/tty + if [[ $response =~ ^(yes|y|Y)$ ]]; then + true + else + false + fi +} + +success() { + echo -e "$(tput setaf 2)$1$(tput sgr0)" +} + +inform() { + echo -e "$(tput setaf 6)$1$(tput sgr0)" +} -cd .. +warning() { + echo -e "$(tput setaf 1)$1$(tput sgr0)" +} -printf "Disabling serial..\n" -# Enable serial terminal over /dev/ttyAMA0 -raspi-config nonint do_serial 0 -# Disable serial port -raspi-config nonint set_config_var enable_uart 0 /boot/config.txt -# Switch serial port back to miniUART -sed -i 's/^dtoverlay=pi3-miniuart-bt # for Enviro+/#dtoverlay=pi3-miniuart-bt # for Enviro+/' /boot/config.txt +printf "%s Python Library: Uninstaller\n\n" "$LIBRARY_NAME" + +user_check +venv_check + +printf "Uninstalling for Python 3...\n" +$PYTHON -m pip uninstall "$LIBRARY_NAME" + +if [ -d "$RESOURCES_DIR" ]; then + if confirm "Would you like to delete $RESOURCES_DIR?"; then + rm -r "$RESOURCES_DIR" + fi +fi printf "Done!\n"