The question was simple: what’s the temperature and humidity in my home office right now? Not the weather outside, not a city average — the actual conditions in the room where I spend most of my day. Opening a weather app for that felt wrong.

A Raspberry Pi was already running on the shelf. A BME280 sensor costs around €10. This should have been a weekend project.

It mostly was, except for the part where I assumed reading a temperature sensor meant reading a register.

:electric_plug: Four wires and a chip

The Bosch BME280 measures temperature, humidity, and atmospheric pressure over I²C. Four wires to the Raspberry Pi GPIO pins, enable I²C in raspi-config, and the sensor shows up at address 0x77 on the bus:

i2cdetect -y 1

That’s the easy part. The catch is what happens next.

:abacus: You don’t just read the temperature

The BME280 doesn’t hand you 21.5°C. It gives you raw ADC values: 20-bit integers that mean absolutely nothing by themselves. To get an actual temperature, you have to:

  1. Read the calibration coefficients Bosch burned into the chip’s EEPROM at the factory (registers 0x88, 0xA1, 0xE1)
  2. Apply Bosch’s compensation formulas: double-precision floating point arithmetic that uses those coefficients to turn raw values into real measurements
  3. Wait for the measurement to finish by polling the status register

The temperature compensation alone takes the raw value, applies a quadratic correction with three calibration constants, and spits out a value in hundredths of degrees Celsius. Pressure depends on the corrected temperature. Humidity depends on both.

It’s all straight from the Bosch datasheet, nothing clever. But it does mean the driver isn’t a five-liner. It’s implementing a spec, not importing a library.

:globe_with_meridians: Making it network-accessible

Once the driver worked, the next question was how to get those values into Home Assistant. The simplest path: a Flask API with two endpoints.

GET /bme280 returns the current reading as JSON. GET /bme280/publish reads the sensor and pushes the three values to an MQTT broker. A cron job on the Pi calls the publish endpoint every few minutes, and Home Assistant picks up the values in real time.

The MQTT discovery mechanism made the Home Assistant side almost frictionless. One mosquitto_pub command per sensor type — publishing a JSON config payload to the right topic — and the entities appear automatically in the UI. No configuration.yaml editing, no restart required.

BME280  ──I²C──►  bme280.py  ──►  Flask API  ──MQTT──►  Home Assistant

The full setup guide is in the repo.

:bulb: What I didn’t expect

:thermometer: The Bosch calibration is non-negotiable. I started by reading the raw temperature register directly and scaling it naively. The result was numbers that looked almost plausible and were completely wrong. The compensation algorithm isn’t optional decoration, it’s what makes the output mean anything.

:clock1: Polling beats events here. The sensor doesn’t push data, you ask it for a reading. A cron job every minute is all you need for room monitoring. Real-time streaming would be overkill and would probably wear out the sensor faster.

:house: MQTT discovery is underrated. Manually declaring sensors in configuration.yaml works, but auto-discovery just feels right. Publish a config payload once, and Home Assistant takes it from there. Adding a new sensor type later takes about thirty seconds.

The room is now 21.4°C and 47% humidity. I know this without opening anything.

:warning: A note on the official Bosch SensorAPI

While writing the driver I peeked at the official Bosch SensorAPI for reference. Two things caught my attention.

The Linux userspace example doesn’t actually work on a Raspberry Pi out of the box. Several contributors tripped over the same bug independently: ioctl is called before dev_addr is assigned, so the I²C device address never gets set properly. The fix is obvious once you see it, and multiple PRs documented it, but they sat open for years. Some still are.

Then there’s PR #94 (still open as of early 2025), reporting undefined behavior in bme280_get_sensor_mode(): the left operand of a bitwise & is an uninitialized variable, caught by static analysis.

The chip itself is great. But manufacturer reference code is a starting point, not gospel. Implementing the compensation algorithm straight from the datasheet meant I understood every line of it. When a reading looks weird, there’s no mystery C library to blame.

guillaumedelre/bme280

Python driver for the BME280 sensor — temperature, humidity, and pressure over I²C, with MQTT publishing and Home Assistant integration.