IoT Automation

DIY Weather Station MQTT Project: Wind, Rain and Temperature with an ESP32

Wire an anemometer, rain gauge and BME280 to an ESP32, publish wind, rain, temperature, humidity and pressure as MQTT JSON, then log and chart the data with a short Python script.

Backyard pole with an anemometer, rain gauge and sensor housing wired to an ESP32 in a diy weather station mqtt build
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

A DIY weather station MQTT build gives you local wind, rain, temperature, humidity and pressure readings that any dashboard, home automation system or irrigation controller can subscribe to. In this tutorial you will wire common weather sensors to an ESP32, publish one JSON message per minute to a broker, and use a Python subscriber to log the readings and draw a chart. Everything is tested first on the free public broker, then moved to private credentials.

Parts and Wiring for the DIY Weather Station MQTT Node

The design uses parts that are widely available and well documented. The wind and rain sensors are the classic "weather meter" kit style: a cup anemometer and a tipping-bucket rain gauge, both using reed switches that close briefly on each rotation or tip.

Part Purpose ESP32 connection
ESP32 DevKit board Wi-Fi and MQTT USB or 5 V supply
BME280 breakout (I2C) Temperature, humidity, pressure SDA GPIO 21, SCL GPIO 22, 3.3 V, GND
Cup anemometer (reed switch) Wind speed One wire to GPIO 27, other to GND
Tipping-bucket rain gauge Rainfall One wire to GPIO 26, other to GND
Radiation shield Keeps the BME280 out of direct sun Mechanical
Weatherproof enclosure Protects the ESP32 Mechanical

The reed switches only need a pull-up; the sketch enables the ESP32's internal pull-ups, so a closure pulls the pin low. A wind vane is often included in these kits; it uses a resistor ladder that you read with an ADC pin, and you can add it later once the basics work.

Two calibration constants come from the sensor datasheet. For the common kit used here, one anemometer closure per second corresponds to 2.4 km/h, and one bucket tip equals 0.2794 mm of rain. If your hardware differs, change the two constants at the top of the sketch.

Topic and Payload Design

Use one telemetry topic with a compact JSON payload, plus a retained status topic driven by MQTT Last Will:

connectmq/demo/<random-id>/weather/station1/telemetry   (JSON every 60 s)
connectmq/demo/<random-id>/weather/station1/status      (online/offline, retained)
{
  "temp_c": 14.8,
  "hum_pct": 71.2,
  "press_hpa": 1012.6,
  "wind_kmh": 9.6,
  "gust_kmh": 16.8,
  "rain_mm": 0.28,
  "rain_today_mm": 3.07,
  "rssi": -61,
  "uptime_s": 86412
}

Sending averages per minute rather than every pulse keeps traffic tiny and well inside the public broker's limits of 10 messages per second and 64 KB per message. A gust value (the highest 3-second average within the minute) preserves the information that averaging would otherwise hide. The free public MQTT broker guide lists the remaining limits, such as 8 topic levels and 10 subscriptions per client, which this design stays well within.

Why not retain the telemetry message? You can, and for a weather station it is often helpful: a dashboard that opens between publishes immediately shows the last reading instead of an empty widget. The trade-off is that a station which died hours ago still appears to report its last values, which is exactly why the separate retained status topic with Last Will matters. Display both, and grey out the reading when the status is offline.

The ESP32 Sketch

Install these libraries from the Arduino Library Manager: PubSubClient by Nick O'Leary, Adafruit BME280 Library and Adafruit Unified Sensor. Change the Wi-Fi credentials and the random ID in the topic prefix.

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

const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* PREFIX = "connectmq/demo/k3x9q2/weather/station1";

const int PIN_WIND = 27;
const int PIN_RAIN = 26;
const float KMH_PER_HZ = 2.4;     // anemometer: 1 closure/s = 2.4 km/h
const float MM_PER_TIP = 0.2794;  // rain gauge: 1 tip = 0.2794 mm

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

volatile uint32_t windCount = 0, rainCount = 0;
volatile uint32_t lastWindUs = 0, lastRainMs = 0;

void IRAM_ATTR onWind() {
  uint32_t now = micros();
  if (now - lastWindUs > 5000) { windCount++; lastWindUs = now; }  // 5 ms debounce
}
void IRAM_ATTR onRain() {
  uint32_t now = millis();
  if (now - lastRainMs > 200) { rainCount++; lastRainMs = now; }   // 200 ms debounce
}

char clientId[32];
char statusTopic[96];
float rainTodayMm = 0;
float maxGust = 0;
uint32_t lastGustCount = 0, lastGustMs = 0, lastPublishMs = 0, windAtPublish = 0;

