Skip to content

Sesión 3 · MQTT e IoT en la red del invernadero ​

Objetivos de la sesión ​

  • Recibir todos los sensores del invernadero con una única suscripción con comodines.
  • Normalizar los payloads al modelo de datos común del sistema y descartar las lecturas no válidas.
  • Detectar los sensores que dejan de publicar y los ESP32 inalcanzables, y distinguir un caso del otro.
  • Enviar comandos a los Shelly y comprobar que se han ejecutado.
  • Justificar el QoS y el retain de datos, estados y comandos, y anunciar el estado de tu propio Node-RED con birth y will.

Conexión con el sistema del invernadero ​

Hasta ahora el sistema escuchaba uno o dos topics sueltos. En esta sesión construyes la capa de comunicaciones completa:

  • Todas las magnitudes del invernadero entran por un único punto, con el mismo formato.
  • El sistema sabe qué fuentes fallan.
  • Actúa por primera vez sobre el invernadero, a través de la bomba y el ventilador.

Lo que dejas en el contexto global lo usarán las sesiones siguientes. El panel de la sesión 4 leerá sensores y actuadores, la base de datos de la sesión 5 guardará los datos normalizados y las alarmas de la sesión 6 saldrán de problemas.

Conceptos clave ​

Esta sesión usa a fondo publicación y suscripción, topics, comodines, QoS, retain y LWT. Si alguno no lo tienes claro, el anexo MQTT en lo que necesitas los explica con ejemplos del invernadero. Aquí solo repasamos lo que cambia al llevarla a un sistema con muchas fuentes.

Los topics del invernadero ​

En el invernadero del laboratorio, una Raspberry Pi reúne los brokers de los siete ESP32 y publica sus datos bajo una jerarquía común. Además, genera un topic online por cada ESP32 que indica si lo alcanza. El simulador publica exactamente esa jerarquía, online incluido, en tu Mosquitto local.

Árbol de topics del broker de la Raspberry Pi. Bajo invernadero cuelgan siete nodos con sus magnitudes y un topic online; invernadero/+/+ recoge exactamente esos topics de tres niveles. Bajo invernadero/shelly cuelgan el estado, la potencia, la energía y la disponibilidad de cada Shelly, y los topics de comando en los que publicas tú; invernadero/shelly/# los recoge todos

Los payloads son números en texto plano ("23.4"), tal como los publican los ESP32. La lista completa de topics, con sus unidades y cuándo publica cada uno, está en Qué publica de la guía del simulador.

Dos nodos que conviene tener claros:

  • tds mide los sólidos disueltos (Total Dissolved Solids) en el agua del depósito de riego, es decir, cuántas sales y nutrientes lleva. tds/tds es la concentración en ppm y tds/voltaje la lectura en bruto de la sonda, de la que el ESP32 calcula los ppm.
  • actuador no mide ni conmuta nada: es el ESP32 que aloja el broker al que se conectan los dos Shelly, los enchufes que encienden la bomba y el ventilador. Los comandos van de Node-RED a la Raspberry Pi, de ahí al actuador y de él al Shelly. Por eso invernadero/actuador/online indica si la Raspberry Pi alcanza ese ESP32, mientras que invernadero/shelly/<id>/online lo publica cada Shelly. Si cae el actuador, pierdes los dos Shelly a la vez.

Retain: último valor conocido, no valor actual ​

Los ESP32 publican sus lecturas con retain. Al suscribirte recibes al instante la última lectura de cada topic, aunque sea de hace una hora o de un sensor que ya no funciona. En Node-RED lo sabes por msg.retain: vale true en los mensajes retenidos que entrega el broker al suscribirte.

QoS y retain según el tipo de mensaje ​

El QoS (Quality of Service) es la garantía de entrega de cada mensaje:

  • QoS 0: como mucho una vez. Se envía sin confirmación y, si se pierde, se pierde.
  • QoS 1: al menos una vez. El emisor reenvía hasta recibir confirmación, así que puede llegar repetido.
  • QoS 2: exactamente una vez, con un intercambio de cuatro pasos. Es el más lento y rara vez hace falta.
Tipo de mensajeEjemploQoSRetain
Lectura periódicainvernadero/dht22/temperatura0: si se pierde una, llega otra en 5 sSí: el último valor conocido es útil
Estadoinvernadero/shelly/<id>/relay/0, …/online1Sí: quien se conecta debe conocer el estado
Comandoinvernadero/shelly/<id>/relay/0/command1Nunca

