ESP32 and Arduino

ESP32 BME280 MQTT Weather Station with a Remote-Configurable Interval

Read temperature, humidity and pressure from a BME280 over I2C, publish JSON to broker.connectmq.com, and change the publish interval at runtime with a retained MQTT command stored in flash.

BME280 breakout connected to an ESP32 over I2C publishing weather data in an esp32 bme280 mqtt weather station
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

An ESP32 BME280 MQTT node measures temperature, humidity and barometric pressure from one small I2C chip and publishes them to a broker. In this tutorial you will wire the sensor, read it in low-power forced mode, publish JSON to broker.connectmq.com, and change the publish interval remotely with an MQTT message that survives reboots.

Why the BME280

The Bosch BME280 is a popular step up from the DHT22. It talks I2C, so there is no fragile single-wire timing, it reads all three values in milliseconds, and its pressure output lets you track weather trends or estimate altitude. It also has a power-saving forced mode in which it takes one measurement and goes back to sleep, which reduces self-heating.

A warning before you buy: many cheap breakouts labelled "BME280" actually carry a BMP280, which has no humidity sensor. The two look identical. The sketch below reads the chip ID and tells you which one you have.

Parts and Libraries

  • An ESP32 development board.
  • A BME280 breakout board (3.3 V version, or one with an onboard regulator).
  • Four jumper wires.
  • Arduino IDE 2.x with the esp32 board package.
  • Libraries: Adafruit BME280 Library, Adafruit Unified Sensor, Adafruit BusIO (installed as dependencies), and PubSubClient.

Wiring

BME280 pin ESP32 pin (classic) Notes
VIN or VCC 3V3 Do not use 5 V on bare 3.3 V modules
GND GND
SCL GPIO 22 Default I2C clock
SDA GPIO 21 Default I2C data
SDO (if present) GND or 3V3 Selects address 0x76 or 0x77
CSB (if present) 3V3 Keeps the chip in I2C mode

On the ESP32-S3, C3 and C6 the default I2C pins differ. Pass explicit pins to Wire.begin(SDA, SCL) as the sketch does, and adjust the numbers to your board.

Topic Design With a Remote Interval

Topic Direction Retained Example payload
connectmq/demo/bme5t8n/telemetry Device publishes No {"temperature":21.84,"humidity":46.2,"pressure":1013.6}
connectmq/demo/bme5t8n/config/interval/set You publish Yes 30
connectmq/demo/bme5t8n/config/interval Device publishes Yes 30
connectmq/demo/bme5t8n/status Device publishes Yes online / offline

The command is retained so that a device that was offline when you sent it still receives it at the next connect. The device then reports the value it actually applied on a separate retained topic. Keeping "desired" and "reported" values on different topics avoids feedback loops and lets you see at a glance whether a setting was accepted. The MQTT QoS levels guide explains why QoS 1 is used for the command subscription.

The ESP32 BME280 MQTT Sketch

#include <WiFi.h>
#include <Wire.h>
#include <PubSubClient.h>
#include <Preferences.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

const char* WIFI_SSID     = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST     = "broker.connectmq.com";
const uint16_t MQTT_PORT  = 1883;
const char* PREFIX        = "connectmq/demo/bme5t8n";   // use your own random id
const int I2C_SDA = 21, I2C_SCL = 22;
const uint32_t MIN_INTERVAL_S = 5, MAX_INTERVAL_S = 3600;

Adafruit_BME280 bme;
WiFiClient   net;
PubSubClient mqtt(net);
Preferences  prefs;

String clientId;
String tTelemetry, tIntervalSet, tIntervalState, tStatus;
uint32_t intervalS = 60;
unsigned long lastPublish = 0;
bool hasHumidity = true;

void reportInterval() {
  char buf[12];
  snprintf(buf, sizeof(buf), "%lu", (unsigned long)intervalS);
  mqtt.publish(tIntervalState.c_str(), buf, true);
}

void onMessage(char* topic, byte* payload, unsigned int len) {
  if (tIntervalSet != topic) return;
  char buf[12];
  len = min(len, (unsigned int)sizeof(buf) - 1);
  memcpy(buf, payload, len);
  buf[len] = '\0';

  long requested = atol(buf);
  if (requested < (long)MIN_INTERVAL_S || requested > (long)MAX_INTERVAL_S) {
    Serial.printf("Ignoring interval %s (allowed %lu-%lu)\n", buf,
                  (unsigned long)MIN_INTERVAL_S, (unsigned long)MAX_INTERVAL_S);
    reportInterval();               // re-assert the value in use
    return;
  }
  if ((uint32_t)requested != intervalS) {
    intervalS = requested;
    prefs.putUInt("interval", intervalS);
    Serial.printf("Interval set to %lu s\n", (unsigned long)intervalS);
  }
  reportInterval();
}

