Una estación meteorológica casera con MQTT te proporciona lecturas locales de viento, lluvia, temperatura, humedad y presión a las que puede suscribirse cualquier dashboard, sistema domótico o controlador de riego. En este tutorial conectarás sensores meteorológicos habituales a un ESP32, publicarás un mensaje JSON por minuto en un broker y usarás un suscriptor en Python para registrar las lecturas y dibujar una gráfica. Todo se prueba primero en el servidor MQTT público gratuito, pensado para pruebas y prototipos, y después se migra a credenciales privadas.
Componentes y cableado del nodo de la estación meteorológica casera MQTT
El diseño utiliza componentes fáciles de conseguir y bien documentados. Los sensores de viento y lluvia son del estilo clásico de los kits "weather meter": un anemómetro de cazoletas y un pluviómetro de cubeta basculante, ambos con interruptores reed que se cierran brevemente en cada vuelta o en cada vuelco.
| Componente | Función | Conexión al ESP32 |
|---|---|---|
| Placa ESP32 DevKit | Wi-Fi y MQTT | USB o fuente de 5 V |
| Módulo BME280 (I2C) | Temperatura, humedad, presión | SDA GPIO 21, SCL GPIO 22, 3,3 V, GND |
| Anemómetro de cazoletas (interruptor reed) | Velocidad del viento | Un cable a GPIO 27, el otro a GND |
| Pluviómetro de cubeta basculante | Precipitación | Un cable a GPIO 26, el otro a GND |
| Protector de radiación | Mantiene el BME280 fuera del sol directo | Mecánico |
| Caja estanca | Protege el ESP32 | Mecánico |
Los interruptores reed solo necesitan una resistencia pull-up; el sketch activa los pull-ups internos del ESP32, de modo que un cierre lleva el pin a nivel bajo. Estos kits suelen incluir también una veleta; utiliza una escalera de resistencias que se lee con un pin ADC, y puedes añadirla más adelante, cuando lo básico funcione.
Dos constantes de calibración salen de la hoja de datos del sensor. Para el kit habitual que se usa aquí, un cierre del anemómetro por segundo equivale a 2,4 km/h, y un vuelco de la cubeta equivale a 0,2794 mm de lluvia. Si tu hardware es distinto, cambia las dos constantes al principio del sketch.
Diseño de topics y payload
Usa un único topic de telemetría con un payload JSON compacto, además de un topic de estado retenido gestionado mediante el Last Will de MQTT:
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
}
Enviar promedios por minuto en lugar de cada pulso mantiene el tráfico mínimo y muy por debajo de los límites del broker público: 10 mensajes por segundo y 64 KB por mensaje. Un valor de racha (el promedio de 3 segundos más alto dentro del minuto) conserva la información que el promedio ocultaría. La guía del servidor público enumera el resto de límites, como 8 niveles de topic y 10 suscripciones por cliente, que este diseño respeta holgadamente.
¿Por qué no retener el mensaje de telemetría? Puedes hacerlo, y en una estación meteorológica suele ser útil: un dashboard que se abre entre dos publicaciones muestra de inmediato la última lectura en lugar de un widget vacío. La contrapartida es que una estación que dejó de funcionar hace horas sigue pareciendo que informa de sus últimos valores, y precisamente por eso es importante el topic status retenido aparte con Last Will. Muestra ambos y atenúa la lectura cuando el estado sea offline.
El sketch del ESP32
Instala estas bibliotecas desde el Gestor de bibliotecas de Arduino: PubSubClient de Nick O'Leary, Adafruit BME280 Library y Adafruit Unified Sensor. Cambia las credenciales Wi-Fi y el ID aleatorio del prefijo del topic.
#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; // anemómetro: 1 cierre/s = 2,4 km/h
const float MM_PER_TIP = 0.2794; // pluviómetro: 1 vuelco = 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; } // antirrebote de 5 ms
}
void IRAM_ATTR onRain() {
uint32_t now = millis();
if (now - lastRainMs > 200) { rainCount++; lastRainMs = now; } // antirrebote de 200 ms
}
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();
// Racha: promedio de 3 segundos más alto dentro de la ventana de publicación
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; // se reinicia a medianoche desde el registrador o con 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;
}
}
Abre el monitor serie a 115200 baudios. Haz girar el anemómetro a mano y vuelca la cubeta del pluviómetro unas cuantas veces; el mensaje del minuto siguiente debería mostrar valores distintos de cero. En otra terminal, comprueba que el broker recibe los datos:
mosquitto_sub -h broker.connectmq.com -p 1883 -i wx-viewer-5521 -t "connectmq/demo/k3x9q2/weather/#" -v
Si la placa no llega a conectarse, el tutorial de ESP32 MQTT explica paso a paso cómo diagnosticar problemas de Wi-Fi y del broker.
Registro y gráficas de las lecturas
El broker entrega mensajes en vivo; no guarda el histórico por ti. Un breve suscriptor en Python puede añadir cada lectura a un archivo CSV y generar una gráfica. Instala las dependencias:
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()
Ejecuta python weather_logger.py para recopilar datos y python weather_logger.py plot para generar weather.png. Para una instalación permanente, muchos sustituyen el CSV por InfluxDB alimentado con Telegraf, o por un dashboard de Node-RED; ambos se conectan al broker como clientes MQTT normales.
Lista de comprobación de precisión
Una estación meteorológica es tan buena como su ubicación. Repasa esta lista antes de fiarte de los números:
- Protege el sensor de temperatura. Un BME280 al sol directo o dentro de una caja cerrada marca varios grados de más. Usa un protector de radiación ventilado.
- Monta el anemómetro alto y despejado. Tejados y vallas generan turbulencias; un mástil por encima de los obstáculos cercanos da un viento mucho más representativo.
- Nivela el pluviómetro y mantenlo alejado de paredes y árboles que bloqueen la lluvia o goteen.
- Mantén el ESP32 lejos del BME280. El calor de la propia placa distorsiona las lecturas si comparten caja.
- Aplica antirrebote por software y revisa el cableado. Los cables largos sin apantallar captan ruido que parece vuelcos de lluvia fantasma.
- Reinicia el total de lluvia diaria de forma coherente, ya sea a la medianoche local en el registrador o en el ESP32 con hora NTP.
Ampliar tu estación meteorológica casera MQTT
Una vez que la estación básica sea estable, unas cuantas mejoras la hacen bastante más útil.
Dirección del viento. La veleta de la mayoría de los kits conmuta entre resistencias internas al girar. Conéctala como divisor de tensión con una resistencia fija (la hoja de datos del kit sugiere un valor) a un pin ADC1 como GPIO 34, léela con analogRead() y asigna cada rango de tensión a uno de los 16 puntos cardinales. Usa solo pines ADC1, porque ADC2 no se puede usar mientras el Wi-Fi está activo en el ESP32. Publica la dirección en grados, que es más fácil de graficar que "NNE".
Alimentación solar y por batería. Un panel pequeño, una celda de litio y un regulador de carga pueden hacer funcionar la estación sin red eléctrica, pero un ESP32 siempre encendido con Wi-Fi consume demasiado para un panel pequeño en invierno. Pasa a deep sleep entre publicaciones y cuenta los pulsos del anemómetro con el coprocesador ULP o con un chip contador dedicado, o acepta un intervalo de publicación más lento. Añade la tensión de la batería al JSON para ver venir los problemas.
Resúmenes diarios. Haz que el registrador publique un resumen diario retenido (temperatura mínima y máxima, lluvia total, racha máxima) en un topic aparte justo después de medianoche. Las herramientas domóticas pueden usar entonces la lluvia de ayer para saltarse un ciclo de riego sin leer todo el histórico.
Valores derivados. El punto de rocío y la sensación térmica se calculan fácilmente a partir de la temperatura y la humedad en el registrador o en Node-RED. Calcúlalos aguas abajo y no en el ESP32, para poder cambiar la fórmula sin volver a flashear.
Sacar la estación del broker de pruebas
El broker público es ideal para validar el montaje, pero cualquiera puede leer todo lo que hay en él y el prefijo del topic es tu única separación. Para una estación permanente, sobre todo si alimenta un sistema de riego automático, usa un broker privado con TLS y credenciales. Un broker MQTT gestionado como ConnectMQ te lo ofrece sin tener que mantener un servidor en casa. Cambia el cliente a WiFiClientSecure, el puerto a 8883 y usa el nombre de usuario, la contraseña y el client ID del portal:
#include <WiFiClientSecure.h>
WiFiClientSecure net; // sustituye a WiFiClient
// en setup(): carga el certificado ISRG Root X1 de Let's Encrypt
// net.setCACert(ISRG_ROOT_X1_PEM);
mqtt.setServer("broker.connectmq.com", 8883);
// conecta con: mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD", statusTopic, 1, true, "offline");
El explorador de topics en vivo del portal de ConnectMQ muestra entonces cada mensaje según llega, y las reglas de alerta por umbral pueden avisarte de heladas o viento fuerte sin código adicional. Encontrarás más ideas orientadas al campo en el artículo sobre IoT para agricultura inteligente y en la guía de automatización IoT, más general.
Publica tus datos meteorológicos en ConnectMQ
ConnectMQ es un broker MQTT gestionado con TLS, acceso por WebSocket para dashboards en el navegador y un cliente integrado en el navegador para comprobaciones rápidas. Crea un broker Serverless gratuito en app.connectmq.com, sin tarjeta de crédito, y pasa tu estación del área de pruebas pública a credenciales privadas en minutos.
Preguntas frecuentes
¿Con qué frecuencia debe publicar por MQTT una estación meteorológica casera?
Una vez por minuto es un buen valor por defecto para valores promediados. Mantiene el tráfico mínimo y aun así captura las rachas si envías la velocidad máxima de viento de 3 segundos junto con el promedio.
¿Por qué mis lecturas de temperatura son demasiado altas?
Casi siempre por el calentamiento solar o por el calor del propio ESP32. Coloca el BME280 dentro de un protector de radiación ventilado, lejos del microcontrolador y del regulador de tensión.
¿Puedo usar un BME680 o un SHT31 en lugar del BME280?
Sí. Cambia la biblioteca y las funciones de lectura; la parte MQTT sigue igual siempre que mantengas los nombres de los campos JSON.
¿Guarda el broker mi histórico meteorológico?
No. Los brokers MQTT entregan mensajes en vivo y el último valor retenido. Guarda el histórico con un suscriptor como el registrador de Python anterior, con Telegraf y una base de datos de series temporales, o con Node-RED.