Un comando retenido se volvería a ejecutar cada vez que el Shelly se reconectara al broker: una bomba que se enciende sola tras un corte de red.

Recorrido de un comando: Node-RED publica con QoS 1, la RPi lo reenvía por el bridge al ESP32 actuador con QoS 0 y este al Shelly; el Shelly publica su nuevo estado, que vuelve por el mismo camino hasta Node-RED y confirma el comando

El QoS se negocia en cada tramo: en el invernadero real, el bridge de la RPi reenvía con QoS 0, así que de extremo a extremo el comando viaja con QoS 0 aunque Node-RED lo publique con QoS 1. Por eso no basta con el QoS: la única prueba de que el comando se ha ejecutado es el estado que publica el Shelly.

El modelo de datos común ​

Cada lectura, venga del sensor que venga, se convierte en:

json
{
  "sensor": "suelo",
  "magnitud": "humedad",
  "valor": 42.5,
  "unidad": "%",
  "ts": 1790334799957,
  "retenido": false
}

Así el panel, la base de datos y las alarmas trabajan con un único formato, y añadir un sensor nuevo no obliga a tocarlos.

Práctica guiada paso a paso ​

Cada paso indica para qué sirve, qué hacer, qué debes ver y si no funciona. Las operaciones del editor que ya conoces de las sesiones 1 y 2 solo se nombran; las nuevas (funciones con varias salidas, trigger, mqtt out, mensajes birth y will) se explican con detalle.

Paso 0 · Comprobar el entorno (5 min) ​

Para qué sirve. En esta sesión enviarás comandos a los Shelly. Node-RED tiene que saber qué Shelly controla cada equipo. Ese dato va en variables de entorno, no en el flujo, porque los IDs de los Shelly del simulador no son los de los dispositivos físicos. Tu .env ya las trae con los IDs de los Shelly del simulador. Además, para ver lo que hacen tus flujos, usarás el panel del simulador.

Qué hacer.

  1. Actualiza tu entorno. Descarga de nuevo entorno-docker.zip y descomprímelo en el mismo sitio que en la sesión 1 (la carpeta que contiene docker, por ejemplo el Escritorio), aceptando reemplazar los ficheros. No pierdes nada: tu .env no va dentro del zip y tus flujos están en el volumen node-red-data.

    Por qué hay que descargarlo

    El simulador y su panel se construyen a partir de los ficheros de tu carpeta docker. docker compose up -d --build reconstruye la imagen con lo que tienes en disco, pero no descarga nada: si tu carpeta es de una versión anterior del curso, reconstruye el simulador antiguo, sin panel.

  2. En una terminal dentro de tu carpeta docker, reconstruye el entorno y comprueba qué Shelly controla la bomba:

    bash
    docker compose up -d --build
    docker compose exec node-red printenv SHELLY_BOMBA
  3. Abre el panel del simulador en http://localhost:8090 y déjalo en una pestaña del navegador durante toda la sesión. La guía del simulador explica cada zona.

Qué debes ver.

  • printenv escribe shellyplug-s-SIM001, el Shelly de la bomba del simulador. Tus flujos siguen en el editor.
  • El panel muestra MQTT conectado, la hora simulada avanzando y, en la tabla Nodos ESP32, los topics de cada nodo con valores que se renuevan cada pocos segundos.

Si no funciona.

  • No escribe nada, o el panel no abre: el zip no ha reemplazado tus ficheros. Comprueba que existe docker/simulador/panel.html; si no, probablemente lo descomprimiste en otro sitio (por ejemplo, dentro de la propia carpeta docker, que crea docker/docker). Repite el punto 1 en la carpeta correcta y luego los dos comandos.

Paso 1 · Explorar el árbol (10 min) ​

Para qué sirve. Ver con tus propios ojos todo lo que publica el broker antes de decidir a qué suscribirte.

Qué hacer.

  1. Crea una pestaña nueva S3 · Integración MQTT.
  2. Añade un mqtt in con Server Broker invernadero, Topic invernadero/# y Output a String, unido a un debug en msg.payload.
  3. Deploy y observa la depuración durante un minuto.

Qué debes ver. Decenas de mensajes, cada uno con su topic en la línea gris superior:

Pestaña debug con mensajes de muchos topics distintos: invernadero/dht11/temperatura, invernadero/suelo/humedad, invernadero/tds/voltaje, invernadero/shelly/shellyplug-s-SIM001/relay/0/power, invernadero/balanza/online…

