├── .github └── workflows │ ├── build.yml │ ├── qa.yml │ └── test.yml ├── .gitignore ├── .stickler.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── bme280 └── __init__.py ├── check.sh ├── examples ├── all-values.py ├── compensated-temperature.py ├── dump-calibration.py ├── local_altitude.py ├── relative-altitude.py ├── temperature-compare.py └── temperature-forced-mode.py ├── install.sh ├── pyproject.toml ├── requirements-dev.txt ├── tests ├── calibration.py ├── conftest.py ├── test_compensation.py └── test_setup.py ├── tox.ini └── uninstall.sh /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | name: Python ${{ matrix.python }} 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python: ['3.9', '3.10', '3.11'] 16 | 17 | env: 18 | RELEASE_FILE: ${{ github.event.repository.name }}-${{ github.event.release.tag_name || github.sha }}-py${{ matrix.python }} 19 | 20 | steps: 21 | - name: Checkout Code 22 | uses: actions/checkout@v4 23 | 24 | - name: Set up Python ${{ matrix.python }} 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: ${{ matrix.python }} 28 | 29 | - name: Install Dependencies 30 | run: | 31 | make dev-deps 32 | 33 | - name: Build Packages 34 | run: | 35 | make build 36 | 37 | - name: Upload Packages 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: ${{ env.RELEASE_FILE }} 41 | path: dist/ 42 | -------------------------------------------------------------------------------- /.github/workflows/qa.yml: -------------------------------------------------------------------------------- 1 | name: QA 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | name: linting & spelling 12 | runs-on: ubuntu-latest 13 | env: 14 | TERM: xterm-256color 15 | 16 | steps: 17 | - name: Checkout Code 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Python '3,11' 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: '3.11' 24 | 25 | - name: Install Dependencies 26 | run: | 27 | make dev-deps 28 | 29 | - name: Run Quality Assurance 30 | run: | 31 | make qa 32 | 33 | - name: Run Code Checks 34 | run: | 35 | make check 36 | 37 | - name: Run Bash Code Checks 38 | run: | 39 | make shellcheck 40 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | name: Python ${{ matrix.python }} 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python: ['3.9', '3.10', '3.11'] 16 | 17 | steps: 18 | - name: Checkout Code 19 | uses: actions/checkout@v3 20 | 21 | - name: Set up Python ${{ matrix.python }} 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: ${{ matrix.python }} 25 | 26 | - name: Install Dependencies 27 | run: | 28 | make dev-deps 29 | 30 | - name: Run Tests 31 | run: | 32 | make pytest 33 | 34 | - name: Coverage 35 | if: ${{ matrix.python == '3.9' }} 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | run: | 39 | python -m pip install coveralls 40 | coveralls --service=github 41 | 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | _build/ 3 | *.o 4 | *.so 5 | *.a 6 | *.py[cod] 7 | *.egg-info 8 | dist/ 9 | __pycache__ 10 | .DS_Store 11 | *.deb 12 | *.dsc 13 | *.build 14 | *.changes 15 | *.orig.* 16 | packaging/*tar.xz 17 | library/debian/ 18 | .coverage 19 | .pytest_cache 20 | .tox 21 | -------------------------------------------------------------------------------- /.stickler.yml: -------------------------------------------------------------------------------- 1 | --- 2 | linters: 3 | flake8: 4 | python: 3 5 | max-line-length: 160 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | ----- 3 | 4 | * Repackage to hatch/pyproject.toml 5 | * Require i2cdevice>=1.0.0 (smbus2) 6 | 7 | 0.1.1 8 | ----- 9 | 10 | * Fix so package is included in .whl releases 11 | 12 | 0.1.0 13 | ----- 14 | 15 | * Switch to setup.cfg 16 | * Match humidity compensation to BOSCH formula 17 | 18 | 0.0.2 19 | ----- 20 | 21 | * Update to i2cdevice>=0.0.6 set/get API 22 | 23 | 0.0.1 24 | ----- 25 | 26 | * Initial Release 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Pimoroni Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | LIBRARY_NAME := $(shell hatch project metadata name 2> /dev/null) 2 | LIBRARY_VERSION := $(shell hatch version 2> /dev/null) 3 | 4 | .PHONY: usage install uninstall check pytest qa build-deps check tag wheel sdist clean dist testdeploy deploy 5 | usage: 6 | ifdef LIBRARY_NAME 7 | @echo "Library: ${LIBRARY_NAME}" 8 | @echo "Version: ${LIBRARY_VERSION}\n" 9 | else 10 | @echo "WARNING: You should 'make dev-deps'\n" 11 | endif 12 | @echo "Usage: make , where target is one of:\n" 13 | @echo "install: install the library locally from source" 14 | @echo "uninstall: uninstall the local library" 15 | @echo "dev-deps: install Python dev dependencies" 16 | @echo "check: perform basic integrity checks on the codebase" 17 | @echo "qa: run linting and package QA" 18 | @echo "pytest: run Python test fixtures" 19 | @echo "clean: clean Python build and dist directories" 20 | @echo "build: build Python distribution files" 21 | @echo "testdeploy: build and upload to test PyPi" 22 | @echo "deploy: build and upload to PyPi" 23 | @echo "tag: tag the repository with the current version\n" 24 | 25 | install: 26 | ./install.sh --unstable 27 | 28 | uninstall: 29 | ./uninstall.sh 30 | 31 | dev-deps: 32 | python3 -m pip install -r requirements-dev.txt 33 | sudo apt install dos2unix shellcheck 34 | 35 | check: 36 | @bash check.sh 37 | 38 | shellcheck: 39 | shellcheck *.sh 40 | 41 | qa: 42 | tox -e qa 43 | 44 | pytest: 45 | tox -e py 46 | 47 | nopost: 48 | @bash check.sh --nopost 49 | 50 | tag: 51 | git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" 52 | 53 | build: check 54 | @hatch build 55 | 56 | clean: 57 | -rm -r dist 58 | 59 | testdeploy: build 60 | twine upload --repository testpypi dist/* 61 | 62 | deploy: nopost build 63 | twine upload dist/* 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BME280 Temperature, Pressure, & Humidity Sensor 2 | 3 | [![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/bme280-python/test.yml?branch=main)](https://github.com/pimoroni/bme280-python/actions/workflows/test.yml) 4 | [![Coverage Status](https://coveralls.io/repos/github/pimoroni/bme280-python/badge.svg?branch=main)](https://coveralls.io/github/pimoroni/bme280-python?branch=main) 5 | [![PyPi Package](https://img.shields.io/pypi/v/pimoroni-bme280.svg)](https://pypi.python.org/pypi/pimoroni-bme280) 6 | [![Python Versions](https://img.shields.io/pypi/pyversions/pimoroni-bme280.svg)](https://pypi.python.org/pypi/pimoroni-bme280) 7 | 8 | Suitable for measuring ambient temperature, barometric pressure, and humidity, the BME280 is a great indoor environmental sensor. 9 | 10 | # Pre-requisites 11 | 12 | You must enable: 13 | 14 | * i2c: `sudo raspi-config nonint do_i2c 0` 15 | 16 | You can optionally run `sudo raspi-config` or the graphical Raspberry Pi Configuration UI to enable interfaces. 17 | 18 | # Installing 19 | 20 | Stable library from PyPi, the smbus library is also needed: 21 | 22 | * Just run `python3 -m pip install pimoroni-bme280` 23 | 24 | Latest/development library from GitHub: 25 | 26 | * `git clone https://github.com/pimoroni/bme280-python` 27 | * `cd bme280-python` 28 | * `sudo ./install.sh` 29 | 30 | -------------------------------------------------------------------------------- /bme280/__init__.py: -------------------------------------------------------------------------------- 1 | """BME280 Driver.""" 2 | import struct 3 | import time 4 | 5 | from i2cdevice import BitField, Device, Register, _int_to_bytes 6 | from i2cdevice.adapter import Adapter, LookupAdapter 7 | 8 | __version__ = "1.0.0" 9 | 10 | CHIP_ID = 0x60 11 | I2C_ADDRESS_GND = 0x76 12 | I2C_ADDRESS_VCC = 0x77 13 | 14 | 15 | class S8Adapter(Adapter): 16 | """Convert unsigned 8bit integer to signed.""" 17 | 18 | def _decode(self, value): 19 | if value & (1 << 7): 20 | value -= 1 << 8 21 | return value 22 | 23 | 24 | class S16Adapter(Adapter): 25 | """Convert unsigned 16bit integer to signed.""" 26 | 27 | def _decode(self, value): 28 | return struct.unpack("> 4) & 0x0F) | (b[1] << 4) 42 | if r & (1 << 11): 43 | r = r - 1 << 12 44 | return r 45 | 46 | 47 | class H4Adapter(S16Adapter): 48 | def _decode(self, value): 49 | b = _int_to_bytes(value, 2) 50 | r = (b[0] << 4) | (b[1] & 0x0F) 51 | if r & (1 << 11): 52 | r = r - 1 << 12 53 | return r 54 | 55 | 56 | class BME280Calibration: 57 | def __init__(self): 58 | self.dig_t1 = 0 59 | self.dig_t2 = 0 60 | self.dig_t3 = 0 61 | 62 | self.dig_p1 = 0 63 | self.dig_p2 = 0 64 | self.dig_p3 = 0 65 | self.dig_p4 = 0 66 | self.dig_p5 = 0 67 | self.dig_p6 = 0 68 | self.dig_p7 = 0 69 | self.dig_p8 = 0 70 | self.dig_p9 = 0 71 | 72 | self.dig_h1 = 0.0 73 | self.dig_h2 = 0.0 74 | self.dig_h3 = 0.0 75 | self.dig_h4 = 0.0 76 | self.dig_h5 = 0.0 77 | self.dig_h6 = 0.0 78 | 79 | self.temperature_fine = 0 80 | 81 | def set_from_namedtuple(self, value): 82 | # Iterate through a tuple supplied by i2cdevice 83 | # and copy its values into the class attributes 84 | for key in self.__dict__.keys(): 85 | try: 86 | setattr(self, key, getattr(value, key)) 87 | except AttributeError: 88 | pass 89 | 90 | def compensate_temperature(self, raw_temperature): 91 | var1 = (raw_temperature / 16384.0 - self.dig_t1 / 1024.0) * self.dig_t2 92 | var2 = raw_temperature / 131072.0 - self.dig_t1 / 8192.0 93 | var2 = var2 * var2 * self.dig_t3 94 | self.temperature_fine = var1 + var2 95 | return self.temperature_fine / 5120.0 96 | 97 | def compensate_pressure(self, raw_pressure): 98 | var1 = self.temperature_fine / 2.0 - 64000.0 99 | var2 = var1 * var1 * self.dig_p6 / 32768.0 100 | var2 = var2 + var1 * self.dig_p5 * 2 101 | var2 = var2 / 4.0 + self.dig_p4 * 65536.0 102 | var1 = (self.dig_p3 * var1 * var1 / 524288.0 + self.dig_p2 * var1) / 524288.0 103 | var1 = (1.0 + var1 / 32768.0) * self.dig_p1 104 | pressure = 1048576.0 - raw_pressure 105 | pressure = (pressure - var2 / 4096.0) * 6250.0 / var1 106 | var1 = self.dig_p9 * pressure * pressure / 2147483648.0 107 | var2 = pressure * self.dig_p8 / 32768.0 108 | return pressure + (var1 + var2 + self.dig_p7) / 16.0 109 | 110 | def compensate_humidity(self, raw_humidity): 111 | var1 = self.temperature_fine - 76800.0 112 | var2 = self.dig_h4 * 64.0 + (self.dig_h5 / 16384.0) * var1 113 | var3 = raw_humidity - var2 114 | var4 = self.dig_h2 / 65536.0 115 | var5 = 1.0 + (self.dig_h3 / 67108864.0) * var1 116 | var6 = 1.0 + (self.dig_h6 / 67108864.0) * var1 * var5 117 | var6 = var3 * var4 * (var5 * var6) 118 | 119 | humidity = var6 * (1.0 - self.dig_h1 * var6 / 524288.0) 120 | return max(0.0, min(100.0, humidity)) 121 | 122 | 123 | class BME280: 124 | def __init__(self, i2c_addr=I2C_ADDRESS_GND, i2c_dev=None): 125 | self.calibration = BME280Calibration() 126 | self._is_setup = False 127 | self._i2c_addr = i2c_addr 128 | self._i2c_dev = i2c_dev 129 | self._bme280 = Device( 130 | [I2C_ADDRESS_GND, I2C_ADDRESS_VCC], 131 | i2c_dev=self._i2c_dev, 132 | bit_width=8, 133 | registers=( 134 | Register("CHIP_ID", 0xD0, fields=(BitField("id", 0xFF),)), 135 | Register("RESET", 0xE0, fields=(BitField("reset", 0xFF),)), 136 | Register( 137 | "STATUS", 138 | 0xF3, 139 | fields=( 140 | BitField("measuring", 0b00001000), # 1 when conversion is running 141 | BitField("im_update", 0b00000001), # 1 when NVM data is being copied 142 | ), 143 | ), 144 | Register( 145 | "CTRL_MEAS", 146 | 0xF4, 147 | fields=( 148 | BitField("osrs_t", 0b11100000, adapter=LookupAdapter({1: 0b001, 2: 0b010, 4: 0b011, 8: 0b100, 16: 0b101})), # Temperature oversampling 149 | BitField("osrs_p", 0b00011100, adapter=LookupAdapter({1: 0b001, 2: 0b010, 4: 0b011, 8: 0b100, 16: 0b101})), # Pressure oversampling 150 | BitField("mode", 0b00000011, adapter=LookupAdapter({"sleep": 0b00, "forced": 0b10, "normal": 0b11})), # Power mode 151 | ), 152 | ), 153 | Register("CTRL_HUM", 0xF2, fields=(BitField("osrs_h", 0b00000111, adapter=LookupAdapter({1: 0b001, 2: 0b010, 4: 0b011, 8: 0b100, 16: 0b101})),)), # Humidity oversampling 154 | Register( 155 | "CONFIG", 156 | 0xF5, 157 | fields=( 158 | BitField( 159 | "t_sb", 160 | 0b11100000, # Temp standby duration in normal mode 161 | adapter=LookupAdapter({0.5: 0b000, 62.5: 0b001, 125: 0b010, 250: 0b011, 500: 0b100, 1000: 0b101, 10: 0b110, 20: 0b111}), 162 | ), 163 | BitField("filter", 0b00011100), # Controls the time constant of the IIR filter 164 | BitField("spi3w_en", 0b0000001, read_only=True), # Enable 3-wire SPI interface when set to 1. IE: Don't set this bit! 165 | ), 166 | ), 167 | Register("DATA", 0xF7, fields=(BitField("humidity", 0x000000000000FFFF), BitField("temperature", 0x000000FFFFF00000), BitField("pressure", 0xFFFFF00000000000)), bit_width=8 * 8), 168 | Register( 169 | "CALIBRATION", 170 | 0x88, 171 | fields=( 172 | BitField("dig_t1", 0xFFFF << 16 * 12, adapter=U16Adapter()), # 0x88 0x89 173 | BitField("dig_t2", 0xFFFF << 16 * 11, adapter=S16Adapter()), # 0x8A 0x8B 174 | BitField("dig_t3", 0xFFFF << 16 * 10, adapter=S16Adapter()), # 0x8C 0x8D 175 | BitField("dig_p1", 0xFFFF << 16 * 9, adapter=U16Adapter()), # 0x8E 0x8F 176 | BitField("dig_p2", 0xFFFF << 16 * 8, adapter=S16Adapter()), # 0x90 0x91 177 | BitField("dig_p3", 0xFFFF << 16 * 7, adapter=S16Adapter()), # 0x92 0x93 178 | BitField("dig_p4", 0xFFFF << 16 * 6, adapter=S16Adapter()), # 0x94 0x95 179 | BitField("dig_p5", 0xFFFF << 16 * 5, adapter=S16Adapter()), # 0x96 0x97 180 | BitField("dig_p6", 0xFFFF << 16 * 4, adapter=S16Adapter()), # 0x98 0x99 181 | BitField("dig_p7", 0xFFFF << 16 * 3, adapter=S16Adapter()), # 0x9A 0x9B 182 | BitField("dig_p8", 0xFFFF << 16 * 2, adapter=S16Adapter()), # 0x9C 0x9D 183 | BitField("dig_p9", 0xFFFF << 16 * 1, adapter=S16Adapter()), # 0x9E 0x9F 184 | BitField("dig_h1", 0x00FF), # 0xA1 uint8 185 | ), 186 | bit_width=26 * 8, 187 | ), 188 | Register( 189 | "CALIBRATION2", 190 | 0xE1, 191 | fields=( 192 | BitField("dig_h2", 0xFFFF0000000000, adapter=S16Adapter()), # 0xE1 0xE2 193 | BitField("dig_h3", 0x0000FF00000000), # 0xE3 uint8 194 | BitField("dig_h4", 0x000000FFFF0000, adapter=H4Adapter()), # 0xE4 0xE5[3:0] 195 | BitField("dig_h5", 0x00000000FFFF00, adapter=H5Adapter()), # 0xE5[7:4] 0xE6 196 | BitField("dig_h6", 0x000000000000FF, adapter=S8Adapter()), # 0xE7 int8 197 | ), 198 | bit_width=7 * 8, 199 | ), 200 | ), 201 | ) 202 | 203 | def setup(self, mode="normal", temperature_oversampling=16, pressure_oversampling=16, humidity_oversampling=16, temperature_standby=500): 204 | if self._is_setup: 205 | return 206 | self._is_setup = True 207 | 208 | self._bme280.select_address(self._i2c_addr) 209 | self._mode = mode 210 | 211 | if mode == "forced": 212 | mode = "sleep" 213 | 214 | try: 215 | chip = self._bme280.get("CHIP_ID") 216 | if chip.id != CHIP_ID: 217 | raise RuntimeError("Unable to find bme280 on 0x{:02x}, CHIP_ID returned {:02x}".format(self._i2c_addr, chip.id)) 218 | except IOError: 219 | raise RuntimeError("Unable to find bme280 on 0x{:02x}, IOError".format(self._i2c_addr)) 220 | 221 | self._bme280.set("RESET", reset=0xB6) 222 | time.sleep(0.1) 223 | 224 | self._bme280.set("CTRL_HUM", osrs_h=humidity_oversampling) 225 | 226 | self._bme280.set("CTRL_MEAS", mode=mode, osrs_t=temperature_oversampling, osrs_p=pressure_oversampling) 227 | 228 | self._bme280.set("CONFIG", t_sb=temperature_standby, filter=2) 229 | 230 | self.calibration.set_from_namedtuple(self._bme280.get("CALIBRATION")) 231 | self.calibration.set_from_namedtuple(self._bme280.get("CALIBRATION2")) 232 | 233 | def update_sensor(self): 234 | self.setup() 235 | 236 | if self._mode == "forced": 237 | self._bme280.set("CTRL_MEAS", mode="forced") 238 | while self._bme280.get("STATUS").measuring: 239 | time.sleep(0.001) 240 | 241 | raw = self._bme280.get("DATA") 242 | 243 | self.temperature = self.calibration.compensate_temperature(raw.temperature) 244 | self.pressure = self.calibration.compensate_pressure(raw.pressure) / 100.0 245 | self.humidity = self.calibration.compensate_humidity(raw.humidity) 246 | 247 | def get_temperature(self): 248 | self.update_sensor() 249 | return self.temperature 250 | 251 | def get_pressure(self): 252 | self.update_sensor() 253 | return self.pressure 254 | 255 | def get_humidity(self): 256 | self.update_sensor() 257 | return self.humidity 258 | 259 | def get_altitude(self, qnh=1013.25): 260 | self.update_sensor() 261 | pressure = self.get_pressure() 262 | altitude = 44330.0 * (1.0 - pow(pressure / qnh, (1.0 / 5.255))) 263 | return altitude 264 | -------------------------------------------------------------------------------- /check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script handles some basic QA checks on the source 4 | 5 | NOPOST=$1 6 | LIBRARY_NAME=$(hatch project metadata name) 7 | LIBRARY_VERSION=$(hatch version | awk -F "." '{print $1"."$2"."$3}') 8 | POST_VERSION=$(hatch version | awk -F "." '{print substr($4,0,length($4))}') 9 | TERM=${TERM:="xterm-256color"} 10 | 11 | success() { 12 | echo -e "$(tput setaf 2)$1$(tput sgr0)" 13 | } 14 | 15 | inform() { 16 | echo -e "$(tput setaf 6)$1$(tput sgr0)" 17 | } 18 | 19 | warning() { 20 | echo -e "$(tput setaf 1)$1$(tput sgr0)" 21 | } 22 | 23 | while [[ $# -gt 0 ]]; do 24 | K="$1" 25 | case $K in 26 | -p|--nopost) 27 | NOPOST=true 28 | shift 29 | ;; 30 | *) 31 | if [[ $1 == -* ]]; then 32 | printf "Unrecognised option: %s\n" "$1"; 33 | exit 1 34 | fi 35 | POSITIONAL_ARGS+=("$1") 36 | shift 37 | esac 38 | done 39 | 40 | inform "Checking $LIBRARY_NAME $LIBRARY_VERSION\n" 41 | 42 | inform "Checking for trailing whitespace..." 43 | if grep -IUrn --color "[[:blank:]]$" --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO; then 44 | warning "Trailing whitespace found!" 45 | exit 1 46 | else 47 | success "No trailing whitespace found." 48 | fi 49 | printf "\n" 50 | 51 | inform "Checking for DOS line-endings..." 52 | if grep -lIUrn --color $'\r' --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile; then 53 | warning "DOS line-endings found!" 54 | exit 1 55 | else 56 | success "No DOS line-endings found." 57 | fi 58 | printf "\n" 59 | 60 | inform "Checking CHANGELOG.md..." 61 | if ! grep "^${LIBRARY_VERSION}" CHANGELOG.md > /dev/null 2>&1; then 62 | warning "Changes missing for version ${LIBRARY_VERSION}! Please update CHANGELOG.md." 63 | exit 1 64 | else 65 | success "Changes found for version ${LIBRARY_VERSION}." 66 | fi 67 | printf "\n" 68 | 69 | inform "Checking for git tag ${LIBRARY_VERSION}..." 70 | if ! git tag -l | grep -E "${LIBRARY_VERSION}$"; then 71 | warning "Missing git tag for version ${LIBRARY_VERSION}" 72 | fi 73 | printf "\n" 74 | 75 | if [[ $NOPOST ]]; then 76 | inform "Checking for .postN on library version..." 77 | if [[ "$POST_VERSION" != "" ]]; then 78 | warning "Found .$POST_VERSION on library version." 79 | inform "Please only use these for testpypi releases." 80 | exit 1 81 | else 82 | success "OK" 83 | fi 84 | fi 85 | -------------------------------------------------------------------------------- /examples/all-values.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | 5 | from smbus2 import SMBus 6 | 7 | from bme280 import BME280 8 | 9 | print( 10 | """all-values.py - Read temperature, pressure, and humidity 11 | 12 | Press Ctrl+C to exit! 13 | 14 | """ 15 | ) 16 | 17 | # Initialise the BME280 18 | bus = SMBus(1) 19 | bme280 = BME280(i2c_dev=bus) 20 | 21 | while True: 22 | temperature = bme280.get_temperature() 23 | pressure = bme280.get_pressure() 24 | humidity = bme280.get_humidity() 25 | print(f"{temperature:05.2f}°C {pressure:05.2f}hPa {humidity:05.2f}%") 26 | time.sleep(1) 27 | -------------------------------------------------------------------------------- /examples/compensated-temperature.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | from subprocess import PIPE, Popen 5 | 6 | from smbus2 import SMBus 7 | 8 | from bme280 import BME280 9 | 10 | print( 11 | """compensated-temperature.py - Use the CPU temperature to compensate temperature 12 | readings from the BME280 sensor. Method adapted from Initial State's Enviro pHAT 13 | review: https://medium.com/@InitialState/tutorial-review-enviro-phat-for-raspberry-pi-4cd6d8c63441 14 | 15 | Press Ctrl+C to exit! 16 | 17 | """ 18 | ) 19 | 20 | # Initialise the BME280 21 | bus = SMBus(1) 22 | bme280 = BME280(i2c_dev=bus) 23 | 24 | 25 | # Gets the CPU temperature in degrees C 26 | def get_cpu_temperature(): 27 | process = Popen(["vcgencmd", "measure_temp"], stdout=PIPE) 28 | output, _error = process.communicate() 29 | output = output.decode() 30 | return float(output[output.index("=") + 1 : output.rindex("'")]) 31 | 32 | 33 | factor = 0.6 # Smaller numbers adjust temp down, vice versa 34 | smooth_size = 10 # Dampens jitter due to rapid CPU temp changes 35 | 36 | cpu_temps = [] 37 | 38 | while True: 39 | cpu_temp = get_cpu_temperature() 40 | cpu_temps.append(cpu_temp) 41 | 42 | if len(cpu_temps) > smooth_size: 43 | cpu_temps = cpu_temps[1:] 44 | 45 | smoothed_cpu_temp = sum(cpu_temps) / float(len(cpu_temps)) 46 | raw_temp = bme280.get_temperature() 47 | comp_temp = raw_temp - ((smoothed_cpu_temp - raw_temp) / factor) 48 | 49 | print(f"Raw: {raw_temp:05.2f}°C, Compensated: {comp_temp:05.2f}°C") 50 | 51 | time.sleep(1.0) 52 | -------------------------------------------------------------------------------- /examples/dump-calibration.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | from smbus2 import SMBus 5 | 6 | from bme280 import BME280 7 | 8 | print( 9 | """dump-calibration.py - Dumps calibration data. 10 | 11 | Press Ctrl+C to exit! 12 | 13 | """ 14 | ) 15 | 16 | # Initialise the BME280 17 | bme280 = BME280(i2c_dev=SMBus(1)) 18 | bme280.setup() 19 | 20 | for key in dir(bme280.calibration): 21 | if key.startswith("dig_"): 22 | value = getattr(bme280.calibration, key) 23 | print(f"{key} = {value}") 24 | -------------------------------------------------------------------------------- /examples/local_altitude.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | 5 | from smbus2 import SMBus 6 | 7 | from bme280 import BME280 8 | 9 | print( 10 | """local_altitude.py - 11 | Allows you to correct the QNH for your local area. 12 | Do not rely on this approximation for landing planes. 13 | Press Ctrl+C to exit! 14 | """ 15 | ) 16 | 17 | # Initialise the BME280 18 | bus = SMBus(1) 19 | bme280 = BME280(i2c_dev=bus) 20 | 21 | # asks the user for their local QNH value and confirms it 22 | local_qnh = input( 23 | """Please enter your local QNH value (air pressure at the mean sea level). 24 | 25 | You can find this by searching for a local METAR report, 26 | eg: "Cambridge METAR" 27 | 28 | And looking for the number prefixed with a "Q", 29 | eg: Q1015 30 | >""" 31 | ) 32 | 33 | # remove a Q prefix if there is one 34 | if local_qnh.startswith("Q") or local_qnh.startswith("q"): 35 | local_qnh = local_qnh[1:] 36 | 37 | print("You have told us the QNH is", local_qnh) 38 | 39 | # converts the input into a floating point number 40 | local_qnh = float(local_qnh) 41 | time.sleep(1) 42 | 43 | # workaround to get rid of the first reading 44 | altitude = bme280.get_altitude() 45 | print("Waiting a couple of seconds for the sensor to initialise...") 46 | time.sleep(2) 47 | 48 | while True: 49 | altitude = bme280.get_altitude(qnh=local_qnh) 50 | print(f"{altitude:0.0f} metres above sea level") 51 | time.sleep(2) 52 | -------------------------------------------------------------------------------- /examples/relative-altitude.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | 5 | from smbus2 import SMBus 6 | 7 | from bme280 import BME280 8 | 9 | print( 10 | """relative-altitude.py - Calculates relative altitude from pressure. 11 | 12 | Press Ctrl+C to exit! 13 | 14 | """ 15 | ) 16 | 17 | # Initialise the BME280 18 | bus = SMBus(1) 19 | bme280 = BME280(i2c_dev=bus) 20 | 21 | baseline_values = [] 22 | baseline_size = 100 23 | 24 | print(f"Collecting baseline values for {baseline_size:d} seconds. Do not move the sensor!\n") 25 | 26 | # Collect some values to calculate a baseline pressure 27 | for i in range(baseline_size): 28 | pressure = bme280.get_pressure() 29 | baseline_values.append(pressure) 30 | time.sleep(1) 31 | 32 | # Calculate average baseline 33 | baseline = sum(baseline_values[:-25]) / len(baseline_values[:-25]) 34 | 35 | while True: 36 | altitude = bme280.get_altitude(qnh=baseline) 37 | print(f"Relative altitude: {altitude:05.2f} metres") 38 | time.sleep(1) 39 | -------------------------------------------------------------------------------- /examples/temperature-compare.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | 5 | from smbus2 import SMBus 6 | 7 | from bme280 import BME280 8 | 9 | print( 10 | """temperature-compare.py - Compares oversampling levels 11 | (requires two BME280s with different addresses). 12 | 13 | Press Ctrl+C to exit! 14 | 15 | """ 16 | ) 17 | 18 | # Initialise the BME280 19 | bus = SMBus(1) 20 | bme280A = BME280(i2c_dev=bus) 21 | bme280B = BME280(i2c_dev=bus, i2c_addr=0x77) 22 | 23 | # Set up in "forced" mode 24 | # In this mode `get_temperature` and `get_pressure` will trigger 25 | # a new reading and wait for the result. 26 | # The chip will return to sleep mode when finished. 27 | bme280A.setup(mode="normal", temperature_oversampling=1, pressure_oversampling=1) 28 | bme280B.setup(mode="normal", temperature_oversampling=16, pressure_oversampling=16) 29 | 30 | while True: 31 | temperatureA = bme280A.get_temperature() 32 | temperatureB = bme280B.get_temperature() 33 | print(f"Forced: {temperatureA:05.2f}°C Normal: {temperatureB:05.2f}°C D: {abs(temperatureA - temperatureB):05.2f}°C") 34 | time.sleep(1) 35 | -------------------------------------------------------------------------------- /examples/temperature-forced-mode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | 5 | from smbus2 import SMBus 6 | 7 | from bme280 import BME280 8 | 9 | # Initialise the BME280 10 | bus = SMBus(1) 11 | bme280 = BME280(i2c_dev=bus) 12 | 13 | # Set up in "forced" mode 14 | # In this mode `get_temperature` and `get_pressure` will trigger 15 | # a new reading and wait for the result. 16 | # The chip will return to sleep mode when finished. 17 | bme280.setup(mode="forced") 18 | 19 | while True: 20 | temperature = bme280.get_temperature() 21 | print(f"{temperature:05.2f}°C") 22 | time.sleep(1) 23 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | LIBRARY_NAME=$(grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}') 3 | MODULE_NAME="bme280" 4 | CONFIG_FILE=config.txt 5 | CONFIG_DIR="/boot/firmware" 6 | DATESTAMP=$(date "+%Y-%m-%d-%H-%M-%S") 7 | CONFIG_BACKUP=false 8 | APT_HAS_UPDATED=false 9 | RESOURCES_TOP_DIR="$HOME/Pimoroni" 10 | VENV_BASH_SNIPPET="$RESOURCES_TOP_DIR/auto_venv.sh" 11 | VENV_DIR="$HOME/.virtualenvs/pimoroni" 12 | USAGE="./install.sh (--unstable)" 13 | POSITIONAL_ARGS=() 14 | FORCE=false 15 | UNSTABLE=false 16 | PYTHON="python" 17 | CMD_ERRORS=false 18 | 19 | 20 | user_check() { 21 | if [ "$(id -u)" -eq 0 ]; then 22 | fatal "Script should not be run as root. Try './install.sh'\n" 23 | fi 24 | } 25 | 26 | confirm() { 27 | if $FORCE; then 28 | true 29 | else 30 | read -r -p "$1 [y/N] " response < /dev/tty 31 | if [[ $response =~ ^(yes|y|Y)$ ]]; then 32 | true 33 | else 34 | false 35 | fi 36 | fi 37 | } 38 | 39 | success() { 40 | echo -e "$(tput setaf 2)$1$(tput sgr0)" 41 | } 42 | 43 | inform() { 44 | echo -e "$(tput setaf 6)$1$(tput sgr0)" 45 | } 46 | 47 | warning() { 48 | echo -e "$(tput setaf 1)⚠ WARNING:$(tput sgr0) $1" 49 | } 50 | 51 | fatal() { 52 | echo -e "$(tput setaf 1)⚠ FATAL:$(tput sgr0) $1" 53 | exit 1 54 | } 55 | 56 | find_config() { 57 | if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then 58 | CONFIG_DIR="/boot" 59 | if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then 60 | fatal "Could not find $CONFIG_FILE!" 61 | fi 62 | fi 63 | inform "Using $CONFIG_FILE in $CONFIG_DIR" 64 | } 65 | 66 | venv_bash_snippet() { 67 | inform "Checking for $VENV_BASH_SNIPPET\n" 68 | if [ ! -f "$VENV_BASH_SNIPPET" ]; then 69 | inform "Creating $VENV_BASH_SNIPPET\n" 70 | mkdir -p "$RESOURCES_TOP_DIR" 71 | cat << EOF > "$VENV_BASH_SNIPPET" 72 | # Add "source $VENV_BASH_SNIPPET" to your ~/.bashrc to activate 73 | # the Pimoroni virtual environment automagically! 74 | VENV_DIR="$VENV_DIR" 75 | if [ ! -f \$VENV_DIR/bin/activate ]; then 76 | printf "Creating user Python environment in \$VENV_DIR, please wait...\n" 77 | mkdir -p \$VENV_DIR 78 | python3 -m venv --system-site-packages \$VENV_DIR 79 | fi 80 | printf " ↓ ↓ ↓ ↓ Hello, we've activated a Python venv for you. To exit, type \"deactivate\".\n" 81 | source \$VENV_DIR/bin/activate 82 | EOF 83 | fi 84 | } 85 | 86 | venv_check() { 87 | PYTHON_BIN=$(which "$PYTHON") 88 | if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then 89 | printf "This script should be run in a virtual Python environment.\n" 90 | if confirm "Would you like us to create and/or use a default one?"; then 91 | printf "\n" 92 | if [ ! -f "$VENV_DIR/bin/activate" ]; then 93 | inform "Creating a new virtual Python environment in $VENV_DIR, please wait...\n" 94 | mkdir -p "$VENV_DIR" 95 | /usr/bin/python3 -m venv "$VENV_DIR" --system-site-packages 96 | venv_bash_snippet 97 | # shellcheck disable=SC1091 98 | source "$VENV_DIR/bin/activate" 99 | else 100 | inform "Activating existing virtual Python environment in $VENV_DIR\n" 101 | printf "source \"%s/bin/activate\"\n" "$VENV_DIR" 102 | # shellcheck disable=SC1091 103 | source "$VENV_DIR/bin/activate" 104 | fi 105 | else 106 | printf "\n" 107 | fatal "Please create and/or activate a virtual Python environment and try again!\n" 108 | fi 109 | fi 110 | printf "\n" 111 | } 112 | 113 | check_for_error() { 114 | if [ $? -ne 0 ]; then 115 | CMD_ERRORS=true 116 | warning "^^^ 😬 previous command did not exit cleanly!" 117 | fi 118 | } 119 | 120 | function do_config_backup { 121 | if [ ! $CONFIG_BACKUP == true ]; then 122 | CONFIG_BACKUP=true 123 | FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" 124 | inform "Backing up $CONFIG_DIR/$CONFIG_FILE to $CONFIG_DIR/$FILENAME\n" 125 | sudo cp "$CONFIG_DIR/$CONFIG_FILE" "$CONFIG_DIR/$FILENAME" 126 | mkdir -p "$RESOURCES_TOP_DIR/config-backups/" 127 | cp $CONFIG_DIR/$CONFIG_FILE "$RESOURCES_TOP_DIR/config-backups/$FILENAME" 128 | if [ -f "$UNINSTALLER" ]; then 129 | echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG_DIR/$CONFIG_FILE" >> "$UNINSTALLER" 130 | fi 131 | fi 132 | } 133 | 134 | function apt_pkg_install { 135 | PACKAGES_NEEDED=() 136 | PACKAGES_IN=("$@") 137 | # Check the list of packages and only run update/install if we need to 138 | for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do 139 | PACKAGE="${PACKAGES_IN[$i]}" 140 | if [ "$PACKAGE" == "" ]; then continue; fi 141 | printf "Checking for %s\n" "$PACKAGE" 142 | dpkg -L "$PACKAGE" > /dev/null 2>&1 143 | if [ "$?" == "1" ]; then 144 | PACKAGES_NEEDED+=("$PACKAGE") 145 | fi 146 | done 147 | PACKAGES="${PACKAGES_NEEDED[*]}" 148 | if ! [ "$PACKAGES" == "" ]; then 149 | printf "\n" 150 | inform "Installing missing packages: $PACKAGES" 151 | if [ ! $APT_HAS_UPDATED ]; then 152 | sudo apt update 153 | APT_HAS_UPDATED=true 154 | fi 155 | # shellcheck disable=SC2086 156 | sudo apt install -y $PACKAGES 157 | check_for_error 158 | if [ -f "$UNINSTALLER" ]; then 159 | echo "apt uninstall -y $PACKAGES" >> "$UNINSTALLER" 160 | fi 161 | fi 162 | } 163 | 164 | function pip_pkg_install { 165 | # A null Keyring prevents pip stalling in the background 166 | PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring $PYTHON -m pip install --upgrade "$@" 167 | check_for_error 168 | } 169 | 170 | while [[ $# -gt 0 ]]; do 171 | K="$1" 172 | case $K in 173 | -u|--unstable) 174 | UNSTABLE=true 175 | shift 176 | ;; 177 | -f|--force) 178 | FORCE=true 179 | shift 180 | ;; 181 | -p|--python) 182 | PYTHON=$2 183 | shift 184 | shift 185 | ;; 186 | *) 187 | if [[ $1 == -* ]]; then 188 | printf "Unrecognised option: %s\n" "$1"; 189 | printf "Usage: %s\n" "$USAGE"; 190 | exit 1 191 | fi 192 | POSITIONAL_ARGS+=("$1") 193 | shift 194 | esac 195 | done 196 | 197 | printf "Installing %s...\n\n" "$LIBRARY_NAME" 198 | 199 | user_check 200 | venv_check 201 | 202 | if [ ! -f "$(which "$PYTHON")" ]; then 203 | fatal "Python path %s not found!\n" "$PYTHON" 204 | fi 205 | 206 | PYTHON_VER=$($PYTHON --version) 207 | 208 | inform "Checking Dependencies. Please wait..." 209 | 210 | # Install toml and try to read pyproject.toml into bash variables 211 | 212 | pip_pkg_install toml 213 | 214 | CONFIG_VARS=$( 215 | $PYTHON - < "$UNINSTALLER" 258 | printf "It's recommended you run these steps manually.\n" 259 | printf "If you want to run the full script, open it in\n" 260 | printf "an editor and remove 'exit 1' from below.\n" 261 | exit 1 262 | source $VIRTUAL_ENV/bin/activate 263 | EOF 264 | 265 | printf "\n" 266 | 267 | inform "Installing for $PYTHON_VER...\n" 268 | 269 | # Install apt packages from pyproject.toml / tool.pimoroni.apt_packages 270 | apt_pkg_install "${APT_PACKAGES[@]}" 271 | 272 | printf "\n" 273 | 274 | if $UNSTABLE; then 275 | warning "Installing unstable library from source.\n" 276 | pip_pkg_install . 277 | else 278 | inform "Installing stable library from pypi.\n" 279 | pip_pkg_install "$LIBRARY_NAME" 280 | fi 281 | 282 | # shellcheck disable=SC2181 # One of two commands run, depending on --unstable flag 283 | if [ $? -eq 0 ]; then 284 | success "Done!\n" 285 | echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> "$UNINSTALLER" 286 | fi 287 | 288 | find_config 289 | 290 | printf "\n" 291 | 292 | # Run the setup commands from pyproject.toml / tool.pimoroni.commands 293 | 294 | inform "Running setup commands...\n" 295 | for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do 296 | CMD="${SETUP_CMDS[$i]}" 297 | # Attempt to catch anything that touches config.txt and trigger a backup 298 | if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG_DIR/$CONFIG_FILE"* ]] || [[ "$CMD" == *"\$CONFIG_DIR/\$CONFIG_FILE"* ]]; then 299 | do_config_backup 300 | fi 301 | if [[ ! "$CMD" == printf* ]]; then 302 | printf "Running: \"%s\"\n" "$CMD" 303 | fi 304 | eval "$CMD" 305 | check_for_error 306 | done 307 | 308 | printf "\n" 309 | 310 | # Add the config.txt entries from pyproject.toml / tool.pimoroni.configtxt 311 | 312 | for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do 313 | CONFIG_LINE="${CONFIG_TXT[$i]}" 314 | if ! [ "$CONFIG_LINE" == "" ]; then 315 | do_config_backup 316 | inform "Adding $CONFIG_LINE to $CONFIG_DIR/$CONFIG_FILE" 317 | sudo sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG_DIR/$CONFIG_FILE 318 | if ! grep -q "^$CONFIG_LINE" $CONFIG_DIR/$CONFIG_FILE; then 319 | printf "%s \n" "$CONFIG_LINE" | sudo tee --append $CONFIG_DIR/$CONFIG_FILE 320 | fi 321 | fi 322 | done 323 | 324 | printf "\n" 325 | 326 | # Just a straight copy of the examples/ dir into ~/Pimoroni/board/examples 327 | 328 | if [ -d "examples" ]; then 329 | if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then 330 | inform "Copying examples to $RESOURCES_DIR" 331 | cp -r examples/ "$RESOURCES_DIR" 332 | echo "rm -r $RESOURCES_DIR" >> "$UNINSTALLER" 333 | success "Done!" 334 | fi 335 | fi 336 | 337 | printf "\n" 338 | 339 | # Use pdoc to generate basic documentation from the installed module 340 | 341 | if confirm "Would you like to generate documentation?"; then 342 | inform "Installing pdoc. Please wait..." 343 | pip_pkg_install pdoc 344 | inform "Generating documentation.\n" 345 | if $PYTHON -m pdoc "$MODULE_NAME" -o "$RESOURCES_DIR/docs" > /dev/null; then 346 | inform "Documentation saved to $RESOURCES_DIR/docs" 347 | success "Done!" 348 | else 349 | warning "Error: Failed to generate documentation." 350 | fi 351 | fi 352 | 353 | printf "\n" 354 | 355 | if [ "$CMD_ERRORS" = true ]; then 356 | warning "One or more setup commands appear to have failed." 357 | printf "This might prevent things from working properly.\n" 358 | printf "Make sure your OS is up to date and try re-running this installer.\n" 359 | printf "If things still don't work, report this or find help at %s.\n\n" "$GITHUB_URL" 360 | else 361 | success "\nAll done!" 362 | fi 363 | 364 | printf "If this is your first time installing you should reboot for hardware changes to take effect.\n" 365 | printf "Find uninstall steps in %s\n\n" "$UNINSTALLER" 366 | 367 | if [ "$CMD_ERRORS" = true ]; then 368 | exit 1 369 | else 370 | exit 0 371 | fi 372 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling", "hatch-fancy-pypi-readme"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "pimoroni-bme280" 7 | dynamic = ["version", "readme"] 8 | description = "Python library for the bme280 temperature, pressure and humidity sensor" 9 | license = {file = "LICENSE"} 10 | requires-python = ">= 3.7" 11 | authors = [ 12 | { name = "Philip Howard", email = "phil@pimoroni.com" }, 13 | ] 14 | maintainers = [ 15 | { name = "Philip Howard", email = "phil@pimoroni.com" }, 16 | ] 17 | keywords = [ 18 | "Pi", 19 | "Raspberry", 20 | ] 21 | classifiers = [ 22 | "Development Status :: 4 - Beta", 23 | "Intended Audience :: Developers", 24 | "License :: OSI Approved :: MIT License", 25 | "Operating System :: POSIX :: Linux", 26 | "Programming Language :: Python :: 3", 27 | "Programming Language :: Python :: 3.7", 28 | "Programming Language :: Python :: 3.8", 29 | "Programming Language :: Python :: 3.9", 30 | "Programming Language :: Python :: 3.10", 31 | "Programming Language :: Python :: 3.11", 32 | "Programming Language :: Python :: 3 :: Only", 33 | "Topic :: Software Development", 34 | "Topic :: Software Development :: Libraries", 35 | "Topic :: System :: Hardware", 36 | ] 37 | dependencies = [ 38 | "i2cdevice>=1.0.0" 39 | ] 40 | 41 | [project.urls] 42 | GitHub = "https://www.github.com/pimoroni/bme280-python" 43 | Homepage = "https://www.pimoroni.com" 44 | 45 | [tool.hatch.version] 46 | path = "bme280/__init__.py" 47 | 48 | [tool.hatch.build] 49 | include = [ 50 | "bme280", 51 | "README.md", 52 | "CHANGELOG.md", 53 | "LICENSE" 54 | ] 55 | 56 | [tool.hatch.build.targets.sdist] 57 | include = [ 58 | "*" 59 | ] 60 | exclude = [ 61 | ".*", 62 | "dist" 63 | ] 64 | 65 | [tool.hatch.metadata.hooks.fancy-pypi-readme] 66 | content-type = "text/markdown" 67 | fragments = [ 68 | { path = "README.md" }, 69 | { text = "\n" }, 70 | { path = "CHANGELOG.md" } 71 | ] 72 | 73 | [tool.ruff] 74 | exclude = [ 75 | '.tox', 76 | '.egg', 77 | '.git', 78 | '__pycache__', 79 | 'build', 80 | 'dist' 81 | ] 82 | line-length = 200 83 | 84 | [tool.codespell] 85 | skip = """ 86 | ./.tox,\ 87 | ./.egg,\ 88 | ./.git,\ 89 | ./__pycache__,\ 90 | ./build,\ 91 | ./dist.\ 92 | """ 93 | 94 | [tool.isort] 95 | line_length = 200 96 | 97 | [tool.black] 98 | line_length = 200 99 | 100 | [tool.check-manifest] 101 | ignore = [ 102 | '.stickler.yml', 103 | 'boilerplate.md', 104 | 'check.sh', 105 | 'install.sh', 106 | 'uninstall.sh', 107 | 'Makefile', 108 | 'tox.ini', 109 | 'tests/*', 110 | 'examples/*', 111 | '.coveragerc', 112 | 'requirements-dev.txt' 113 | ] 114 | 115 | [tool.pimoroni] 116 | apt_packages = [] 117 | configtxt = [] 118 | commands = [ 119 | "printf \"Setting up i2c...\n\"", 120 | "sudo raspi-config nonint do_i2c 0" 121 | ] 122 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | check-manifest 2 | ruff 3 | codespell 4 | isort 5 | twine 6 | hatch 7 | hatch-fancy-pypi-readme 8 | tox 9 | pdoc 10 | -------------------------------------------------------------------------------- /tests/calibration.py: -------------------------------------------------------------------------------- 1 | import bme280 2 | 3 | 4 | class BME280Calibration(bme280.BME280Calibration): 5 | """Prefil the calibration class with known values.""" 6 | 7 | def __init__(self): 8 | bme280.BME280Calibration.__init__(self) 9 | 10 | self.dig_t1 = 28009 11 | self.dig_t2 = 25654 12 | self.dig_t3 = 50 13 | 14 | self.dig_p1 = 39145 15 | self.dig_p2 = -10750 16 | self.dig_p3 = 3024 17 | self.dig_p4 = 5667 18 | self.dig_p5 = -120 19 | self.dig_p6 = -7 20 | self.dig_p7 = 15500 21 | self.dig_p8 = -14600 22 | self.dig_p9 = 6000 23 | 24 | self.dig_h1 = 75 25 | self.dig_h2 = 376 26 | self.dig_h3 = 0 27 | self.dig_h4 = 286 28 | self.dig_h5 = 50 29 | self.dig_h6 = 30 30 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import mock 4 | import pytest 5 | from i2cdevice import MockSMBus 6 | 7 | 8 | class SMBusFakeDevice(MockSMBus): 9 | def __init__(self, i2c_bus): 10 | MockSMBus.__init__(self, i2c_bus) 11 | self.regs[0xD0] = 0x60 # Fake chip ID 12 | 13 | 14 | @pytest.fixture(scope="function", autouse=False) 15 | def bme280(): 16 | import bme280 17 | yield bme280 18 | del sys.modules["bme280"] 19 | 20 | 21 | @pytest.fixture(scope="function", autouse=False) 22 | def smbus2_mock(): 23 | smbus = mock.Mock() 24 | smbus.SMBus = SMBusFakeDevice 25 | sys.modules["smbus2"] = smbus 26 | yield smbus 27 | del sys.modules["smbus2"] 28 | 29 | 30 | @pytest.fixture(scope="function", autouse=False) 31 | def smbus2(): 32 | smbus = mock.Mock() 33 | sys.modules["smbus2"] = smbus 34 | yield smbus 35 | del sys.modules["smbus2"] 36 | -------------------------------------------------------------------------------- /tests/test_compensation.py: -------------------------------------------------------------------------------- 1 | TEST_TEMP_RAW = 529191 2 | TEST_TEMP_CMP = 24.7894877676 3 | 4 | TEST_PRES_RAW = 326816 5 | TEST_PRES_CMP = 1006.61517564 6 | TEST_ALT_CMP = 55.385 7 | 8 | TEST_HUM_RAW = 30281 9 | TEST_HUM_CMP = 68.66996648709039 10 | 11 | 12 | def test_temperature(smbus2_mock, bme280): 13 | from calibration import BME280Calibration 14 | 15 | dev = smbus2_mock.SMBus(1) 16 | 17 | # Load the fake temperature into the virtual registers 18 | dev.regs[0xFC] = (TEST_TEMP_RAW & 0x0000F) << 4 19 | dev.regs[0xFB] = (TEST_TEMP_RAW & 0x00FF0) >> 4 20 | dev.regs[0xFA] = (TEST_TEMP_RAW & 0xFF000) >> 12 21 | 22 | sensor = bme280.BME280(i2c_dev=dev) 23 | sensor.setup() 24 | 25 | # Replace the loaded calibration with our known values 26 | sensor.calibration = BME280Calibration() 27 | 28 | assert round(sensor.get_temperature(), 4) == round(TEST_TEMP_CMP, 4) 29 | 30 | 31 | def test_temperature_forced(smbus2_mock, bme280): 32 | from calibration import BME280Calibration 33 | 34 | dev = smbus2_mock.SMBus(1) 35 | 36 | # Load the fake temperature into the virtual registers 37 | dev.regs[0xFC] = (TEST_TEMP_RAW & 0x0000F) << 4 38 | dev.regs[0xFB] = (TEST_TEMP_RAW & 0x00FF0) >> 4 39 | dev.regs[0xFA] = (TEST_TEMP_RAW & 0xFF000) >> 12 40 | 41 | sensor = bme280.BME280(i2c_dev=dev) 42 | sensor.setup(mode="forced") 43 | 44 | # Replace the loaded calibration with our known values 45 | sensor.calibration = BME280Calibration() 46 | 47 | assert round(sensor.get_temperature(), 4) == round(TEST_TEMP_CMP, 4) 48 | 49 | 50 | def test_pressure(smbus2_mock, bme280): 51 | from calibration import BME280Calibration 52 | 53 | dev = smbus2_mock.SMBus(1) 54 | 55 | # Load the fake temperature values into the virtual registers 56 | # Pressure is temperature compensated!!! 57 | dev.regs[0xFC] = (TEST_TEMP_RAW & 0x0000F) << 4 58 | dev.regs[0xFB] = (TEST_TEMP_RAW & 0x00FF0) >> 4 59 | dev.regs[0xFA] = (TEST_TEMP_RAW & 0xFF000) >> 12 60 | 61 | # Load the fake pressure values 62 | dev.regs[0xF9] = (TEST_PRES_RAW & 0x0000F) << 4 63 | dev.regs[0xF8] = (TEST_PRES_RAW & 0x00FF0) >> 4 64 | dev.regs[0xF7] = (TEST_PRES_RAW & 0xFF000) >> 12 65 | 66 | sensor = bme280.BME280(i2c_dev=dev) 67 | sensor.setup() 68 | 69 | # Replace the loaded calibration with our known values 70 | sensor.calibration = BME280Calibration() 71 | 72 | assert round(sensor.get_pressure(), 4) == round(TEST_PRES_CMP, 4) 73 | 74 | 75 | def test_altitude(smbus2_mock, bme280): 76 | from calibration import BME280Calibration 77 | 78 | dev = smbus2_mock.SMBus(1) 79 | 80 | # Load the fake temperature values into the virtual registers 81 | # Pressure is temperature compensated!!! 82 | dev.regs[0xFC] = (TEST_TEMP_RAW & 0x0000F) << 4 83 | dev.regs[0xFB] = (TEST_TEMP_RAW & 0x00FF0) >> 4 84 | dev.regs[0xFA] = (TEST_TEMP_RAW & 0xFF000) >> 12 85 | 86 | # Load the fake pressure values 87 | dev.regs[0xF9] = (TEST_PRES_RAW & 0x0000F) << 4 88 | dev.regs[0xF8] = (TEST_PRES_RAW & 0x00FF0) >> 4 89 | dev.regs[0xF7] = (TEST_PRES_RAW & 0xFF000) >> 12 90 | 91 | sensor = bme280.BME280(i2c_dev=dev) 92 | sensor.setup() 93 | 94 | # Replace the loaded calibration with our known values 95 | sensor.calibration = BME280Calibration() 96 | 97 | assert round(sensor.get_altitude(), 4) == round(TEST_ALT_CMP, 4) 98 | 99 | 100 | def test_humidity(smbus2_mock, bme280): 101 | from calibration import BME280Calibration 102 | 103 | dev = smbus2_mock.SMBus(1) 104 | 105 | # Load the fake temperature values into the virtual registers 106 | # Humidity is temperature compensated!!! 107 | dev.regs[0xFC] = (TEST_TEMP_RAW & 0x0000F) << 4 108 | dev.regs[0xFB] = (TEST_TEMP_RAW & 0x00FF0) >> 4 109 | dev.regs[0xFA] = (TEST_TEMP_RAW & 0xFF000) >> 12 110 | 111 | # Load the fake humidity values 112 | dev.regs[0xFD] = TEST_HUM_RAW >> 8 113 | dev.regs[0xFE] = TEST_HUM_RAW & 0xFF 114 | 115 | sensor = bme280.BME280(i2c_dev=dev) 116 | sensor.setup() 117 | 118 | # Replace the loaded calibration with our known values 119 | sensor.calibration = BME280Calibration() 120 | 121 | assert round(sensor.get_humidity(), 4) == round(TEST_HUM_CMP, 4) 122 | -------------------------------------------------------------------------------- /tests/test_setup.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_setup_not_present(smbus2_mock, bme280): 5 | dev = smbus2_mock.SMBus(1) 6 | dev.regs[0xD0] = 0x00 # Incorrect chip ID 7 | 8 | sensor = bme280.BME280(i2c_dev=dev) 9 | with pytest.raises(RuntimeError): 10 | sensor.setup() 11 | 12 | 13 | def test_setup_mock_present(smbus2_mock, bme280): 14 | sensor = bme280.BME280() 15 | sensor.setup() 16 | 17 | 18 | def test_setup_forced_mode(smbus2_mock, bme280): 19 | 20 | sensor = bme280.BME280() 21 | sensor.setup(mode="forced") 22 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py,qa 3 | skip_missing_interpreters = True 4 | isolated_build = true 5 | minversion = 4.0.0 6 | 7 | [testenv] 8 | commands = 9 | coverage run -m pytest -v -r wsx 10 | coverage report 11 | deps = 12 | mock 13 | pytest>=3.1 14 | pytest-cov 15 | build 16 | 17 | [testenv:qa] 18 | commands = 19 | check-manifest 20 | python -m build --no-isolation 21 | python -m twine check dist/* 22 | isort --check . 23 | ruff . 24 | codespell . 25 | deps = 26 | check-manifest 27 | ruff 28 | codespell 29 | isort 30 | twine 31 | build 32 | hatch 33 | hatch-fancy-pypi-readme 34 | 35 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | FORCE=false 4 | LIBRARY_NAME=$(grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}') 5 | RESOURCES_DIR=$HOME/Pimoroni/$LIBRARY_NAME 6 | PYTHON="python" 7 | 8 | 9 | venv_check() { 10 | PYTHON_BIN=$(which $PYTHON) 11 | if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then 12 | printf "This script should be run in a virtual Python environment.\n" 13 | exit 1 14 | fi 15 | } 16 | 17 | user_check() { 18 | if [ "$(id -u)" -eq 0 ]; then 19 | printf "Script should not be run as root. Try './uninstall.sh'\n" 20 | exit 1 21 | fi 22 | } 23 | 24 | confirm() { 25 | if $FORCE; then 26 | true 27 | else 28 | read -r -p "$1 [y/N] " response < /dev/tty 29 | if [[ $response =~ ^(yes|y|Y)$ ]]; then 30 | true 31 | else 32 | false 33 | fi 34 | fi 35 | } 36 | 37 | prompt() { 38 | read -r -p "$1 [y/N] " response < /dev/tty 39 | if [[ $response =~ ^(yes|y|Y)$ ]]; then 40 | true 41 | else 42 | false 43 | fi 44 | } 45 | 46 | success() { 47 | echo -e "$(tput setaf 2)$1$(tput sgr0)" 48 | } 49 | 50 | inform() { 51 | echo -e "$(tput setaf 6)$1$(tput sgr0)" 52 | } 53 | 54 | warning() { 55 | echo -e "$(tput setaf 1)$1$(tput sgr0)" 56 | } 57 | 58 | printf "%s Python Library: Uninstaller\n\n" "$LIBRARY_NAME" 59 | 60 | user_check 61 | venv_check 62 | 63 | printf "Uninstalling for Python 3...\n" 64 | $PYTHON -m pip uninstall "$LIBRARY_NAME" 65 | 66 | if [ -d "$RESOURCES_DIR" ]; then 67 | if confirm "Would you like to delete $RESOURCES_DIR?"; then 68 | rm -r "$RESOURCES_DIR" 69 | fi 70 | fi 71 | 72 | printf "Done!\n" 73 | --------------------------------------------------------------------------------