void connectWifi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) delay(400);
  Serial.printf("Wi-Fi OK, IP %s\n", WiFi.localIP().toString().c_str());
}

void connectMqtt() {
  while (!mqtt.connected()) {
    if (mqtt.connect(clientId.c_str(), tStatus.c_str(), 1, true, "offline")) {
      mqtt.publish(tStatus.c_str(), "online", true);
      mqtt.subscribe(tIntervalSet.c_str(), 1);
      reportInterval();
      Serial.println("MQTT connected");
    } else {
      Serial.printf("MQTT failed, state %d\n", mqtt.state());
      delay(5000);
    }
  }
}

bool startSensor() {
  Wire.begin(I2C_SDA, I2C_SCL);
  if (!bme.begin(0x76, &Wire) && !bme.begin(0x77, &Wire)) return false;

  uint8_t id = bme.sensorID();
  hasHumidity = (id == 0x60);
  Serial.printf("Sensor chip ID 0x%02X (%s)\n", id, hasHumidity ? "BME280" : "BMP280, no humidity");

  // Forced mode: one measurement on demand, then sleep. Recommended for weather monitoring.
  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);
  return true;
}

void setup() {
  Serial.begin(115200);
  prefs.begin("bme-node", false);
  intervalS = prefs.getUInt("interval", 60);

  if (!startSensor()) {
    Serial.println("No BME280 found at 0x76 or 0x77. Check wiring.");
    while (true) delay(1000);
  }

  clientId       = "esp32-bme280-" + String((uint32_t)ESP.getEfuseMac(), HEX);
  tTelemetry     = String(PREFIX) + "/telemetry";
  tIntervalSet   = String(PREFIX) + "/config/interval/set";
  tIntervalState = String(PREFIX) + "/config/interval";
  tStatus        = String(PREFIX) + "/status";

  connectWifi();
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setBufferSize(512);
  connectMqtt();
  lastPublish = millis() - intervalS * 1000UL;   // publish right away
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) connectWifi();
  if (!mqtt.connected()) connectMqtt();
  mqtt.loop();

  if (millis() - lastPublish < intervalS * 1000UL) return;
  lastPublish = millis();

  if (!bme.takeForcedMeasurement()) {
    Serial.println("Forced measurement failed");
    return;
  }
  float t = bme.readTemperature();           // degrees C
  float p = bme.readPressure() / 100.0f;     // Pa -> hPa
  float h = hasHumidity ? bme.readHumidity() : NAN;

  char payload[200];
  if (hasHumidity) {
    snprintf(payload, sizeof(payload),
             "{\"temperature\":%.2f,\"humidity\":%.1f,\"pressure\":%.1f,\"interval\":%lu}",
             t, h, p, (unsigned long)intervalS);
  } else {
    snprintf(payload, sizeof(payload),
             "{\"temperature\":%.2f,\"pressure\":%.1f,\"interval\":%lu}",
             t, p, (unsigned long)intervalS);
  }
  mqtt.publish(tTelemetry.c_str(), payload);
  Serial.println(payload);
}

Testing ESP32 BME280 MQTT Readings and Changing the Interval

Watch everything under your prefix:

mosquitto_sub -h broker.connectmq.com -p 1883 -i bme-view-5t8n -t "connectmq/demo/bme5t8n/#" -v

Now change the interval to 10 seconds with a retained command:

mosquitto_pub -h broker.connectmq.com -p 1883 -i bme-cmd-5t8n -q 1 -r \
  -t "connectmq/demo/bme5t8n/config/interval/set" -m "10"

Within a moment, config/interval reports 10 and telemetry speeds up. Reset the board: it reads 10 from flash, and even if the flash were erased the retained command would set it again at connect. Send 2 and the device rejects it and re-reports the current value. To remove the retained command later, publish an empty retained message to the same topic with -r -n.

The public area of the broker is shared, so anyone could publish to your command topic if they guessed it. That is acceptable for a demo but is exactly why production devices should use their own authenticated broker.