Responde mirando la depuración:

  • ¿Cuántos topics distintos aparecen? Compáralos con el árbol de arriba.
  • ¿Qué IDs tienen los Shelly? Están en los topics invernadero/shelly/<id>/….
  • ¿Qué llega nada más desplegar, antes de que ningún sensor publique? Son los mensajes retenidos.

Después desactiva ese debug: pulsa el cuadradito verde de su derecha (se vuelve gris). # recibe todo y llena la depuración.

Con otras herramientas

docker compose exec mosquitto mosquitto_sub -u alumno -P alumno -t "invernadero/#" -v, que ya usaste en la sesión 1, muestra lo mismo en la terminal. También MQTT Explorer, con el usuario alumno.

Paso 2 · Una suscripción para todos los sensores (15 min) ​

Para qué sirve. En vez de un mqtt in por sensor, uno solo con comodines que reciba todas las magnitudes. Así un sensor nuevo entra solo en el sistema.

Qué hacer. Cambia el Topic del mqtt in por invernadero/+/+ y ponle Name invernadero/+/+.

Diálogo Edit mqtt in node: Server Broker invernadero, Action Subscribe to single topic, Topic invernadero/+/+, QoS 1, Output a String

Antes de desplegar, predice cuáles de estos topics recibirás:

Topic¿Coincide con invernadero/+/+?
invernadero/dht22/temperatura?
invernadero/tds/online?
invernadero/shelly/<id>/relay/0?
invernadero/shelly/command?

Deploy y activa de nuevo el debug un momento para comprobar tu predicción.

Qué debes ver. Solo topics de tres niveles: invernadero/<nodo>/<magnitud> y los invernadero/<nodo>/online. Ninguno de los Shelly, que tienen más niveles. + sustituye exactamente un nivel.

Paso 3 · Normalizar (30 min) ​

Para qué sirve. Convertir cada mensaje, venga del sensor que venga, al modelo de datos común y separar tres tipos de mensaje: datos válidos, estado de los ESP32 (online) y lecturas erróneas.

Qué hacer.

  1. Añade un function detrás del mqtt in. Name: normalizar.

  2. Tres salidas: en la pestaña Setup del diálogo, cambia Outputs de 1 a 3.

    Pestaña Setup del diálogo del function: Outputs 3, Timeout 0

  3. En On Message pega:

    js
    // invernadero/<nodo>/<magnitud>  ->  modelo de datos común
    // Salidas: 1 datos · 2 estado del puente (online) · 3 descartados
    const UNIDADES = { temperatura: "ºC", humedad: "%", distancia: "cm", peso: "g", lectura: "ADC", tds: "ppm", voltaje: "V" };
    // Límites físicos de cada magnitud: fuera de ellos la lectura es un error del sensor
    const RANGOS = { temperatura: [-20, 60], humedad: [0, 100], distancia: [2, 450], peso: [-100, 20000], lectura: [0, 4095], tds: [0, 2000], voltaje: [0, 3.3] };
    
    const [, sensor, magnitud] = msg.topic.split("/");
    const texto = String(msg.payload).trim();
    
    if (magnitud === "online") {
        msg.tipo = "online";
        msg.payload = { sensor, valor: Number(texto) };
        return [null, msg, null];
    }
    
    const valor = Number(texto);
    const rango = RANGOS[magnitud];
    if (!rango || texto === "" || !Number.isFinite(valor) || valor < rango[0] || valor > rango[1]) {
        msg.payload = { topic: msg.topic, recibido: texto, motivo: rango ? "valor no válido" : "magnitud desconocida" };
        return [null, null, msg];
    }
    
    msg.tipo = "dato";
    msg.payload = {
        sensor,
        magnitud,
        valor,
        unidad: UNIDADES[magnitud],
        ts: Date.now(),                 // hora de recepción, no de medida
        retenido: msg.retain === true   // true: último valor conocido, entregado al suscribirse
    };
    return [msg, null, null];
  4. Done. El nodo tiene ahora tres puertos de salida. Une un debug a cada uno: datos normalizados (salida 1), online (salida 2) y descartados (salida 3).

  5. Caso de prueba: un inject con msg.payload string 130 y msg.topic invernadero/dht22/humedad, unido a la entrada de normalizar. Deploy.

Qué hace el código:

  • msg.topic.split("/") parte el topic en trozos: de invernadero/suelo/humedad salen sensor = "suelo" y magnitud = "humedad".
  • Una función con varias salidas devuelve un array, con una posición por salida. [null, msg, null] envía el mensaje solo por la segunda; null significa "nada por esa salida".
  • msg.topic no se modifica: el watchdog del paso 5 lo necesita.
  • msg.tipo marca la clase de cada mensaje; lo usarás en el paso 6.

