IoT / Web
Advanced
Battery-Powered Sensor: BME280 + MQTT + Deep Sleep for Months of Runtime
The board wakes every 10 minutes, measures temperature, humidity, pressure and battery level, publishes to MQTT and goes back to sleep. We cover how to squeeze the most out of a battery.
The goal
Put a sensor on a balcony or in a greenhouse with no power outlet and not touch the battery for months. The key is deep sleep: the ESP32 draws ~10 µA in this mode and only wakes up for 2–3 seconds to measure and send data.
Parts
- A board with low sleep current: FireBeetle ESP32, LOLIN D32, TinyPICO, or a bare ESP32-WROOM module with an HT7333/MCP1700 regulator. Regular DevKits with a USB-UART chip and AMS1117 draw 5–15 mA even while asleep!
- BME280 sensor (I²C) — temperature, humidity, pressure
- 18650 Li-Ion or 1000–3000 mAh Li-Po battery
- 2 × 100 kΩ voltage divider for battery measurement
- MQTT broker (Mosquitto, e.g. the Home Assistant add-on)
Wiring
| Part | ESP32 |
|---|---|
| BME280 VIN / GND | 3V3 / GND |
| BME280 SDA / SCL | GPIO 21 / GPIO 22 |
| Divider midpoint BAT+ → 100k → • → 100k → GND | GPIO 35 |
Libraries
- PubSubClient (Nick O'Leary)
- Adafruit BME280 Library
Code
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
const char* WIFI_SSID = "YOUR_SSID";
const char* WIFI_PASS = "YOUR_PASSWORD";
const char* MQTT_HOST = "192.168.1.10";
const int MQTT_PORT = 1883;
const char* MQTT_TOPIC = "home/balcony/sensor";
const uint64_t SLEEP_MINUTES = 10;
const int BATTERY_PIN = 35; // via 100k/100k divider
RTC_DATA_ATTR uint32_t bootCount = 0; // survives deep sleep
WiFiClient net;
PubSubClient mqtt(net);
Adafruit_BME280 bme;
void goToSleep() {
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
esp_sleep_enable_timer_wakeup(SLEEP_MINUTES * 60ULL * 1000000ULL);
esp_deep_sleep_start();
}
void setup() {
unsigned long started = millis();
Serial.begin(115200);
bootCount++;
if (!bme.begin(0x76)) {
Serial.println("BME280 not found");
goToSleep();
}
// Forced mode: the sensor measures once and goes back to sleep
bme.setSampling(Adafruit_BME280::MODE_FORCED,
Adafruit_BME280::SAMPLING_X1, // temperature
Adafruit_BME280::SAMPLING_X1, // pressure
Adafruit_BME280::SAMPLING_X1, // humidity
Adafruit_BME280::FILTER_OFF);
bme.takeForcedMeasurement();
float t = bme.readTemperature();
float h = bme.readHumidity();
float p = bme.readPressure() / 100.0F;
float vbat = analogReadMilliVolts(BATTERY_PIN) * 2 / 1000.0;
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) {
if (millis() - started > 10000) { // don't drain the battery if the router is down
Serial.println("WiFi timeout");
goToSleep();
}
delay(50);
}
mqtt.setServer(MQTT_HOST, MQTT_PORT);
if (mqtt.connect("esp32-balcony")) {
char payload[128];
snprintf(payload, sizeof payload,
"{\"t\":%.2f,\"h\":%.1f,\"p\":%.1f,\"vbat\":%.2f,\"boot\":%lu}",
t, h, p, vbat, (unsigned long)bootCount);
mqtt.publish(MQTT_TOPIC, payload, true); // retained
mqtt.loop();
delay(50);
mqtt.disconnect();
}
Serial.printf("Awake for %lu ms\n", millis() - started);
goToSleep();
}
void loop() {} // never reached
Battery life estimate
| Phase | Current | Duration |
|---|---|---|
| Active (Wi-Fi + MQTT) | ~120 mA | ~3 s |
| Deep sleep (good board) | ~0.05 mA | 597 s |
Average current ≈ 120 × 3 / 600 + 0.05 ≈ 0.65 mA. A 2000 mAh cell → ≈ 3000 h ≈ 4 months.
Wake faster (and live longer)
- Static IP —
WiFi.config(ip, gateway, subnet, dns)saves 0.5–1 s of DHCP. - Remember the router's channel and BSSID in
RTC_DATA_ATTRand pass them toWiFi.begin(ssid, pass, channel, bssid)— another second off. - Publish less often: only send when the temperature changed by more than 0.2 °C (keep the previous value in RTC memory).
- ESP-NOW instead of Wi-Fi (see that project) — the active phase drops to ~100 ms.
Home Assistant
Add MQTT sensors to configuration.yaml:
mqtt:
sensor:
- name: "Balcony temperature"
state_topic: "home/balcony/sensor"
unit_of_measurement: "°C"
device_class: temperature
value_template: "{{ value_json.t }}"
- name: "Balcony battery"
state_topic: "home/balcony/sensor"
unit_of_measurement: "V"
device_class: voltage
value_template: "{{ value_json.vbat }}"
Troubleshooting
- Battery dies within a week — a board with USB-UART/AMS1117 or a power LED. Measure the sleep current with a multimeter.
- BME280 reads 2 °C high — self-heating; that's exactly why forced mode is used.
- Battery reading jumps around — add a 100 nF capacitor between GPIO 35 and GND.