Automatización IoT

Estación meteorológica casera con MQTT: viento, lluvia y temperatura con un ESP32

Conecta un anemómetro, un pluviómetro y un BME280 a un ESP32, publica viento, lluvia, temperatura, humedad y presión como JSON por MQTT y registra y grafica los datos con un breve script de Python.

Mástil en un jardín con anemómetro, pluviómetro y carcasa de sensores conectados a un ESP32 en una estación meteorológica casera MQTT
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

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:

  1. 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.
  2. 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.
  3. Nivela el pluviómetro y mantenlo alejado de paredes y árboles que bloqueen la lluvia o goteen.
  4. Mantén el ESP32 lejos del BME280. El calor de la propia placa distorsiona las lecturas si comparten caja.
  5. Aplica antirrebote por software y revisa el cableado. Los cables largos sin apantallar captan ruido que parece vuelcos de lluvia fantasma.
  6. 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.

Artículos relacionados

Guía para configurar Zigbee2MQTT: del coordinador al panel

Instala Zigbee2MQTT con Docker, configura el coordinador y la conexión MQTT, empareja y renombra dispositivos, contrólalos por MQTT y consulta sus estados en un panel, probando primero en un servidor MQTT público gratuito.

8 min de lectura

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.