← All projects
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.

Difficulty
Advanced
Time
3–4 h
Category
IoT / Web

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)

  1. Static IP — WiFi.config(ip, gateway, subnet, dns) saves 0.5–1 s of DHCP.
  2. Remember the router's channel and BSSID in RTC_DATA_ATTR and pass them to WiFi.begin(ssid, pass, channel, bssid) — another second off.
  3. Publish less often: only send when the temperature changed by more than 0.2 °C (keep the previous value in RTC memory).
  4. 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.