Tema
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.
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:
tdsmide los sólidos disueltos (Total Dissolved Solids) en el agua del depósito de riego, es decir, cuántas sales y nutrientes lleva.tds/tdses la concentración en ppm ytds/voltajela lectura en bruto de la sonda, de la que el ESP32 calcula los ppm.actuadorno 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 esoinvernadero/actuador/onlineindica si la Raspberry Pi alcanza ese ESP32, mientras queinvernadero/shelly/<id>/onlinelo 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 mensaje | Ejemplo | QoS | Retain |
|---|---|---|---|
| Lectura periódica | invernadero/dht22/temperatura | 0: si se pierde una, llega otra en 5 s | Sí: el último valor conocido es útil |
| Estado | invernadero/shelly/<id>/relay/0, …/online | 1 | Sí: quien se conecta debe conocer el estado |
| Comando | invernadero/shelly/<id>/relay/0/command | 1 | Nunca |
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.
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.
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.envno va dentro del zip y tus flujos están en el volumennode-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 --buildreconstruye 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.En una terminal dentro de tu carpeta
docker, reconstruye el entorno y comprueba qué Shelly controla la bomba:bashdocker compose up -d --build docker compose exec node-red printenv SHELLY_BOMBAAbre 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.
printenvescribeshellyplug-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 carpetadocker, que creadocker/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.
- Crea una pestaña nueva
S3 · Integración MQTT. - Añade un
mqtt incon ServerBroker invernadero, Topicinvernadero/#y Output a String, unido a undebugen msg.payload. - 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:

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/+/+.

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.
Añade un
functiondetrás delmqtt in. Name:normalizar.Tres salidas: en la pestaña Setup del diálogo, cambia Outputs de
1a3.
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];Done. El nodo tiene ahora tres puertos de salida. Une un
debuga cada uno:datos normalizados(salida 1),online(salida 2) ydescartados(salida 3).Caso de prueba: un
injectcon msg.payload string130y msg.topicinvernadero/dht22/humedad, unido a la entrada denormalizar. Deploy.
Qué hace el código:
msg.topic.split("/")parte el topic en trozos: deinvernadero/suelo/humedadsalensensor = "suelo"ymagnitud = "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;nullsignifica "nada por esa salida". msg.topicno se modifica: el watchdog del paso 5 lo necesita.msg.tipomarca 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
injectde prueba, endescartados:{ 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
descartadosaparecen, de vez en cuando, lecturas del DHT22 conrecibido: "-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
lectura4095yhumedad0, y pasan adatos 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:

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.
Añade un trigger (sección function) y une a su entrada la salida 1 de
normalizar. Ábrelo:
Campo Valor Por qué Send nothing Al llegar un dato no hay que avisar de nada then wait for 30secondsEl tiempo de silencio que se tolera casilla ✔ extend delay if new message arrives Cada dato nuevo reinicia la cuenta atrás then send tipo string, sin datosLo que se envía si se agota el tiempo Handling each msg.topicindependentlyUna cuenta atrás por topic, con un solo nodo Name watchdog 30 sEl campo Handling está al final del diálogo:
Detrás del
trigger, unchangeque asignemsg.tipo=silencio(tipo az).
Qué debes ver. Nada mientras los sensores publiquen: esa es la idea. Lo comprobarás en el paso 6.
Paso 6 · Silencio frente a inalcanzable (30 min)
Para qué sirve. Hay dos formas distintas de fallar, y el sistema debe distinguirlas:
| Situación | Cómo se detecta | Qué suele significar |
|---|---|---|
| El ESP32 está conectado, pero un topic deja de publicar | Watchdog: sin datos | Sensor averiado o desconectado del ESP32 |
| El ESP32 no es alcanzable | invernadero/<nodo>/online = 0 | ESP32 apagado o fuera de la Wi-Fi |
Qué hacer.
Añade un
functionllamadoestado de sensoresy une a su entrada tres cables: la salida 1 denormalizar(datos), la salida 2 (online) y la salida delchangetipo = silencio.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;Detrás, un
debugllamadoeventos. 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 okQué debes ver. En el debug eventos, por orden:

inalcanzableparainvernadero/tds, al instante: no depende del watchdog.sin datospara los dos topics del suelo, unos 30 s después del primer comando.sin datostambié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.recuperadoyalcanzableal 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.
Botones de prueba. Cuatro
inject:bomba ON,bomba OFF,ventilador ONyventilador OFF. En cada uno:- msg.payload: tipo string,
onuoff. - Borra msg.topic con su × y añade con +add una fila msg.
equipo, tipo string, conbombaoventilador.

- msg.payload: tipo string,
Punto único. Un link in (sección common) llamado
comandos a actuadoresy, detrás, unfunctionllamadoa 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
injecta la entrada dea topic de comando. Ellink inservirá en la sesión 4 para que el panel envíe comandos por el mismo camino.Publicar. Un mqtt out (sección network) detrás del
function. Ábrelo y deja Topic, QoS y Retain vacíos:
Vacíos significa "toma el valor del mensaje":
msg.topic,msg.qosymsg.retain, que acaba de fijar la función. Name:comando.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.
Un
mqtt incon Topicinvernadero/shelly/+/relay/0y Nameestado del relé, y detrás unfunctionestado 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
debugactuadores.Un
injectpedir estado (update)con msg.topicinvernadero/shelly/commandy msg.payload stringupdate, unido directamente almqtt out. Pide a los Shelly que publiquen su estado completo.Deploy, pulsa
pedir estado (update)y despuésbomba 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".
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.
Abre el nodo de configuración
Broker invernadero: doble clic en cualquiermqtt in→ lápiz de Server. Ve a la pestaña Messages.
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.Bloque Payload QoS Retain Cuándo se publica Message sent on connection (birth) online1 trueAl conectarse Message sent before disconnecting (close) offline1 trueAl desconectarse de forma ordenada Message sent on an unexpected disconnection (will) offline1 trueLo publica el broker si la conexión se corta sin despedirse 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, ycdhasta la carpeta) y suscríbete a los anuncios de todos los alumnos:bashdocker compose exec mosquitto mosquitto_sub -u alumno -P alumno -v -t "alumnos/+/nodered"Es el
mosquitto_subque trae el contenedor del broker, como en la sesión 1: no tienes que instalar nada.-vescribe 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.Prueba las dos formas de parar:
docker compose stop node-redcierra la conexión ordenadamente y publica el close message.docker compose kill node-redcorta 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 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.
- Selecciona el
triggery elchangedel watchdog → menú ☰ → Subflows → Selection to Subflow. - Edita el subflujo (doble clic en su plantilla, en la paleta) → Edit properties → añade la propiedad
TIMEOUT_S, tipo número, valor por defecto30. - Dentro del subflujo, antes del
trigger, añade unchangeque asignemsg.delaya la expresión JSONata$number($env("TIMEOUT_S")) * 1000. En eltrigger, marca Override delay with msg.delay. - 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
- Todos los sensores integrados.
global.sensorescontiene todas las magnitudes del invernadero en el modelo común. Las lecturas no válidas se descartan y se ven en la depuración. - Vigilancia. Se detectan tanto el silencio de un topic (30 s) como un ESP32 inalcanzable, con su evento de recuperación.
global.problemasrefleja el estado actual. - 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:oncuando haya que regar,offcuando no. - Envía el comando solo cuando la decisión cambie (usa el campo
cambiode 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.
- Conecta la decisión de riego de tu flujo de la sesión 1 a la bomba con
- API ampliada.
GET /api/invernadero/estado(sesión 2) devuelve tambiénsensores,problemasyactuadores.
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.sensorestiene todos los nodos y magnitudes y se actualiza en tiempo real.- Con los fallos del simulador aparecen los eventos
sin datos,inalcanzable,recuperadoyalcanzable, 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, aparececomando 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,problemasyactuadores. - [ ] Captura de la depuración con la secuencia de eventos de las pruebas de fallo.
- [ ] Captura de
mosquitto_sub(o MQTT Explorer) mostrando tualumnos/<tu-nombre>/noderedenonliney enoffline. - [ ] 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 datoseinalcanzable, y por qué hacen falta dos mecanismos para detectarlos? - ¿Cuándo publica el broker tu will y cuándo tu close message?