Qué debes ver.

  • En datos normalizados, objetos como { sensor: "suelo", magnitud: "humedad", valor: 42.5, unidad: "%", ts: …, retenido: false }.
  • En online, { sensor: "dht11", valor: 1 } para cada ESP32.
  • Al pulsar el inject de prueba, en descartados: { topic: "invernadero/dht22/humedad", recibido: "130", motivo: "valor no válido" } (una humedad del 130 % es imposible).

Prueba con sensores averiados. En el panel del simulador, pulsa el escenario Sensores averiados y espera un minuto:

  • En descartados aparecen, de vez en cuando, lecturas del DHT22 con recibido: "-3276.7" y "3276.7": el fallo típico de este sensor. Tu validación las para.
  • En cambio, el sensor de suelo, con el cable suelto, publica lectura 4095 y humedad 0, y pasan a datos normalizados: están dentro del rango físico. Validar por rango detecta lo imposible, no lo improbable. Fíjate: con el suelo al 0 % la balanza no ha cambiado de peso. Contrastar sensores entre sí es lo que delata la avería.

Vuelve al escenario Normal al terminar.

Si no funciona. Todo sale por la primera salida, o falla con TypeError: revisa Outputs en la pestaña Setup; con 1 salida, el array se interpreta de otra forma.

Paso 4 · Guardar el último valor (10 min) ​

Para qué sirve. Tener siempre a mano el estado actual de todas las magnitudes, para que cualquier pestaña (el panel de la sesión 4, tu API) lo pueda leer.

Qué hacer. Añade un function llamado guardar último valor a la salida 1 de normalizar:

js
// Último valor de cada magnitud, accesible desde cualquier pestaña
const p = msg.payload;
const sensores = global.get("sensores") || {};
sensores[p.sensor] = sensores[p.sensor] || {};
sensores[p.sensor][p.magnitud] = { valor: p.valor, unidad: p.unidad, ts: p.ts };
global.set("sensores", sensores);
return null;

return null porque este nodo solo guarda: no tiene que enviar nada más.

Qué debes ver. En Context Data → Global (pulsa ↻), sensores con un apartado por nodo del invernadero y, dentro, sus magnitudes:

Context Data, sección Global: sensores desplegado con suelo, dht22, dht11, balanza, ultrasonidos y tds; suelo muestra humedad y lectura con valor, unidad y ts

Paso 5 · Watchdog: detectar el silencio (25 min) ​

Para qué sirve. Si un sensor falla, el ESP32 deja de publicar: no manda ningún aviso. Hay que detectar la ausencia de mensajes. Todos los ESP32 publican como mínimo cada 5 segundos, así que 30 segundos sin datos de un topic indican un problema.

Qué hacer.

  1. Añade un trigger (sección function) y une a su entrada la salida 1 de normalizar. Ábrelo:

    Diálogo Edit trigger node: Send nothing, then wait for 30 seconds, extend delay if new message arrives marcado, then send the string sin datos, Handling each msg.topic independently, Name watchdog 30 s

    CampoValorPor qué
    SendnothingAl llegar un dato no hay que avisar de nada
    thenwait for 30 secondsEl tiempo de silencio que se tolera
    casilla✔ extend delay if new message arrivesCada dato nuevo reinicia la cuenta atrás
    then sendtipo string, sin datosLo que se envía si se agota el tiempo
    Handlingeach msg.topic independentlyUna cuenta atrás por topic, con un solo nodo
    Namewatchdog 30 s

    El campo Handling está al final del diálogo:

    Detalle del diálogo del trigger: Handling each msg.topic

  2. Detrás del trigger, un change que asigne msg.tipo = silencio (tipo az).

Qué debes ver. Nada mientras los sensores publiquen: esa es la idea. Lo comprobarás en el paso 6.

Línea de tiempo del watchdog: el sensor publica cada 5 segundos hasta el segundo 35 y cada mensaje reinicia la cuenta atrás de 30 segundos; al callar, la cuenta llega a cero en el segundo 65 y se emite sin datos; en el segundo 80 vuelve un mensaje y el estado pasa a recuperado

Paso 6 · Silencio frente a inalcanzable (30 min) ​

Para qué sirve. Hay dos formas distintas de fallar, y el sistema debe distinguirlas:

SituaciónCómo se detectaQué suele significar
El ESP32 está conectado, pero un topic deja de publicarWatchdog: sin datosSensor averiado o desconectado del ESP32
El ESP32 no es alcanzableinvernadero/<nodo>/online = 0ESP32 apagado o fuera de la Wi-Fi