void connectMqtt() {
  while (!mqtt.connected()) {
    if (mqtt.connect(clientId, nullptr, nullptr, statusTopic, 1, true, "offline")) {
      mqtt.publish(statusTopic, "online", true);
    } else {
      Serial.printf("MQTT failed, state=%d\n", mqtt.state());
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(PIN_WIND, INPUT_PULLUP);
  pinMode(PIN_RAIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(PIN_WIND), onWind, FALLING);
  attachInterrupt(digitalPinToInterrupt(PIN_RAIN), onRain, FALLING);

  if (!bme.begin(0x76) && !bme.begin(0x77)) {
    Serial.println("BME280 not found, check wiring");
  }

  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }

  snprintf(clientId, sizeof(clientId), "wx-%llx", ESP.getEfuseMac());
  snprintf(statusTopic, sizeof(statusTopic), "%s/status", PREFIX);
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  connectMqtt();
  lastPublishMs = lastGustMs = millis();
}

void loop() {
  if (!mqtt.connected()) connectMqtt();
  mqtt.loop();

  uint32_t now = millis();

  // Gust: highest 3-second average within the publish window
  if (now - lastGustMs >= 3000) {
    noInterrupts(); uint32_t c = windCount; interrupts();
    float gust = (c - lastGustCount) / ((now - lastGustMs) / 1000.0) * KMH_PER_HZ;
    if (gust > maxGust) maxGust = gust;
    lastGustCount = c; lastGustMs = now;
  }

  if (now - lastPublishMs >= 60000) {
    noInterrupts();
    uint32_t w = windCount; uint32_t r = rainCount; rainCount = 0;
    interrupts();

    float seconds = (now - lastPublishMs) / 1000.0;
    float windKmh = (w - windAtPublish) / seconds * KMH_PER_HZ;
    float rainMm = r * MM_PER_TIP;
    rainTodayMm += rainMm;   // reset at midnight from your logger or with NTP

    char payload[256];
    snprintf(payload, sizeof(payload),
      "{\"temp_c\":%.1f,\"hum_pct\":%.1f,\"press_hpa\":%.1f,"
      "\"wind_kmh\":%.1f,\"gust_kmh\":%.1f,\"rain_mm\":%.2f,"
      "\"rain_today_mm\":%.2f,\"rssi\":%d,\"uptime_s\":%lu}",
      bme.readTemperature(), bme.readHumidity(), bme.readPressure() / 100.0,
      windKmh, maxGust, rainMm, rainTodayMm, WiFi.RSSI(), now / 1000);

    char topic[96];
    snprintf(topic, sizeof(topic), "%s/telemetry", PREFIX);
    mqtt.publish(topic, payload);
    Serial.println(payload);

    windAtPublish = w;
    maxGust = 0;
    lastPublishMs = now;
  }
}

Open the serial monitor at 115200 baud. Spin the anemometer by hand and tip the rain bucket a few times; the next minute's message should show non-zero values. In another terminal, confirm that the broker receives the data:

mosquitto_sub -h broker.connectmq.com -p 1883 -i wx-viewer-5521 -t "connectmq/demo/k3x9q2/weather/#" -v

If the board never connects, the ESP32 MQTT tutorial walks through Wi-Fi and broker troubleshooting step by step.

Logging and Charting the Readings

The broker delivers live messages; it does not store history for you. A short Python subscriber can append every reading to a CSV file and render a chart. Install the dependencies:

pip install "paho-mqtt>=2.0" matplotlib
import csv
import json
import os
import sys
from datetime import datetime, timezone
import paho.mqtt.client as mqtt

TOPIC = "connectmq/demo/k3x9q2/weather/station1/telemetry"
CSV_FILE = "weather.csv"
FIELDS = ["time", "temp_c", "hum_pct", "press_hpa", "wind_kmh", "gust_kmh", "rain_mm"]


def on_connect(client, userdata, flags, reason_code, properties):
    print("Connected:", reason_code)
    client.subscribe(TOPIC, qos=1)


def on_message(client, userdata, msg):
    data = json.loads(msg.payload)
    row = {"time": datetime.now(timezone.utc).isoformat(timespec="seconds")}
    row.update({k: data.get(k) for k in FIELDS[1:]})
    new_file = not os.path.exists(CSV_FILE)
    with open(CSV_FILE, "a", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=FIELDS)
        if new_file:
            writer.writeheader()
        writer.writerow(row)
    print(row)


def plot():
    import matplotlib.pyplot as plt
    times, temp, wind, gust, rain = [], [], [], [], []
    with open(CSV_FILE) as f:
        for r in csv.DictReader(f):
            times.append(datetime.fromisoformat(r["time"]))
            temp.append(float(r["temp_c"]))
            wind.append(float(r["wind_kmh"]))
            gust.append(float(r["gust_kmh"]))
            rain.append(float(r["rain_mm"]))
    fig, (ax1, ax2, ax3) = plt.subplots(3, 1, sharex=True, figsize=(10, 8))
    ax1.plot(times, temp); ax1.set_ylabel("Temp (C)")
    ax2.plot(times, wind, label="wind"); ax2.plot(times, gust, label="gust")
    ax2.set_ylabel("km/h"); ax2.legend()
    ax3.bar(times, rain, width=0.0007); ax3.set_ylabel("Rain (mm/min)")
    fig.tight_layout()
    fig.savefig("weather.png", dpi=120)
    print("Saved weather.png")


if __name__ == "__main__":
    if len(sys.argv) > 1 and sys.argv[1] == "plot":
        plot()
    else:
        client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="wx-logger-8812")
        client.on_connect = on_connect
        client.on_message = on_message
        client.connect("broker.connectmq.com", 1883, keepalive=60)
        client.loop_forever()

