Skip to content

Sesión 4 · Paneles de monitorización ​

Objetivos de la sesión ​

  • Construir un panel de operador con Dashboard 2.0: páginas, grupos, gauges, textos, tablas y gráficas.
  • Aplicar criterios de diseño HMI: estado general arriba, agrupación por proceso, color solo para lo anómalo y unidades siempre visibles.
  • Controlar la bomba y el ventilador desde el panel mostrando su estado confirmado, no el clic.
  • Hacer editables los umbrales de control desde el panel, con validación.
  • Distinguir en el panel un valor actual de uno congelado por un sensor averiado.

Conexión con el sistema del invernadero ​

Hasta ahora el estado del invernadero solo se veía en la barra de depuración y en Context Data. En esta sesión construyes la interfaz del operador: una página web que cualquiera puede abrir desde el móvil o desde un PC para saber cómo está el invernadero y actuar sobre él.

El panel no recibe datos por su cuenta. Lee el contexto global que llenan las sesiones anteriores (sensores, actuadores y problemas de la S3, meteo y ventilacion de la S2) y envía los comandos al punto único de la S3. Así se muestra lo mismo que usan la lógica de control y las alarmas, sin repetir código.

Este es el panel que construirás, funcionando con el simulador:

Captura del panel del invernadero: arriba, el estado del sistema con "Todo en orden"; debajo, los grupos Clima (gauges de temperatura interior y humedad del aire, temperatura exterior), Riego y agua (gauge de humedad del suelo, depósito al 62 %, calidad del agua y peso de la maceta) y Actuadores (interruptores de bomba y ventilador, motivo de la ventilación); después, las gráficas de temperatura y humedad del suelo de la última hora y los deslizadores de consignas

Conceptos clave ​

Dashboard 2.0 ​

Dashboard 2.0 (@flowfuse/node-red-dashboard) añade a la paleta nodos que se convierten en elementos de una página web servida por el mismo Node-RED. Ya viene instalado en la imagen del curso. Su estructura tiene tres niveles de configuración, y los widgets cuelgan del último:

Jerarquía de Dashboard 2.0: ui-base define la aplicación en /dashboard; dentro, la ui-page Invernadero en /invernadero con disposición en rejilla; dentro, grupos ui-group de 12 o 4 columnas; dentro de cada grupo, los widgets. La URL resultante es /dashboard/invernadero

NodoQué esDónde se ve
ui-baseLa aplicación del panel. Define la ruta base (/dashboard)Nodo de configuración
ui-pageUna página: ruta, disposición (grid, fixed, notebook) y temaNodo de configuración; aparece en el menú lateral del panel
ui-groupUna tarjeta dentro de la página, con un ancho en columnas (de 12)Nodo de configuración
ui-themeColores y espaciadosNodo de configuración
Widgets (ui-gauge, ui-text, ui-switch, ui-chart…)Lo que ve el operadorNodos del flujo, cada uno asignado a un grupo

Documentación: Dashboard 2.0.

Principios de una HMI industrial ​

Un panel de operador no es una web vistosa: su trabajo es que el operador detecte en segundos si algo va mal. Algunas reglas, inspiradas en la norma ISA-101 (High Performance HMI):

PrincipioEn el panel del invernadero
Lo más importante, arribaEl grupo Estado del sistema ocupa todo el ancho y dice "Todo en orden" o cuántas fuentes fallan
Agrupar por proceso, no por tipo de widgetClima, Riego y agua, Actuadores: no "todos los gauges" juntos
Fondo neutro, color solo para lo anómaloPágina gris claro; verde, ámbar y rojo solo en los rangos de los gauges
Unidades siempre visiblesºC, %, ppm, kg en cada valor
Mostrar el estado realEl interruptor refleja lo que dice el Shelly, no lo que se pulsó
No mentir con datos viejosUn valor congelado por un sensor averiado debe distinguirse de uno actual

Leer del contexto frente a recibir mensajes ​