Qué hacer.

  1. Añade un function llamado estado de sensores y une a su entrada tres cables: la salida 1 de normalizar (datos), la salida 2 (online) y la salida del change tipo = silencio.

  2. Pega el código:

    js
    // Mantiene la lista de fuentes con problemas y emite solo los cambios
    const problemas = global.get("problemas") || {};
    const ahora = Date.now();
    let evento = null;
    
    if (msg.tipo === "silencio") {
        // El trigger avisa: este topic lleva demasiado tiempo sin publicar
        if (!problemas[msg.topic]) {
            problemas[msg.topic] = { motivo: "sin datos", desde: ahora };
            evento = { fuente: msg.topic, estado: "sin datos" };
        }
    } else if (msg.tipo === "online") {
        // La RPi informa de si alcanza el ESP32
        const clave = `invernadero/${msg.payload.sensor}`;
        if (msg.payload.valor === 0 && !problemas[clave]) {
            problemas[clave] = { motivo: "ESP32 inalcanzable", desde: ahora };
            evento = { fuente: clave, estado: "inalcanzable" };
        } else if (msg.payload.valor === 1 && problemas[clave]) {
            delete problemas[clave];
            evento = { fuente: clave, estado: "alcanzable" };
        }
    } else if (msg.tipo === "dato" && problemas[msg.topic]) {
        // Vuelven a llegar datos de un topic que estaba en silencio
        delete problemas[msg.topic];
        evento = { fuente: msg.topic, estado: "recuperado" };
    }
    
    global.set("problemas", problemas);
    const n = Object.keys(problemas).length;
    node.status({ fill: n ? "red" : "green", shape: "dot", text: n ? `${n} con problemas` : "todo OK" });
    
    if (!evento) return null;
    msg.payload = { ...evento, ts: new Date(ahora).toISOString() };
    return msg;
  3. Detrás, un debug llamado eventos. Deploy.

La función solo emite cuando algo cambia: una fuente empieza a fallar o se recupera. Con cada lectura normal no sale nada. En la sesión 6, estos eventos serán las alarmas que lleguen a Telegram.

