From 550fac120543f945c38733a072c958a02705eea2 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 23 Oct 2023 12:27:10 +0100 Subject: [PATCH 01/19] Repackage to latest boilerplate. --- .github/workflows/build.yml | 41 +++ .github/workflows/qa.yml | 36 +++ .github/workflows/test.yml | 22 +- library/CHANGELOG.txt => CHANGELOG.md | 0 library/MANIFEST.in => MANIFEST.in | 0 Makefile | 89 +++--- check-install.py | 109 -------- check.sh | 87 ++++++ .../enviroplus => enviroplus}/__init__.py | 0 {library/enviroplus => enviroplus}/gas.py | 0 {library/enviroplus => enviroplus}/noise.py | 0 install-bullseye.sh | 254 ------------------ install.sh | 183 +++++++++---- library/LICENSE.txt | 21 -- library/README.md | 107 -------- library/setup.cfg | 76 ------ library/setup.py | 33 --- library/tox.ini | 24 -- pyproject.toml | 145 ++++++++++ requirements-dev.txt | 9 + {library/tests => tests}/conftest.py | 0 {library/tests => tests}/test_noise.py | 0 {library/tests => tests}/test_setup.py | 0 tox.ini | 34 +++ uninstall.sh | 83 ++++-- 25 files changed, 590 insertions(+), 763 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/qa.yml rename library/CHANGELOG.txt => CHANGELOG.md (100%) rename library/MANIFEST.in => MANIFEST.in (100%) delete mode 100755 check-install.py create mode 100755 check.sh rename {library/enviroplus => enviroplus}/__init__.py (100%) rename {library/enviroplus => enviroplus}/gas.py (100%) rename {library/enviroplus => enviroplus}/noise.py (100%) delete mode 100755 install-bullseye.sh delete mode 100644 library/LICENSE.txt delete mode 100644 library/README.md delete mode 100644 library/setup.cfg delete mode 100755 library/setup.py delete mode 100644 library/tox.ini create mode 100644 pyproject.toml create mode 100644 requirements-dev.txt rename {library/tests => tests}/conftest.py (100%) rename {library/tests => tests}/test_noise.py (100%) rename {library/tests => tests}/test_setup.py (100%) create mode 100644 tox.ini diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..87200ef --- /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@v3 + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v3 + 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@v3 + 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..4f85883 --- /dev/null +++ b/.github/workflows/qa.yml @@ -0,0 +1,36 @@ +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@v2 + + - name: Set up Python '3,11' + uses: actions/setup-python@v3 + 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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e8e6358..016a678 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@v3 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 100% rename from library/CHANGELOG.txt rename to CHANGELOG.md 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..9e0c15c 100644 --- a/Makefile +++ b/Makefile @@ -1,69 +1,60 @@ -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 -python-readme: library/README.md - -python-license: library/LICENSE.txt +check: + @bash check.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/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..cbb1565 --- /dev/null +++ b/check.sh @@ -0,0 +1,87 @@ +#!/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))}'` + +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: $1\n"; + exit 1 + fi + POSITIONAL_ARGS+=("$1") + shift + esac +done + +inform "Checking $LIBRARY_NAME $LIBRARY_VERSION\n" + +inform "Checking for trailing whitespace..." +grep -IUrn --color "[[:blank:]]$" --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO +if [[ $? -eq 0 ]]; then + warning "Trailing whitespace found!" + exit 1 +else + success "No trailing whitespace found." +fi +printf "\n" + +inform "Checking for DOS line-endings..." +grep -lIUrn --color $'\r' --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile +if [[ $? -eq 0 ]]; then + warning "DOS line-endings found!" + exit 1 +else + success "No DOS line-endings found." +fi +printf "\n" + +inform "Checking CHANGELOG.md..." +cat CHANGELOG.md | grep ^${LIBRARY_VERSION} > /dev/null 2>&1 +if [[ $? -eq 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}..." +git tag -l | grep -E "${LIBRARY_VERSION}$" +if [[ $? -eq 1 ]]; 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/library/enviroplus/__init__.py b/enviroplus/__init__.py similarity index 100% rename from library/enviroplus/__init__.py rename to enviroplus/__init__.py diff --git a/library/enviroplus/gas.py b/enviroplus/gas.py similarity index 100% rename from library/enviroplus/gas.py rename to enviroplus/gas.py diff --git a/library/enviroplus/noise.py b/enviroplus/noise.py similarity index 100% rename from library/enviroplus/noise.py rename to enviroplus/noise.py 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..bee710c 100755 --- a/install.sh +++ b/install.sh @@ -1,31 +1,28 @@ #!/bin/bash - +LIBRARY_NAME=`grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}'` 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 +RESOURCES_TOP_DIR=$HOME/Pimoroni +VENV_BASH_SNIPPET=$RESOURCES_TOP_DIR/auto_venv.sh +VENV_DIR=$RESOURCES_TOP_DIR/venv WD=`pwd` -USAGE="sudo $0 (--unstable)" +USAGE="./install.sh (--unstable)" POSITIONAL_ARGS=() +FORCE=false UNSTABLE=false -CODENAME=`lsb_release -sc` - -if [[ $CODENAME == "bullseye" ]]; then - bash ./install-bullseye.sh $@ - exit $? -fi +PYTHON="python" user_check() { - if [ $(id -u) -ne 0 ]; then - printf "Script must be run as root. Try 'sudo $0'\n" + if [ $(id -u) -eq 0 ]; then + printf "Script should not be run as root. Try './install.sh'\n" exit 1 fi } confirm() { - if [ "$FORCE" == '-y' ]; then + if $FORCE; then true else read -r -p "$1 [y/N] " response < /dev/tty @@ -58,12 +55,52 @@ warning() { echo -e "$(tput setaf 1)$1$(tput sgr0)" } +venv_bash_snippet() { + if [ ! -f $VENV_BASH_SNIPPET ]; then + cat << EOF > $VENV_BASH_SNIPPET +# Add \`source $RESOURCES_TOP_DIR/auto_venv.sh\` to your ~/.bashrc to activate +# the Pimoroni virtual environment automagically! +PY_ENV_DIR=~/Pimoroni/venv +if [ ! -f \$PY_ENV_DIR/bin/activate ]; then + printf "Creating user Python environment in \$PY_ENV_DIR, please wait...\n" + mkdir -p \$PY_ENV_DIR + python3 -m venv --system-site-packages --prompt Pimoroni \$PY_ENV_DIR +fi +printf " ↓ ↓ ↓ ↓ Hello, we've activated a Python venv for you. To exit, type \"deactivate\".\n" +source \$PY_ENV_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 one for you?"; then + if [ ! -f $VENV_DIR/bin/activate ]; then + inform "Creating virtual Python environment in $VENV_DIR, please wait...\n" + mkdir -p $VENV_DIR + /usr/bin/python3 -m venv $VENV_DIR --system-site-packages --prompt Pimoroni + venv_bash_snippet + else + inform "Found existing virtual Python environment in $VENV_DIR\n" + fi + inform "Activating virtual Python environment in $VENV_DIR..." + inform "source $VENV_DIR/bin/activate\n" + source $VENV_DIR/bin/activate + + else + exit 1 + fi + 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 + sudo cp $CONFIG /boot/$FILENAME mkdir -p $RESOURCES_TOP_DIR/config-backups/ cp $CONFIG $RESOURCES_TOP_DIR/config-backups/$FILENAME if [ -f "$UNINSTALLER" ]; then @@ -88,16 +125,20 @@ function apt_pkg_install { if ! [ "$PACKAGES" == "" ]; then echo "Installing missing packages: $PACKAGES" if [ ! $APT_HAS_UPDATED ]; then - apt update + sudo apt update APT_HAS_UPDATED=true fi - apt install -y $PACKAGES + sudo apt install -y $PACKAGES if [ -f "$UNINSTALLER" ]; then - echo "apt uninstall -y $PACKAGES" + echo "apt uninstall -y $PACKAGES" >> $UNINSTALLER fi fi } +function pip_pkg_install { + PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring $PYTHON -m pip install --upgrade "$@" +} + while [[ $# -gt 0 ]]; do K="$1" case $K in @@ -105,6 +146,15 @@ 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"; @@ -117,28 +167,31 @@ while [[ $# -gt 0 ]]; do done user_check +venv_check -apt_pkg_install python-configparser - -CONFIG_VARS=`python - < $UNINSTALLER @@ -161,33 +221,25 @@ 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" - if $UNSTABLE; then warning "Installing unstable library from source.\n\n" else printf "Installing stable library from pypi.\n\n" 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 +inform "Installing for $PYTHON_VER...\n" +apt_pkg_install "${APT_PACKAGES[@]}" +if $UNSTABLE; then + pip_pkg_install . else - printf "/usr/bin/python3 not found. Unable to install!\n" - exit 1 + pip_pkg_install $LIBRARY_NAME +fi +if [ $? -eq 0 ]; then + success "Done!\n" + echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> $UNINSTALLER fi cd $WD @@ -206,9 +258,9 @@ for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do if ! [ "$CONFIG_LINE" == "" ]; then do_config_backup inform "Adding $CONFIG_LINE to $CONFIG\n" - sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG + sudo sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG if ! grep -q "^$CONFIG_LINE" $CONFIG; then - printf "$CONFIG_LINE\n" >> $CONFIG + printf "$CONFIG_LINE\n" | sudo tee --append $CONFIG fi fi done @@ -222,7 +274,20 @@ if [ -d "examples" ]; then fi fi +printf "\n" + +if confirm "Would you like to generate documentation?"; then + pip_pkg_install pdoc + printf "Generating documentation.\n" + $PYTHON -m pdoc $LIBRARY_NAME -o $RESOURCES_DIR/docs > /dev/null + if [ $? -eq 0 ]; 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 "If this is your first time installing you should reboot for hardware changes to take effect.\n" inform "Find uninstall steps in $UNINSTALLER\n" 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/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/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..0d296bf --- /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 = [ + "pimoroni-bme280", + "pms5003", + "ltr559", + "st7735", + "ads1015 >= 0.0.7", + "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.check-manifest] +ignore = [ + '.stickler.yml', + 'boilerplate.md', + 'check.sh', + 'install.sh', + 'uninstall.sh', + 'Makefile', + 'tox.ini', + 'tests/*', + 'examples/*', + '.coveragerc', + 'requirements-dev.txt' +] + +[pimoroni] +apt_packages = [ + "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", + "raspi-config nonint set_config_var enable_uart 1 $CONFIG" +] 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/library/tests/conftest.py b/tests/conftest.py similarity index 100% rename from library/tests/conftest.py rename to tests/conftest.py diff --git a/library/tests/test_noise.py b/tests/test_noise.py similarity index 100% rename from library/tests/test_noise.py rename to tests/test_noise.py diff --git a/library/tests/test_setup.py b/tests/test_setup.py similarity index 100% rename from library/tests/test_setup.py rename to tests/test_setup.py 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..f213fc5 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 "$LIBRARY_NAME Python Library: Uninstaller\n\n" + +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" From 1f2df5a68d914c3e3e7f33aa7f911808f52ca527 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 23 Oct 2023 12:29:52 +0100 Subject: [PATCH 02/19] QA: Fix isort suggestions. --- enviroplus/gas.py | 3 ++- enviroplus/noise.py | 2 +- examples/adc.py | 3 ++- examples/all-in-one-enviro-mini.py | 16 +++++++++------- examples/all-in-one-no-pm.py | 14 ++++++++------ examples/all-in-one.py | 17 ++++++++++------- examples/combined.py | 20 ++++++++++++-------- examples/compensated-temperature.py | 1 + examples/gas.py | 3 ++- examples/lcd.py | 5 +++-- examples/light.py | 3 ++- examples/luftdaten.py | 10 ++++++---- examples/luftdaten_combined.py | 12 +++++++----- examples/mqtt-all.py | 12 +++++++----- examples/noise-amps-at-freqs.py | 1 + examples/noise-profile.py | 1 + examples/particulates.py | 3 ++- examples/weather-and-light.py | 17 ++++++++--------- examples/weather.py | 1 + tests/conftest.py | 1 + 20 files changed, 86 insertions(+), 59 deletions(-) diff --git a/enviroplus/gas.py b/enviroplus/gas.py index 54c240f..6e96d8e 100644 --- a/enviroplus/gas.py +++ b/enviroplus/gas.py @@ -1,7 +1,8 @@ """Read the MICS6814 via an ads1015 ADC""" -import time import atexit +import time + import ads1015 import RPi.GPIO as GPIO diff --git a/enviroplus/noise.py b/enviroplus/noise.py index 0d413bd..873fc26 100644 --- a/enviroplus/noise.py +++ b/enviroplus/noise.py @@ -1,5 +1,5 @@ -import sounddevice import numpy +import sounddevice class Noise(): diff --git a/examples/adc.py b/examples/adc.py index a345d23..6a86242 100755 --- a/examples/adc.py +++ b/examples/adc.py @@ -1,8 +1,9 @@ #!/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', diff --git a/examples/all-in-one-enviro-mini.py b/examples/all-in-one-enviro-mini.py index d7a001f..9e87784 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 time + import ST7735 + try: # Transitional fix for breaking change in LTR559 from ltr559 import LTR559 @@ -12,14 +14,14 @@ 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 + +from enviroplus import gas logging.basicConfig( format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', diff --git a/examples/all-in-one-no-pm.py b/examples/all-in-one-no-pm.py index ec1ed1f..aa67fef 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 time + import ST7735 + try: # Transitional fix for breaking change in LTR559 from ltr559 import LTR559 @@ -12,13 +14,13 @@ 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', diff --git a/examples/all-in-one.py b/examples/all-in-one.py index ac53999..5498603 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 time + import ST7735 + try: # Transitional fix for breaking change in LTR559 from ltr559 import LTR559 @@ -11,14 +13,15 @@ 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', diff --git a/examples/combined.py b/examples/combined.py index 556bcb0..1ffaccb 100755 --- a/examples/combined.py +++ b/examples/combined.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 -import time import colorsys import sys +import time + import ST7735 + try: # Transitional fix for breaking change in LTR559 from ltr559 import LTR559 @@ -11,15 +13,17 @@ 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', diff --git a/examples/compensated-temperature.py b/examples/compensated-temperature.py index b648f57..1d36140 100755 --- a/examples/compensated-temperature.py +++ b/examples/compensated-temperature.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import time + from bme280 import BME280 try: diff --git a/examples/gas.py b/examples/gas.py index 5d72cb9..a107696 100755 --- a/examples/gas.py +++ b/examples/gas.py @@ -1,8 +1,9 @@ #!/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', diff --git a/examples/lcd.py b/examples/lcd.py index 10413b9..0c0606e 100755 --- a/examples/lcd.py +++ b/examples/lcd.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 +import logging + import ST7735 -from PIL import Image, ImageDraw, ImageFont 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', diff --git a/examples/light.py b/examples/light.py index db61e6a..ead18a2 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 diff --git a/examples/luftdaten.py b/examples/luftdaten.py index a78909f..9e58dba 100755 --- a/examples/luftdaten.py +++ b/examples/luftdaten.py @@ -1,18 +1,20 @@ #!/usr/bin/env python3 +import time +from subprocess import PIPE, Popen, check_output + import requests import ST7735 -import time 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 +from PIL import Image, ImageDraw, ImageFont +from pms5003 import PMS5003, ChecksumMismatchError, ReadTimeoutError try: from smbus2 import SMBus except ImportError: from smbus import SMBus + import logging logging.basicConfig( diff --git a/examples/luftdaten_combined.py b/examples/luftdaten_combined.py index 1d920db..19e9fdc 100644 --- a/examples/luftdaten_combined.py +++ b/examples/luftdaten_combined.py @@ -1,14 +1,16 @@ +import colorsys import logging import sys +import time +from subprocess import PIPE, Popen, check_output + 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 from fonts.ttf import RobotoMedium as UserFont +from PIL import Image, ImageDraw, ImageFont +from pms5003 import PMS5003, ReadTimeoutError + from enviroplus import gas try: diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index 5e564af..efda0b7 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,13 @@ 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 diff --git a/examples/noise-amps-at-freqs.py b/examples/noise-amps-at-freqs.py index 4c14c58..ddf1a31 100755 --- a/examples/noise-amps-at-freqs.py +++ b/examples/noise-amps-at-freqs.py @@ -1,5 +1,6 @@ import ST7735 from PIL import Image, ImageDraw + from enviroplus.noise import Noise print("""noise-amps-at-freqs.py - Measure amplitude from specific frequency bins diff --git a/examples/noise-profile.py b/examples/noise-profile.py index 4084439..c37ba35 100755 --- a/examples/noise-profile.py +++ b/examples/noise-profile.py @@ -1,5 +1,6 @@ import ST7735 from PIL import Image, ImageDraw + from enviroplus.noise import Noise print("""noise-profile.py - Get a simple noise profile. diff --git a/examples/particulates.py b/examples/particulates.py index 04a4950..88d30e0 100755 --- a/examples/particulates.py +++ b/examples/particulates.py @@ -1,8 +1,9 @@ #!/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', diff --git a/examples/weather-and-light.py b/examples/weather-and-light.py index cd8ae96..926c07b 100755 --- a/examples/weather-and-light.py +++ b/examples/weather-and-light.py @@ -3,22 +3,21 @@ 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 +from datetime import datetime, timedelta +import numpy +import pytz import ST7735 +from astral.geocoder import database, lookup +from astral.sun import sun from bme280 import BME280 +from fonts.ttf import RobotoMedium as UserFont from ltr559 import LTR559 - -import pytz +from PIL import Image, ImageDraw, ImageFilter, ImageFont from pytz import timezone -from astral.geocoder import database, lookup -from astral.sun import sun -from datetime import datetime, timedelta try: from smbus2 import SMBus diff --git a/examples/weather.py b/examples/weather.py index 66f18e0..23a6a86 100755 --- a/examples/weather.py +++ b/examples/weather.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import time + from bme280 import BME280 try: diff --git a/tests/conftest.py b/tests/conftest.py index b3fa376..a7b3b33 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ that might otherwise have runtime side-effects. """ import sys + import mock import pytest from i2cdevice import MockSMBus From 207d6eb18cf79343bce05a5b0f6e54be96d9e173 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 23 Oct 2023 12:35:41 +0100 Subject: [PATCH 03/19] QA: Apply ruff suggestions. --- examples/all-in-one-enviro-mini.py | 2 -- examples/luftdaten.py | 2 +- examples/luftdaten_combined.py | 1 - examples/mqtt-all.py | 1 - examples/weather-and-light.py | 3 --- tests/test_setup.py | 10 +++++----- 6 files changed, 6 insertions(+), 13 deletions(-) diff --git a/examples/all-in-one-enviro-mini.py b/examples/all-in-one-enviro-mini.py index 9e87784..449c49d 100755 --- a/examples/all-in-one-enviro-mini.py +++ b/examples/all-in-one-enviro-mini.py @@ -21,8 +21,6 @@ from fonts.ttf import RobotoMedium as UserFont from PIL import Image, ImageDraw, ImageFont -from enviroplus import gas - logging.basicConfig( format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s', level=logging.INFO, diff --git a/examples/luftdaten.py b/examples/luftdaten.py index 9e58dba..f71ee43 100755 --- a/examples/luftdaten.py +++ b/examples/luftdaten.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import time -from subprocess import PIPE, Popen, check_output +from subprocess import check_output import requests import ST7735 diff --git a/examples/luftdaten_combined.py b/examples/luftdaten_combined.py index 19e9fdc..978ce28 100644 --- a/examples/luftdaten_combined.py +++ b/examples/luftdaten_combined.py @@ -1,6 +1,5 @@ import colorsys import logging -import sys import time from subprocess import PIPE, Popen, check_output diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index efda0b7..16e8f0d 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -27,7 +27,6 @@ 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 diff --git a/examples/weather-and-light.py b/examples/weather-and-light.py index 926c07b..12b43de 100755 --- a/examples/weather-and-light.py +++ b/examples/weather-and-light.py @@ -1,8 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -f"Sorry! This program requires Python >= 3.6 šŸ˜…" - import colorsys import os import time @@ -17,7 +15,6 @@ from fonts.ttf import RobotoMedium as UserFont from ltr559 import LTR559 from PIL import Image, ImageDraw, ImageFilter, ImageFont -from pytz import timezone try: from smbus2 import SMBus diff --git a/tests/test_setup.py b/tests/test_setup.py index 40bf80d..9b300e1 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -12,7 +12,7 @@ def test_gas_unavailable(GPIO, mocksmbus): from enviroplus import gas mocksmbus.SMBus(1).read_i2c_block_data.side_effect = IOError("Oh noes!") gas._is_setup = False - assert gas.available() == False + assert gas.available() is False with pytest.raises(RuntimeError): gas.read_all() @@ -21,7 +21,7 @@ def test_gas_unavailable(GPIO, mocksmbus): def test_gas_available(GPIO, 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): @@ -29,13 +29,13 @@ def test_gas_read_all(GPIO, smbus): 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) From fc5a49bbacae740aa431aa9bc6fd9215616f6f54 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 23 Oct 2023 12:38:44 +0100 Subject: [PATCH 04/19] QA: Apply spelling corrections. --- enviroplus/gas.py | 2 +- enviroplus/noise.py | 4 ++-- examples/luftdaten.py | 2 +- tests/test_setup.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/enviroplus/gas.py b/enviroplus/gas.py index 6e96d8e..fe60fb8 100644 --- a/enviroplus/gas.py +++ b/enviroplus/gas.py @@ -92,7 +92,7 @@ def cleanup(): def read_all(): - """Return gas resistence for oxidising, reducing and NH3""" + """Return gas resistance for oxidising, reducing and NH3""" setup() if not _is_available: diff --git a/enviroplus/noise.py b/enviroplus/noise.py index 873fc26..e9288b2 100644 --- a/enviroplus/noise.py +++ b/enviroplus/noise.py @@ -39,7 +39,7 @@ 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("Maximum frequency is {}".format(n)) recording = self._record() magnitude = numpy.abs(numpy.fft.rfft(recording[:, 0], n=self.sample_rate)) @@ -50,7 +50,7 @@ def get_noise_profile(self, low=0.12, mid=0.36, high=None): - """Returns a noise charateristic profile. + """Returns a noise characteristic profile. Bins all frequencies into 3 weighted groups expressed as a percentage of the total frequency range. diff --git a/examples/luftdaten.py b/examples/luftdaten.py index f71ee43..969c9f1 100755 --- a/examples/luftdaten.py +++ b/examples/luftdaten.py @@ -72,7 +72,7 @@ def read_values(): 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)) diff --git a/tests/test_setup.py b/tests/test_setup.py index 9b300e1..ac254e0 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -10,7 +10,7 @@ def test_gas_setup(GPIO, smbus): def test_gas_unavailable(GPIO, 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() is False From 6e9e4318af70449a81b5537cd0cb4182aea03a17 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 23 Oct 2023 12:49:48 +0100 Subject: [PATCH 05/19] Install: Fix for do_serial showing a UI. --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0d296bf..a39b551 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -140,6 +140,6 @@ commands = [ "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", - "raspi-config nonint set_config_var enable_uart 1 $CONFIG" + "sudo raspi-config nonint do_serial_cons 1", + "sudo raspi-config nonint do_serial_hw 1" ] From 342cf4ee4be92c805c0d44ee8a1039dd6e0e0aa2 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 15 Nov 2023 11:24:23 +0000 Subject: [PATCH 06/19] Bump requirement versions. --- pyproject.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a39b551..bf030cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,11 +35,11 @@ classifiers = [ "Topic :: System :: Hardware", ] dependencies = [ - "pimoroni-bme280", - "pms5003", - "ltr559", - "st7735", - "ads1015 >= 0.0.7", + "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", From 2cfed377ddcf5af73dae26dd01c60a879343e0fa Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 15 Nov 2023 13:01:04 +0000 Subject: [PATCH 07/19] Port to new libraries. Fixup examples and tests. --- enviroplus/__init__.py | 2 +- enviroplus/gas.py | 58 +++++++++------- enviroplus/noise.py | 18 ++--- examples/adc.py | 4 +- examples/all-in-one-enviro-mini.py | 24 +++---- examples/all-in-one-no-pm.py | 18 ++--- examples/all-in-one.py | 18 ++--- examples/combined.py | 26 +++---- examples/compensated-temperature.py | 15 ++-- examples/gas.py | 4 +- examples/lcd.py | 19 ++--- examples/light.py | 10 +-- examples/luftdaten.py | 80 +++++++++++---------- examples/luftdaten_combined.py | 49 +++++++------ examples/mqtt-all.py | 28 +++++--- examples/noise-amps-at-freqs.py | 16 +++-- examples/noise-profile.py | 16 +++-- examples/particulates.py | 4 +- examples/weather-and-light.py | 50 +++++++------ examples/weather.py | 21 +++--- pyproject.toml | 3 + tests/conftest.py | 104 ++++++++++++++-------------- tests/test_noise.py | 4 +- tests/test_setup.py | 23 +++--- 24 files changed, 312 insertions(+), 302 deletions(-) diff --git a/enviroplus/__init__.py b/enviroplus/__init__.py index fa9c4ec..034f46c 100644 --- a/enviroplus/__init__.py +++ b/enviroplus/__init__.py @@ -1 +1 @@ -__version__ = '0.0.6' +__version__ = "0.0.6" diff --git a/enviroplus/gas.py b/enviroplus/gas.py index fe60fb8..e1c4b76 100644 --- a/enviroplus/gas.py +++ b/enviroplus/gas.py @@ -4,20 +4,29 @@ 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) +PLATFORMS = { + "Radxa ROCK 5B": {"heater": ("PIN_18", OUTH)}, + "Raspberry Pi 5": {"heater": ("PIN18", OUTH)}, + "Raspberry Pi 4": {"heater": ("GPIO24", OUTH)} +} + 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 @@ -26,24 +35,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 @@ -56,17 +61,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_pins_for_platform(PLATFORMS)[0] + atexit.register(cleanup) @@ -88,7 +91,10 @@ 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(): @@ -98,9 +104,9 @@ def read_all(): 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) @@ -121,11 +127,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/enviroplus/noise.py b/enviroplus/noise.py index e9288b2..261c3ab 100644 --- a/enviroplus/noise.py +++ b/enviroplus/noise.py @@ -2,10 +2,8 @@ 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,17 +37,13 @@ def get_amplitude_at_frequency_range(self, start, end): """ n = self.sample_rate // 2 if start > n or end > n: - raise ValueError("Maximum 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): + 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 6a86242..983aec3 100755 --- a/examples/adc.py +++ b/examples/adc.py @@ -6,9 +6,9 @@ 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("""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 449c49d..01491f3 100755 --- a/examples/all-in-one-enviro-mini.py +++ b/examples/all-in-one-enviro-mini.py @@ -5,7 +5,7 @@ import sys import time -import ST7735 +import st7735 try: # Transitional fix for breaking change in LTR559 @@ -22,11 +22,11 @@ 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="PIN21", # "GPIO9" on a Raspberry Pi 4 + backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 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 aa67fef..c2d9078 100755 --- a/examples/all-in-one-no-pm.py +++ b/examples/all-in-one-no-pm.py @@ -5,7 +5,7 @@ import sys import time -import ST7735 +import st7735 try: # Transitional fix for breaking change in LTR559 @@ -23,9 +23,9 @@ 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! @@ -35,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="PIN21", # "GPIO9" on a Raspberry Pi 4 + backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 rotation=270, spi_speed_hz=10000000 ) @@ -51,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 @@ -72,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)): @@ -136,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 5498603..9b7121e 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -4,7 +4,7 @@ import sys import time -import ST7735 +import st7735 try: # Transitional fix for breaking change in LTR559 @@ -24,9 +24,9 @@ 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 @@ -41,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="PIN21", # "GPIO9" on a Raspberry Pi 4 + backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 rotation=270, spi_speed_hz=10000000 ) @@ -57,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) @@ -77,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)): @@ -144,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 1ffaccb..eaab10f 100755 --- a/examples/combined.py +++ b/examples/combined.py @@ -4,7 +4,7 @@ import sys import time -import ST7735 +import st7735 try: # Transitional fix for breaking change in LTR559 @@ -26,9 +26,9 @@ 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 @@ -44,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="PIN21", # "GPIO9" on a Raspberry Pi 4 + backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 rotation=270, spi_speed_hz=10000000 ) @@ -60,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 @@ -140,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)): @@ -163,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) @@ -178,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)): @@ -190,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(): @@ -223,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 1d36140..fb692f0 100755 --- a/examples/compensated-temperature.py +++ b/examples/compensated-temperature.py @@ -1,20 +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 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. @@ -50,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 a107696..c5fce5f 100755 --- a/examples/gas.py +++ b/examples/gas.py @@ -6,9 +6,9 @@ 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("""gas.py - Print readings from the MICS6814 Gas sensor. diff --git a/examples/lcd.py b/examples/lcd.py index 0c0606e..cb2af2f 100755 --- a/examples/lcd.py +++ b/examples/lcd.py @@ -2,14 +2,14 @@ import logging -import ST7735 +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. @@ -18,11 +18,11 @@ """) # Create LCD class instance. -disp = ST7735.ST7735( +disp = st7735.ST7735( port=0, cs=1, - dc=9, - backlight=12, + dc="PIN21", # "GPIO9" on a Raspberry Pi 4 + backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 rotation=270, spi_speed_hz=10000000 ) @@ -35,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. @@ -45,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 ead18a2..70414db 100755 --- a/examples/light.py +++ b/examples/light.py @@ -12,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. @@ -26,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/luftdaten.py b/examples/luftdaten.py index 969c9f1..ca1e4c9 100755 --- a/examples/luftdaten.py +++ b/examples/luftdaten.py @@ -1,40 +1,35 @@ #!/usr/bin/env python3 +import logging import time from subprocess import check_output import requests -import ST7735 +import st7735 from bme280 import BME280 from fonts.ttf import RobotoMedium as UserFont from PIL import Image, ImageDraw, ImageFont from pms5003 import PMS5003, ChecksumMismatchError, ReadTimeoutError - -try: - from smbus2 import SMBus -except ImportError: - from smbus import SMBus - -import logging +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. +PM2.5, and PM10 from Enviro plus and sends data to Luftdaten, +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 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. -#Press Ctrl+C to exit! +Press Ctrl+C to exit! -#""") +""") bus = SMBus(1) @@ -42,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="PIN21", # "GPIO9" on a Raspberry Pi 4 + backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 rotation=270, spi_speed_hz=10000000 ) @@ -64,9 +59,9 @@ 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)) @@ -90,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 @@ -110,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) @@ -147,11 +144,11 @@ 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 (Luftdaten) 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 (Luftdaten) 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 (Luftdaten) PM Request Error: {e}") try: resp_bmp = requests.post( @@ -169,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 (Luftdaten) 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 (Luftdaten) 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 (Luftdaten) 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"Luftdaten Error. PM: {resp_pm.reason}, Climate: {resp_bmp.reason}") return False else: return False @@ -200,8 +197,9 @@ 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() @@ -220,4 +218,4 @@ def send_to_luftdaten(values, id): logging.warning("Luftdaten 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/luftdaten_combined.py index 978ce28..293e414 100644 --- a/examples/luftdaten_combined.py +++ b/examples/luftdaten_combined.py @@ -4,18 +4,15 @@ from subprocess import PIPE, Popen, check_output import requests -import ST7735 +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 @@ -48,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) @@ -120,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 @@ -130,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="PIN21", # "GPIO9" on a Raspberry Pi 4 + backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 rotation=270, spi_speed_hz=10000000 ) @@ -169,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 @@ -190,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) @@ -204,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)): @@ -235,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)): @@ -312,8 +309,9 @@ 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() @@ -350,7 +348,8 @@ def send_to_luftdaten(values, id): raw_humid, raw_pm25, raw_pm10) resp = send_to_luftdaten(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/mqtt-all.py b/examples/mqtt-all.py index 16e8f0d..7238d3b 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -9,7 +9,7 @@ import ssl import time -import ST7735 +import st7735 from bme280 import BME280 from pms5003 import PMS5003, ReadTimeoutError, SerialTimeoutError @@ -133,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) @@ -174,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( @@ -231,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="PIN21", # "GPIO9" on a Raspberry Pi 4 + backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + rotation=270, + spi_speed_hz=10000000 ) # Initialize display @@ -249,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 ddf1a31..825f53c 100755 --- a/examples/noise-amps-at-freqs.py +++ b/examples/noise-amps-at-freqs.py @@ -1,4 +1,4 @@ -import ST7735 +import st7735 from PIL import Image, ImageDraw from enviroplus.noise import Noise @@ -15,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="PIN21", # "GPIO9" on a Raspberry Pi 4 + backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + 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 c37ba35..17ada82 100755 --- a/examples/noise-profile.py +++ b/examples/noise-profile.py @@ -1,4 +1,4 @@ -import ST7735 +import st7735 from PIL import Image, ImageDraw from enviroplus.noise import Noise @@ -13,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="PIN21", # "GPIO9" on a Raspberry Pi 4 + backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + 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 88d30e0..6ecaf70 100755 --- a/examples/particulates.py +++ b/examples/particulates.py @@ -6,9 +6,9 @@ from pms5003 import PMS5003, ReadTimeoutError 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/weather-and-light.py b/examples/weather-and-light.py index 12b43de..efbd2a8 100755 --- a/examples/weather-and-light.py +++ b/examples/weather-and-light.py @@ -8,18 +8,14 @@ import numpy import pytz -import ST7735 +import st7735 from astral.geocoder import database, lookup from astral.sun import sun from bme280 import BME280 from fonts.ttf import RobotoMedium as UserFont from ltr559 import LTR559 from PIL import Image, ImageDraw, ImageFilter, ImageFont - -try: - from smbus2 import SMBus -except ImportError: - from smbus import SMBus +from smbus2 import SMBus def calculate_y_pos(x, centre): @@ -44,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 @@ -81,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()) @@ -137,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) @@ -149,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 @@ -166,9 +162,19 @@ def draw_background(progress, period, day): return composite +def text_width(font, text): + x1, y1, x2, y2 = font.getbbox(text) + return x2 - x1 + + +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 @@ -179,7 +185,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)) @@ -287,11 +293,11 @@ def describe_light(light): # Initialise the LCD -disp = ST7735.ST7735( +disp = st7735.ST7735( port=0, cs=1, - dc=9, - backlight=12, + dc="PIN21", # "GPIO9" on a Raspberry Pi 4 + backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 rotation=270, spi_speed_hz=10000000 ) @@ -352,7 +358,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) @@ -378,7 +384,7 @@ 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 + spacing = text_width(font_lg, temp_string) + 1 if min_temp is not None and max_temp is not None: range_string = f"{min_temp:.0f}-{max_temp:.0f}" else: @@ -392,7 +398,7 @@ 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 + spacing = text_width(font_lg, humidity_string) + 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") @@ -402,7 +408,7 @@ 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 + spacing = text_width(font_lg, light_string.replace(",", "")) + 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") @@ -415,7 +421,7 @@ 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 + spacing = text_width(font_lg, pressure_string.replace(",", "")) + 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 23a6a86..0b671d3 100755 --- a/examples/weather.py +++ b/examples/weather.py @@ -1,20 +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 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. @@ -29,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/pyproject.toml b/pyproject.toml index bf030cc..e7346f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,9 @@ skip = """ [tool.isort] line_length = 200 +[tool.black] +line-length = 200 + [tool.check-manifest] ignore = [ '.stickler.yml', diff --git a/tests/conftest.py b/tests/conftest.py index a7b3b33..6ad5557 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,96 +21,96 @@ def __init__(self, i2c_bus): self.regs[0x00:0x01] = 0x0f, 0x80 -@pytest.fixture(scope='function', autouse=True) +@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) + 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)] + + 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 + sys.modules["spidev"] = spidev yield spidev - del sys.modules['spidev'] + del sys.modules["spidev"] -@pytest.fixture(scope='function', autouse=False) +@pytest.fixture(scope="function", autouse=False) def smbus(): - """Mock smbus module.""" + """Mock smbus2 module.""" smbus = mock.MagicMock() smbus.SMBus = SMBusFakeDevice - sys.modules['smbus'] = smbus + sys.modules["smbus2"] = smbus yield smbus - del sys.modules['smbus'] + del sys.modules["smbus2"] -@pytest.fixture(scope='function', autouse=False) +@pytest.fixture(scope="function", autouse=False) def smbus_notimeout(): - """Mock smbus module.""" + """Mock smbus2 module.""" smbus = mock.MagicMock() smbus.SMBus = SMBusFakeDeviceNoTimeout - sys.modules['smbus'] = smbus + sys.modules["smbus2"] = smbus yield smbus - del sys.modules['smbus'] + del sys.modules["smbus2"] -@pytest.fixture(scope='function', autouse=False) +@pytest.fixture(scope="function", autouse=False) def mocksmbus(): - """Mock smbus module.""" + """Mock smbus2 module.""" smbus = mock.MagicMock() - sys.modules['smbus'] = smbus + sys.modules["smbus2"] = smbus yield smbus - del sys.modules['smbus'] + del sys.modules["smbus2"] -@pytest.fixture(scope='function', autouse=False) +@pytest.fixture(scope="function", autouse=False) def atexit(): """Mock atexit module.""" atexit = mock.MagicMock() - sys.modules['atexit'] = atexit + sys.modules["atexit"] = atexit yield atexit - del sys.modules['atexit'] + del sys.modules["atexit"] -@pytest.fixture(scope='function', autouse=False) +@pytest.fixture(scope="function", autouse=False) def sounddevice(): """Mock sounddevice module.""" sounddevice = mock.MagicMock() - sys.modules['sounddevice'] = sounddevice + sys.modules["sounddevice"] = sounddevice yield sounddevice - del sys.modules['sounddevice'] + del sys.modules["sounddevice"] -@pytest.fixture(scope='function', autouse=False) +@pytest.fixture(scope="function", autouse=False) def numpy(): """Mock numpy module.""" numpy = mock.MagicMock() - sys.modules['numpy'] = numpy + sys.modules["numpy"] = numpy yield numpy - del sys.modules['numpy'] + del sys.modules["numpy"] diff --git a/tests/test_noise.py b/tests/test_noise.py index 4949a3d..a5eb7da 100644 --- a/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/tests/test_setup.py b/tests/test_setup.py index ac254e0..fa7fb93 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -1,14 +1,14 @@ 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 no!") gas._is_setup = False @@ -18,13 +18,13 @@ def test_gas_unavailable(GPIO, mocksmbus): 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() 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() @@ -41,7 +41,7 @@ def test_gas_read_all(GPIO, smbus): 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() From f6ee18bc0f4286dfe0e9dd2376d22264821de6cd Mon Sep 17 00:00:00 2001 From: "Michael (MicroD)" <72285408+michaeldufault@users.noreply.github.com> Date: Wed, 15 Nov 2023 13:19:58 +0000 Subject: [PATCH 08/19] Update Luftdaten examples to Sensor.Community Co-authored-by: Phil Howard --- examples/{luftdaten.py => sensorcommunity.py} | 40 +++++++++---------- ...ombined.py => sensorcommunity_combined.py} | 28 ++++++------- 2 files changed, 34 insertions(+), 34 deletions(-) rename examples/{luftdaten.py => sensorcommunity.py} (82%) rename examples/{luftdaten_combined.py => sensorcommunity_combined.py} (94%) diff --git a/examples/luftdaten.py b/examples/sensorcommunity.py similarity index 82% rename from examples/luftdaten.py rename to examples/sensorcommunity.py index ca1e4c9..7128ad7 100755 --- a/examples/luftdaten.py +++ b/examples/sensorcommunity.py @@ -17,15 +17,15 @@ level=logging.INFO, 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, +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 +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! @@ -118,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")) @@ -132,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={ @@ -144,17 +144,17 @@ def send_to_luftdaten(values, id): timeout=5 ) except requests.exceptions.ConnectionError as e: - logging.warning(f"Sensor.Community (Luftdaten) PM Connection Error: {e}") + logging.warning(f"Sensor.Community PM Connection Error: {e}") except requests.exceptions.Timeout as e: - logging.warning(f"Sensor.Community (Luftdaten) PM Timeout Error: {e}") + logging.warning(f"Sensor.Community PM Timeout Error: {e}") except requests.exceptions.RequestException as e: - logging.warning(f"Sensor.Community (Luftdaten) PM Request Error: {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={ @@ -166,17 +166,17 @@ def send_to_luftdaten(values, id): timeout=5 ) except requests.exceptions.ConnectionError as e: - logging.warning(f"Sensor.Community (Luftdaten) Climate Connection Error: {e}") + logging.warning(f"Sensor.Community Climate Connection Error: {e}") except requests.exceptions.Timeout as e: - logging.warning(f"Sensor.Community (Luftdaten) Climate Timeout Error: {e}") + logging.warning(f"Sensor.Community Climate Timeout Error: {e}") except requests.exceptions.RequestException as e: - logging.warning(f"Sensor.Community (Luftdaten) Climate Request Error: {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(f"Luftdaten Error. PM: {resp_pm.reason}, Climate: {resp_bmp.reason}") + logging.warning(f"Sensor.Community Error. PM: {resp_pm.reason}, Climate: {resp_bmp.reason}") return False else: return False @@ -185,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 @@ -204,7 +204,7 @@ def send_to_luftdaten(values, id): 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(f"Main Loop Exception: {e}") diff --git a/examples/luftdaten_combined.py b/examples/sensorcommunity_combined.py similarity index 94% rename from examples/luftdaten_combined.py rename to examples/sensorcommunity_combined.py index 293e414..8dac723 100644 --- a/examples/luftdaten_combined.py +++ b/examples/sensorcommunity_combined.py @@ -20,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! @@ -242,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")) @@ -252,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={ @@ -266,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={ @@ -288,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() @@ -317,7 +317,7 @@ def send_to_luftdaten(values, id): 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() @@ -346,7 +346,7 @@ 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 status = "ok" if resp else "failed" print(f"Response: {status}\n") From 11c218874f7cf81ad4898557b07d49a3ea72686d Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 15 Nov 2023 13:49:21 +0000 Subject: [PATCH 09/19] Fix for config.txt location. --- check.sh | 1 + install.sh | 64 +++++++++++++++++++++++++++++++++----------------- pyproject.toml | 15 ++++-------- 3 files changed, 48 insertions(+), 32 deletions(-) diff --git a/check.sh b/check.sh index cbb1565..4395d89 100755 --- a/check.sh +++ b/check.sh @@ -6,6 +6,7 @@ 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)" diff --git a/install.sh b/install.sh index bee710c..38f19e9 100755 --- a/install.sh +++ b/install.sh @@ -1,12 +1,13 @@ #!/bin/bash LIBRARY_NAME=`grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}'` -CONFIG=/boot/config.txt +CONFIG_FILE=config.txt +CONFIG_DIR="/boot/firmware" DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` CONFIG_BACKUP=false APT_HAS_UPDATED=false RESOURCES_TOP_DIR=$HOME/Pimoroni -VENV_BASH_SNIPPET=$RESOURCES_TOP_DIR/auto_venv.sh -VENV_DIR=$RESOURCES_TOP_DIR/venv +VENV_BASH_SNIPPET=$RESOURCES_DIR/auto_venv.sh +VENV_DIR=$HOME/.virtualenvs/pimoroni WD=`pwd` USAGE="./install.sh (--unstable)" POSITIONAL_ARGS=() @@ -14,6 +15,7 @@ FORCE=false UNSTABLE=false PYTHON="python" + user_check() { if [ $(id -u) -eq 0 ]; then printf "Script should not be run as root. Try './install.sh'\n" @@ -55,19 +57,35 @@ warning() { echo -e "$(tput setaf 1)$1$(tput sgr0)" } +find_config() { + if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then + CONFIG_DIR="/boot" + if [ ! -f "$CONFIG_DIR/$CONFIG_FILE"]; then + warning "Could not find $CONFIG_FILE!" + exit 1 + 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() { if [ ! -f $VENV_BASH_SNIPPET ]; then cat << EOF > $VENV_BASH_SNIPPET -# Add \`source $RESOURCES_TOP_DIR/auto_venv.sh\` to your ~/.bashrc to activate +# Add `source $RESOURCES_DIR/auto_venv.sh` to your ~/.bashrc to activate # the Pimoroni virtual environment automagically! -PY_ENV_DIR=~/Pimoroni/venv -if [ ! -f \$PY_ENV_DIR/bin/activate ]; then - printf "Creating user Python environment in \$PY_ENV_DIR, please wait...\n" - mkdir -p \$PY_ENV_DIR - python3 -m venv --system-site-packages --prompt Pimoroni \$PY_ENV_DIR +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 \$PY_ENV_DIR/bin/activate +source \$VENV_DIR/bin/activate EOF fi } @@ -80,7 +98,7 @@ venv_check() { if [ ! -f $VENV_DIR/bin/activate ]; then inform "Creating virtual Python environment in $VENV_DIR, please wait...\n" mkdir -p $VENV_DIR - /usr/bin/python3 -m venv $VENV_DIR --system-site-packages --prompt Pimoroni + /usr/bin/python3 -m venv $VENV_DIR --system-site-packages venv_bash_snippet else inform "Found existing virtual Python environment in $VENV_DIR\n" @@ -99,12 +117,12 @@ 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" - sudo cp $CONFIG /boot/$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 $RESOURCES_TOP_DIR/config-backups/$FILENAME + 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 } @@ -185,7 +203,7 @@ pip_pkg_install toml CONFIG_VARS=`$PYTHON - < Date: Tue, 21 Nov 2023 16:01:41 +0000 Subject: [PATCH 10/19] Fix VENV_DIR path. --- install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 38f19e9..9f11aca 100755 --- a/install.sh +++ b/install.sh @@ -6,7 +6,7 @@ DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` CONFIG_BACKUP=false APT_HAS_UPDATED=false RESOURCES_TOP_DIR=$HOME/Pimoroni -VENV_BASH_SNIPPET=$RESOURCES_DIR/auto_venv.sh +VENV_BASH_SNIPPET=$RESOURCES_TOP_DIR/auto_venv.sh VENV_DIR=$HOME/.virtualenvs/pimoroni WD=`pwd` USAGE="./install.sh (--unstable)" @@ -76,7 +76,7 @@ find_config() { venv_bash_snippet() { if [ ! -f $VENV_BASH_SNIPPET ]; then cat << EOF > $VENV_BASH_SNIPPET -# Add `source $RESOURCES_DIR/auto_venv.sh` to your ~/.bashrc to activate +# 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 From f1508a4939279be7c74efc666b32970b76a8f4f8 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 22 Nov 2023 10:42:39 +0000 Subject: [PATCH 11/19] Fix auto_venv.sh creation. --- install.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 9f11aca..6c9b474 100755 --- a/install.sh +++ b/install.sh @@ -74,9 +74,11 @@ find_config() { } venv_bash_snippet() { + inform "Checking for $VENV_BASH_SNIPPET\n" if [ ! -f $VENV_BASH_SNIPPET ]; then + inform "Creating $VENV_BASH_SNIPPET\n" cat << EOF > $VENV_BASH_SNIPPET -# Add `source $VENV_BASH_SNIPPET` to your ~/.bashrc to activate +# 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 From a1f67c9f9f0856626b25869a19508fef64753451 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 22 Nov 2023 17:15:30 +0000 Subject: [PATCH 12/19] examples/weather-and-light.py: fix bug with vertical label spacing. --- examples/weather-and-light.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/examples/weather-and-light.py b/examples/weather-and-light.py index efbd2a8..fcfb533 100755 --- a/examples/weather-and-light.py +++ b/examples/weather-and-light.py @@ -162,11 +162,6 @@ def draw_background(progress, period, day): return composite -def text_width(font, text): - x1, y1, x2, y2 = font.getbbox(text) - return x2 - x1 - - def text_size(font, text): x1, y1, x2, y2 = font.getbbox(text) return x2 - x1, y2 - y1 @@ -384,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 = text_width(font_lg, temp_string) + 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: @@ -398,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 = text_width(font_lg, humidity_string) + 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") @@ -408,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 = text_width(font_lg, light_string.replace(",", "")) + 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") @@ -421,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 = text_width(font_lg, pressure_string.replace(",", "")) + 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) From e6cd19c57b210000b3fb01e6e4b133a57d16c042 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 23 Nov 2023 10:18:07 +0000 Subject: [PATCH 13/19] install.sh: rework for better error reporting and fix some bugs. Fix a bug where auto_venv.sh was being created in a non-existent directory. Trap exit codes for some commands and add some help text + GitHUb url at the end of the install process. Try to comment what some sections do, and insert linebreaks so they are more logically broken up in the installer output. Try to be more consistent with colours. Try to be more friendly with colours- remove full red warning text in favour of a prefix so the errors/warnings are easier to read. Return a failure exit code if bits of the script have failed. Try to re-order output so it's more logical. Re-word venv creation message. --- install.sh | 144 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 102 insertions(+), 42 deletions(-) diff --git a/install.sh b/install.sh index 6c9b474..d42c48d 100755 --- a/install.sh +++ b/install.sh @@ -5,21 +5,21 @@ CONFIG_DIR="/boot/firmware" DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` CONFIG_BACKUP=false APT_HAS_UPDATED=false -RESOURCES_TOP_DIR=$HOME/Pimoroni -VENV_BASH_SNIPPET=$RESOURCES_TOP_DIR/auto_venv.sh -VENV_DIR=$HOME/.virtualenvs/pimoroni +RESOURCES_TOP_DIR="$HOME/Pimoroni" +VENV_BASH_SNIPPET="$RESOURCES_TOP_DIR/auto_venv.sh" +VENV_DIR="$HOME/.virtualenvs/pimoroni" WD=`pwd` USAGE="./install.sh (--unstable)" POSITIONAL_ARGS=() FORCE=false UNSTABLE=false PYTHON="python" +CMD_ERRORS=false user_check() { if [ $(id -u) -eq 0 ]; then - printf "Script should not be run as root. Try './install.sh'\n" - exit 1 + fatal "Script should not be run as root. Try './install.sh'\n" fi } @@ -54,29 +54,34 @@ 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 - warning "Could not find $CONFIG_FILE!" - exit 1 + 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 - 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" + inform "Using $CONFIG_FILE in $CONFIG_DIR" } venv_bash_snippet() { - inform "Checking for $VENV_BASH_SNIPPET\n" + inform "Checking for $VENV_BASH_SNIPPET\n" if [ ! -f $VENV_BASH_SNIPPET ]; then - inform "Creating $VENV_BASH_SNIPPET\n" + 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! @@ -96,23 +101,32 @@ 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 one for you?"; then + 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 virtual Python environment in $VENV_DIR, please wait...\n" + 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 + source $VENV_DIR/bin/activate else - inform "Found existing virtual Python environment in $VENV_DIR\n" + inform "Activating existing virtual Python environment in $VENV_DIR\n" + printf "source $VENV_DIR/bin/activate\n" + source $VENV_DIR/bin/activate fi - inform "Activating virtual Python environment in $VENV_DIR..." - inform "source $VENV_DIR/bin/activate\n" - source $VENV_DIR/bin/activate - else - exit 1 + 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 "^^^ 😬" + fi } function do_config_backup { @@ -132,6 +146,7 @@ function do_config_backup { function apt_pkg_install { PACKAGES=() 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 @@ -143,12 +158,14 @@ function apt_pkg_install { done PACKAGES="${PACKAGES[@]}" if ! [ "$PACKAGES" == "" ]; then - echo "Installing missing packages: $PACKAGES" + printf "\n" + inform "Installing missing packages: $PACKAGES" if [ ! $APT_HAS_UPDATED ]; then sudo apt update APT_HAS_UPDATED=true fi sudo apt install -y $PACKAGES + check_for_error if [ -f "$UNINSTALLER" ]; then echo "apt uninstall -y $PACKAGES" >> $UNINSTALLER fi @@ -156,7 +173,9 @@ function apt_pkg_install { } 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 @@ -186,30 +205,33 @@ while [[ $# -gt 0 ]]; do esac done +printf "Installing $LIBRARY_NAME...\n\n" + user_check venv_check if [ ! -f `which $PYTHON` ]; then - printf "Python path $PYTHON not found!\n" - exit 1 + fatal "Python path $PYTHON not found!\n" fi PYTHON_VER=`$PYTHON --version` -printf "$LIBRARY_NAME Python Library: Installer\n\n" - inform "Checking Dependencies. Please wait..." +# Install toml and try to read pyproject.toml into bash variables + pip_pkg_install toml 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" @@ -244,19 +270,23 @@ exit 1 source $VIRTUAL_ENV/bin/activate EOF -if $UNSTABLE; then - warning "Installing unstable library from source.\n\n" -else - printf "Installing stable library from pypi.\n\n" -fi +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" pip_pkg_install . else + inform "Installing stable library from pypi.\n" pip_pkg_install $LIBRARY_NAME fi + if [ $? -eq 0 ]; then success "Done!\n" echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> $UNINSTALLER @@ -266,6 +296,8 @@ cd $WD find_config +# Run the setup commands from pyproject.toml / tool.pimoroni.commands + for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do CMD="${SETUP_CMDS[$i]}" # Attempt to catch anything that touches config.txt and trigger a backup @@ -273,13 +305,18 @@ for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do do_config_backup 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_DIR/$CONFIG_FILE\n" + 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 "$CONFIG_LINE\n" | sudo tee --append $CONFIG_DIR/$CONFIG_FILE @@ -287,6 +324,10 @@ for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do 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" @@ -298,9 +339,12 @@ 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 - printf "Generating documentation.\n" + inform "Generating documentation.\n" $PYTHON -m pdoc $LIBRARY_NAME -o $RESOURCES_DIR/docs > /dev/null if [ $? -eq 0 ]; then inform "Documentation saved to $RESOURCES_DIR/docs" @@ -310,6 +354,22 @@ if confirm "Would you like to generate documentation?"; then fi fi -success "\nAll done!" -inform "If this is your first time installing you should reboot for hardware changes to take effect.\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 $GITHUB_URL.\n\n" +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 $UNINSTALLER\n\n" + +if [ "$CMD_ERRORS" = true ]; then + exit 1 +else + exit 0 +fi \ No newline at end of file From 8b1ab0afc6caa393004d494c927452b933752011 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 10 Jan 2024 14:29:50 +0000 Subject: [PATCH 14/19] Sync with latest boilerplate. * CI: Update GitHub Actions versions. * QA: Add shellcheck and fix/ignore all issues. * install.sh: slightly better feedback for setup commands. --- .github/workflows/build.yml | 6 +- .github/workflows/qa.yml | 9 ++- .github/workflows/test.yml | 2 +- Makefile | 5 +- check.sh | 20 +++--- install.sh | 119 ++++++++++++++++++------------------ uninstall.sh | 14 ++--- 7 files changed, 87 insertions(+), 88 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 87200ef..07620e3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,10 +19,10 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} @@ -35,7 +35,7 @@ jobs: make build - name: Upload Packages - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ env.RELEASE_FILE }} path: dist/ diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 4f85883..ac672a5 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -10,16 +10,15 @@ jobs: test: name: linting & spelling runs-on: ubuntu-latest - env: TERM: xterm-256color steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python '3,11' - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: '3.11' @@ -34,3 +33,7 @@ jobs: - 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 016a678..6f8cff7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} diff --git a/Makefile b/Makefile index 9e0c15c..34f4a7d 100644 --- a/Makefile +++ b/Makefile @@ -30,11 +30,14 @@ uninstall: dev-deps: python3 -m pip install -r requirements-dev.txt - sudo apt install dos2unix + sudo apt install dos2unix shellcheck check: @bash check.sh +shellcheck: + shellcheck *.sh + qa: tox -e qa diff --git a/check.sh b/check.sh index 4395d89..38dfc3a 100755 --- a/check.sh +++ b/check.sh @@ -3,9 +3,9 @@ # 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))}'` +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() { @@ -29,7 +29,7 @@ while [[ $# -gt 0 ]]; do ;; *) if [[ $1 == -* ]]; then - printf "Unrecognised option: $1\n"; + printf "Unrecognised option: %s\n" "$1"; exit 1 fi POSITIONAL_ARGS+=("$1") @@ -40,8 +40,7 @@ done inform "Checking $LIBRARY_NAME $LIBRARY_VERSION\n" inform "Checking for trailing whitespace..." -grep -IUrn --color "[[:blank:]]$" --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO -if [[ $? -eq 0 ]]; then +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 @@ -50,8 +49,7 @@ fi printf "\n" inform "Checking for DOS line-endings..." -grep -lIUrn --color $'\r' --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile -if [[ $? -eq 0 ]]; then +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 @@ -60,8 +58,7 @@ fi printf "\n" inform "Checking CHANGELOG.md..." -cat CHANGELOG.md | grep ^${LIBRARY_VERSION} > /dev/null 2>&1 -if [[ $? -eq 1 ]]; then +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 @@ -70,8 +67,7 @@ fi printf "\n" inform "Checking for git tag ${LIBRARY_VERSION}..." -git tag -l | grep -E "${LIBRARY_VERSION}$" -if [[ $? -eq 1 ]]; then +if ! git tag -l | grep -E "${LIBRARY_VERSION}$"; then warning "Missing git tag for version ${LIBRARY_VERSION}" fi printf "\n" diff --git a/install.sh b/install.sh index d42c48d..059d3c4 100755 --- a/install.sh +++ b/install.sh @@ -1,14 +1,13 @@ #!/bin/bash -LIBRARY_NAME=`grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}'` +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"` +DATESTAMP=$(date "+%Y-%m-%d-%H-%M-%S") CONFIG_BACKUP=false APT_HAS_UPDATED=false RESOURCES_TOP_DIR="$HOME/Pimoroni" VENV_BASH_SNIPPET="$RESOURCES_TOP_DIR/auto_venv.sh" VENV_DIR="$HOME/.virtualenvs/pimoroni" -WD=`pwd` USAGE="./install.sh (--unstable)" POSITIONAL_ARGS=() FORCE=false @@ -18,7 +17,7 @@ CMD_ERRORS=false user_check() { - if [ $(id -u) -eq 0 ]; then + if [ "$(id -u)" -eq 0 ]; then fatal "Script should not be run as root. Try './install.sh'\n" fi } @@ -36,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)" } @@ -79,10 +69,10 @@ find_config() { venv_bash_snippet() { inform "Checking for $VENV_BASH_SNIPPET\n" - if [ ! -f $VENV_BASH_SNIPPET ]; then + if [ ! -f "$VENV_BASH_SNIPPET" ]; then inform "Creating $VENV_BASH_SNIPPET\n" - mkdir -p $RESOURCES_TOP_DIR - cat << EOF > $VENV_BASH_SNIPPET + 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" @@ -98,21 +88,23 @@ EOF } venv_check() { - PYTHON_BIN=`which $PYTHON` + 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 + 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 + mkdir -p "$VENV_DIR" + /usr/bin/python3 -m venv "$VENV_DIR" --system-site-packages venv_bash_snippet - source $VENV_DIR/bin/activate + # shellcheck disable=SC1091 + source "$VENV_DIR/bin/activate" else inform "Activating existing virtual Python environment in $VENV_DIR\n" - printf "source $VENV_DIR/bin/activate\n" - source $VENV_DIR/bin/activate + printf "source \"%s/bin/activate\"\n" "$VENV_DIR" + # shellcheck disable=SC1091 + source "$VENV_DIR/bin/activate" fi else printf "\n" @@ -125,7 +117,7 @@ venv_check() { check_for_error() { if [ $? -ne 0 ]; then CMD_ERRORS=true - warning "^^^ 😬" + warning "^^^ 😬 previous command did not exit cleanly!" fi } @@ -134,29 +126,29 @@ function do_config_backup { CONFIG_BACKUP=true FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" 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 + 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_DIR/$CONFIG_FILE" >> $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 printf "\n" inform "Installing missing packages: $PACKAGES" @@ -164,10 +156,10 @@ function apt_pkg_install { sudo apt update APT_HAS_UPDATED=true fi - sudo apt install -y $PACKAGES + sudo apt install -y "$PACKAGES" check_for_error if [ -f "$UNINSTALLER" ]; then - echo "apt uninstall -y $PACKAGES" >> $UNINSTALLER + echo "apt uninstall -y $PACKAGES" >> "$UNINSTALLER" fi fi } @@ -196,8 +188,8 @@ while [[ $# -gt 0 ]]; do ;; *) 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") @@ -205,16 +197,16 @@ while [[ $# -gt 0 ]]; do esac done -printf "Installing $LIBRARY_NAME...\n\n" +printf "Installing %s...\n\n" "$LIBRARY_NAME" user_check venv_check -if [ ! -f `which $PYTHON` ]; then - fatal "Python path $PYTHON not found!\n" +if [ ! -f "$(which "$PYTHON")" ]; then + fatal "Python path %s not found!\n" "$PYTHON" fi -PYTHON_VER=`$PYTHON --version` +PYTHON_VER=$($PYTHON --version) inform "Checking Dependencies. Please wait..." @@ -222,7 +214,8 @@ inform "Checking Dependencies. Please wait..." pip_pkg_install toml -CONFIG_VARS=`$PYTHON - < $UNINSTALLER +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" @@ -284,27 +279,30 @@ if $UNSTABLE; then pip_pkg_install . else inform "Installing stable library from pypi.\n" - pip_pkg_install $LIBRARY_NAME + pip_pkg_install "$LIBRARY_NAME" fi +# 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 + 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 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 + printf "\"%s\"\n" "$CMD" + eval "$CMD" check_for_error done @@ -319,7 +317,7 @@ for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do 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 "$CONFIG_LINE\n" | sudo tee --append $CONFIG_DIR/$CONFIG_FILE + printf "%s \n" "$CONFIG_LINE" | sudo tee --append $CONFIG_DIR/$CONFIG_FILE fi fi done @@ -331,8 +329,8 @@ printf "\n" 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 @@ -345,8 +343,7 @@ if confirm "Would you like to generate documentation?"; then inform "Installing pdoc. Please wait..." pip_pkg_install pdoc inform "Generating documentation.\n" - $PYTHON -m pdoc $LIBRARY_NAME -o $RESOURCES_DIR/docs > /dev/null - if [ $? -eq 0 ]; then + if $PYTHON -m pdoc "$LIBRARY_NAME" -o "$RESOURCES_DIR/docs" > /dev/null; then inform "Documentation saved to $RESOURCES_DIR/docs" success "Done!" else @@ -360,16 +357,16 @@ 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 $GITHUB_URL.\n\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 $UNINSTALLER\n\n" +printf "Find uninstall steps in %s\n\n" "$UNINSTALLER" if [ "$CMD_ERRORS" = true ]; then exit 1 else exit 0 -fi \ No newline at end of file +fi diff --git a/uninstall.sh b/uninstall.sh index f213fc5..3314b7f 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,13 +1,13 @@ #!/bin/bash FORCE=false -LIBRARY_NAME=`grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}'` +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" venv_check() { - PYTHON_BIN=`which $PYTHON` + 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 @@ -15,7 +15,7 @@ venv_check() { } user_check() { - if [ $(id -u) -eq 0 ]; then + if [ "$(id -u)" -eq 0 ]; then printf "Script should not be run as root. Try './uninstall.sh'\n" exit 1 fi @@ -55,17 +55,17 @@ warning() { echo -e "$(tput setaf 1)$1$(tput sgr0)" } -printf "$LIBRARY_NAME Python Library: Uninstaller\n\n" +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 +$PYTHON -m pip uninstall "$LIBRARY_NAME" -if [ -d $RESOURCES_DIR ]; then +if [ -d "$RESOURCES_DIR" ]; then if confirm "Would you like to delete $RESOURCES_DIR?"; then - rm -r $RESOURCES_DIR + rm -r "$RESOURCES_DIR" fi fi From 97c824a23e9dd1245b7a4f94f5e1a473129dba77 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 10 Jan 2024 14:52:23 +0000 Subject: [PATCH 15/19] Sync with latest boilerplate. * install.sh: fix quoting bug in do_config_backup. * install.sh: don't output printf commands. --- install.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 059d3c4..bb671f2 100755 --- a/install.sh +++ b/install.sh @@ -126,7 +126,7 @@ function do_config_backup { CONFIG_BACKUP=true FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" inform "Backing up $CONFIG_DIR/$CONFIG_FILE to $CONFIG_DIR/$FILENAME\n" - sudo cp "$CONFIG_DIR/$CONFIG_FILE $CONFIG_DIR/$FILENAME" + 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 @@ -301,7 +301,9 @@ for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG_DIR/$CONFIG_FILE"* ]] || [[ "$CMD" == *"\$CONFIG_DIR/\$CONFIG_FILE"* ]]; then do_config_backup fi - printf "\"%s\"\n" "$CMD" + if [[ ! "$CMD" == printf* ]]; then + printf "Running: \"%s\"\n" "$CMD" + fi eval "$CMD" check_for_error done From 34c3efaa0e538e2e9d753474a0dfd28b94ef1df2 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 17 Apr 2024 09:21:59 +0100 Subject: [PATCH 16/19] Standardise on GPIOn pin labels. As of https://github.com/raspberrypi/linux/commit/bd9542b8271ccb251490c814e9237ed94d7ceb56 all downstream GPIO line names use the form GPIOn, and PIN_n is deprecated. Simplify code and examples to reflect this. --- enviroplus/gas.py | 8 ++------ examples/all-in-one-enviro-mini.py | 4 ++-- examples/all-in-one-no-pm.py | 4 ++-- examples/all-in-one.py | 4 ++-- examples/combined.py | 4 ++-- examples/lcd.py | 4 ++-- examples/mqtt-all.py | 4 ++-- examples/noise-amps-at-freqs.py | 4 ++-- examples/noise-profile.py | 4 ++-- examples/sensorcommunity.py | 4 ++-- examples/sensorcommunity_combined.py | 4 ++-- examples/weather-and-light.py | 4 ++-- tests/conftest.py | 1 + 13 files changed, 25 insertions(+), 28 deletions(-) diff --git a/enviroplus/gas.py b/enviroplus/gas.py index e1c4b76..3583428 100644 --- a/enviroplus/gas.py +++ b/enviroplus/gas.py @@ -11,11 +11,7 @@ MICS6814_GAIN = 6.144 OUTH = gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE) -PLATFORMS = { - "Radxa ROCK 5B": {"heater": ("PIN_18", OUTH)}, - "Raspberry Pi 5": {"heater": ("PIN18", OUTH)}, - "Raspberry Pi 4": {"heater": ("GPIO24", OUTH)} -} + ads1015.I2C_ADDRESS_DEFAULT = ads1015.I2C_ADDRESS_ALTERNATE _is_setup = False @@ -68,7 +64,7 @@ def setup(): else: adc.set_sample_rate(1600) - _heater = gpiodevice.get_pins_for_platform(PLATFORMS)[0] + _heater = gpiodevice.get_pin("GPIO24") atexit.register(cleanup) diff --git a/examples/all-in-one-enviro-mini.py b/examples/all-in-one-enviro-mini.py index 01491f3..4aea5c4 100755 --- a/examples/all-in-one-enviro-mini.py +++ b/examples/all-in-one-enviro-mini.py @@ -37,8 +37,8 @@ st7735 = st7735.ST7735( port=0, cs=1, - dc="PIN21", # "GPIO9" on a Raspberry Pi 4 - backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) diff --git a/examples/all-in-one-no-pm.py b/examples/all-in-one-no-pm.py index c2d9078..c017205 100755 --- a/examples/all-in-one-no-pm.py +++ b/examples/all-in-one-no-pm.py @@ -38,8 +38,8 @@ st7735 = st7735.ST7735( port=0, cs=1, - dc="PIN21", # "GPIO9" on a Raspberry Pi 4 - backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) diff --git a/examples/all-in-one.py b/examples/all-in-one.py index 9b7121e..b558f48 100755 --- a/examples/all-in-one.py +++ b/examples/all-in-one.py @@ -44,8 +44,8 @@ st7735 = st7735.ST7735( port=0, cs=1, - dc="PIN21", # "GPIO9" on a Raspberry Pi 4 - backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) diff --git a/examples/combined.py b/examples/combined.py index eaab10f..79e5d62 100755 --- a/examples/combined.py +++ b/examples/combined.py @@ -47,8 +47,8 @@ st7735 = st7735.ST7735( port=0, cs=1, - dc="PIN21", # "GPIO9" on a Raspberry Pi 4 - backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) diff --git a/examples/lcd.py b/examples/lcd.py index cb2af2f..97c2be4 100755 --- a/examples/lcd.py +++ b/examples/lcd.py @@ -21,8 +21,8 @@ disp = st7735.ST7735( port=0, cs=1, - dc="PIN21", # "GPIO9" on a Raspberry Pi 4 - backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) diff --git a/examples/mqtt-all.py b/examples/mqtt-all.py index 7238d3b..ecab4a3 100755 --- a/examples/mqtt-all.py +++ b/examples/mqtt-all.py @@ -234,8 +234,8 @@ def main(): disp = st7735.ST7735( port=0, cs=1, - dc="PIN21", # "GPIO9" on a Raspberry Pi 4 - backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) diff --git a/examples/noise-amps-at-freqs.py b/examples/noise-amps-at-freqs.py index 825f53c..957511b 100755 --- a/examples/noise-amps-at-freqs.py +++ b/examples/noise-amps-at-freqs.py @@ -18,8 +18,8 @@ disp = st7735.ST7735( port=0, cs=1, - dc="PIN21", # "GPIO9" on a Raspberry Pi 4 - backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) diff --git a/examples/noise-profile.py b/examples/noise-profile.py index 17ada82..be6a185 100755 --- a/examples/noise-profile.py +++ b/examples/noise-profile.py @@ -16,8 +16,8 @@ disp = st7735.ST7735( port=0, cs=1, - dc="PIN21", # "GPIO9" on a Raspberry Pi 4 - backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) diff --git a/examples/sensorcommunity.py b/examples/sensorcommunity.py index 7128ad7..cdd3a48 100755 --- a/examples/sensorcommunity.py +++ b/examples/sensorcommunity.py @@ -40,8 +40,8 @@ disp = st7735.ST7735( port=0, cs=1, - dc="PIN21", # "GPIO9" on a Raspberry Pi 4 - backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) diff --git a/examples/sensorcommunity_combined.py b/examples/sensorcommunity_combined.py index 8dac723..a7aeb7b 100644 --- a/examples/sensorcommunity_combined.py +++ b/examples/sensorcommunity_combined.py @@ -153,8 +153,8 @@ def check_wifi(): st7735 = st7735.ST7735( port=0, cs=1, - dc="PIN21", # "GPIO9" on a Raspberry Pi 4 - backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) diff --git a/examples/weather-and-light.py b/examples/weather-and-light.py index fcfb533..04f1bd8 100755 --- a/examples/weather-and-light.py +++ b/examples/weather-and-light.py @@ -291,8 +291,8 @@ def describe_light(light): disp = st7735.ST7735( port=0, cs=1, - dc="PIN21", # "GPIO9" on a Raspberry Pi 4 - backlight="PIN32", # "GPIO12" on a Raspberry Pi 4 + dc="GPIO9", + backlight="GPIO12", rotation=270, spi_speed_hz=10000000 ) diff --git a/tests/conftest.py b/tests/conftest.py index 6ad5557..2023743 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -45,6 +45,7 @@ def gpiod(): 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 From b7fad2415edcd667b6efb190298334a4b8347251 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 17 Apr 2024 10:01:25 +0100 Subject: [PATCH 17/19] Prep for v1.0.0. --- CHANGELOG.md | 5 +++++ enviroplus/__init__.py | 2 +- pyproject.toml | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21f175a..67206be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +1.0.0 +----- + +* BREAKING: Port to gpiod/gpiodevice for Pi 5/Bookworm. + 0.0.6 ----- diff --git a/enviroplus/__init__.py b/enviroplus/__init__.py index 034f46c..5becc17 100644 --- a/enviroplus/__init__.py +++ b/enviroplus/__init__.py @@ -1 +1 @@ -__version__ = "0.0.6" +__version__ = "1.0.0" diff --git a/pyproject.toml b/pyproject.toml index a6a4e6b..d97b417 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,8 @@ classifiers = [ "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", From 6391486dcadb32c0bdf4f7d9547bea32219cd20a Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 17 Apr 2024 09:37:23 +0100 Subject: [PATCH 18/19] install.sh: drop quotes around apt packages. Quotes would cause a list of packages to be treated as a single package and lookup would fail. Reported-by: thirdr --- install.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index bb671f2..495919b 100755 --- a/install.sh +++ b/install.sh @@ -156,7 +156,8 @@ function apt_pkg_install { sudo apt update APT_HAS_UPDATED=true fi - sudo apt install -y "$PACKAGES" + # shellcheck disable=SC2086 + sudo apt install -y $PACKAGES check_for_error if [ -f "$UNINSTALLER" ]; then echo "apt uninstall -y $PACKAGES" >> "$UNINSTALLER" From 65c7e9da80521ef4d42a9cc7ba0cd99f7a086635 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 17 Apr 2024 11:16:46 +0100 Subject: [PATCH 19/19] README.md: Absolute image URLs for PyPI. --- CHANGELOG.md | 5 +++++ README.md | 4 ++-- enviroplus/__init__.py | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67206be..d28efc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +1.0.1 +----------- + +* README.md: Fix images + 1.0.0 ----- 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/enviroplus/__init__.py b/enviroplus/__init__.py index 5becc17..5c4105c 100644 --- a/enviroplus/__init__.py +++ b/enviroplus/__init__.py @@ -1 +1 @@ -__version__ = "1.0.0" +__version__ = "1.0.1"