Hay dos formas de alimentar un widget: conectarlo al cable por el que llegan los datos (eventos) o leer el contexto global cada cierto tiempo (muestreo). Aquí se usa el muestreo cada 2 segundos, enviando solo lo que cambia:

  • El panel queda desacoplado de las otras pestañas: depende de los nombres del contexto global, no de cómo están hechos los flujos de las sesiones 2 y 3.
  • Un navegador que se conecta tarde recibe enseguida el estado actual.
  • Enviar solo lo que cambia evita saturar la conexión con el navegador.

Las gráficas son la excepción: toman una muestra cada 10 s aunque el valor no cambie, para que el eje de tiempo sea regular.

Práctica guiada paso a paso ​

Cada paso indica para qué sirve, qué hacer, qué debes ver y si no funciona. Lo nuevo de esta sesión son los nodos de Dashboard 2.0, que están en la paleta en la sección dashboard 2 (color turquesa), y sus nodos de configuración, que se crean desde el lápiz de cada widget.

Paso 0 · Preparar la sesión 3 para el panel (5 min) ​

Para qué sirve. El panel enviará comandos a la bomba y al ventilador por el punto único de la sesión 3, no directamente a MQTT.

Qué hacer. Abre tu pestaña de la sesión 3 y comprueba que delante de a topic de comando hay un link in llamado comandos a actuadores. Si importaste el flujo de referencia, ya está. Si hiciste tu propio flujo, añádelo (sección common) y únelo a la entrada de tu función de comandos.

Paso 1 · Primer widget (20 min) ​

Para qué sirve. Poner un primer valor en una página web y, de paso, crear la cadena de configuración del panel: base → página → grupo.

Qué hacer.

  1. Crea una pestaña S4 · Panel. Añade un inject con msg.payload de tipo number y valor 42, y detrás un gauge (sección dashboard 2).

  2. Abre el gauge. En Group aparece Add new ui-group...: pulsa el lápiz de su derecha.

    Diálogo Edit gauge node: Name Temperatura interior, Group [Invernadero] Clima, Size 2 x 3, Type 3/4 Gauge, Style Needle, Range 0 a 50, cuatro Segments de color desde 0, 15, 30 y 35, y en Labelling el Label Temperatura interior y las Units ºC

  3. En el diálogo del grupo:

    Diálogo Edit ui-group node: Name Clima, Page Invernadero [/dashboard/invernadero], Type Default, Size 4 x 1 y Display group name marcado

    • Name: Clima.
    • Size: pulsa el botón de tamaño y elige 4 columnas de ancho (la página tiene 12): queda 4 x 1.
    • En Page aparece Add new ui-page...: pulsa su lápiz.
  4. En el diálogo de la página:

    Diálogo Edit ui-page node: Name Invernadero, UI Invernadero [/dashboard], Path /invernadero, Icon sprout, Theme HMI invernadero y Layout Grid

    • Name: Invernadero.
    • Path: /invernadero.
    • Layout: Grid.
    • En UI aparece Add new ui-base...: pulsa su lápiz, deja la ruta /dashboard y pulsa Add.
    • Pulsa Add en la página y después en el grupo. Vuelves al gauge con todo creado.
  5. En el gauge: Name Temperatura interior, Range de 0 a 50 y, en la sección Labelling (más abajo), Label Temperatura interior y Units ºC. Done y Deploy.

  6. Abre http://localhost:1880/dashboard/invernadero en otra pestaña del navegador y pulsa el inject en el editor.