Pruebas. Provoca los fallos con el simulador. Lo más cómodo es el panel (http://localhost:8090): en la tabla Nodos ESP32, pulsa sensor en la fila suelo y, unos segundos después, desconectado en la fila tds. Para restaurar, ok en las dos. El panel marca en rojo los topics que dejan de publicar y en el registro verás cada cambio.

Lo mismo desde la terminal (en la carpeta docker/). La guía del simulador explica cada tipo de fallo y cómo tenerlos a mano en una pestaña de pruebas de Node-RED:

bash
# El sensor de suelo deja de publicar (el ESP32 sigue "alcanzable")
docker compose exec mosquitto mosquitto_pub -u alumno -P alumno -t simulador/fallo/suelo -m sensor
# El ESP32 del TDS desaparece de la red
docker compose exec mosquitto mosquitto_pub -u alumno -P alumno -t simulador/fallo/tds -m desconectado
# Todo vuelve a la normalidad
docker compose exec mosquitto mosquitto_pub -u alumno -P alumno -t simulador/fallo/suelo -m ok
docker compose exec mosquitto mosquitto_pub -u alumno -P alumno -t simulador/fallo/tds -m ok

Qué debes ver. En el debug eventos, por orden:

Pestaña debug con los eventos: invernadero/tds inalcanzable; invernadero/suelo/humedad y invernadero/suelo/lectura sin datos; invernadero/tds/tds y invernadero/tds/voltaje sin datos; después recuperado y alcanzable

  • inalcanzable para invernadero/tds, al instante: no depende del watchdog.
  • sin datos para los dos topics del suelo, unos 30 s después del primer comando.
  • sin datos también para los topics del TDS: un ESP32 inalcanzable tampoco publica. Un solo fallo genera varios eventos; en la sesión 6 agruparás las alarmas para avisar solo de la causa.
  • recuperado y alcanzable al restaurar.

Los sin datos llegan unos 25-30 s después del fallo, según cuándo publicó cada topic por última vez. Bajo el nodo, el contador: 5 con problemas y luego todo OK.

Prueba más dura: la tormenta. Lanza el escenario Tormenta en el panel. La Wi-Fi del invernadero se corta a ratos y los nodos se desconectan y vuelven cada poco: en eventos verás inalcanzable y alcanzable una y otra vez. Así de ruidoso sería un sistema de alarmas que avisara de cada cambio; en la sesión 6 aprenderás a filtrarlo (antirrebote). Vuelve a Normal al terminar.

Si no funciona. No sale ningún sin datos: comprueba que el trigger tiene each msg.topic independently y extend delay; sin extend, avisa a los 30 s aunque sigan llegando datos.

Paso 7 · Enviar comandos a los Shelly (25 min) ​

Para qué sirve. Actuar sobre el invernadero: encender y apagar la bomba y el ventilador. Toda la traducción "equipo → Shelly → topic" se hace en un único punto.

Qué hacer.

  1. Botones de prueba. Cuatro inject: bomba ON, bomba OFF, ventilador ON y ventilador OFF. En cada uno:

    • msg.payload: tipo string, on u off.
    • Borra msg.topic con su × y añade con +add una fila msg. equipo, tipo string, con bomba o ventilador.

    Diálogo Edit inject node: Name "bomba ON", msg.payload de tipo texto on y msg.equipo de tipo texto bomba

  2. Punto único. Un link in (sección common) llamado comandos a actuadores y, detrás, un function llamado a topic de comando:

    js
    // Traduce { equipo, payload } al topic de comando de su Shelly
    const ids = { bomba: env.get("SHELLY_BOMBA"), ventilador: env.get("SHELLY_VENTILADOR") };
    const id = ids[msg.equipo];
    if (!id) {
        node.warn(`No hay Shelly configurado para "${msg.equipo}"`);
        return null;
    }
    if (!["on", "off", "toggle"].includes(msg.payload)) {
        node.warn(`Orden no válida: ${msg.payload}`);
        return null;
    }
    msg.topic = `invernadero/shelly/${id}/relay/0/command`;
    msg.qos = 1;
    msg.retain = false;   // un comando nunca se retiene
    return msg;

    Une los cuatro inject a la entrada de a topic de comando. El link in servirá en la sesión 4 para que el panel envíe comandos por el mismo camino.

  3. Publicar. Un mqtt out (sección network) detrás del function. Ábrelo y deja Topic, QoS y Retain vacíos:

    Diálogo Edit mqtt out node: Server Broker invernadero; Topic vacío; QoS y Retain vacíos; Name comando

    Vacíos significa "toma el valor del mensaje": msg.topic, msg.qos y msg.retain, que acaba de fijar la función. Name: comando.

  4. Deploy.

La función es la única que sabe qué Shelly es cada equipo y qué órdenes son válidas. El resto del sistema solo dice "bomba on".

Qué debes ver. Con el simulador, al pulsar bomba ON, en el panel se enciende la tarjeta Bomba de riego (gotea mientras riega) y el registro muestra shellyplug-s-SIM001 -> on (bomba, MQTT). Si prefieres la terminal, lo mismo sale en el log del simulador (docker compose logs -f simulador). La confirmación en Node-RED la verás en el paso 8.

Si no funciona. Aviso No hay Shelly configurado para "bomba": faltan SHELLY_BOMBA y SHELLY_VENTILADOR en el .env, o no recreaste el contenedor (paso 0).

Prueba libremente

Con el simulador, la bomba y el ventilador son solo tuyos: nadie más ve tus comandos. Los Shelly del invernadero real, en cambio, los comparte toda la clase (ver Trabajar con el invernadero real).

Paso 8 · Confirmar el estado (15 min) ​

Para qué sirve. Enviar el comando no garantiza que se ejecute (recuerda el QoS 0 del bridge). La prueba es el estado que publica el Shelly.

Qué hacer.

  1. Un mqtt in con Topic invernadero/shelly/+/relay/0 y Name estado del relé, y detrás un function estado actuadores:

    js
    // invernadero/shelly/<id>/relay/0  ->  estado confirmado de cada equipo
    const id = msg.topic.split("/")[2];
    const equipos = { [env.get("SHELLY_BOMBA")]: "bomba", [env.get("SHELLY_VENTILADOR")]: "ventilador" };
    const equipo = equipos[id] || id;
    
    const actuadores = global.get("actuadores") || {};
    actuadores[equipo] = { estado: msg.payload, id, ts: Date.now() };
    global.set("actuadores", actuadores);
    
    node.status({ fill: msg.payload === "on" ? "blue" : "grey", shape: "dot", text: `${equipo}: ${msg.payload}` });
    msg.payload = { equipo, estado: msg.payload };
    return msg;

    y un debug actuadores.

  2. Un inject pedir estado (update) con msg.topic invernadero/shelly/command y msg.payload string update, unido directamente al mqtt out. Pide a los Shelly que publiquen su estado completo.

  3. Deploy, pulsa pedir estado (update) y después bomba ON.

Qué debes ver. Con update, un mensaje por Shelly: { equipo: "bomba", estado: "off" } y el del ventilador. Al pulsar bomba ON, en uno o dos segundos llega { equipo: "bomba", estado: "on" } y el nodo muestra bomba: on. En Context Data → Global, actuadores.bomba.estado es "on".

Recorrido de un comando: Node-RED publica con QoS 1, la RPi lo reenvía por el bridge al ESP32 actuador con QoS 0 y este al Shelly; el Shelly publica su nuevo estado, que vuelve por el mismo camino hasta Node-RED y confirma el comando

Prueba: alguien pulsa el enchufe. En el panel, pulsa Pulsar botón del enchufe en la tarjeta del ventilador. Es lo que pasa si alguien aprieta el botón físico del Shelly: el ventilador se enciende sin que Node-RED lo haya pedido. Aun así, estado actuadores muestra ventilador: on, porque tu flujo lee el estado que publica el Shelly en vez de suponer que su último comando se cumplió. Púlsalo otra vez para apagarlo.

Si no funciona. El comando sale pero el estado no cambia: mira en el panel que la fila actuador esté en ok y que el escenario no sea Corte de red ni Tormenta: la tarjeta de la bomba dice "Sin red" cuando el actuador no recibe comandos.

Paso 9 · Tu Node-RED también se anuncia: birth y will (10 min) ​

Para qué sirve. Los Shelly publican online para que se sepa si están conectados. Tu Node-RED puede hacer lo mismo: al conectarse publica un mensaje (birth) y, si se desconecta, el broker publica otro en su nombre (will). Más detalle en will y birth.

Qué hacer.

  1. Abre el nodo de configuración Broker invernadero: doble clic en cualquier mqtt in → lápiz de Server. Ve a la pestaña Messages.

    Diálogo del broker, pestaña Messages con sus tres bloques: Message sent on connection (birth message), Message sent before disconnecting (close message) y Message sent on an unexpected disconnection (will message), todos con el topic alumnos/tu-nombre/nodered, Retain true y QoS 1; el payload es online en el primero y offline en los otros dos

  2. La pestaña tiene tres bloques; despliega cada uno pulsando en su título. En los tres, el mismo Topic: alumnos/<tu-nombre>/nodered, con tu nombre sin espacios.

    BloquePayloadQoSRetainCuándo se publica
    Message sent on connection (birth)online1trueAl conectarse
    Message sent before disconnecting (close)offline1trueAl desconectarse de forma ordenada
    Message sent on an unexpected disconnection (will)offline1trueLo publica el broker si la conexión se corta sin despedirse
  3. Update y Deploy. Para comprobarlo, abre una segunda terminal dentro de tu carpeta docker (en Windows Terminal, botón + de la barra de pestañas, y cd hasta la carpeta) y suscríbete a los anuncios de todos los alumnos:

    bash
    docker compose exec mosquitto mosquitto_sub -u alumno -P alumno -v -t "alumnos/+/nodered"

    Es el mosquitto_sub que trae el contenedor del broker, como en la sesión 1: no tienes que instalar nada. -v escribe el topic delante de cada mensaje y + vale por cualquier nombre de alumno. La terminal se queda esperando mensajes: déjala abierta y usa la primera para el punto 4. Sal con Ctrl+C cuando termines.

  4. Prueba las dos formas de parar:

    • docker compose stop node-red cierra la conexión ordenadamente y publica el close message.
    • docker compose kill node-red corta en seco: Node-RED no llega a despedirse y es el broker quien publica el will, cuando detecta la desconexión (tras el keep alive, unos segundos).

    Vuelve a arrancarlo con docker compose up -d.

Qué debes ver. alumnos/<tu-nombre>/nodered online al arrancar y offline al parar, en los dos casos, aunque por caminos distintos.

El flujo completo de la práctica guiada queda así:

Flujo S3 en el editor: invernadero/+/+ va a normalizar, con tres salidas hacia datos normalizados, guardar último valor, el watchdog de 30 s y estado de sensores, y hacia descartados; debajo, los actuadores: invernadero/shelly/#, los inject bomba y ventilador, el link in comandos a actuadores, a topic de comando y el mqtt out comando; y estado del relé con estado actuadores

Flujo de referencia: s3-integracion-mqtt.json. No incluye el birth/will, que es personal: configúralo tú.

Node-RED como plataforma ​

Nodos de configuración ​

Broker invernadero no es un nodo del flujo: es un nodo de configuración que comparten todos los mqtt in y mqtt out.

  • Lo ves en la barra lateral Configuration nodes (menú ☰ → Configuration nodes), que también marca los que no usa ningún nodo.
  • Node-RED abre una sola conexión por nodo de configuración, no una por cada mqtt in. Cambiar el host o el birth en un sitio afecta a todos los nodos que lo usan.
  • Por eso el Client ID debe ser único por Node-RED. Dos Node-RED con el mismo ID se expulsan del broker el uno al otro en bucle. Dejarlo vacío genera uno aleatorio.

Subflujos ​

Un subflujo empaqueta varios nodos en uno reutilizable, con propiedades configurables en cada instancia.

Ejercicio: convierte el watchdog en un subflujo vigilante con un tiempo configurable.

  1. Selecciona el trigger y el change del watchdog → menú ☰ → Subflows → Selection to Subflow.
  2. Edita el subflujo (doble clic en su plantilla, en la paleta) → Edit properties → añade la propiedad TIMEOUT_S, tipo número, valor por defecto 30.
  3. Dentro del subflujo, antes del trigger, añade un change que asigne msg.delay a la expresión JSONata $number($env("TIMEOUT_S")) * 1000. En el trigger, marca Override delay with msg.delay.
  4. Usa dos instancias: una de 30 s para los sensores y otra de 90 s para un topic que publique más despacio.

Entregable y evidencias para el informe ​

Qué debe funcionar ​

  1. Todos los sensores integrados. global.sensores contiene todas las magnitudes del invernadero en el modelo común. Las lecturas no válidas se descartan y se ven en la depuración.
  2. Vigilancia. Se detectan tanto el silencio de un topic (30 s) como un ESP32 inalcanzable, con su evento de recuperación. global.problemas refleja el estado actual.
  3. Riego automático con confirmación.
    • Conecta la decisión de riego de tu flujo de la sesión 1 a la bomba con link out/link in: on cuando haya que regar, off cuando no.
    • Envía el comando solo cuando la decisión cambie (usa el campo cambio de la sesión 1), no con cada lectura.
    • Si 10 segundos después de un comando el estado del Shelly no coincide, emite el evento comando sin confirmar.
  4. API ampliada. GET /api/invernadero/estado (sesión 2) devuelve también sensores, problemas y actuadores.

Reto opcional: enclavamiento de seguridad. La bomba no debe encenderse si el depósito está casi vacío (distancia ≥ 35 cm) ni si el sensor de distancia está en problemas. Si no sabes cuánta agua hay, no bombeas. Esto se llama diseño fail-safe: ante la duda, el estado seguro.

Pruébalo con el simulador, provocando fallos y vaciando el depósito.

Cómo se comprueba ​

  • global.sensores tiene todos los nodos y magnitudes y se actualiza en tiempo real.
  • Con los fallos del simulador aparecen los eventos sin datos, inalcanzable, recuperado y alcanzable, uno por cambio.
  • Al cambiar la decisión de riego, la bomba cambia de estado y se confirma: en el panel del simulador se enciende y se apaga sola. Para forzar la decisión, pon a mano la humedad del suelo en el panel (zona Estado físico, Aplicar). Si apagas el Shelly o provocas simulador/fallo/actuador desconectado, aparece comando sin confirmar.

Criterios de calidad:

  • Los comandos nunca se publican con retain.
  • Hay un solo punto que traduce equipo → Shelly.
  • No se envían comandos repetidos.
  • Nodos con nombre y flujo organizado en grupos.

Evidencias que debes guardar ​

  • [ ] Captura del flujo completo.
  • [ ] Captura de Context Data → Global con sensores, problemas y actuadores.
  • [ ] Captura de la depuración con la secuencia de eventos de las pruebas de fallo.
  • [ ] Captura de mosquitto_sub (o MQTT Explorer) mostrando tu alumnos/<tu-nombre>/nodered en online y en offline.
  • [ ] Flujo exportado: s3-<tu-apellido>.json.
  • [ ] En el informe, respuesta breve a:
    • ¿Por qué un comando nunca debe publicarse con retain? Describe qué pasaría en el invernadero.
    • ¿Con qué QoS llega tu comando al Shelly? ¿Qué te garantiza entonces que se ha ejecutado?
    • ¿Qué diferencia hay entre sin datos e inalcanzable, y por qué hacen falta dos mecanismos para detectarlos?
    • ¿Cuándo publica el broker tu will y cuándo tu close message?

Recursos adicionales ​