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 c139ee1..016a678 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,36 +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: [2.7, 3.5, 3.7, 3.9] + 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.9' }} + diff --git a/library/CHANGELOG.txt b/CHANGELOG.md similarity index 100% rename from library/CHANGELOG.txt rename to CHANGELOG.md diff --git a/Makefile b/Makefile index 3c4c65f..9e0c15c 100644 --- a/Makefile +++ b/Makefile @@ -1,71 +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}') -PACKAGE_NAME="bme280" +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.md from README.md + library/CHANGELOG.txt" - @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/${PACKAGE_NAME}/__init__.py" - @cat library/${PACKAGE_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\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 - cd library; python setup.py bdist_wheel +nopost: + @bash check.sh --nopost -python-sdist: python-readme python-license - cd library; python setup.py sdist +tag: + git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" -python-clean: - -rm -r library/dist - -rm -r library/build - -rm -r library/*.egg-info +build: check + @hatch build -python-dist: python-clean python-wheels python-sdist - ls library/dist +clean: + -rm -r dist -python-testdeploy: python-dist - twine upload --repository-url https://test.pypi.org/legacy/ library/dist/* +testdeploy: build + twine upload --repository testpypi dist/* -python-deploy: check python-dist - twine upload library/dist/* +deploy: nopost build + twine upload dist/* diff --git a/README.md b/README.md index a78f0d4..a375fbb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # BME280 Temperature, Pressure, & Humidity Sensor -[![Build Status](https://travis-ci.com/pimoroni/bme280-python.svg?branch=master)](https://travis-ci.com/pimoroni/bme280-python) +[![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) [![Coverage Status](https://coveralls.io/repos/github/pimoroni/bme280-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/bme280-python?branch=master) [![PyPi Package](https://img.shields.io/pypi/v/pimoroni-bme280.svg)](https://pypi.python.org/pypi/pimoroni-bme280) [![Python Versions](https://img.shields.io/pypi/pyversions/pimoroni-bme280.svg)](https://pypi.python.org/pypi/pimoroni-bme280) @@ -19,7 +19,7 @@ You can optionally run `sudo raspi-config` or the graphical Raspberry Pi Configu Stable library from PyPi, the smbus library is also needed: -* Just run `sudo pip install pimoroni-bme280 smbus` +* Just run `python3 -m pip install pimoroni-bme280` Latest/development library from GitHub: diff --git a/bme280/__init__.py b/bme280/__init__.py new file mode 100644 index 0000000..8e116d7 --- /dev/null +++ b/bme280/__init__.py @@ -0,0 +1,263 @@ +"""BME280 Driver.""" +import struct +import time + +from i2cdevice import BitField, Device, Register, _int_to_bytes +from i2cdevice.adapter import Adapter, LookupAdapter + +__version__ = "0.1.1" + +CHIP_ID = 0x60 +I2C_ADDRESS_GND = 0x76 +I2C_ADDRESS_VCC = 0x77 + + +class S8Adapter(Adapter): + """Convert unsigned 8bit integer to signed.""" + + def _decode(self, value): + if value & (1 << 7): + value -= 1 << 8 + return value + + +class S16Adapter(Adapter): + """Convert unsigned 16bit integer to signed.""" + + def _decode(self, value): + return struct.unpack("> 4) & 0x0F) | (b[1] << 4) + if r & (1 << 11): + r = r - 1 << 12 + return r + + +class H4Adapter(S16Adapter): + def _decode(self, value): + b = _int_to_bytes(value, 2) + r = (b[0] << 4) | (b[1] & 0x0F) + if r & (1 << 11): + r = r - 1 << 12 + return r + + +class BME280Calibration: + def __init__(self): + self.dig_t1 = 0 + self.dig_t2 = 0 + self.dig_t3 = 0 + + self.dig_p1 = 0 + self.dig_p2 = 0 + self.dig_p3 = 0 + self.dig_p4 = 0 + self.dig_p5 = 0 + self.dig_p6 = 0 + self.dig_p7 = 0 + self.dig_p8 = 0 + self.dig_p9 = 0 + + self.dig_h1 = 0.0 + self.dig_h2 = 0.0 + self.dig_h3 = 0.0 + self.dig_h4 = 0.0 + self.dig_h5 = 0.0 + self.dig_h6 = 0.0 + + self.temperature_fine = 0 + + def set_from_namedtuple(self, value): + # Iterate through a tuple supplied by i2cdevice + # and copy its values into the class attributes + for key in self.__dict__.keys(): + try: + setattr(self, key, getattr(value, key)) + except AttributeError: + pass + + def compensate_temperature(self, raw_temperature): + var1 = (raw_temperature / 16384.0 - self.dig_t1 / 1024.0) * self.dig_t2 + var2 = raw_temperature / 131072.0 - self.dig_t1 / 8192.0 + var2 = var2 * var2 * self.dig_t3 + self.temperature_fine = var1 + var2 + return self.temperature_fine / 5120.0 + + def compensate_pressure(self, raw_pressure): + var1 = self.temperature_fine / 2.0 - 64000.0 + var2 = var1 * var1 * self.dig_p6 / 32768.0 + var2 = var2 + var1 * self.dig_p5 * 2 + var2 = var2 / 4.0 + self.dig_p4 * 65536.0 + var1 = (self.dig_p3 * var1 * var1 / 524288.0 + self.dig_p2 * var1) / 524288.0 + var1 = (1.0 + var1 / 32768.0) * self.dig_p1 + pressure = 1048576.0 - raw_pressure + pressure = (pressure - var2 / 4096.0) * 6250.0 / var1 + var1 = self.dig_p9 * pressure * pressure / 2147483648.0 + var2 = pressure * self.dig_p8 / 32768.0 + return pressure + (var1 + var2 + self.dig_p7) / 16.0 + + def compensate_humidity(self, raw_humidity): + var1 = self.temperature_fine - 76800.0 + var2 = self.dig_h4 * 64.0 + (self.dig_h5 / 16384.0) * var1 + var3 = raw_humidity - var2 + var4 = self.dig_h2 / 65536.0 + var5 = 1.0 + (self.dig_h3 / 67108864.0) * var1 + var6 = 1.0 + (self.dig_h6 / 67108864.0) * var1 * var5 + var6 = var3 * var4 * (var5 * var6) + + humidity = var6 * (1.0 - self.dig_h1 * var6 / 524288.0) + return max(0.0, min(100.0, humidity)) + + +class BME280: + def __init__(self, i2c_addr=I2C_ADDRESS_GND, i2c_dev=None): + self.calibration = BME280Calibration() + self._is_setup = False + self._i2c_addr = i2c_addr + self._i2c_dev = i2c_dev + self._bme280 = Device( + [I2C_ADDRESS_GND, I2C_ADDRESS_VCC], + i2c_dev=self._i2c_dev, + bit_width=8, + registers=( + Register("CHIP_ID", 0xD0, fields=(BitField("id", 0xFF),)), + Register("RESET", 0xE0, fields=(BitField("reset", 0xFF),)), + Register( + "STATUS", + 0xF3, + fields=( + BitField("measuring", 0b00001000), # 1 when conversion is running + BitField("im_update", 0b00000001), # 1 when NVM data is being copied + ), + ), + Register( + "CTRL_MEAS", + 0xF4, + fields=( + BitField("osrs_t", 0b11100000, adapter=LookupAdapter({1: 0b001, 2: 0b010, 4: 0b011, 8: 0b100, 16: 0b101})), # Temperature oversampling + BitField("osrs_p", 0b00011100, adapter=LookupAdapter({1: 0b001, 2: 0b010, 4: 0b011, 8: 0b100, 16: 0b101})), # Pressure oversampling + BitField("mode", 0b00000011, adapter=LookupAdapter({"sleep": 0b00, "forced": 0b10, "normal": 0b11})), # Power mode + ), + ), + Register("CTRL_HUM", 0xF2, fields=(BitField("osrs_h", 0b00000111, adapter=LookupAdapter({1: 0b001, 2: 0b010, 4: 0b011, 8: 0b100, 16: 0b101})),)), # Humidity oversampling + Register( + "CONFIG", + 0xF5, + fields=( + BitField( + "t_sb", + 0b11100000, # Temp standby duration in normal mode + adapter=LookupAdapter({0.5: 0b000, 62.5: 0b001, 125: 0b010, 250: 0b011, 500: 0b100, 1000: 0b101, 10: 0b110, 20: 0b111}), + ), + BitField("filter", 0b00011100), # Controls the time constant of the IIR filter + BitField("spi3w_en", 0b0000001, read_only=True), # Enable 3-wire SPI interface when set to 1. IE: Don't set this bit! + ), + ), + Register("DATA", 0xF7, fields=(BitField("humidity", 0x000000000000FFFF), BitField("temperature", 0x000000FFFFF00000), BitField("pressure", 0xFFFFF00000000000)), bit_width=8 * 8), + Register( + "CALIBRATION", + 0x88, + fields=( + BitField("dig_t1", 0xFFFF << 16 * 12, adapter=U16Adapter()), # 0x88 0x89 + BitField("dig_t2", 0xFFFF << 16 * 11, adapter=S16Adapter()), # 0x8A 0x8B + BitField("dig_t3", 0xFFFF << 16 * 10, adapter=S16Adapter()), # 0x8C 0x8D + BitField("dig_p1", 0xFFFF << 16 * 9, adapter=U16Adapter()), # 0x8E 0x8F + BitField("dig_p2", 0xFFFF << 16 * 8, adapter=S16Adapter()), # 0x90 0x91 + BitField("dig_p3", 0xFFFF << 16 * 7, adapter=S16Adapter()), # 0x92 0x93 + BitField("dig_p4", 0xFFFF << 16 * 6, adapter=S16Adapter()), # 0x94 0x95 + BitField("dig_p5", 0xFFFF << 16 * 5, adapter=S16Adapter()), # 0x96 0x97 + BitField("dig_p6", 0xFFFF << 16 * 4, adapter=S16Adapter()), # 0x98 0x99 + BitField("dig_p7", 0xFFFF << 16 * 3, adapter=S16Adapter()), # 0x9A 0x9B + BitField("dig_p8", 0xFFFF << 16 * 2, adapter=S16Adapter()), # 0x9C 0x9D + BitField("dig_p9", 0xFFFF << 16 * 1, adapter=S16Adapter()), # 0x9E 0x9F + BitField("dig_h1", 0x00FF), # 0xA1 uint8 + ), + bit_width=26 * 8, + ), + Register( + "CALIBRATION2", + 0xE1, + fields=( + BitField("dig_h2", 0xFFFF0000000000, adapter=S16Adapter()), # 0xE1 0xE2 + BitField("dig_h3", 0x0000FF00000000), # 0xE3 uint8 + BitField("dig_h4", 0x000000FFFF0000, adapter=H4Adapter()), # 0xE4 0xE5[3:0] + BitField("dig_h5", 0x00000000FFFF00, adapter=H5Adapter()), # 0xE5[7:4] 0xE6 + BitField("dig_h6", 0x000000000000FF, adapter=S8Adapter()), # 0xE7 int8 + ), + bit_width=7 * 8, + ), + ), + ) + + def setup(self, mode="normal", temperature_oversampling=16, pressure_oversampling=16, humidity_oversampling=16, temperature_standby=500): + if self._is_setup: + return + self._is_setup = True + + self._bme280.select_address(self._i2c_addr) + self._mode = mode + + if mode == "forced": + mode = "sleep" + + try: + chip = self._bme280.get("CHIP_ID") + if chip.id != CHIP_ID: + raise RuntimeError("Unable to find bme280 on 0x{:02x}, CHIP_ID returned {:02x}".format(self._i2c_addr, chip.id)) + except IOError: + raise RuntimeError("Unable to find bme280 on 0x{:02x}, IOError".format(self._i2c_addr)) + + self._bme280.set("RESET", reset=0xB6) + time.sleep(0.1) + + self._bme280.set("CTRL_HUM", osrs_h=humidity_oversampling) + + self._bme280.set("CTRL_MEAS", mode=mode, osrs_t=temperature_oversampling, osrs_p=pressure_oversampling) + + self._bme280.set("CONFIG", t_sb=temperature_standby, filter=2) + + self.calibration.set_from_namedtuple(self._bme280.get("CALIBRATION")) + self.calibration.set_from_namedtuple(self._bme280.get("CALIBRATION2")) + + def update_sensor(self): + self.setup() + + if self._mode == "forced": + self._bme280.set("CTRL_MEAS", mode="forced") + while self._bme280.get("STATUS").measuring: + time.sleep(0.001) + + raw = self._bme280.get("DATA") + + self.temperature = self.calibration.compensate_temperature(raw.temperature) + self.pressure = self.calibration.compensate_pressure(raw.pressure) / 100.0 + self.humidity = self.calibration.compensate_humidity(raw.humidity) + + def get_temperature(self): + self.update_sensor() + return self.temperature + + def get_pressure(self): + self.update_sensor() + return self.pressure + + def get_humidity(self): + self.update_sensor() + return self.humidity + + def get_altitude(self, qnh=1013.25): + self.update_sensor() + pressure = self.get_pressure() + altitude = 44330.0 * (1.0 - pow(pressure / qnh, (1.0 / 5.255))) + return altitude 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/examples/all-values.py b/examples/all-values.py index 2907d80..d61daff 100755 --- a/examples/all-values.py +++ b/examples/all-values.py @@ -1,17 +1,21 @@ #!/usr/bin/env python import time + try: from smbus2 import SMBus except ImportError: from smbus import SMBus + from bme280 import BME280 -print("""all-values.py - Read temperature, pressure, and humidity +print( + """all-values.py - Read temperature, pressure, and humidity Press Ctrl+C to exit! -""") +""" +) # Initialise the BME280 bus = SMBus(1) @@ -21,5 +25,5 @@ temperature = bme280.get_temperature() pressure = bme280.get_pressure() humidity = bme280.get_humidity() - print('{:05.2f}*C {:05.2f}hPa {:05.2f}%'.format(temperature, pressure, humidity)) + print("{:05.2f}*C {:05.2f}hPa {:05.2f}%".format(temperature, pressure, humidity)) time.sleep(1) diff --git a/examples/compensated-temperature.py b/examples/compensated-temperature.py index 807d353..fbfb9ef 100755 --- a/examples/compensated-temperature.py +++ b/examples/compensated-temperature.py @@ -1,32 +1,37 @@ #!/usr/bin/env python import time -from bme280 import BME280 from subprocess import PIPE, Popen +from bme280 import BME280 + try: from smbus2 import SMBus except ImportError: from smbus import SMBus -print("""compensated-temperature.py - Use the CPU temperature to compensate temperature +print( + """compensated-temperature.py - Use the CPU temperature to compensate temperature readings from the BME280 sensor. Method adapted from Initial State's Enviro pHAT review: https://medium.com/@InitialState/tutorial-review-enviro-phat-for-raspberry-pi-4cd6d8c63441 Press Ctrl+C to exit! -""") +""" +) # Initialise the BME280 bus = SMBus(1) bme280 = BME280(i2c_dev=bus) + # Gets the CPU temperature in degrees C def get_cpu_temperature(): - process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE) + process = Popen(["vcgencmd", "measure_temp"], stdout=PIPE) output, _error = process.communicate() output = output.decode() - return float(output[output.index('=') + 1:output.rindex("'")]) + return float(output[output.index("=") + 1 : output.rindex("'")]) + factor = 0.6 # Smaller numbers adjust temp down, vice versa smooth_size = 10 # Dampens jitter due to rapid CPU temp changes diff --git a/examples/dump-calibration.py b/examples/dump-calibration.py index fc25e24..ba322a0 100755 --- a/examples/dump-calibration.py +++ b/examples/dump-calibration.py @@ -6,17 +6,19 @@ from smbus import SMBus from bme280 import BME280 -print("""dump-calibration.py - Dumps calibration data. +print( + """dump-calibration.py - Dumps calibration data. Press Ctrl+C to exit! -""") +""" +) # Initialise the BME280 bme280 = BME280(i2c_dev=SMBus(1)) bme280.setup() for key in dir(bme280.calibration): - if key.startswith('dig_'): + if key.startswith("dig_"): value = getattr(bme280.calibration, key) - print('{} = {}'.format(key, value)) + print("{} = {}".format(key, value)) diff --git a/examples/local_altitude.py b/examples/local_altitude.py index 7b5cfed..51addc3 100644 --- a/examples/local_altitude.py +++ b/examples/local_altitude.py @@ -8,20 +8,24 @@ from smbus import SMBus from bme280 import BME280 -print("""local_altitude.py - +print( + """local_altitude.py - Allows you to correct the QNH for your local area. Do not rely on this approximation for landing planes. Press Ctrl+C to exit! -""") +""" +) # Initialise the BME280 bus = SMBus(1) bme280 = BME280(i2c_dev=bus) # asks the user for their local QNH value and confirms it -local_qnh = input("""Please enter your local QNH value. +local_qnh = input( + """Please enter your local QNH value. You can find this by searching for a local METAR on the internet. ->""") +>""" +) print("You have told us the QNH is", local_qnh) # converts the input into a floating point number diff --git a/examples/relative-altitude.py b/examples/relative-altitude.py index 9bd1166..0e498ae 100755 --- a/examples/relative-altitude.py +++ b/examples/relative-altitude.py @@ -8,11 +8,13 @@ from smbus import SMBus from bme280 import BME280 -print("""relative-altitude.py - Calculates relative altitude from pressure. +print( + """relative-altitude.py - Calculates relative altitude from pressure. Press Ctrl+C to exit! -""") +""" +) # Initialise the BME280 bus = SMBus(1) @@ -34,5 +36,5 @@ while True: altitude = bme280.get_altitude(qnh=baseline) - print('Relative altitude: {:05.2f} metres'.format(altitude)) + print("Relative altitude: {:05.2f} metres".format(altitude)) time.sleep(1) diff --git a/examples/temperature-compare.py b/examples/temperature-compare.py index f5681a1..e58ddfc 100755 --- a/examples/temperature-compare.py +++ b/examples/temperature-compare.py @@ -1,18 +1,22 @@ #!/usr/bin/env python import time + try: from smbus2 import SMBus except ImportError: from smbus import SMBus + from bme280 import BME280 -print("""temperature-compare.py - Compares oversampling levels +print( + """temperature-compare.py - Compares oversampling levels (requires two BME280s with different addresses). Press Ctrl+C to exit! -""") +""" +) # Initialise the BME280 bus = SMBus(1) @@ -29,5 +33,5 @@ while True: temperatureA = bme280A.get_temperature() temperatureB = bme280B.get_temperature() - print('Forced: {:05.2f}*C Normal: {:05.2f}*C D: {:05.2f}'.format(temperatureA, temperatureB, abs(temperatureA - temperatureB))) + print("Forced: {:05.2f}*C Normal: {:05.2f}*C D: {:05.2f}".format(temperatureA, temperatureB, abs(temperatureA - temperatureB))) time.sleep(1) diff --git a/examples/temperature-forced-mode.py b/examples/temperature-forced-mode.py index c3bdb5c..08b1362 100755 --- a/examples/temperature-forced-mode.py +++ b/examples/temperature-forced-mode.py @@ -1,10 +1,12 @@ #!/usr/bin/env python import time + try: from smbus2 import SMBus except ImportError: from smbus import SMBus + from bme280 import BME280 # Initialise the BME280 @@ -19,5 +21,5 @@ while True: temperature = bme280.get_temperature() - print('{:05.2f}*C'.format(temperature)) + print("{:05.2f}*C".format(temperature)) time.sleep(1) diff --git a/install-bullseye.sh b/install-bullseye.sh deleted file mode 100755 index 76743a9..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 ./install.sh (--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 ./install.sh'\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!" -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/install.sh b/install.sh index 0c73d6a..aebc385 100755 --- a/install.sh +++ b/install.sh @@ -1,31 +1,29 @@ #!/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_DIR/auto_venv.sh +VENV_DIR=$HOME/.virtualenvs/pimoroni WD=`pwd` -USAGE="sudo ./install.sh (--unstable)" +USAGE="./install.sh (--unstable)" POSITIONAL_ARGS=() +FORCE=false UNSTABLE=false -CODENAME=`lsb_release -sc` +PYTHON="python" -if [[ $CODENAME == "bullseye" ]]; then - bash ./install-bullseye.sh - exit $? -fi user_check() { - if [ $(id -u) -ne 0 ]; then - printf "Script must be run as root. Try 'sudo ./install.sh'\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 +56,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_DIR/auto_venv.sh` to your ~/.bashrc to activate +# the Pimoroni virtual environment automagically! +VENV_DIR="$VENV_DIR" +if [ ! -f \$VENV_DIR/bin/activate ]; then + printf "Creating user Python environment in \$VENV_DIR, please wait...\n" + mkdir -p \$VENV_DIR + python3 -m venv --system-site-packages \$VENV_DIR +fi +printf " ↓ ↓ ↓ ↓ Hello, we've activated a Python venv for you. To exit, type \"deactivate\".\n" +source \$VENV_DIR/bin/activate +EOF + fi +} + +venv_check() { + PYTHON_BIN=`which $PYTHON` + if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then + printf "This script should be run in a virtual Python environment.\n" + if confirm "Would you like us to create 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 + 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 +126,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 +147,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 +168,31 @@ while [[ $# -gt 0 ]]; do done user_check +venv_check -apt_pkg_install python-configparser - -CONFIG_VARS=`python - < $UNINSTALLER @@ -161,42 +222,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 - -printf "Installing for Python 2..\n" -apt_pkg_install "${PY2_DEPS[@]}" +inform "Installing for $PYTHON_VER...\n" +apt_pkg_install "${APT_PACKAGES[@]}" if $UNSTABLE; then - python setup.py install > /dev/null + pip_pkg_install . else - pip install --upgrade $LIBRARY_NAME + pip_pkg_install $LIBRARY_NAME fi if [ $? -eq 0 ]; then success "Done!\n" - echo "pip uninstall $LIBRARY_NAME" >> $UNINSTALLER -fi - -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 + echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> $UNINSTALLER fi cd $WD @@ -215,9 +259,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 @@ -233,13 +277,12 @@ fi printf "\n" -if [ -f "/usr/bin/pydoc" ]; then +if confirm "Would you like to generate documentation?"; then + pip_pkg_install pdoc 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" + $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." diff --git a/library/.coveragerc b/library/.coveragerc deleted file mode 100644 index cb1f321..0000000 --- a/library/.coveragerc +++ /dev/null @@ -1,4 +0,0 @@ -[run] -source = bme280 -omit = - .tox/* 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/MANIFEST.in b/library/MANIFEST.in deleted file mode 100644 index d39e87b..0000000 --- a/library/MANIFEST.in +++ /dev/null @@ -1,5 +0,0 @@ -include CHANGELOG.txt -include LICENSE.txt -include README.md -include setup.py -recursive-include bme280 *.py diff --git a/library/README.md b/library/README.md deleted file mode 100644 index ed413f3..0000000 --- a/library/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# BME280 Temperature, Pressure, & Humidity Sensor - -[![Build Status](https://travis-ci.com/pimoroni/bme280-python.svg?branch=master)](https://travis-ci.com/pimoroni/bme280-python) -[![Coverage Status](https://coveralls.io/repos/github/pimoroni/bme280-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/bme280-python?branch=master) -[![PyPi Package](https://img.shields.io/pypi/v/pimoroni-bme280.svg)](https://pypi.python.org/pypi/pimoroni-bme280) -[![Python Versions](https://img.shields.io/pypi/pyversions/pimoroni-bme280.svg)](https://pypi.python.org/pypi/pimoroni-bme280) - -Suitable for measuring ambient temperature, barometric pressure, and humidity, the BME280 is a great indoor environmental sensor. - -# Pre-requisites - -You must enable: - -* i2c: `sudo raspi-config nonint do_i2c 0` - -You can optionally run `sudo raspi-config` or the graphical Raspberry Pi Configuration UI to enable interfaces. - -# Installing - -Stable library from PyPi, the smbus library is also needed: - -* Just run `sudo pip install pimoroni-bme280 smbus` - -Latest/development library from GitHub: - -* `git clone https://github.com/pimoroni/bme280-python` -* `cd bme280-python` -* `sudo ./install.sh` - - -# Changelog - -0.1.1 ------ - -* Fix so package is included in .whl releases - -0.1.0 ------ - -* Switch to setup.cfg -* Match humidity compensation to BOSCH formula - -0.0.2 ------ - -* Update to i2cdevice>=0.0.6 set/get API - -0.0.1 ------ - -* Initial Release diff --git a/library/bme280/__init__.py b/library/bme280/__init__.py deleted file mode 100644 index bef640b..0000000 --- a/library/bme280/__init__.py +++ /dev/null @@ -1,279 +0,0 @@ -"""BME280 Driver.""" -from i2cdevice import Device, Register, BitField, _int_to_bytes -from i2cdevice.adapter import LookupAdapter, Adapter -import struct -import time - - -__version__ = '0.1.1' - -CHIP_ID = 0x60 -I2C_ADDRESS_GND = 0x76 -I2C_ADDRESS_VCC = 0x77 - - -class S8Adapter(Adapter): - """Convert unsigned 8bit integer to signed.""" - - def _decode(self, value): - if value & (1 << 7): - value -= 1 << 8 - return value - - -class S16Adapter(Adapter): - """Convert unsigned 16bit integer to signed.""" - - def _decode(self, value): - return struct.unpack('> 4) & 0x0F) | (b[1] << 4) - if r & (1 << 11): - r = r - 1 << 12 - return r - - -class H4Adapter(S16Adapter): - def _decode(self, value): - b = _int_to_bytes(value, 2) - r = (b[0] << 4) | (b[1] & 0x0F) - if r & (1 << 11): - r = r - 1 << 12 - return r - - -class BME280Calibration(): - def __init__(self): - self.dig_t1 = 0 - self.dig_t2 = 0 - self.dig_t3 = 0 - - self.dig_p1 = 0 - self.dig_p2 = 0 - self.dig_p3 = 0 - self.dig_p4 = 0 - self.dig_p5 = 0 - self.dig_p6 = 0 - self.dig_p7 = 0 - self.dig_p8 = 0 - self.dig_p9 = 0 - - self.dig_h1 = 0.0 - self.dig_h2 = 0.0 - self.dig_h3 = 0.0 - self.dig_h4 = 0.0 - self.dig_h5 = 0.0 - self.dig_h6 = 0.0 - - self.temperature_fine = 0 - - def set_from_namedtuple(self, value): - # Iterate through a tuple supplied by i2cdevice - # and copy its values into the class attributes - for key in self.__dict__.keys(): - try: - setattr(self, key, getattr(value, key)) - except AttributeError: - pass - - def compensate_temperature(self, raw_temperature): - var1 = (raw_temperature / 16384.0 - self.dig_t1 / 1024.0) * self.dig_t2 - var2 = raw_temperature / 131072.0 - self.dig_t1 / 8192.0 - var2 = var2 * var2 * self.dig_t3 - self.temperature_fine = (var1 + var2) - return self.temperature_fine / 5120.0 - - def compensate_pressure(self, raw_pressure): - var1 = self.temperature_fine / 2.0 - 64000.0 - var2 = var1 * var1 * self.dig_p6 / 32768.0 - var2 = var2 + var1 * self.dig_p5 * 2 - var2 = var2 / 4.0 + self.dig_p4 * 65536.0 - var1 = (self.dig_p3 * var1 * var1 / 524288.0 + self.dig_p2 * var1) / 524288.0 - var1 = (1.0 + var1 / 32768.0) * self.dig_p1 - pressure = 1048576.0 - raw_pressure - pressure = (pressure - var2 / 4096.0) * 6250.0 / var1 - var1 = self.dig_p9 * pressure * pressure / 2147483648.0 - var2 = pressure * self.dig_p8 / 32768.0 - return pressure + (var1 + var2 + self.dig_p7) / 16.0 - - def compensate_humidity(self, raw_humidity): - var1 = self.temperature_fine - 76800.0 - var2 = self.dig_h4 * 64.0 + (self.dig_h5 / 16384.0) * var1 - var3 = raw_humidity - var2 - var4 = self.dig_h2 / 65536.0 - var5 = 1.0 + (self.dig_h3 / 67108864.0) * var1 - var6 = 1.0 + (self.dig_h6 / 67108864.0) * var1 * var5 - var6 = var3 * var4 * (var5 * var6) - - humidity = var6 * (1.0 - self.dig_h1 * var6 / 524288.0) - return max(0.0, min(100.0, humidity)) - - -class BME280: - def __init__(self, i2c_addr=I2C_ADDRESS_GND, i2c_dev=None): - self.calibration = BME280Calibration() - self._is_setup = False - self._i2c_addr = i2c_addr - self._i2c_dev = i2c_dev - self._bme280 = Device([I2C_ADDRESS_GND, I2C_ADDRESS_VCC], i2c_dev=self._i2c_dev, bit_width=8, registers=( - Register('CHIP_ID', 0xD0, fields=( - BitField('id', 0xFF), - )), - Register('RESET', 0xE0, fields=( - BitField('reset', 0xFF), - )), - Register('STATUS', 0xF3, fields=( - BitField('measuring', 0b00001000), # 1 when conversion is running - BitField('im_update', 0b00000001), # 1 when NVM data is being copied - )), - Register('CTRL_MEAS', 0xF4, fields=( - BitField('osrs_t', 0b11100000, # Temperature oversampling - adapter=LookupAdapter({ - 1: 0b001, - 2: 0b010, - 4: 0b011, - 8: 0b100, - 16: 0b101 - })), - BitField('osrs_p', 0b00011100, # Pressure oversampling - adapter=LookupAdapter({ - 1: 0b001, - 2: 0b010, - 4: 0b011, - 8: 0b100, - 16: 0b101})), - BitField('mode', 0b00000011, # Power mode - adapter=LookupAdapter({ - 'sleep': 0b00, - 'forced': 0b10, - 'normal': 0b11})), - )), - Register('CTRL_HUM', 0xF2, fields=( - BitField('osrs_h', 0b00000111, # Humidity oversampling - adapter=LookupAdapter({ - 1: 0b001, - 2: 0b010, - 4: 0b011, - 8: 0b100, - 16: 0b101})), - )), - Register('CONFIG', 0xF5, fields=( - BitField('t_sb', 0b11100000, # Temp standby duration in normal mode - adapter=LookupAdapter({ - 0.5: 0b000, - 62.5: 0b001, - 125: 0b010, - 250: 0b011, - 500: 0b100, - 1000: 0b101, - 10: 0b110, - 20: 0b111})), - BitField('filter', 0b00011100), # Controls the time constant of the IIR filter - BitField('spi3w_en', 0b0000001, read_only=True), # Enable 3-wire SPI interface when set to 1. IE: Don't set this bit! - )), - Register('DATA', 0xF7, fields=( - BitField('humidity', 0x000000000000FFFF), - BitField('temperature', 0x000000FFFFF00000), - BitField('pressure', 0xFFFFF00000000000) - ), bit_width=8 * 8), - Register('CALIBRATION', 0x88, fields=( - BitField('dig_t1', 0xFFFF << 16 * 12, adapter=U16Adapter()), # 0x88 0x89 - BitField('dig_t2', 0xFFFF << 16 * 11, adapter=S16Adapter()), # 0x8A 0x8B - BitField('dig_t3', 0xFFFF << 16 * 10, adapter=S16Adapter()), # 0x8C 0x8D - BitField('dig_p1', 0xFFFF << 16 * 9, adapter=U16Adapter()), # 0x8E 0x8F - BitField('dig_p2', 0xFFFF << 16 * 8, adapter=S16Adapter()), # 0x90 0x91 - BitField('dig_p3', 0xFFFF << 16 * 7, adapter=S16Adapter()), # 0x92 0x93 - BitField('dig_p4', 0xFFFF << 16 * 6, adapter=S16Adapter()), # 0x94 0x95 - BitField('dig_p5', 0xFFFF << 16 * 5, adapter=S16Adapter()), # 0x96 0x97 - BitField('dig_p6', 0xFFFF << 16 * 4, adapter=S16Adapter()), # 0x98 0x99 - BitField('dig_p7', 0xFFFF << 16 * 3, adapter=S16Adapter()), # 0x9A 0x9B - BitField('dig_p8', 0xFFFF << 16 * 2, adapter=S16Adapter()), # 0x9C 0x9D - BitField('dig_p9', 0xFFFF << 16 * 1, adapter=S16Adapter()), # 0x9E 0x9F - BitField('dig_h1', 0x00FF), # 0xA1 uint8 - ), bit_width=26 * 8), - Register('CALIBRATION2', 0xE1, fields=( - BitField('dig_h2', 0xFFFF0000000000, adapter=S16Adapter()), # 0xE1 0xE2 - BitField('dig_h3', 0x0000FF00000000), # 0xE3 uint8 - BitField('dig_h4', 0x000000FFFF0000, adapter=H4Adapter()), # 0xE4 0xE5[3:0] - BitField('dig_h5', 0x00000000FFFF00, adapter=H5Adapter()), # 0xE5[7:4] 0xE6 - BitField('dig_h6', 0x000000000000FF, adapter=S8Adapter()) # 0xE7 int8 - ), bit_width=7 * 8) - )) - - def setup(self, mode='normal', temperature_oversampling=16, pressure_oversampling=16, humidity_oversampling=16, temperature_standby=500): - if self._is_setup: - return - self._is_setup = True - - self._bme280.select_address(self._i2c_addr) - self._mode = mode - - if mode == "forced": - mode = "sleep" - - try: - chip = self._bme280.get('CHIP_ID') - if chip.id != CHIP_ID: - raise RuntimeError("Unable to find bme280 on 0x{:02x}, CHIP_ID returned {:02x}".format(self._i2c_addr, chip.id)) - except IOError: - raise RuntimeError("Unable to find bme280 on 0x{:02x}, IOError".format(self._i2c_addr)) - - self._bme280.set('RESET', reset=0xB6) - time.sleep(0.1) - - self._bme280.set('CTRL_HUM', osrs_h=humidity_oversampling) - - self._bme280.set('CTRL_MEAS', - mode=mode, - osrs_t=temperature_oversampling, - osrs_p=pressure_oversampling) - - self._bme280.set('CONFIG', - t_sb=temperature_standby, - filter=2) - - self.calibration.set_from_namedtuple(self._bme280.get('CALIBRATION')) - self.calibration.set_from_namedtuple(self._bme280.get('CALIBRATION2')) - - def update_sensor(self): - self.setup() - - if self._mode == "forced": - self._bme280.set('CTRL_MEAS', mode="forced") - while self._bme280.get('STATUS').measuring: - time.sleep(0.001) - - raw = self._bme280.get('DATA') - - self.temperature = self.calibration.compensate_temperature(raw.temperature) - self.pressure = self.calibration.compensate_pressure(raw.pressure) / 100.0 - self.humidity = self.calibration.compensate_humidity(raw.humidity) - - def get_temperature(self): - self.update_sensor() - return self.temperature - - def get_pressure(self): - self.update_sensor() - return self.pressure - - def get_humidity(self): - self.update_sensor() - return self.humidity - - def get_altitude(self, qnh=1013.25): - self.update_sensor() - pressure = self.get_pressure() - altitude = 44330.0 * (1.0 - pow(pressure / qnh, (1.0 / 5.255))) - return altitude diff --git a/library/setup.py b/library/setup.py deleted file mode 100755 index 40d6dbc..0000000 --- a/library/setup.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2016 Pimoroni - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -from setuptools import setup, __version__ -from pkg_resources import parse_version - -minimum_version = parse_version('30.4.0') - -if parse_version(__version__) < minimum_version: - raise RuntimeError("Package setuptools must be at least version {}".format(minimum_version)) - -setup() diff --git a/library/tests/test_compensation.py b/library/tests/test_compensation.py deleted file mode 100644 index 4126a56..0000000 --- a/library/tests/test_compensation.py +++ /dev/null @@ -1,126 +0,0 @@ -TEST_TEMP_RAW = 529191 -TEST_TEMP_CMP = 24.7894877676 - -TEST_PRES_RAW = 326816 -TEST_PRES_CMP = 1006.61517564 -TEST_ALT_CMP = 55.385 - -TEST_HUM_RAW = 30281 -TEST_HUM_CMP = 68.66996648709039 - - -def test_temperature(): - from tools import SMBusFakeDevice - from bme280 import BME280 - from calibration import BME280Calibration - dev = SMBusFakeDevice(1) - - # Load the fake temperature into the virtual registers - dev.regs[0xfc] = (TEST_TEMP_RAW & 0x0000F) << 4 - dev.regs[0xfb] = (TEST_TEMP_RAW & 0x00FF0) >> 4 - dev.regs[0xfa] = (TEST_TEMP_RAW & 0xFF000) >> 12 - - bme280 = BME280(i2c_dev=dev) - bme280.setup() - - # Replace the loaded calibration with our known values - bme280.calibration = BME280Calibration() - - assert round(bme280.get_temperature(), 4) == round(TEST_TEMP_CMP, 4) - - -def test_temperature_forced(): - from tools import SMBusFakeDevice - from bme280 import BME280 - from calibration import BME280Calibration - dev = SMBusFakeDevice(1) - - # Load the fake temperature into the virtual registers - dev.regs[0xfc] = (TEST_TEMP_RAW & 0x0000F) << 4 - dev.regs[0xfb] = (TEST_TEMP_RAW & 0x00FF0) >> 4 - dev.regs[0xfa] = (TEST_TEMP_RAW & 0xFF000) >> 12 - - bme280 = BME280(i2c_dev=dev) - bme280.setup(mode="forced") - - # Replace the loaded calibration with our known values - bme280.calibration = BME280Calibration() - - assert round(bme280.get_temperature(), 4) == round(TEST_TEMP_CMP, 4) - - -def test_pressure(): - from tools import SMBusFakeDevice - from bme280 import BME280 - from calibration import BME280Calibration - dev = SMBusFakeDevice(1) - - # Load the fake temperature values into the virtual registers - # Pressure is temperature compensated!!! - dev.regs[0xfc] = (TEST_TEMP_RAW & 0x0000F) << 4 - dev.regs[0xfb] = (TEST_TEMP_RAW & 0x00FF0) >> 4 - dev.regs[0xfa] = (TEST_TEMP_RAW & 0xFF000) >> 12 - - # Load the fake pressure values - dev.regs[0xf9] = (TEST_PRES_RAW & 0x0000F) << 4 - dev.regs[0xf8] = (TEST_PRES_RAW & 0x00FF0) >> 4 - dev.regs[0xf7] = (TEST_PRES_RAW & 0xFF000) >> 12 - - bme280 = BME280(i2c_dev=dev) - bme280.setup() - - # Replace the loaded calibration with our known values - bme280.calibration = BME280Calibration() - - assert round(bme280.get_pressure(), 4) == round(TEST_PRES_CMP, 4) - - -def test_altitude(): - from tools import SMBusFakeDevice - from bme280 import BME280 - from calibration import BME280Calibration - dev = SMBusFakeDevice(1) - - # Load the fake temperature values into the virtual registers - # Pressure is temperature compensated!!! - dev.regs[0xfc] = (TEST_TEMP_RAW & 0x0000F) << 4 - dev.regs[0xfb] = (TEST_TEMP_RAW & 0x00FF0) >> 4 - dev.regs[0xfa] = (TEST_TEMP_RAW & 0xFF000) >> 12 - - # Load the fake pressure values - dev.regs[0xf9] = (TEST_PRES_RAW & 0x0000F) << 4 - dev.regs[0xf8] = (TEST_PRES_RAW & 0x00FF0) >> 4 - dev.regs[0xf7] = (TEST_PRES_RAW & 0xFF000) >> 12 - - bme280 = BME280(i2c_dev=dev) - bme280.setup() - - # Replace the loaded calibration with our known values - bme280.calibration = BME280Calibration() - - assert round(bme280.get_altitude(), 4) == round(TEST_ALT_CMP, 4) - - -def test_humidity(): - from tools import SMBusFakeDevice - from bme280 import BME280 - from calibration import BME280Calibration - dev = SMBusFakeDevice(1) - - # Load the fake temperature values into the virtual registers - # Humidity is temperature compensated!!! - dev.regs[0xfc] = (TEST_TEMP_RAW & 0x0000F) << 4 - dev.regs[0xfb] = (TEST_TEMP_RAW & 0x00FF0) >> 4 - dev.regs[0xfa] = (TEST_TEMP_RAW & 0xFF000) >> 12 - - # Load the fake humidity values - dev.regs[0xfd] = TEST_HUM_RAW >> 8 - dev.regs[0xfe] = TEST_HUM_RAW & 0xFF - - bme280 = BME280(i2c_dev=dev) - bme280.setup() - - # Replace the loaded calibration with our known values - bme280.calibration = BME280Calibration() - - assert round(bme280.get_humidity(), 4) == round(TEST_HUM_CMP, 4) diff --git a/library/tests/test_setup.py b/library/tests/test_setup.py deleted file mode 100644 index 3e800ca..0000000 --- a/library/tests/test_setup.py +++ /dev/null @@ -1,31 +0,0 @@ -import sys -import mock -import pytest - - -def test_setup_not_present(): - sys.modules['smbus'] = mock.MagicMock() - from bme280 import BME280 - bme280 = BME280() - with pytest.raises(RuntimeError): - bme280.setup() - - -def test_setup_mock_present(): - from tools import SMBusFakeDevice - smbus = mock.Mock() - smbus.SMBus = SMBusFakeDevice - sys.modules['smbus'] = smbus - from bme280 import BME280 - bme280 = BME280() - bme280.setup() - - -def test_setup_forced_mode(): - from tools import SMBusFakeDevice - smbus = mock.Mock() - smbus.SMBus = SMBusFakeDevice - sys.modules['smbus'] = smbus - from bme280 import BME280 - bme280 = BME280() - bme280.setup(mode="forced") diff --git a/library/tests/tools.py b/library/tests/tools.py deleted file mode 100644 index c64d6f8..0000000 --- a/library/tests/tools.py +++ /dev/null @@ -1,7 +0,0 @@ -from i2cdevice import MockSMBus - - -class SMBusFakeDevice(MockSMBus): - def __init__(self, i2c_bus): - MockSMBus.__init__(self, i2c_bus) - self.regs[0xD0] = 0x60 # Fake chip ID diff --git a/library/tox.ini b/library/tox.ini deleted file mode 100644 index a8f3249..0000000 --- a/library/tox.ini +++ /dev/null @@ -1,24 +0,0 @@ -[tox] -envlist = py{27,35,37,39},qa -skip_missing_interpreters = True - -[testenv] -commands = - python setup.py install - coverage run -m py.test -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 sdist bdist_wheel - twine check dist/* - flake8 --ignore E501 -deps = - check-manifest - flake8 - twine diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..438adfb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,118 @@ +[build-system] +requires = ["hatchling", "hatch-fancy-pypi-readme"] +build-backend = "hatchling.build" + +[project] +name = "pimoroni-bme280" +dynamic = ["version", "readme"] +description = "Python library for the bme280 temperature, pressure and humidity sensor" +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 = [ + "i2cdevice>=1.0.0" +] + +[project.urls] +GitHub = "https://www.github.com/pimoroni/bme280-python" +Homepage = "https://www.pimoroni.com" + +[tool.hatch.version] +path = "bme280/__init__.py" + +[tool.hatch.build] +include = [ + "bme280", + "README.md", + "CHANGELOG.md", + "LICENSE" +] + +[tool.hatch.build.targets.sdist] +include = [ + "*" +] +exclude = [ + ".*", + "dist" +] + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/markdown" +fragments = [ + { path = "README.md" }, + { text = "\n" }, + { path = "CHANGELOG.md" } +] + +[tool.ruff] +exclude = [ + '.tox', + '.egg', + '.git', + '__pycache__', + 'build', + 'dist' +] +line-length = 200 + +[tool.codespell] +skip = """ +./.tox,\ +./.egg,\ +./.git,\ +./__pycache__,\ +./build,\ +./dist.\ +""" + +[tool.isort] +line_length = 200 + +[tool.black] +line_length = 200 + +[tool.check-manifest] +ignore = [ + '.stickler.yml', + 'boilerplate.md', + 'check.sh', + 'install.sh', + 'uninstall.sh', + 'Makefile', + 'tox.ini', + 'tests/*', + 'examples/*', + '.coveragerc', + 'requirements-dev.txt' +] + +[tool.pimoroni] +apt_packages = [] +configtxt = [] +commands = [] 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/setup.cfg b/setup.cfg similarity index 100% rename from library/setup.cfg rename to setup.cfg diff --git a/library/tests/calibration.py b/tests/calibration.py similarity index 99% rename from library/tests/calibration.py rename to tests/calibration.py index 1b231c0..b3a7665 100644 --- a/library/tests/calibration.py +++ b/tests/calibration.py @@ -3,6 +3,7 @@ class BME280Calibration(bme280.BME280Calibration): """Prefil the calibration class with known values.""" + def __init__(self): bme280.BME280Calibration.__init__(self) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..984648c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,35 @@ +import sys + +import mock +import pytest +from i2cdevice import MockSMBus + + +class SMBusFakeDevice(MockSMBus): + def __init__(self, i2c_bus): + MockSMBus.__init__(self, i2c_bus) + self.regs[0xD0] = 0x60 # Fake chip ID + + +@pytest.fixture(scope="function", autouse=False) +def bme280(): + import bme280 + yield bme280 + del sys.modules["bme280"] + + +@pytest.fixture(scope="function", autouse=False) +def smbus2_mock(): + smbus = mock.Mock() + smbus.SMBus = SMBusFakeDevice + sys.modules["smbus2"] = smbus + yield smbus + del sys.modules["smbus2"] + + +@pytest.fixture(scope="function", autouse=False) +def smbus2(): + smbus = mock.Mock() + sys.modules["smbus2"] = smbus + yield smbus + del sys.modules["smbus2"] diff --git a/tests/test_compensation.py b/tests/test_compensation.py new file mode 100644 index 0000000..964ee98 --- /dev/null +++ b/tests/test_compensation.py @@ -0,0 +1,121 @@ +TEST_TEMP_RAW = 529191 +TEST_TEMP_CMP = 24.7894877676 + +TEST_PRES_RAW = 326816 +TEST_PRES_CMP = 1006.61517564 +TEST_ALT_CMP = 55.385 + +TEST_HUM_RAW = 30281 +TEST_HUM_CMP = 68.66996648709039 + + +def test_temperature(smbus2_mock, bme280): + from calibration import BME280Calibration + + dev = smbus2_mock.SMBus(1) + + # Load the fake temperature into the virtual registers + dev.regs[0xFC] = (TEST_TEMP_RAW & 0x0000F) << 4 + dev.regs[0xFB] = (TEST_TEMP_RAW & 0x00FF0) >> 4 + dev.regs[0xFA] = (TEST_TEMP_RAW & 0xFF000) >> 12 + + sensor = bme280.BME280(i2c_dev=dev) + sensor.setup() + + # Replace the loaded calibration with our known values + sensor.calibration = BME280Calibration() + + assert round(sensor.get_temperature(), 4) == round(TEST_TEMP_CMP, 4) + + +def test_temperature_forced(smbus2_mock, bme280): + from calibration import BME280Calibration + + dev = smbus2_mock.SMBus(1) + + # Load the fake temperature into the virtual registers + dev.regs[0xFC] = (TEST_TEMP_RAW & 0x0000F) << 4 + dev.regs[0xFB] = (TEST_TEMP_RAW & 0x00FF0) >> 4 + dev.regs[0xFA] = (TEST_TEMP_RAW & 0xFF000) >> 12 + + sensor = bme280.BME280(i2c_dev=dev) + sensor.setup(mode="forced") + + # Replace the loaded calibration with our known values + sensor.calibration = BME280Calibration() + + assert round(sensor.get_temperature(), 4) == round(TEST_TEMP_CMP, 4) + + +def test_pressure(smbus2_mock, bme280): + from calibration import BME280Calibration + + dev = smbus2_mock.SMBus(1) + + # Load the fake temperature values into the virtual registers + # Pressure is temperature compensated!!! + dev.regs[0xFC] = (TEST_TEMP_RAW & 0x0000F) << 4 + dev.regs[0xFB] = (TEST_TEMP_RAW & 0x00FF0) >> 4 + dev.regs[0xFA] = (TEST_TEMP_RAW & 0xFF000) >> 12 + + # Load the fake pressure values + dev.regs[0xF9] = (TEST_PRES_RAW & 0x0000F) << 4 + dev.regs[0xF8] = (TEST_PRES_RAW & 0x00FF0) >> 4 + dev.regs[0xF7] = (TEST_PRES_RAW & 0xFF000) >> 12 + + sensor = bme280.BME280(i2c_dev=dev) + sensor.setup() + + # Replace the loaded calibration with our known values + sensor.calibration = BME280Calibration() + + assert round(sensor.get_pressure(), 4) == round(TEST_PRES_CMP, 4) + + +def test_altitude(smbus2_mock, bme280): + from calibration import BME280Calibration + + dev = smbus2_mock.SMBus(1) + + # Load the fake temperature values into the virtual registers + # Pressure is temperature compensated!!! + dev.regs[0xFC] = (TEST_TEMP_RAW & 0x0000F) << 4 + dev.regs[0xFB] = (TEST_TEMP_RAW & 0x00FF0) >> 4 + dev.regs[0xFA] = (TEST_TEMP_RAW & 0xFF000) >> 12 + + # Load the fake pressure values + dev.regs[0xF9] = (TEST_PRES_RAW & 0x0000F) << 4 + dev.regs[0xF8] = (TEST_PRES_RAW & 0x00FF0) >> 4 + dev.regs[0xF7] = (TEST_PRES_RAW & 0xFF000) >> 12 + + sensor = bme280.BME280(i2c_dev=dev) + sensor.setup() + + # Replace the loaded calibration with our known values + sensor.calibration = BME280Calibration() + + assert round(sensor.get_altitude(), 4) == round(TEST_ALT_CMP, 4) + + +def test_humidity(smbus2_mock, bme280): + from calibration import BME280Calibration + + dev = smbus2_mock.SMBus(1) + + # Load the fake temperature values into the virtual registers + # Humidity is temperature compensated!!! + dev.regs[0xFC] = (TEST_TEMP_RAW & 0x0000F) << 4 + dev.regs[0xFB] = (TEST_TEMP_RAW & 0x00FF0) >> 4 + dev.regs[0xFA] = (TEST_TEMP_RAW & 0xFF000) >> 12 + + # Load the fake humidity values + dev.regs[0xFD] = TEST_HUM_RAW >> 8 + dev.regs[0xFE] = TEST_HUM_RAW & 0xFF + + sensor = bme280.BME280(i2c_dev=dev) + sensor.setup() + + # Replace the loaded calibration with our known values + sensor.calibration = BME280Calibration() + + assert round(sensor.get_humidity(), 4) == round(TEST_HUM_CMP, 4) diff --git a/tests/test_setup.py b/tests/test_setup.py new file mode 100644 index 0000000..613a7ef --- /dev/null +++ b/tests/test_setup.py @@ -0,0 +1,21 @@ +import pytest + + +def test_setup_not_present(smbus2_mock, bme280): + dev = smbus2_mock.SMBus(1) + dev.regs[0xD0] = 0x00 # Incorrect chip ID + + sensor = bme280.BME280(i2c_dev=dev) + with pytest.raises(RuntimeError): + sensor.setup() + + +def test_setup_mock_present(smbus2_mock, bme280): + sensor = bme280.BME280() + sensor.setup() + + +def test_setup_forced_mode(smbus2_mock, bme280): + + sensor = bme280.BME280() + sensor.setup(mode="forced") 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 0709cc0..f213fc5 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,25 +1,72 @@ #!/bin/bash -LIBRARY_VERSION=`cat library/setup.py | grep version | awk -F"'" '{print $2}'` -LIBRARY_NAME=`cat library/setup.py | 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)" +} + +warning() { + echo -e "$(tput setaf 1)$1$(tput sgr0)" +} -cd .. +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"