Qué debes ver. Una tarjeta Clima con un gauge cuya aguja marca 42. Abre la misma dirección en el móvil (http://<IP de tu PC>:1880/dashboard/invernadero, en la misma red): los dos navegadores muestran lo mismo.

La barra lateral Dashboard 2.0

En el editor, la pestaña de la barra lateral con el icono de rejilla muestra la estructura del panel: páginas, grupos y widgets. Arrastrándolos ahí cambias su orden en la página, y el botón Open Dashboard abre el panel.

Barra lateral Dashboard 2.0 del editor, pestaña Layout: la base Invernadero con los botones Edit Settings y Open Dashboard; bajo Pages, la página Invernadero con 6 grupos y, dentro, Estado del sistema, Clima y Riego y agua con sus widgets

Si no funciona. La página sale en blanco o con "No pages": falta Deploy, o la ruta de la página no coincide con la que abres.

Paso 2 · Estructura del panel (15 min) ​

Para qué sirve. Decidir dónde va cada cosa antes de llenar el panel de widgets, siguiendo los principios HMI: el estado general arriba y el resto agrupado por proceso.

Qué hacer. Crea los grupos que faltan. Para no tener que añadir un widget por cada uno, usa la barra lateral Dashboard 2.0: pasa el ratón sobre la fila de la página Invernadero y pulsa el botón + que aparece (add group).

GrupoAnchoContenido
Estado del sistema12Resumen y tabla de problemas
Clima4Temperatura interior, humedad del aire, temperatura exterior
Riego y agua4Humedad del suelo, depósito, calidad del agua, peso
Actuadores4Bomba, ventilador, motivo de la ventilación
Tendencias (última hora)12Gráficas
Consignas6Umbrales editables

Ordénalos arrastrándolos en la barra lateral: Estado del sistema el primero.

Qué debes ver. En la barra lateral, los seis grupos bajo la página Invernadero, en ese orden. Los grupos vacíos no se muestran en el panel.

Paso 3 · Clima: del contexto global a los widgets (30 min) ​

Para qué sirve. Llevar los valores de global.sensores y global.meteo (sesiones 2 y 3) a los widgets. Cada 2 segundos, una función lee el contexto y envía solo lo que ha cambiado.

Qué hacer.

  1. Cambia el inject del paso 1: msg.payload de tipo timestamp, Inject once after 1 segundo y Repeat interval cada 2 seconds. Name: cada 2 s.

  2. Inserta entre el inject y el gauge un function llamado grupo Clima con 3 salidas (pestaña Setup → Outputs 3):

    js
    // Contexto global -> widgets del grupo Clima (solo lo que cambia)
    const ultimo = context.get("ultimo") || {};
    const si_cambia = (clave, valor) => {
        if (valor === undefined || valor === null) return null;
        const huella = JSON.stringify(valor);
        if (huella === ultimo[clave]) return null;
        ultimo[clave] = huella;
        return { payload: valor };
    };
    const s = global.get("sensores") || {};
    const meteo = global.get("meteo");
    const salidas = [
        si_cambia("temp", s.dht22?.temperatura?.valor),
        si_cambia("hum", s.dht22?.humedad?.valor),
        si_cambia("ext", meteo ? `${meteo.tempExterior} ºC` : "sin datos")
    ];
    context.set("ultimo", ultimo);
    return salidas;
    • ?. (optional chaining) devuelve undefined en vez de fallar si todavía no ha llegado ningún dato de ese sensor.
    • si_cambia recuerda, en el contexto del nodo (context), lo último que envió por cada salida.
  3. Une la salida 1 al gauge Temperatura interior. Ábrelo y configura:

    • Type: 3/4 Gauge.
    • Segments (los rangos de color): azul desde 0, verde desde 15, ámbar desde 30 y rojo desde 35. El botón + bajo la lista añade un segmento; el cuadrado de color de cada fila abre el selector de color.
  4. Salida 2 → un gauge nuevo Humedad del aire en el grupo Clima: 3/4 Gauge, rango 0–100, Units % y un solo segmento azul desde 0. La humedad del aire no tiene un rango "malo" definido en el sistema, así que no lleva colores de estado: el color se reserva para lo que exige atención.

  5. Salida 3 → un text (sección dashboard 2) en Clima con Label Exterior.

  6. Deploy.

Qué debes ver. El grupo Clima con los dos gauges y la temperatura exterior, actualizándose solos:

Grupo Clima del panel: gauge de temperatura interior a 25,7 ºC con los segmentos de color, gauge de humedad del aire a 63,9 % en azul y la temperatura exterior 28,5 ºC

Si no funciona. Los gauges no se mueven: comprueba en Context Data → Global que existe sensores (el flujo de la sesión 3 tiene que estar desplegado) y que el inject se repite (icono ↻ junto a su nombre).

Paso 4 · Riego y agua: el depósito en % (20 min) ​

Para qué sirve. Mostrar el agua como la entiende el operador. El sensor de ultrasonidos mide la distancia hasta el agua, pero lo que interesa es el nivel del depósito.

Qué hacer.

  1. Un inject con Inject once after marcado y msg.payload de tipo JSON:

    json
    { "llenoCm": 5, "vacioCm": 40 }

    y un change con Set flow.deposito to the value msg.payload. Son las distancias del sensor al agua con el depósito lleno y vacío, las del simulador.

  2. Un function llamado grupo Riego y agua con 4 salidas, conectado al inject cada 2 s:

    js
    // Contexto global -> widgets del grupo Riego y agua
    const ultimo = context.get("ultimo") || {};
    const si_cambia = (clave, valor) => {
        if (valor === undefined || valor === null) return null;
        const huella = JSON.stringify(valor);
        if (huella === ultimo[clave]) return null;
        ultimo[clave] = huella;
        return { payload: valor };
    };
    const s = global.get("sensores") || {};
    const dep = flow.get("deposito");   // dimensiones del depósito
    const d = s.ultrasonidos?.distancia?.valor;
    
    // Distancia del sensor al agua -> nivel en %
    let nivel;
    if (d !== undefined && dep) {
        nivel = (dep.vacioCm - d) / (dep.vacioCm - dep.llenoCm) * 100;
        nivel = Math.round(Math.min(100, Math.max(0, nivel)));
    }
    const tds = s.tds?.tds?.valor;
    const peso = s.balanza?.peso?.valor;
    const salidas = [
        si_cambia("suelo", s.suelo?.humedad?.valor),
        si_cambia("nivel", nivel),
        si_cambia("tds", tds !== undefined ? `${Math.round(tds)} ppm` : undefined),
        si_cambia("peso", peso !== undefined ? `${(peso / 1000).toFixed(2)} kg` : undefined)
    ];
    context.set("ultimo", ultimo);
    return salidas;
  3. Los widgets, todos en el grupo Riego y agua:

    SalidaWidgetConfiguración
    1gauge Humedad del sueloType Half Gauge, rango 0–100, %, segmentos rojo 0 · ámbar 25 · verde 35 (los umbrales de la sesión 1)
    2gauge DepósitoType Tank Level, rango 0–100, %
    3text Calidad del agua
    4text Peso de la maceta

Qué debes ver. El grupo con el gauge del suelo, el depósito "lleno de agua" hasta su nivel, y los valores en ppm y kg.

Paso 5 · Actuadores: mostrar el estado confirmado (25 min) ​

Para qué sirve. Encender y apagar la bomba y el ventilador desde el panel, pero con un interruptor que refleje lo que dice el Shelly, no el clic del operador.

Qué hacer.

  1. Añade dos switch de dashboard 2 (no el switch de function), Bomba de riego y Ventilador, en el grupo Actuadores:

    Diálogo Edit switch node: Name y Label Bomba de riego, Group [Invernadero] Actuadores, pass through desmarcado, Indicator "Switch icon shows state of the input" y, en When clicked, send, On Payload on, Off Payload off y Topic bomba, todos de tipo texto

    CampoValor
    If msg arrives on input, pass through to outputdesmarcado
    IndicatorSwitch icon shows state of the input
    On Payload / Off Payloadtipo string: on / off
    Topictipo string: bomba (o ventilador)
  2. Une la salida de los dos interruptores a un change que asigne msg.equipo = msg.topic (en to the value, tipo msg., topic). Detrás, un link out. Ábrelo y marca en la lista el link in comandos a actuadores de la pestaña S3.

  3. Un function grupo Actuadores con 3 salidas, conectado al inject cada 2 s: la salida 1 al interruptor de la bomba, la 2 al del ventilador y la 3 a un text Ventilación:

    js
    // Estado CONFIRMADO de los actuadores -> interruptores (modo decouple)
    const ultimo = context.get("ultimo") || {};
    const si_cambia = (clave, valor) => {
        if (valor === undefined || valor === null) return null;
        const huella = JSON.stringify(valor);
        if (huella === ultimo[clave]) return null;
        ultimo[clave] = huella;
        return { payload: valor };
    };
    const a = global.get("actuadores") || {};
    const v = global.get("ventilacion");
    const salidas = [
        si_cambia("bomba", a.bomba?.estado),
        si_cambia("ventilador", a.ventilador?.estado),
        si_cambia("vent", v ? v.motivo : undefined)
    ];
    context.set("ultimo", ultimo);
    return salidas;
  4. Deploy.

Con Switch icon shows state of the input (la propiedad decouple), el interruptor no cambia al hacer clic: envía el comando y espera a que llegue el estado del Shelly por su entrada.

Ciclo del interruptor: el clic envía "on" sin cambiar el indicador; el comando pasa por el punto único de la sesión 3 y llega al Shelly; el Shelly publica su estado, la sesión 3 lo guarda en global.actuadores, el panel lo lee y solo entonces el indicador pasa a on

Qué debes ver. Con el simulador, al pulsar el interruptor de la bomba, pasa a on en uno o dos segundos (el tiempo de ida y vuelta del comando). Ahora provoca simulador/fallo/actuador = desconectado (en el panel del simulador, botón desconectado de la fila actuador) y pulsa de nuevo: el comando se envía, pero el interruptor se queda como estaba, porque el Shelly no lo ha confirmado. Eso es exactamente lo que debe ver el operador. Restaura con ok.

Si no funciona. El interruptor cambia al instante al hacer clic: el Indicator está en state of the output. No pasa nada al pulsar: el link out no apunta a comandos a actuadores.

Prueba libremente

Con el simulador, los interruptores mueven solo tu bomba y tu ventilador. Con el invernadero real, en cambio, cualquier clic enciende la bomba para toda la clase (ver Trabajar con el invernadero real).

Paso 6 · Tendencias (20 min) ​

Para qué sirve. Ver la evolución de la última hora, no solo el valor actual.

Qué hacer.

  1. Un inject que se repita cada 10 segundos → un function muestrear tendencias con 2 salidas:

    js
    // Muestreo para las gráficas: un punto cada 10 s por serie
    const s = global.get("sensores") || {};
    const meteo = global.get("meteo");
    const temp = [];
    if (s.dht22?.temperatura) temp.push({ topic: "interior", payload: s.dht22.temperatura.valor });
    if (meteo) temp.push({ topic: "exterior", payload: meteo.tempExterior });
    const suelo = s.suelo?.humedad ? { topic: "suelo", payload: s.suelo.humedad.valor } : null;
    // Un array en una salida envía varios mensajes seguidos
    return [temp, suelo];
  2. Dos chart (sección dashboard 2) en el grupo Tendencias:

    Diálogo Edit chart node: Label Temperatura (ºC), Group Tendencias, Type Line, Action Append, X-Axis Type Timescale, X-Axis Limit last 1 Hours, y en Properties Series msg.topic, X timestamp, Y msg.payload

    CampoValorPor qué
    TypeLineEvolución en el tiempo
    ActionAppendCada mensaje añade un punto
    X-Axis TypeTimescaleEl eje X es la hora de llegada
    X-Axis Limitlast 1 HoursSolo se conserva la última hora
    Properties → Seriesmsg.topicCada topic distinto es una línea: interior, exterior
    Properties → X / Ytimestamp / msg.payloadHora de llegada y valor

    La salida 1 va a la gráfica Temperatura (ºC) y la salida 2 a Humedad del suelo (%) (esta con Y-Axis min. 0 y max. 100).

Qué debes ver. Dos gráficas que crecen con un punto cada 10 s. La serie exterior sale casi plana porque la previsión solo se actualiza cada 15 minutos.

Una gráfica por unidad. La temperatura interior y la exterior comparten gráfica porque tienen la misma unidad. La humedad del suelo va en otra: mezclar ºC y % con dos ejes Y en la misma gráfica invita a comparar alturas que no son comparables. En la sesión 5 estas gráficas se alimentarán del histórico de la base de datos y no se perderán al recargar.

Paso 7 · Estado del sistema (15 min) ​

Para qué sirve. Que lo primero que vea el operador sea si todo va bien o no, y qué falla.

Qué hacer.

  1. Un function grupo Estado con 2 salidas, conectado al inject cada 2 s:

    js
    // Resumen y tabla de problemas (global.problemas, sesión 3)
    const ultimo = context.get("ultimo") || {};
    const si_cambia = (clave, valor) => {
        if (valor === undefined || valor === null) return null;
        const huella = JSON.stringify(valor);
        if (huella === ultimo[clave]) return null;
        ultimo[clave] = huella;
        return { payload: valor };
    };
    const p = global.get("problemas") || {};
    const filas = Object.entries(p).map(([fuente, info]) => ({
        fuente,
        motivo: info.motivo,
        desde: new Date(info.desde).toLocaleTimeString("es-ES")
    }));
    const resumen = filas.length ? `⚠ ${filas.length} fuente(s) con problemas` : "✓ Todo en orden";
    const salidas = [si_cambia("resumen", resumen), si_cambia("tabla", filas)];
    context.set("ultimo", ultimo);
    return salidas;
  2. Salida 1 → un text Estado en el grupo Estado del sistema, de ancho 12.

  3. Salida 2 → un table (sección dashboard 2) en el mismo grupo, de ancho 12, con Action replace: cada mensaje sustituye la tabla completa. Las columnas salen solas de los campos de cada fila.

Qué debes ver. Con todo funcionando, ✓ Todo en orden y la tabla vacía. Provoca un fallo con el simulador (simulador/fallo/suelo = sensor) y espera 30 s:

Panel con fallos: el estado indica 5 fuentes con problemas y la tabla lista el ESP32 del TDS inalcanzable y los topics de suelo y TDS sin datos; mientras tanto, el gauge de humedad del suelo sigue marcando 52,1 %

Fíjate en el gauge de humedad del suelo: sigue marcando 52,1 %, el último valor que llegó antes de que el sensor fallara. Un operador que solo mire el gauge creerá que todo va bien. Resolverlo es parte del entregable.

Paso 8 · Consignas editables (30 min) ​

Para qué sirve. Que el operador pueda ajustar los umbrales desde el panel, con una validación que impida valores absurdos.

8.1 · Migra los umbrales al contexto global. Los umbrales estaban en el contexto de flujo de las pestañas S1 y S2, y el panel está en otra pestaña. Pasan al contexto global, con un único objeto para todo el sistema:

json
{
  "suelo": { "critico": 25, "aviso": 35 },
  "temperatura": { "aviso": 30, "critico": 35 },
  "margenExterior": 2
}

Cambios en tus flujos:

PestañaNodoCambio
S1change que guarda los umbralesSet global. umbrales (tipo global. en el desplegable)
S1switch clasificarLas reglas leen de tipo global. en lugar de flow.
S1decidir riegoflow.get("umbrales") → global.get("umbrales")
S2decidir ventilaciónflow.get("umbrales") → global.get("umbrales")

Los inject cargar umbrales de S1 y S2 ya no hacen falta: los carga el panel (8.3). Bórralos o desactívalos.

8.2 · Los deslizadores. Tres slider (sección dashboard 2) en el grupo Consignas:

Diálogo Edit slider node: Label "Suelo: umbral crítico (%)", Group Consignas, Range min 5 step 1 max 60, Output only on release y, en When changed, send, Topic suelo.critico

LabelRangeTopic
Suelo: umbral crítico (%)5 – 60suelo.critico
Suelo: umbral de aviso (%)5 – 80suelo.aviso
Temperatura: aviso (ºC)15 – 45temperatura.aviso

En los tres, Output: only on release, para no enviar un valor por cada píxel del arrastre; y Topic con la ruta del umbral, que es lo que indica a la función qué valor cambiar.

8.3 · Validar y guardar. Une los tres deslizadores a un function actualizar umbrales con 2 salidas:

js
// msg.topic = ruta del umbral ("suelo.critico"), msg.payload = nuevo valor
const u = global.get("umbrales");
const [grupo, clave] = msg.topic.split(".");
const nuevo = structuredClone(u);
nuevo[grupo][clave] = Number(msg.payload);

// Validación: el umbral crítico del suelo debe quedar por debajo del de aviso
if (nuevo.suelo.critico >= nuevo.suelo.aviso) {
    // Rechazado: aviso al operador y los deslizadores vuelven a su valor
    return [{ payload: "Consigna rechazada: el umbral crítico debe ser menor que el de aviso" }, { payload: u }];
}
global.set("umbrales", nuevo);
return [{ payload: `Consigna actualizada: ${msg.topic} = ${msg.payload}` }, { payload: nuevo }];

structuredClone hace una copia del objeto: así, si la validación falla, los umbrales guardados no se han tocado.

  • Salida 1 → un notification (sección dashboard 2), que muestra el mensaje como aviso emergente en el panel.

  • Salida 2 → un function sincronizar consignas con 3 salidas, una a cada deslizador. Devuelve a cada uno el valor guardado, así que tras un rechazo el deslizador vuelve a su sitio. También carga los umbrales por defecto si aún no existen:

    js
    // Carga los umbrales por defecto si no existen y sincroniza los deslizadores
    let u = global.get("umbrales");
    if (!u) {
        u = msg.payload;          // valores por defecto del inject
        global.set("umbrales", u);
    }
    return [
        { payload: u.suelo.critico },
        { payload: u.suelo.aviso },
        { payload: u.temperatura.aviso }
    ];
  • Un inject umbrales por defecto, con Inject once after y el JSON de 8.1 como msg.payload, unido también a sincronizar consignas.

Qué debes ver. Los tres deslizadores en 25, 35 y 30. Sube el crítico a 30 y suéltalo: aviso Consigna actualizada. Súbelo a 40, por encima del aviso: aviso Consigna rechazada… y el deslizador vuelve a 30.

Los umbrales se pierden al reiniciar

El contexto está en memoria. Si reinicias Node-RED, los umbrales vuelven a los valores por defecto. En la sesión 6 configurarás un almacenamiento persistente del contexto en settings.js.

Paso 9 · Tema (5 min) ​

Para qué sirve. Aplicar el principio HMI de fondo neutro: las tarjetas se distinguen del fondo sin colores.

Qué hacer. Abre la página Invernadero (en la barra lateral Dashboard 2.0, o desde el lápiz de cualquier grupo) y en Theme pulsa el lápiz para crear un tema nuevo:

Diálogo Edit ui-theme node: Name HMI invernadero, Preset Custom; en Colors, Dashboard Primary azul, Pages Background gris claro y Groups Background blanco con Outline gris; en Sizings, Row Height y Page Padding

  • Name: HMI invernadero.
  • Pages → Background (fondo de la página): gris claro, #eef0f2.
  • Groups → Background: blanco, #ffffff. Groups → Outline (el borde de las tarjetas): gris, #d9d9e0.

Add, Update en la página y Deploy.

Qué debes ver. El panel completo, como en la captura del principio de la sesión.

Flujo de referencia de la práctica guiada: s4-panel.json. Necesita que estén importados los de las sesiones 2 y 3 (lee su contexto global y envía los comandos al link in de la S3).

Flujo del panel en el editor de Node-RED: un inject cada 2 s alimenta las funciones de los grupos Clima, Riego y agua, Estado y Actuadores, conectadas a sus widgets; los interruptores salen por un link out; debajo, las tendencias cada 10 s y las consignas con sus deslizadores

Node-RED como plataforma ​

Dashboard 2.0 frente a Dashboard 1.0 ​

Si buscas ejemplos en Internet, encontrarás muchos del dashboard antiguo. No se mezclan:

Dashboard 1.0Dashboard 2.0
Paquetenode-red-dashboard@flowfuse/node-red-dashboard
Nombres de nodosui_gauge, ui_chart… (guion bajo)ui-gauge, ui-chart… (guion medio)
TecnologíaAngularJS (sin mantenimiento desde 2021)Vue 3 + Vuetify
EstadoObsoletoMantenido
EstructuraTab → GroupBase → Page → Group
URL por defecto/ui/dashboard

Un flujo con nodos ui_… no funciona en Dashboard 2.0. Existe una guía de migración.

Cómo se sirve el panel ​

  • El panel lo sirve el mismo Node-RED, en el mismo puerto que el editor (1880), bajo /dashboard. No hay otro servidor.
  • El navegador descarga una aplicación web y mantiene abierta una conexión WebSocket (socket.io) con Node-RED. Por ella llegan los valores y salen los clics.
  • Por defecto, todos los navegadores comparten el mismo estado: lo que envías a un widget lo ven todos. Por eso un clic en un interruptor afecta a todos los que tienen el panel abierto.
  • Cualquiera que llegue a http://<tu-IP>:1880/dashboard puede usar el panel. En la sesión 6 protegerás el editor, y el panel se protege aparte.

Temas ​

Un ui-theme define los colores (primario, fondo de página, fondo y borde de grupo) y los espaciados. Cada página usa un tema, así que puedes tener, por ejemplo, un tema claro para la oficina y otro de alto contraste para una pantalla de planta.

Entregable y evidencias para el informe ​

Qué debe funcionar ​

  1. Panel completo en /dashboard/invernadero, con los seis grupos del paso 2, alimentado por tus flujos de las sesiones 2 y 3.

  2. Actuadores con estado confirmado: los interruptores envían al punto único de comandos y muestran el estado real del Shelly.

  3. Consignas editables y validadas en global.umbrales, usadas por la decisión de riego (S1) y de ventilación (S2).

  4. Sin datos congelados: si una magnitud está en problemas, su valor se distingue claramente en el panel. Por ejemplo:

    • el texto muestra — sin datos en lugar del último valor;
    • o el gauge se oculta y aparece un aviso;
    • o el widget cambia de aspecto enviándole msg.class con una clase CSS.

    Elige una opción y justifícala en el informe.

Reto opcional (elige uno):

  • Página de detalle: una segunda ui-page, Sensores, con una ui-table de todas las magnitudes de global.sensores y la antigüedad de cada lectura en segundos.
  • Confirmar antes de actuar: al pulsar un interruptor, un ui-notification en modo diálogo pide confirmación antes de enviar el comando.

Cómo se comprueba ​

  • El panel refleja en pocos segundos los cambios del simulador.
  • Con simulador/fallo/actuador = desconectado, pulsar la bomba no cambia el interruptor.
  • Una consigna inválida se rechaza con aviso, y una válida cambia el comportamiento de la decisión de riego.
  • Con simulador/fallo/suelo = sensor, la humedad del suelo aparece como sin datos a los 30 s.

Criterios de calidad:

  • Estado general arriba y grupos por proceso.
  • Unidades en todos los valores.
  • Color solo en los rangos de estado.
  • Una gráfica por unidad.
  • Ningún comando enviado sin pasar por el punto único de la S3.

Evidencias que debes guardar ​

  • [ ] Captura del panel completo funcionando.
  • [ ] Captura del panel con un fallo provocado, donde se vea cómo distingues el dato sin actualizar.
  • [ ] Captura del flujo del panel en el editor.
  • [ ] Captura de la notificación de consigna rechazada.
  • [ ] Flujo exportado: s4-<tu-apellido>.json.
  • [ ] En el informe, respuesta breve a:
    • ¿Por qué el interruptor debe mostrar el estado del Shelly y no el del clic? Describe una situación en el invernadero en la que la diferencia importe.
    • ¿Qué principio HMI has aplicado en cada grupo? ¿Cuál te ha costado más respetar?
    • ¿Qué ventajas e inconvenientes tiene alimentar el panel leyendo el contexto global cada 2 s frente a conectarlo directamente a los mensajes MQTT?

Recursos adicionales ​