Understanding the Readings

  • Pressure is reported at station level in hPa. Weather services report pressure reduced to sea level, so your number will be lower at altitude. Trends matter more than the absolute value: a steady fall over a few hours often comes before worse weather.
  • Altitude can be estimated with bme.readAltitude(1013.25), but it is only as good as the sea-level pressure you supply, and it drifts with the weather.
  • Self-heating is minimal in forced mode at low sample rates. In normal mode with oversampling and a fast standby time, the chip and the nearby ESP32 can warm the reading noticeably.

Troubleshooting

  1. "No BME280 found". Run an I2C scanner sketch. No device at all means wiring; a device at an unexpected address means the SDO pin setting.
  2. Humidity always 0 or NaN. You have a BMP280. The chip ID print confirms it.
  3. Pressure about 10 percent off. You forgot to divide pascals by 100, or you are comparing with sea-level pressure.
  4. Interval command ignored. Check that the topic matches exactly, including case, and that the payload is a plain number.
  5. Board resets during Wi-Fi connect. A weak USB supply browns out the ESP32. Use a better cable or power supply.

Production Setup

For real deployments, create a broker at app.connectmq.com and switch the sketch to WiFiClientSecure on port 8883 with the ISRG Root X1 certificate, connecting with YOUR_CLIENT_ID, YOUR_USERNAME and YOUR_PASSWORD. Only clients with your credentials can then send interval commands. Storing Wi-Fi credentials in code is the next weak spot; the WiFiManager setup guide shows how to enter them through a captive portal instead. If you are comparing sensors, the ESP32 DHT22 tutorial covers the cheaper single-wire option.

Storing and Charting the Weather Data

The broker delivers messages; it is not a database. For weather history you need a subscriber that writes each reading somewhere durable. Three common patterns work well with this payload:

  1. Telegraf with the MQTT consumer input subscribes to .../telemetry, parses the JSON fields and writes them to InfluxDB, ready for Grafana charts.
  2. Node-RED subscribes with an mqtt in node, reshapes the message and writes to any database it has a node for, which is handy if you also want to send notifications.
  3. A small Python script using paho-mqtt inserts rows into SQLite or PostgreSQL. It is the most transparent option when you are learning.

All three connect to the broker as ordinary MQTT clients with their own client IDs, exactly like the ESP32. In the ConnectMQ portal they show up alongside your devices, and the live topic explorer is a quick way to confirm the JSON shape before you write any parsing code. Because the device includes the active interval in every message, your charts can also show when the sample rate changed, which avoids misreading a gap as an outage.

Get Started with ConnectMQ

A weather node is only as useful as the broker that collects its data. ConnectMQ is a hosted MQTT broker with TLS, retained messages, MQTT 3.1.1 and 5.0, and a portal with a live topic explorer and threshold alerts, so you can be notified when pressure drops fast. The free Serverless plan needs no credit card. If you are new to the concepts, read what an MQTT broker does, then explore the full ESP32 IoT tutorial series.

Frequently Asked Questions

What is the difference between the BME280 and the BMP280?

The BME280 measures temperature, humidity and pressure; the BMP280 measures only temperature and pressure. They share a footprint and library, and the chip ID (0x60 versus 0x58) tells them apart.

Why make the publish interval configurable over MQTT?

It lets you sample faster while debugging or during an event and slower the rest of the time, without reflashing. Storing it in flash keeps the setting across reboots.

Should the interval command be retained?

Yes, for settings. A retained command reaches the device even if it was offline when you sent it, and the device reports the applied value on its own retained topic.

Can I run the BME280 node on batteries?

Yes. Use deep sleep between readings, keep the sensor in forced mode and connect to Wi-Fi only to publish. Wi-Fi time dominates the energy budget.

Related articles

ESP-IDF MQTT Example: Build an esp-mqtt Client from Scratch

Build an ESP-IDF project from an empty folder with the esp-mqtt component: Wi-Fi station setup, the MQTT event handler, publish and subscribe, MQTT 5 properties and TLS with the certificate bundle.

8 min read

Using esp-mqtt MQTT 5 Features on ESP32 with ESP-IDF

A hands-on ESP-IDF project that enables MQTT 5.0 in esp-mqtt and uses user properties, message expiry, topic aliases, session expiry and response topics against a real MQTT 5 broker.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.