DIY ESP32 + ESPHome
BME280 over WiFi, ESPHome YAML
A bring-your-own-hardware path for tinkerers and anyone whose sensor mix is not covered by a commercial unit. Total bill of materials: ≈ €8.
BOM
- ESP32 dev board: WROOM-32 or ESP32-S3-DevKit, ~€4.
- BME280 breakout (temperature + humidity + pressure), ~€2.
- 18650 holder + cell, optional, ~€2.
- Four jumper wires.
The BME280 talks I²C. Default address on most breakouts is 0x76.
Wire
| BME280 | ESP32 (WROOM) |
|---|---|
| VCC | 3V3 |
| GND | GND |
| SDA | GPIO 21 |
| SCL | GPIO 22 |
If you bought the BMP280 by mistake (no humidity, only T + P), the wiring
is the same and ESPHome's bmp280 platform works in place of bme280.
Flash ESPHome
Install the ESPHome CLI (pip install esphome or use the Docker image).
Save the following to opensense-fridge.yaml:
esphome:
name: opensense-fridge
friendly_name: Walk-in fridge
esp32:
board: esp32dev
framework:
type: arduino
logger:
level: INFO
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_pwd
manual_ip:
static_ip: 192.168.1.42
gateway: 192.168.1.1
subnet: 255.255.255.0
api:
encryption:
key: !secret api_key
ota:
- platform: esphome
i2c:
sda: 21
scl: 22
scan: true
sensor:
- platform: bme280_i2c
address: 0x76
update_interval: 60s
temperature:
name: "Temperature"
id: temp
humidity:
name: "Humidity"
id: hum
pressure:
name: "Pressure"
id: pres
http_request:
useragent: opensense-esp32
timeout: 10s
interval:
- interval: 60s
then:
- http_request.post:
url: !secret opensense_ingest_url
headers:
Content-Type: application/json
body: |-
{
"ts": "",
"measurements":
{ "type": "temperature", "value": },
{ "type": "humidity", "value": },
{ "type": "pressure", "value": }
}
Create a secrets.yaml next to it:
wifi_ssid: 'CafeBratislava'
wifi_pwd: 'pourover4eva'
api_key: '<32-byte base64 from esphome wizard or openssl rand -base64 32>'
opensense_ingest_url: 'https://api.opensense.murzin.digital/v1/ingest?token=ds_live_…&device=fridge01'
Compile and flash:
esphome run opensense-fridge.yaml
After the first boot the ESP32 starts POSTing to OpenSense every 60 s. Add
the device via + ADD DEVICE → DIY → ESP32 to receive the ingest URL.
Deep sleep (battery operation)
For battery operation, replace the interval block with a deep-sleep
strategy:
deep_sleep:
run_duration: 8s
sleep_duration: 600s # 10 minutes
id: ds
Trigger one read+post on boot, then sleep
on_boot:
- priority: -100.0
then:
- sensor.template.publish:
id: temp
state: !lambda 'return id(bme280).temperature->state;'
- http_request.post: …
- deep_sleep.enter: ds
A WROOM-32 in this regime draws < 12 µA during sleep; a 3000 mAh 18650 runs for about 6 months at 10-minute cadence. The ESP32-S3 is worse; prefer WROOM-32 or ESP32-C3 for battery sensors.
Multiple probes
The BME280 is one I²C address. To run two BME280s on the same ESP32,
solder the SDO line of the second one to 3V3 to switch its I²C address
to 0x77. In the YAML:
sensor:
- platform: bme280_i2c
address: 0x76
temperature: { name: "Fridge front" }
…
- platform: bme280_i2c
address: 0x77
temperature: { name: "Fridge back" }
…
In the JSON POST, include a label per measurement so OpenSense can map
to two channels:
{
"measurements": [
{ "type": "temperature", "value": 4.1, "label": "front" },
{ "type": "temperature", "value": 4.3, "label": "back" }
]
}
Calibration
If you need calibrated readings (lab / pharmacy), pair this ESP32 with a calibrated thermistor (PT100 + MAX31865, or a Sensirion SHT45 instead of the BME280). Run a single-point calibration in ice water at 0 °C and a two-point against a calibrated reference. Apply the offset in ESPHome:
sensor:
- platform: bme280_i2c
temperature:
filters:
- offset: -0.3 # measured at 0 °C against a Fluke 1551A
The OpenSense data model has no concept of "raw vs calibrated" — whatever your device sends is what is stored. If you need to record the calibration event, attach it to the device page as a free-text note. We may formalise calibration as a first-class object later.