Run python weather_logger.py to collect data and python weather_logger.py plot to produce weather.png. For a permanent setup, many people replace the CSV with InfluxDB fed by Telegraf, or a Node-RED dashboard; both connect to the broker as ordinary MQTT clients.

Accuracy Checklist

A weather station is only as good as its siting. Work through this list before trusting the numbers:

  1. Shield the temperature sensor. A BME280 in direct sun or inside a sealed box reads several degrees high. Use a ventilated radiation shield.
  2. Mount the anemometer high and clear. Roofs and fences create turbulence; a mast above nearby obstacles gives far more representative wind.
  3. Level the rain gauge and keep it away from walls and trees that block or drip.
  4. Keep the ESP32 away from the BME280. The board's own heat skews readings if they share an enclosure.
  5. Debounce in software and check wiring. Long unshielded cables pick up noise that looks like phantom rain tips.
  6. Reset the daily rain total consistently, either at local midnight in the logger or on the ESP32 with NTP time.

Extending Your DIY Weather Station MQTT Setup

Once the core station is stable, a few additions make it noticeably more useful.

Wind direction. The vane in most kits switches between internal resistors as it turns. Wire it as a voltage divider with a fixed resistor (the kit datasheet suggests a value) to an ADC1 pin such as GPIO 34, read it with analogRead(), and map each voltage band to one of 16 compass points. Use ADC1 pins only, because ADC2 cannot be used while Wi-Fi is active on the ESP32. Publish the direction as degrees, which is easier for charts than "NNE".

Solar and battery power. A small panel, a lithium cell and a charge controller can run the station off-grid, but an always-on ESP32 with Wi-Fi draws too much for a small panel in winter. Switch to deep sleep between publishes and count anemometer pulses with the ULP coprocessor or a dedicated counter chip, or accept a slower publish interval. Add the battery voltage to the JSON so you can see trouble coming.

Daily summaries. Have the logger publish a retained daily summary (minimum and maximum temperature, total rain, peak gust) to a separate topic just after midnight. Home automation tools can then use yesterday's rainfall to skip an irrigation run without reading the full history.

Derived values. Dew point and "feels like" temperature are easy to compute from temperature and humidity in the logger or in Node-RED. Calculate them downstream rather than on the ESP32 so you can change the formula without reflashing.

Taking the Station Off the Test Broker

The public broker is ideal for proving the build, but everything on it is readable by anyone and the topic prefix is your only separation. For a permanent station, especially one that feeds an automatic irrigation system, use a private broker with TLS and credentials. A hosted MQTT broker such as ConnectMQ gives you that without running a server at home. Change the client to WiFiClientSecure, port 8883, and the username, password and client ID from the portal:

#include <WiFiClientSecure.h>
WiFiClientSecure net;          // replace WiFiClient
// in setup(): load the ISRG Root X1 certificate for Let's Encrypt
// net.setCACert(ISRG_ROOT_X1_PEM);
mqtt.setServer("broker.connectmq.com", 8883);
// connect with: mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD", statusTopic, 1, true, "offline");

The ConnectMQ portal's live topic explorer then shows each message as it arrives, and threshold alert rules can warn you about frost or strong wind without extra code. More farm-oriented ideas live in the smart agriculture IoT article and the broader IoT automation guide.

Publish Your Weather Data on ConnectMQ

ConnectMQ is a managed MQTT broker with TLS, WebSocket access for browser dashboards and an in-browser client for quick checks. Create a free Serverless broker at app.connectmq.com, no credit card needed, and move your station from the public test area to private credentials in minutes.

Frequently Asked Questions

How often should a DIY weather station publish over MQTT?

Once per minute is a good default for averaged values. It keeps traffic tiny while still capturing gusts if you send the maximum 3-second wind speed alongside the average.

Why are my temperature readings too high?

Almost always because of solar heating or heat from the ESP32 itself. Move the BME280 into a ventilated radiation shield, away from the microcontroller and voltage regulator.

Can I use a BME680 or SHT31 instead of the BME280?

Yes. Change the library and read functions; the MQTT side stays the same as long as you keep the JSON field names.

Does the broker keep my weather history?

No. MQTT brokers deliver live messages and the last retained value. Store history with a subscriber such as the Python logger above, Telegraf with a time-series database, or Node-RED.

Related articles

Zigbee2MQTT Setup Guide: From Coordinator to Dashboard

Install Zigbee2MQTT with Docker, configure the coordinator and MQTT connection, pair and rename devices, control them over MQTT and view their states on a dashboard, testing first on a free public 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.