From 10b1d21164bebfe2f97b62084b1a43dfe42a849d Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Sun, 5 Jul 2026 21:27:34 +0100 Subject: [PATCH 1/8] CI: modernise packaging to uv-modernize method - hatchling dynamic optional-dependencies via hatch-requirements-txt - [dependency-groups] dev; ruff import sorting (drop isort/black) - pre-commit + editorconfig; uv-based CI and tox qa - requires-python >= 3.9, classifiers 3.9-3.13 - remove check.sh, requirements-dev.txt, .stickler.yml --- .editorconfig | 14 ++++++ .github/dependabot.yml | 14 ++++++ .github/workflows/build.yml | 19 ++++---- .github/workflows/install.yml | 43 ++++++++++++++++++ .github/workflows/qa.yml | 22 ++++----- .github/workflows/test.yml | 26 +++++------ .gitignore | 2 + .pre-commit-config.yaml | 22 +++++++++ Makefile | 31 ++++++++----- check.sh | 84 ----------------------------------- pyproject.toml | 41 +++++++++++------ requirements-dev.txt | 9 ---- requirements-examples.txt | 0 tox.ini | 13 +----- 14 files changed, 179 insertions(+), 161 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/install.yml create mode 100644 .pre-commit-config.yaml delete mode 100755 check.sh delete mode 100644 requirements-dev.txt create mode 100644 requirements-examples.txt diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3537c50 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +[Makefile] +indent_style = tab + +[*.{py,cfg,ini,toml,yaml,yml,md,sh}] +indent_style = space +indent_size = 4 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..4f8a87a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "pre-commit" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 07620e3..1b1d4ea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,33 +6,34 @@ on: branches: - main +permissions: + contents: read + jobs: test: - name: Python ${{ matrix.python }} + name: Build (Python ${{ matrix.python }}) runs-on: ubuntu-latest strategy: matrix: - python: ['3.9', '3.10', '3.11'] + python: ['3.9', '3.10', '3.11', '3.12', '3.13'] env: + TERM: xterm-256color RELEASE_FILE: ${{ github.event.repository.name }}-${{ github.event.release.tag_name || github.sha }}-py${{ matrix.python }} steps: - name: Checkout Code uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v5 + - name: Set up uv + uses: astral-sh/setup-uv@v6 with: + enable-cache: true python-version: ${{ matrix.python }} - - name: Install Dependencies - run: | - make dev-deps - - name: Build Packages run: | - make build + uv run make build - name: Upload Packages uses: actions/upload-artifact@v4 diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml new file mode 100644 index 0000000..b8e5da8 --- /dev/null +++ b/.github/workflows/install.yml @@ -0,0 +1,43 @@ +name: Install Test + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + test: + name: Install (Python ${{ matrix.python }}) + runs-on: ubuntu-latest + env: + TERM: xterm-256color + strategy: + matrix: + python: ['3.9', '3.10', '3.11', '3.12', '3.13'] + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + + - name: Stub files & Patch install.sh + run: | + mkdir -p boot/firmware + touch boot/firmware/config.txt + sed -i "s|/boot/firmware|`pwd`/boot/firmware|g" install.sh + sed -i "s|sudo raspi-config|raspi-config|g" pyproject.toml + touch raspi-config + chmod +x raspi-config + echo `pwd` >> $GITHUB_PATH + + - name: Run install.sh + run: | + ./install.sh --unstable --force diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index ac672a5..903a649 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -6,9 +6,12 @@ on: branches: - main +permissions: + contents: read + jobs: test: - name: linting & spelling + name: Linting & Spelling runs-on: ubuntu-latest env: TERM: xterm-256color @@ -17,23 +20,20 @@ jobs: - name: Checkout Code uses: actions/checkout@v4 - - name: Set up Python '3,11' - uses: actions/setup-python@v5 + - name: Set up uv + uses: astral-sh/setup-uv@v6 with: + enable-cache: true python-version: '3.11' - - name: Install Dependencies - run: | - make dev-deps - - name: Run Quality Assurance run: | - make qa + uv run make qa - name: Run Code Checks run: | - make check + uv run make check - - name: Run Bash Code Checks + - name: Run Pre-Commit run: | - make shellcheck + uv run pre-commit run --all-files diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6f8cff7..8e4fb06 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,36 +6,36 @@ on: branches: - main +permissions: + contents: read + jobs: test: - name: Python ${{ matrix.python }} + name: Test (Python ${{ matrix.python }}) runs-on: ubuntu-latest + env: + TERM: xterm-256color strategy: matrix: - python: ['3.9', '3.10', '3.11'] + python: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v5 + - name: Set up uv + uses: astral-sh/setup-uv@v6 with: + enable-cache: true python-version: ${{ matrix.python }} - - name: Install Dependencies - run: | - make dev-deps - - name: Run Tests run: | - make pytest + uv run make pytest - name: Coverage if: ${{ matrix.python == '3.9' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - python -m pip install coveralls - coveralls --service=github - + uvx coveralls --service=github diff --git a/.gitignore b/.gitignore index 5efe1d1..6ecb8df 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ pip-delete-this-directory.txt .coverage .tox/ .pytest_cache/ +.venv +uv.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..53210a8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,22 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: mixed-line-ending + args: ['--fix=lf'] + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.20 + hooks: + - id: ruff-check + args: ['--fix'] + - repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell + additional_dependencies: ['tomli'] + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: v0.10.0.1 + hooks: + - id: shellcheck diff --git a/Makefile b/Makefile index 56cf0df..29700a2 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ LIBRARY_NAME := $(shell hatch project metadata name 2> /dev/null) LIBRARY_VERSION := $(shell hatch version 2> /dev/null) -.PHONY: usage install uninstall check pytest qa build-deps check tag wheel sdist clean dist testdeploy deploy +.PHONY: usage version install uninstall dev-deps check pre-commit qa pytest nopost tag build clean testdeploy deploy usage: ifdef LIBRARY_NAME @echo "Library: ${LIBRARY_NAME}" @@ -13,8 +13,9 @@ endif @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 "check: verify CHANGELOG.md has an entry for the current version" + @echo "qa: run package QA (check-manifest, build, twine)" + @echo "pre-commit: run pre-commit hooks (lint, whitespace) on all files" @echo "pytest: run Python test fixtures" @echo "clean: clean Python build and dist directories" @echo "build: build Python distribution files" @@ -32,14 +33,20 @@ uninstall: ./uninstall.sh dev-deps: - python3 -m pip install -r requirements-dev.txt - sudo apt install dos2unix shellcheck + python3 -m pip install --group dev + pre-commit install check: - @bash check.sh + @LIBRARY_VERSION=`hatch version | awk -F '.' '{print $$1"."$$2"."$$3}'`; \ + if grep -q "^$$LIBRARY_VERSION" CHANGELOG.md; then \ + echo "Changes found for version $$LIBRARY_VERSION."; \ + else \ + echo "Changes missing for version $$LIBRARY_VERSION! Please update CHANGELOG.md."; \ + exit 1; \ + fi -shellcheck: - shellcheck *.sh +pre-commit: + pre-commit run --all-files qa: tox -e qa @@ -48,13 +55,17 @@ pytest: tox -e py nopost: - @bash check.sh --nopost + @POST_VERSION=`hatch version | awk -F '.' '{print $$4}'`; \ + if [ -n "$$POST_VERSION" ]; then \ + echo "Found .$$POST_VERSION on library version; only use these for testpypi releases."; \ + exit 1; \ + fi tag: version git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" build: check - @hatch build + uv build clean: -rm -r dist diff --git a/check.sh b/check.sh deleted file mode 100755 index 38dfc3a..0000000 --- a/check.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash - -# This script handles some basic QA checks on the source - -NOPOST=$1 -LIBRARY_NAME=$(hatch project metadata name) -LIBRARY_VERSION=$(hatch version | awk -F "." '{print $1"."$2"."$3}') -POST_VERSION=$(hatch version | awk -F "." '{print substr($4,0,length($4))}') -TERM=${TERM:="xterm-256color"} - -success() { - echo -e "$(tput setaf 2)$1$(tput sgr0)" -} - -inform() { - echo -e "$(tput setaf 6)$1$(tput sgr0)" -} - -warning() { - echo -e "$(tput setaf 1)$1$(tput sgr0)" -} - -while [[ $# -gt 0 ]]; do - K="$1" - case $K in - -p|--nopost) - NOPOST=true - shift - ;; - *) - if [[ $1 == -* ]]; then - printf "Unrecognised option: %s\n" "$1"; - exit 1 - fi - POSITIONAL_ARGS+=("$1") - shift - esac -done - -inform "Checking $LIBRARY_NAME $LIBRARY_VERSION\n" - -inform "Checking for trailing whitespace..." -if grep -IUrn --color "[[:blank:]]$" --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO; then - warning "Trailing whitespace found!" - exit 1 -else - success "No trailing whitespace found." -fi -printf "\n" - -inform "Checking for DOS line-endings..." -if grep -lIUrn --color $'\r' --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile; then - warning "DOS line-endings found!" - exit 1 -else - success "No DOS line-endings found." -fi -printf "\n" - -inform "Checking CHANGELOG.md..." -if ! grep "^${LIBRARY_VERSION}" CHANGELOG.md > /dev/null 2>&1; then - warning "Changes missing for version ${LIBRARY_VERSION}! Please update CHANGELOG.md." - exit 1 -else - success "Changes found for version ${LIBRARY_VERSION}." -fi -printf "\n" - -inform "Checking for git tag ${LIBRARY_VERSION}..." -if ! git tag -l | grep -E "${LIBRARY_VERSION}$"; then - warning "Missing git tag for version ${LIBRARY_VERSION}" -fi -printf "\n" - -if [[ $NOPOST ]]; then - inform "Checking for .postN on library version..." - if [[ "$POST_VERSION" != "" ]]; then - warning "Found .$POST_VERSION on library version." - inform "Please only use these for testpypi releases." - exit 1 - else - success "OK" - fi -fi diff --git a/pyproject.toml b/pyproject.toml index 7230aec..910bb70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,13 @@ [build-system] -requires = ["hatchling", "hatch-fancy-pypi-readme"] +requires = ["hatchling", "hatch-fancy-pypi-readme", "hatch-requirements-txt"] build-backend = "hatchling.build" [project] name = "bme680" -dynamic = ["version", "readme"] +dynamic = ["version", "readme", "optional-dependencies"] description = "Python library for the BME680 temperature, humidity and gas sensor" license = {file = "LICENSE"} -requires-python = ">= 3.7" +requires-python = ">= 3.9" authors = [ { name = "Philip Howard", email = "phil@pimoroni.com" }, ] @@ -24,11 +24,11 @@ classifiers = [ "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.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development", "Topic :: Software Development :: Libraries", @@ -38,19 +38,33 @@ dependencies = [ "smbus2" ] +[tool.hatch.metadata.hooks.requirements_txt.optional-dependencies] +example-depends = ["requirements-examples.txt"] + [project.urls] GitHub = "https://www.github.com/pimoroni/bme680-python" Homepage = "https://www.pimoroni.com" +[dependency-groups] +dev = [ + "check-manifest", + "ruff", + "codespell", + "pre-commit", + "twine", + "hatch", + "hatch-fancy-pypi-readme", + "hatch-requirements-txt", + "tox", + "pdoc", +] + [tool.hatch.version] path = "bme680/__init__.py" [tool.hatch.build] include = [ "bme680", - "README.md", - "CHANGELOG.md", - "LICENSE" ] [tool.hatch.build.targets.sdist] @@ -81,6 +95,9 @@ exclude = [ ] line-length = 200 +[tool.ruff.lint] +extend-select = ["I"] + [tool.codespell] skip = """ ./.tox,\ @@ -91,14 +108,9 @@ skip = """ ./dist.\ """ -[tool.isort] -line_length = 200 - [tool.check-manifest] ignore = [ - '.stickler.yml', 'boilerplate.md', - 'check.sh', 'install.sh', 'uninstall.sh', 'Makefile', @@ -106,7 +118,8 @@ ignore = [ 'tests/*', 'examples/*', '.coveragerc', - 'requirements-dev.txt' + '.editorconfig', + '.pre-commit-config.yaml', ] [tool.pimoroni] diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 525b042..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,9 +0,0 @@ -check-manifest -ruff -codespell -isort -twine -hatch -hatch-fancy-pypi-readme -tox -pdoc diff --git a/requirements-examples.txt b/requirements-examples.txt new file mode 100644 index 0000000..e69de29 diff --git a/tox.ini b/tox.ini index 4726cef..6ad3c71 100644 --- a/tox.ini +++ b/tox.ini @@ -19,16 +19,7 @@ commands = check-manifest python -m build --no-isolation python -m twine check dist/* - isort --check . ruff check . codespell . -deps = - check-manifest - ruff - codespell - isort - twine - build - hatch - hatch-fancy-pypi-readme - +dependency_groups = + dev From f32920107b49ea84fb3cfe8afc715ebf64555714 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 6 Jul 2026 13:16:11 +0100 Subject: [PATCH 2/8] style: satisfy pre-commit hooks (end-of-file-fixer) --- CHANGELOG.md | 1 - README.md | 1 - 2 files changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78fec49..5b50a98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,4 +47,3 @@ ----- * Initial release - diff --git a/README.md b/README.md index 0c71b7f..86a26df 100644 --- a/README.md +++ b/README.md @@ -53,4 +53,3 @@ sudo raspi-config nonint do_i2c 0 * Guides and tutorials - https://learn.pimoroni.com/bme680-breakout * Get help - http://forums.pimoroni.com/c/support - From 4405b5d6f381e97bdb0bbf637532b7082e2d5384 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 6 Jul 2026 13:45:44 +0100 Subject: [PATCH 3/8] CI: sync install.sh/uninstall.sh to boilerplate (uniform across repos) --- install.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/install.sh b/install.sh index 3db90bc..61f1a4a 100755 --- a/install.sh +++ b/install.sh @@ -166,6 +166,12 @@ function pip_pkg_install { check_for_error } +function pip_requirements_install { + # A null Keyring prevents pip stalling in the background + PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring $PYTHON -m pip install -r "$@" + check_for_error +} + while [[ $# -gt 0 ]]; do K="$1" case $K in @@ -335,6 +341,15 @@ fi printf "\n" +if [ -f "requirements-examples.txt" ]; then + if confirm "Would you like to install example dependencies?"; then + inform "Installing dependencies from requirements-examples.txt..." + pip_requirements_install requirements-examples.txt + fi +fi + +printf "\n" + # Use pdoc to generate basic documentation from the installed module if confirm "Would you like to generate documentation?"; then From 8f09d8b2b8a5fafbc651c5548f3a567f5b664acd Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 26 Aug 2026 19:26:33 +0100 Subject: [PATCH 4/8] Add data.gas_valid alongside data.heat_stable The gasm_valid status bit was folded into data.status but never surfaced, so callers could only check heat_stable. --- CHANGELOG.md | 1 + bme680/__init__.py | 1 + bme680/constants.py | 1 + tests/test_setup.py | 17 +++++++++++++++++ 4 files changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b50a98..f2b8414 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ 2.0.0 ----- +* New: data.gas_valid, exposing the sensor's gasm_valid status bit * Repackage to hatch/pyproject.toml * Drop Python 2.7 support * Switch from smbu2 to smbus2 diff --git a/bme680/__init__.py b/bme680/__init__.py index 56d547a..177e554 100644 --- a/bme680/__init__.py +++ b/bme680/__init__.py @@ -315,6 +315,7 @@ def get_sensor_data(self): self.data.status |= regs[14] & constants.HEAT_STAB_MSK self.data.heat_stable = (self.data.status & constants.HEAT_STAB_MSK) > 0 + self.data.gas_valid = (self.data.status & constants.GASM_VALID_MSK) > 0 temperature = self._calc_temperature(adc_temp) self.data.temperature = temperature / 100.0 diff --git a/bme680/constants.py b/bme680/constants.py index d77415d..f43ebf3 100644 --- a/bme680/constants.py +++ b/bme680/constants.py @@ -261,6 +261,7 @@ def __init__(self): # noqa D107 # Contains new_data, gasm_valid & heat_stab self.status = None self.heat_stable = False + self.gas_valid = False # The index of the heater profile used self.gas_index = None # Measurement index to track order diff --git a/tests/test_setup.py b/tests/test_setup.py index 0c1554d..954a752 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -12,3 +12,20 @@ def test_setup_not_present(smbus_notpresent): def test_setup_mock_present(smbus): """Mock the presence of a BME680 and test initialisation.""" sensor = bme680.BME680() # noqa F841 + + +def test_gas_valid_and_heat_stable_flags(smbus, calibration): + """Validate that gasm_valid and heat_stab are unpacked from field 0.""" + sensor = bme680.BME680() + sensor.calibration_data = calibration + + sensor._i2c.regs[bme680.FIELD0_ADDR] = bme680.NEW_DATA_MSK + sensor._i2c.regs[bme680.FIELD0_ADDR + 14] = 0 + assert sensor.get_sensor_data() is True + assert sensor.data.gas_valid is False + assert sensor.data.heat_stable is False + + sensor._i2c.regs[bme680.FIELD0_ADDR + 14] = bme680.GASM_VALID_MSK | bme680.HEAT_STAB_MSK + assert sensor.get_sensor_data() is True + assert sensor.data.gas_valid is True + assert sensor.data.heat_stable is True From 25bfc730b3d49c238bca87a8bec8d1753c814f60 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 26 Aug 2026 19:31:33 +0100 Subject: [PATCH 5/8] Document set_power_mode Answers #46. --- bme680/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bme680/__init__.py b/bme680/__init__.py index 177e554..7a38c5a 100644 --- a/bme680/__init__.py +++ b/bme680/__init__.py @@ -261,7 +261,14 @@ def set_gas_heater_duration(self, value, nb_profile=0): self._set_regs(constants.GAS_WAIT0_ADDR + nb_profile, temp) def set_power_mode(self, value, blocking=True): - """Set power mode.""" + """Set power mode. + + get_sensor_data sets FORCED_MODE to take a reading. Set SLEEP_MODE to + idle the sensor. + + :param value: One of SLEEP_MODE or FORCED_MODE + + """ if value not in (constants.SLEEP_MODE, constants.FORCED_MODE): raise ValueError('Power mode should be one of SLEEP_MODE or FORCED_MODE') From c1868d98184cee6411355c74fb75616b03538216 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 26 Aug 2026 19:36:12 +0100 Subject: [PATCH 6/8] Fix get_power_mode masking and set_power_mode blocking wait get_power_mode returned CONF_T_P_MODE_ADDR unmasked, so it reported 0x8d rather than FORCED_MODE once oversampling was configured. set_power_mode's wait could never block: get_power_mode reassigns self.power_mode as a side effect, so both sides of the comparison were always equal. Masking alone leaves the wait a no-op; removing the aliasing alone spins forever, since FORCED_MODE self-clears to SLEEP_MODE and can never be observed. Only SLEEP_MODE is waited on, and the wait is bounded. --- CHANGELOG.md | 2 ++ bme680/__init__.py | 10 +++++++--- tests/test_setup.py | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2b8414..8623323 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ----- * New: data.gas_valid, exposing the sensor's gasm_valid status bit +* Fix: get_power_mode now masks out the oversampling bits +* Fix: set_power_mode's blocking wait had no effect * Repackage to hatch/pyproject.toml * Drop Python 2.7 support * Switch from smbu2 to smbus2 diff --git a/bme680/__init__.py b/bme680/__init__.py index 7a38c5a..6d3ecbc 100644 --- a/bme680/__init__.py +++ b/bme680/__init__.py @@ -276,12 +276,16 @@ def set_power_mode(self, value, blocking=True): self._set_bits(constants.CONF_T_P_MODE_ADDR, constants.MODE_MSK, constants.MODE_POS, value) - while blocking and self.get_power_mode() != self.power_mode: - time.sleep(constants.POLL_PERIOD_MS / 1000.0) + # FORCED_MODE self-clears to SLEEP_MODE, so waiting to observe it can never terminate. + if blocking and value == constants.SLEEP_MODE: + for _ in range(10): + if self.get_power_mode() == value: + break + time.sleep(constants.POLL_PERIOD_MS / 1000.0) def get_power_mode(self): """Get power mode.""" - self.power_mode = self._get_regs(constants.CONF_T_P_MODE_ADDR, 1) + self.power_mode = (self._get_regs(constants.CONF_T_P_MODE_ADDR, 1) & constants.MODE_MSK) >> constants.MODE_POS return self.power_mode def get_sensor_data(self): diff --git a/tests/test_setup.py b/tests/test_setup.py index 954a752..5651fcf 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -29,3 +29,43 @@ def test_gas_valid_and_heat_stable_flags(smbus, calibration): assert sensor.get_sensor_data() is True assert sensor.data.gas_valid is True assert sensor.data.heat_stable is True + + +def test_get_power_mode_masks_register(smbus): + """Validate that get_power_mode returns only the mode bits.""" + sensor = bme680.BME680() + + sensor.set_temperature_oversample(bme680.OS_8X) + sensor.set_pressure_oversample(bme680.OS_4X) + sensor.set_power_mode(bme680.FORCED_MODE) + + assert sensor._i2c.regs[bme680.CONF_T_P_MODE_ADDR] & ~bme680.MODE_MSK != 0 + assert sensor.get_power_mode() == bme680.FORCED_MODE + + +def test_set_power_mode_forced_does_not_hang(smbus): + """Validate that setting FORCED_MODE returns when the sensor self-clears to sleep.""" + sensor = bme680.BME680() + + original = type(sensor._i2c).read_byte_data + + def read_byte_data(self, addr, register): + value = original(self, addr, register) + if register == bme680.CONF_T_P_MODE_ADDR: + value &= ~bme680.MODE_MSK + return value + + type(sensor._i2c).read_byte_data = read_byte_data + try: + sensor.set_power_mode(bme680.FORCED_MODE) + assert sensor.get_power_mode() == bme680.SLEEP_MODE + finally: + type(sensor._i2c).read_byte_data = original + + +def test_set_power_mode_sleep_waits(smbus): + """Validate that setting SLEEP_MODE polls until the sensor reports sleep.""" + sensor = bme680.BME680() + + sensor.set_power_mode(bme680.SLEEP_MODE) + assert sensor.get_power_mode() == bme680.SLEEP_MODE From 89df6248ca0c63434c8ee9f1a1a4e86d86911316 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 26 Aug 2026 19:50:18 +0100 Subject: [PATCH 7/8] Widen ruff ruleset and fix findings --- bme680/__init__.py | 26 +++++++++-------------- bme680/constants.py | 10 ++++----- examples/compensated-temperature.py | 4 ++-- examples/indoor-air-quality.py | 13 ++++-------- examples/read-all.py | 15 +++++-------- examples/temperature-offset.py | 9 +++----- examples/temperature-pressure-humidity.py | 7 ++---- pyproject.toml | 4 ++-- tests/conftest.py | 4 ++-- tox.ini | 1 - 10 files changed, 35 insertions(+), 58 deletions(-) diff --git a/bme680/__init__.py b/bme680/__init__.py index 6d3ecbc..6d79184 100644 --- a/bme680/__init__.py +++ b/bme680/__init__.py @@ -45,9 +45,9 @@ def __init__(self, i2c_addr=constants.I2C_ADDR_PRIMARY, i2c_device=None): try: self.chip_id = self._get_regs(constants.CHIP_ID_ADDR, 1) if self.chip_id != constants.CHIP_ID: - raise RuntimeError('BME680 Not Found. Invalid CHIP ID: 0x{0:02x}'.format(self.chip_id)) - except IOError: - raise RuntimeError("Unable to identify BME680 at 0x{:02x} (IOError)".format(self.i2c_addr)) + raise RuntimeError(f'BME680 Not Found. Invalid CHIP ID: 0x{self.chip_id:02x}') + except OSError: + raise RuntimeError(f"Unable to identify BME680 at 0x{self.i2c_addr:02x} (IOError)") from None self._variant = self._get_regs(constants.CHIP_VARIANT_ADDR, 1) @@ -182,7 +182,7 @@ def select_gas_heater_profile(self, value): """ if value > constants.NBCONV_MAX or value < constants.NBCONV_MIN: - raise ValueError("Profile '{}' should be between {} and {}".format(value, constants.NBCONV_MIN, constants.NBCONV_MAX)) + raise ValueError(f"Profile '{value}' should be between {constants.NBCONV_MIN} and {constants.NBCONV_MAX}") self.gas_settings.nb_conv = value self._set_bits(constants.CONF_ODR_RUN_GAS_NBC_ADDR, constants.NBCONV_MSK, constants.NBCONV_POS, value) @@ -203,10 +203,7 @@ def get_gas_heater_status(self): def set_gas_status(self, value): """Enable/disable gas sensor.""" if value == -1: - if self._variant == constants.VARIANT_HIGH: - value = constants.ENABLE_GAS_MEAS_HIGH - else: - value = constants.ENABLE_GAS_MEAS_LOW + value = constants.ENABLE_GAS_MEAS_HIGH if self._variant == constants.VARIANT_HIGH else constants.ENABLE_GAS_MEAS_LOW self.gas_settings.run_gas = value self._set_bits(constants.CONF_ODR_RUN_GAS_NBC_ADDR, constants.RUN_GAS_MSK, constants.RUN_GAS_POS, value) @@ -235,7 +232,7 @@ def set_gas_heater_temperature(self, value, nb_profile=0): """ if nb_profile > constants.NBCONV_MAX or value < constants.NBCONV_MIN: - raise ValueError('Profile "{}" should be between {} and {}'.format(nb_profile, constants.NBCONV_MIN, constants.NBCONV_MAX)) + raise ValueError(f'Profile "{nb_profile}" should be between {constants.NBCONV_MIN} and {constants.NBCONV_MAX}') self.gas_settings.heatr_temp = value temp = int(self._calc_heater_resistance(self.gas_settings.heatr_temp)) @@ -254,7 +251,7 @@ def set_gas_heater_duration(self, value, nb_profile=0): """ if nb_profile > constants.NBCONV_MAX or value < constants.NBCONV_MIN: - raise ValueError('Profile "{}" should be between {} and {}'.format(nb_profile, constants.NBCONV_MIN, constants.NBCONV_MAX)) + raise ValueError(f'Profile "{nb_profile}" should be between {constants.NBCONV_MIN} and {constants.NBCONV_MAX}') self.gas_settings.heatr_dur = value temp = self._calc_heater_duration(self.gas_settings.heatr_dur) @@ -296,7 +293,7 @@ def get_sensor_data(self): """ self.set_power_mode(constants.FORCED_MODE) - for attempt in range(10): + for _attempt in range(10): status = self._get_regs(constants.FIELD0_ADDR, 1) if (status & constants.NEW_DATA_MSK) == 0: @@ -394,10 +391,7 @@ def _calc_pressure(self, pressure_adc): calc_pressure = 1048576 - pressure_adc calc_pressure = ((calc_pressure - (var2 >> 12)) * (3125)) - if calc_pressure >= (1 << 31): - calc_pressure = ((calc_pressure // var1) << 1) - else: - calc_pressure = ((calc_pressure << 1) // var1) + calc_pressure = calc_pressure // var1 << 1 if calc_pressure >= 1 << 31 else (calc_pressure << 1) // var1 var1 = (self.calibration_data.par_p9 * (((calc_pressure >> 3) * (calc_pressure >> 3)) >> 13)) >> 12 @@ -415,7 +409,7 @@ def _calc_pressure(self, pressure_adc): def _calc_humidity(self, humidity_adc): """Convert the raw humidity using calibration data.""" temp_scaled = ((self.calibration_data.t_fine * 5) + 128) >> 8 - var1 = (humidity_adc - ((self.calibration_data.par_h1 * 16))) -\ + var1 = (humidity_adc - (self.calibration_data.par_h1 * 16)) -\ (((temp_scaled * self.calibration_data.par_h3) // (100)) >> 1) var2 = (self.calibration_data.par_h2 * (((temp_scaled * self.calibration_data.par_h4) // (100)) + diff --git a/bme680/constants.py b/bme680/constants.py index f43ebf3..c073718 100644 --- a/bme680/constants.py +++ b/bme680/constants.py @@ -257,7 +257,7 @@ def twos_comp(val, bits=16): class FieldData: """Structure for storing BME680 sensor data.""" - def __init__(self): # noqa D107 + def __init__(self): # Contains new_data, gasm_valid & heat_stab self.status = None self.heat_stable = False @@ -279,7 +279,7 @@ def __init__(self): # noqa D107 class CalibrationData: """Structure for storing BME680 calibration data.""" - def __init__(self): # noqa D107 + def __init__(self): self.par_h1 = None self.par_h2 = None self.par_h3 = None @@ -359,7 +359,7 @@ class TPHSettings: """ - def __init__(self): # noqa D107 + def __init__(self): # Humidity oversampling self.os_hum = None # Temperature oversampling @@ -373,7 +373,7 @@ def __init__(self): # noqa D107 class GasSettings: """Structure for storing BME680 gas settings and status.""" - def __init__(self): # noqa D107 + def __init__(self): # Variable to store nb conversion self.nb_conv = None # Variable to store heater control @@ -389,7 +389,7 @@ def __init__(self): # noqa D107 class BME680Data: """Structure to represent BME680 device.""" - def __init__(self): # noqa D107 + def __init__(self): # Chip Id self.chip_id = None # Device Id diff --git a/examples/compensated-temperature.py b/examples/compensated-temperature.py index 275f7ff..91dbc20 100755 --- a/examples/compensated-temperature.py +++ b/examples/compensated-temperature.py @@ -15,7 +15,7 @@ try: sensor = bme680.BME680(bme680.I2C_ADDR_PRIMARY) -except (RuntimeError, IOError): +except (OSError, RuntimeError): sensor = bme680.BME680(bme680.I2C_ADDR_SECONDARY) # These oversampling settings can be tweaked to @@ -52,6 +52,6 @@ def get_cpu_temperature(): raw_temp = sensor.data.temperature comp_temp = raw_temp - ((smoothed_cpu_temp - raw_temp) / factor) - print("Compensated temperature: {:05.2f} *C".format(comp_temp)) + print(f"Compensated temperature: {comp_temp:05.2f} *C") time.sleep(1.0) diff --git a/examples/indoor-air-quality.py b/examples/indoor-air-quality.py index 64e47e3..0157511 100755 --- a/examples/indoor-air-quality.py +++ b/examples/indoor-air-quality.py @@ -16,7 +16,7 @@ try: sensor = bme680.BME680(bme680.I2C_ADDR_PRIMARY) -except (RuntimeError, IOError): +except (OSError, RuntimeError): sensor = bme680.BME680(bme680.I2C_ADDR_SECONDARY) # These oversampling settings can be tweaked to @@ -52,7 +52,7 @@ if sensor.get_sensor_data() and sensor.data.heat_stable: gas = sensor.data.gas_resistance burn_in_data.append(gas) - print('Gas: {0} Ohms'.format(gas)) + print(f'Gas: {gas} Ohms') time.sleep(1) gas_baseline = sum(burn_in_data[-50:]) / 50.0 @@ -64,9 +64,7 @@ # calculation of air_quality_score (25:75, humidity:gas) hum_weighting = 0.25 - print('Gas baseline: {0} Ohms, humidity baseline: {1:.2f} %RH\n'.format( - gas_baseline, - hum_baseline)) + print(f'Gas baseline: {gas_baseline} Ohms, humidity baseline: {hum_baseline:.2f} %RH\n') while True: if sensor.get_sensor_data() and sensor.data.heat_stable: @@ -98,10 +96,7 @@ # Calculate air_quality_score. air_quality_score = hum_score + gas_score - print('Gas: {0:.2f} Ohms,humidity: {1:.2f} %RH,air quality: {2:.2f}'.format( - gas, - hum, - air_quality_score)) + print(f'Gas: {gas:.2f} Ohms,humidity: {hum:.2f} %RH,air quality: {air_quality_score:.2f}') time.sleep(1) diff --git a/examples/read-all.py b/examples/read-all.py index 00c2d40..dc6f261 100755 --- a/examples/read-all.py +++ b/examples/read-all.py @@ -12,7 +12,7 @@ try: sensor = bme680.BME680(bme680.I2C_ADDR_PRIMARY) -except (RuntimeError, IOError): +except (OSError, RuntimeError): sensor = bme680.BME680(bme680.I2C_ADDR_SECONDARY) # These calibration data can safely be commented @@ -25,7 +25,7 @@ value = getattr(sensor.calibration_data, name) if isinstance(value, int): - print('{}: {}'.format(name, value)) + print(f'{name}: {value}') # These oversampling settings can be tweaked to # change the balance between accuracy and noise in @@ -42,7 +42,7 @@ value = getattr(sensor.data, name) if not name.startswith('_'): - print('{}: {}'.format(name, value)) + print(f'{name}: {value}') sensor.set_gas_heater_temperature(320) sensor.set_gas_heater_duration(150) @@ -57,15 +57,10 @@ try: while True: if sensor.get_sensor_data(): - output = '{0:.2f} C,{1:.2f} hPa,{2:.2f} %RH'.format( - sensor.data.temperature, - sensor.data.pressure, - sensor.data.humidity) + output = f'{sensor.data.temperature:.2f} C,{sensor.data.pressure:.2f} hPa,{sensor.data.humidity:.2f} %RH' if sensor.data.heat_stable: - print('{0},{1} Ohms'.format( - output, - sensor.data.gas_resistance)) + print(f'{output},{sensor.data.gas_resistance} Ohms') else: print(output) diff --git a/examples/temperature-offset.py b/examples/temperature-offset.py index 4514a92..e8d400d 100755 --- a/examples/temperature-offset.py +++ b/examples/temperature-offset.py @@ -10,7 +10,7 @@ try: sensor = bme680.BME680(bme680.I2C_ADDR_PRIMARY) -except (RuntimeError, IOError): +except (OSError, RuntimeError): sensor = bme680.BME680(bme680.I2C_ADDR_SECONDARY) # These oversampling settings can be tweaked to @@ -26,12 +26,9 @@ def display_data(offset=0): sensor.set_temp_offset(offset) sensor.get_sensor_data() - output = '{0:.2f} C, {1:.2f} hPa, {2:.3f} %RH'.format( - sensor.data.temperature, - sensor.data.pressure, - sensor.data.humidity) + output = f'{sensor.data.temperature:.2f} C, {sensor.data.pressure:.2f} hPa, {sensor.data.humidity:.3f} %RH' print(output) - print('') + print() print('Initial readings') diff --git a/examples/temperature-pressure-humidity.py b/examples/temperature-pressure-humidity.py index 4a81d2a..5da6976 100755 --- a/examples/temperature-pressure-humidity.py +++ b/examples/temperature-pressure-humidity.py @@ -13,7 +13,7 @@ try: sensor = bme680.BME680(bme680.I2C_ADDR_PRIMARY) -except (RuntimeError, IOError): +except (OSError, RuntimeError): sensor = bme680.BME680(bme680.I2C_ADDR_SECONDARY) # These oversampling settings can be tweaked to @@ -29,10 +29,7 @@ try: while True: if sensor.get_sensor_data(): - output = '{0:.2f} C,{1:.2f} hPa,{2:.3f} %RH'.format( - sensor.data.temperature, - sensor.data.pressure, - sensor.data.humidity) + output = f'{sensor.data.temperature:.2f} C,{sensor.data.pressure:.2f} hPa,{sensor.data.humidity:.3f} %RH' print(output) except KeyboardInterrupt: diff --git a/pyproject.toml b/pyproject.toml index 910bb70..477ce68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ Homepage = "https://www.pimoroni.com" [dependency-groups] dev = [ "check-manifest", - "ruff", + "ruff==0.15.20", "codespell", "pre-commit", "twine", @@ -96,7 +96,7 @@ exclude = [ line-length = 200 [tool.ruff.lint] -extend-select = ["I"] +extend-select = ["I", "UP", "SIM", "PIE", "C4", "B", "RUF", "PERF", "FURB", "PLE"] [tool.codespell] skip = """ diff --git a/tests/conftest.py b/tests/conftest.py index 57c485f..d6cf183 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,6 @@ import sys +from unittest import mock -import mock import pytest import bme680 @@ -14,7 +14,7 @@ class MockSMBus: """ - def __init__(self, bus): # noqa D107 + def __init__(self, bus): pass def read_byte_data(self, addr, register): diff --git a/tox.ini b/tox.ini index 6ad3c71..c287b7b 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,6 @@ commands = coverage run -m pytest -v -r wsx coverage report deps = - mock pytest>=3.1 pytest-cov build From 8b2d93e91239ba86b7537a225bcd47c00fe3b3cc Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 26 Aug 2026 21:41:26 +0100 Subject: [PATCH 8/8] Move new entries to 2.0.1 2.0.0 is published; the fixes landed under it by mistake. --- CHANGELOG.md | 6 +++++- bme680/__init__.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8623323..1242b82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,13 @@ -2.0.0 +2.0.1 ----- * New: data.gas_valid, exposing the sensor's gasm_valid status bit * Fix: get_power_mode now masks out the oversampling bits * Fix: set_power_mode's blocking wait had no effect + +2.0.0 +----- + * Repackage to hatch/pyproject.toml * Drop Python 2.7 support * Switch from smbu2 to smbus2 diff --git a/bme680/__init__.py b/bme680/__init__.py index 6d79184..c4d6f6b 100644 --- a/bme680/__init__.py +++ b/bme680/__init__.py @@ -5,7 +5,7 @@ from . import constants from .constants import BME680Data, lookupTable1, lookupTable2 -__version__ = '2.0.0' +__version__ = '2.0.1' # Export constants to global namespace