diff --git a/README.md b/README.md index 7a6992c..cc64217 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,492 @@ -# bme280 +# 🌑️ BME280 Environmental Sensor -## BME280 driver +[![CI](https://github.com/guillaumedelre/bme280/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/guillaumedelre/bme280/actions/workflows/ci.yml) +[![Python](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/downloads/) +[![Flask](https://img.shields.io/badge/flask-3.x-lightgrey.svg)](https://flask.palletsprojects.com/) +[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) -Run `python bme280.py` in a shell to get the sensor's data, it will output: +> Python 3 driver for the Bosch BME280 environmental sensor β€” reads temperature, pressure and humidity over IΒ²C on a Raspberry Pi, and exposes them via a REST API with MQTT publishing for Home Assistant integration. +--- + +## πŸ“‹ Table of Contents + +- [Hardware](#-hardware) +- [Architecture](#-architecture) +- [Installation](#-installation) +- [Configuration](#️-configuration) +- [Usage](#-usage) + - [CLI](#cli) + - [HTTP API](#http-api) + - [Docker](#docker) +- [API Reference](#-api-reference) +- [Home Assistant Integration](#-home-assistant-integration) +- [Development](#-development) +- [Project Structure](#-project-structure) + +--- + +## πŸ”Œ Hardware + +| Component | Details | +|-----------|---------| +| **Sensor** | Bosch BME280 β€” Waveshare Environmental Sensor (SKU 15231) | +| **Board** | Raspberry Pi Rev 2+ (IΒ²C bus 1) | +| **Interface** | IΒ²C β€” default address `0x77`, alternate `0x76` (SDO pin) | +| **Protocol** | SMBus via `/dev/i2c-1` | + +### Wiring (Raspberry Pi GPIO) + +``` +BME280 Raspberry Pi +────── ──────────── +VCC ──────► Pin 1 (3.3V) +GND ──────► Pin 6 (GND) +SDA ──────► Pin 3 (GPIO2 / SDA1) +SCL ──────► Pin 5 (GPIO3 / SCL1) +``` + +> πŸ’‘ Make sure IΒ²C is enabled: `sudo raspi-config` β†’ Interface Options β†’ I2C β†’ Enable + +--- + +## πŸ—οΈ Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Raspberry Pi β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ bme280.py β”‚ β”‚ sensor_api.py β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ (Flask) β”‚ β”‚ +β”‚ β”‚ IΒ²C driver │◄────│ GET /bme280 β”‚ β”‚ +β”‚ β”‚ Calibration β”‚ β”‚ GET /bme280/publish β”‚ β”‚ +β”‚ β”‚ algorithms β”‚ β”‚ GET /health β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β–Ό β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ BME280 chip β”‚ β”‚ MQTT Broker β”‚ β”‚ +β”‚ β”‚ (IΒ²C 0x77) β”‚ β”‚ (Home Assistant) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## πŸ“¦ Installation + +### Prerequisites + +```bash +# Enable IΒ²C on Raspberry Pi +sudo raspi-config # Interface Options β†’ I2C β†’ Enable +sudo reboot +``` + +### Clone & install + +```bash +git clone https://github.com/guillaumedelre/bme280.git +cd bme280 + +pip install -r requirements.txt +``` + +### Configure + +```bash +cp .env.example .env +# Edit .env with your MQTT broker details +nano .env +``` + +--- + +## βš™οΈ Configuration + +All settings are driven by environment variables. Copy `.env.example` to `.env` and fill in your values. + +| Variable | Default | Description | +|----------|---------|-------------| +| `BME280_I2C_BUS` | `1` | IΒ²C bus number (`1` for Pi Rev 2+, `0` for Rev 1) | +| `BME280_I2C_ADDRESS` | `0x77` | Sensor IΒ²C address (`0x77` or `0x76` depending on SDO pin) | +| `MQTT_BROKER_HOST` | `localhost` | IP or hostname of your MQTT broker | +| `MQTT_USERNAME` | _(empty)_ | MQTT username (leave empty for anonymous) | +| `MQTT_PASSWORD` | _(empty)_ | MQTT password | +| `MQTT_CLIENT_ID` | `rpi-bme280` | MQTT client identifier | +| `FLASK_PORT` | `5000` | HTTP port for the API | +| `FLASK_DEBUG` | `false` | Enable Flask debug mode (`true` / `false`) | + +**Example `.env`:** + +```ini +BME280_I2C_BUS=1 +BME280_I2C_ADDRESS=0x77 + +MQTT_BROKER_HOST=192.168.1.10 +MQTT_USERNAME=homeassistant +MQTT_PASSWORD=your_password_here +MQTT_CLIENT_ID=rpi-bme280 + +FLASK_PORT=5000 +FLASK_DEBUG=false +``` + +--- + +## πŸš€ Usage + +### CLI + +Read sensor data directly from the terminal (requires hardware): + +```bash +python bme280.py ``` -Chip ID : 96 -Version : 0 -Temperature : 21.04 Β°C -Pressure : 1001.86890002 hPa -Humidity : 52.7615936619 %RH + +``` +Chip ID : 96 +Version : 0 +Temperature : 21.55 Β°C +Pressure : 1005.16 hPa +Humidity : 44.57 %RH +``` + +Use an alternate IΒ²C address: + +```bash +BME280_I2C_ADDRESS=0x76 python bme280.py ``` -## Http API +### HTTP API -Run `python sensor-api.py` to expose the http api on port `5000`. +Start the API server: -### Endpoints +```bash +python sensor_api.py +# Listening on http://0.0.0.0:5000 +``` -| Path | Status | Data | Comment | -|---|---|---|---| -| `/` | 200 | `{}` | nc | -| `/bme280` | 200 | [Sensor Resource](#sensor-resource) | return the sensor resource with measure | -| `/bme280/publish` | 200 | `{}` | publish in mqtt the sensor measureΒ | +```bash +# Health check +curl http://rpi.local:5000/health -### Resource +# Read sensor data +curl http://rpi.local:5000/bme280 | python -m json.tool +# Publish to MQTT +curl http://rpi.local:5000/bme280/publish +``` + +#### Automate with cron + +Publish sensor data every minute via cron: + +```bash +crontab -e +``` + +```cron +* * * * * curl -s http://localhost:5000/bme280/publish >> /var/log/bme280.log 2>&1 +``` + +### Docker + +#### Run the API in Docker (Raspberry Pi) + +```bash +# Build the app image +docker build --target app -t bme280-app . + +# Run with IΒ²C device passthrough and env file +docker run -d \ + --name bme280 \ + --device /dev/i2c-1:/dev/i2c-1 \ + --env-file .env \ + -p 5000:5000 \ + bme280-app +``` + +Or with docker compose: + +```bash +# Edit docker-compose.yml and uncomment the devices section +docker compose up app +``` + +#### Run tests in Docker (no hardware required) + +```bash +docker compose run --rm test +# or +docker build --target test -t bme280-test . && docker run --rm bme280-test +``` + +--- + +## πŸ“‘ API Reference + +### `GET /health` + +Service health check. + +**Response `200`:** +```json +{ + "status": "ok" +} +``` + +--- + +### `GET /` + +Root endpoint. + +**Response `200`:** ```json +{} +``` + +--- + +### `GET /bme280` + +Returns current sensor readings with full device metadata. +**Response `200`:** +```json { - "sku": 15231, "name": "bme280", "brand": "Waveshare", + "part_number": "BME280 Environmental Sensor", + "sku": 15231, "upc": 614961952638, + "chip": { + "id": 96, + "version": 0 + }, "capabilities": { - "pressure": { - "unit_of_measurement": "hPa", - "max": 1100, - "accuracy": 0.0018, - "resolution": 0.008, - "min": 300 - }, "temperature": { - "unit_of_measurement": "\u00b0C", + "unit_of_measurement": "Β°C", + "min": -40, "max": 85, - "accuracy": 1, "resolution": 0.01, - "min": -40 + "accuracy": 1 }, "humidity": { "unit_of_measurement": "%RH", + "min": 0, "max": 100, - "accuracy": 3, "resolution": 0.008, - "min": 0 + "accuracy": 3 + }, + "pressure": { + "unit_of_measurement": "hPa", + "min": 300, + "max": 1100, + "resolution": 0.008, + "accuracy": 0.0018 } }, - "part_number": "BME280 Environmental Sensor", "data": { - "pressure": 1005.1562396432168, "temperature": 21.55, - "humidity": 44.566495623372525 - }, - "chip": { - "version": 0, - "id": 96 + "humidity": 44.57, + "pressure": 1005.16 } } ``` -## Setup the crontab +**Response `503`** β€” sensor unavailable (IΒ²C error): +```json +{ + "error": "Sensor unavailable", + "detail": "[Errno 2] No such file or directory: '/dev/i2c-1'" +} +``` + +--- + +### `GET /bme280/publish` + +Reads sensor data and publishes each measurement to the configured MQTT broker. + +**MQTT topics published:** + +| Topic | Payload | Example | +|-------|---------|---------| +| `sensor/bme280_temperature` | float string | `21.55` | +| `sensor/bme280_humidity` | float string | `44.57` | +| `sensor/bme280_pressure` | float string | `1005.16` | + +**Response `200`:** +```json +{ + "published": true, + "topics": [ + "sensor/bme280_temperature", + "sensor/bme280_humidity", + "sensor/bme280_pressure" + ] +} +``` + +**Response `503`** β€” sensor unavailable: +```json +{ + "error": "Sensor unavailable", + "detail": "..." +} +``` + +**Response `502`** β€” MQTT broker unreachable: +```json +{ + "error": "MQTT publish failed", + "detail": "Connection refused" +} +``` + +--- + +## 🏠 Home Assistant Integration + +### Manual MQTT sensors + +Add to your `configuration.yaml`: + +```yaml +mqtt: + sensor: + - name: "BME280 Temperature" + state_topic: "sensor/bme280_temperature" + unit_of_measurement: "Β°C" + device_class: temperature + state_class: measurement + + - name: "BME280 Humidity" + state_topic: "sensor/bme280_humidity" + unit_of_measurement: "%" + device_class: humidity + state_class: measurement + + - name: "BME280 Pressure" + state_topic: "sensor/bme280_pressure" + unit_of_measurement: "hPa" + device_class: atmospheric_pressure + state_class: measurement +``` + +### Automation example + +Trigger an alert when humidity exceeds 70%: + +```yaml +automation: + - alias: "High humidity alert" + trigger: + - platform: numeric_state + entity_id: sensor.bme280_humidity + above: 70 + action: + - service: notify.mobile_app + data: + message: "⚠️ Humidity is {{ states('sensor.bme280_humidity') }}%" +``` + +--- + +## πŸ› οΈ Development + +### Install dev dependencies + +```bash +pip install -r requirements-dev.txt +``` + +### Run the test suite + +```bash +pytest tests/ -v +``` + +``` +tests/test_api.py::test_health_returns_ok PASSED +tests/test_api.py::test_index_returns_empty_json PASSED +tests/test_api.py::test_bme280_returns_sensor_data PASSED +tests/test_api.py::test_bme280_returns_503_when_sensor_unavailable PASSED +tests/test_api.py::test_publish_returns_200_with_topics PASSED +tests/test_api.py::test_publish_returns_503_when_sensor_unavailable PASSED +tests/test_api.py::test_publish_returns_502_when_mqtt_fails PASSED +tests/test_bme280.py::test_get_short_positive PASSED +tests/test_bme280.py::test_get_short_negative PASSED +tests/test_bme280.py::test_get_ushort PASSED +tests/test_bme280.py::test_get_char_positive PASSED +tests/test_bme280.py::test_get_char_negative PASSED +tests/test_bme280.py::test_get_uchar PASSED +tests/test_bme280.py::test_read_id_returns_chip_id_and_version PASSED +tests/test_bme280.py::test_read_id_custom_chip_id PASSED +tests/test_bme280.py::test_sensor_returns_required_keys PASSED +tests/test_bme280.py::test_sensor_data_has_three_measurements PASSED +tests/test_bme280.py::test_humidity_clamped_within_range PASSED +tests/test_bme280.py::test_pressure_zero_when_p1_calibration_is_zero PASSED + +19 passed in 0.22s +``` + +> πŸ’‘ All tests run without physical hardware β€” `smbus2` is fully mocked via `unittest.mock`. + +### Run tests in Docker + +```bash +docker compose run --rm test +``` + +--- + +## πŸ“ Project Structure + +``` +bme280/ +β”œβ”€β”€ πŸ“„ bme280.py # IΒ²C driver β€” Bosch calibration algorithms +β”œβ”€β”€ πŸ“„ sensor_api.py # Flask HTTP API + MQTT publisher +β”‚ +β”œβ”€β”€ πŸ§ͺ tests/ +β”‚ β”œβ”€β”€ test_bme280.py # Driver unit tests (hardware mocked) +β”‚ └── test_api.py # API route tests (Flask test client) +β”‚ +β”œβ”€β”€ 🐳 Dockerfile # Multi-stage: test / app +β”œβ”€β”€ 🐳 docker-compose.yml # test + app services +β”‚ +β”œβ”€β”€ βš™οΈ .env.example # Configuration template +β”œβ”€β”€ πŸ“‹ requirements.txt # Runtime dependencies +β”œβ”€β”€ πŸ“‹ requirements-dev.txt # Dev/test dependencies +β”œβ”€β”€ πŸ”§ pytest.ini # Pytest configuration +└── πŸ”’ .gitignore +``` + +### Key dependencies + +| Package | Version | Role | +|---------|---------|------| +| `smbus2` | β‰₯ 0.4.3 | IΒ²C communication | +| `flask` | β‰₯ 3.0.0 | HTTP API framework | +| `paho-mqtt` | β‰₯ 1.6.1 | MQTT client | +| `python-dotenv` | β‰₯ 1.0.0 | `.env` file loading | + +--- + +## πŸ“Š Sensor Specifications + +| Measurement | Range | Resolution | Accuracy | +|-------------|-------|------------|----------| +| 🌑️ Temperature | -40 Β°C β†’ +85 Β°C | 0.01 Β°C | Β± 1 Β°C | +| πŸ’§ Humidity | 0 β†’ 100 %RH | 0.008 %RH | Β± 3 %RH | +| πŸ”΅ Pressure | 300 β†’ 1100 hPa | 0.008 hPa | Β± 0.0018 hPa | + +--- -This is a cron job that actually sends the data via mqtt by calling the API every minute. +*Bosch BME280 datasheet: [BST-BME280-DS002][bme280-datasheet]* -`* * * * * curl 192.168.86.31:5000/bme280/publish` +[bme280-datasheet]: https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme280-ds002.pdf