Tema
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:

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:
| Nodo | Qué es | Dónde se ve |
|---|---|---|
ui-base | La aplicación del panel. Define la ruta base (/dashboard) | Nodo de configuración |
ui-page | Una página: ruta, disposición (grid, fixed, notebook) y tema | Nodo de configuración; aparece en el menú lateral del panel |
ui-group | Una tarjeta dentro de la página, con un ancho en columnas (de 12) | Nodo de configuración |
ui-theme | Colores y espaciados | Nodo de configuración |
Widgets (ui-gauge, ui-text, ui-switch, ui-chart…) | Lo que ve el operador | Nodos 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):
| Principio | En el panel del invernadero |
|---|---|
| Lo más importante, arriba | El 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 widget | Clima, Riego y agua, Actuadores: no "todos los gauges" juntos |
| Fondo neutro, color solo para lo anómalo | Pá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 real | El interruptor refleja lo que dice el Shelly, no lo que se pulsó |
| No mentir con datos viejos | Un 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.
Crea una pestaña
S4 · Panel. Añade uninjectcon msg.payload de tipo number y valor42, y detrás un gauge (sección dashboard 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](/curso-node-red/assets/s04-dialogo-gauge.DeIjV_kv.png)
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](/curso-node-red/assets/s04-dialogo-grupo.Bq5RF5kM.png)
- 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.
- Name:
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](/curso-node-red/assets/s04-dialogo-pagina.Cw9JfNNM.png)
- Name:
Invernadero. - Path:
/invernadero. - Layout: Grid.
- En UI aparece Add new ui-base...: pulsa su lápiz, deja la ruta
/dashboardy pulsa Add. - Pulsa Add en la página y después en el grupo. Vuelves al gauge con todo creado.
- Name:
En el gauge: Name
Temperatura interior, Range de0a50y, en la sección Labelling (más abajo), LabelTemperatura interiory UnitsºC. Done y Deploy.Abre http://localhost:1880/dashboard/invernadero en otra pestaña del navegador y pulsa el
injecten 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.

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).
| Grupo | Ancho | Contenido |
|---|---|---|
| Estado del sistema | 12 | Resumen y tabla de problemas |
| Clima | 4 | Temperatura interior, humedad del aire, temperatura exterior |
| Riego y agua | 4 | Humedad del suelo, depósito, calidad del agua, peso |
| Actuadores | 4 | Bomba, ventilador, motivo de la ventilación |
| Tendencias (última hora) | 12 | Gráficas |
| Consignas | 6 | Umbrales 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.
Cambia el
injectdel paso 1: msg.payload de tipo timestamp, Inject once after1segundo y Repeat interval cada2seconds. Name:cada 2 s.Inserta entre el
injecty el gauge unfunctionllamadogrupo Climacon 3 salidas (pestaña Setup → Outputs3):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) devuelveundefineden vez de fallar si todavía no ha llegado ningún dato de ese sensor.si_cambiarecuerda, en el contexto del nodo (context), lo último que envió por cada salida.
Une la salida 1 al gauge
Temperatura interior. Ábrelo y configura:- Type: 3/4 Gauge.
- Segments (los rangos de color): azul desde
0, verde desde15, ámbar desde30y rojo desde35. El botón + bajo la lista añade un segmento; el cuadrado de color de cada fila abre el selector de color.
Salida 2 → un gauge nuevo
Humedad del aireen el grupoClima: 3/4 Gauge, rango0–100, Units%y un solo segmento azul desde0. 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.Salida 3 → un text (sección dashboard 2) en
Climacon LabelExterior.Deploy.
Qué debes ver. El grupo Clima con los dos gauges y la temperatura exterior, actualizándose solos:

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.
Un
injectcon Inject once after marcado y msg.payload de tipo JSON:json{ "llenoCm": 5, "vacioCm": 40 }y un
changecon Setflow.depositoto the valuemsg.payload. Son las distancias del sensor al agua con el depósito lleno y vacío, las del simulador.Un
functionllamadogrupo Riego y aguacon 4 salidas, conectado alinjectcada 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;Los widgets, todos en el grupo
Riego y agua:Salida Widget Configuración 1 gauge Humedad del sueloType Half Gauge, rango 0–100,%, segmentos rojo0· ámbar25· verde35(los umbrales de la sesión 1)2 gauge DepósitoType Tank Level, rango 0–100,%3 text Calidad del agua4 text 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.
Añade dos switch de dashboard 2 (no el
switchde function),Bomba de riegoyVentilador, en el grupoActuadores:![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](/curso-node-red/assets/s04-dialogo-switch.DKwUf2In.png)
Campo Valor If msg arrives on input, pass through to output desmarcado Indicator Switch icon shows state of the input On Payload / Off Payload tipo string: on/offTopic tipo string: bomba(oventilador)Une la salida de los dos interruptores a un
changeque asignemsg.equipo=msg.topic(en to the value, tipo msg.,topic). Detrás, un link out. Ábrelo y marca en la lista ellink incomandos a actuadoresde la pestaña S3.Un
functiongrupo Actuadorescon 3 salidas, conectado alinjectcada 2 s: la salida 1 al interruptor de la bomba, la 2 al del ventilador y la 3 a un textVentilació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;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.
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.
Un
injectque se repita cada 10 segundos → unfunctionmuestrear tendenciascon 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];Dos chart (sección dashboard 2) en el grupo
Tendencias:
Campo Valor Por qué Type Line Evolución en el tiempo Action Append Cada mensaje añade un punto X-Axis Type Timescale El eje X es la hora de llegada X-Axis Limit last 1HoursSolo se conserva la última hora Properties → Series msg.topicCada topic distinto es una línea: interior,exteriorProperties → X / Y timestamp / msg.payloadHora de llegada y valor La salida 1 va a la gráfica
Temperatura (ºC)y la salida 2 aHumedad del suelo (%)(esta con Y-Axis min.0y 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.
Un
functiongrupo Estadocon 2 salidas, conectado alinjectcada 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;Salida 1 → un text
Estadoen el grupoEstado del sistema, de ancho 12.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:

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ña | Nodo | Cambio |
|---|---|---|
| S1 | change que guarda los umbrales | Set global. umbrales (tipo global. en el desplegable) |
| S1 | switch clasificar | Las reglas leen de tipo global. en lugar de flow. |
| S1 | decidir riego | flow.get("umbrales") → global.get("umbrales") |
| S2 | decidir ventilación | flow.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:

| Label | Range | Topic |
|---|---|---|
Suelo: umbral crítico (%) | 5 – 60 | suelo.critico |
Suelo: umbral de aviso (%) | 5 – 80 | suelo.aviso |
Temperatura: aviso (ºC) | 15 – 45 | temperatura.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
functionsincronizar consignascon 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
injectumbrales por defecto, con Inject once after y el JSON de 8.1 como msg.payload, unido también asincronizar 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:

- 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).

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.0 | Dashboard 2.0 | |
|---|---|---|
| Paquete | node-red-dashboard | @flowfuse/node-red-dashboard |
| Nombres de nodos | ui_gauge, ui_chart… (guion bajo) | ui-gauge, ui-chart… (guion medio) |
| Tecnología | AngularJS (sin mantenimiento desde 2021) | Vue 3 + Vuetify |
| Estado | Obsoleto | Mantenido |
| Estructura | Tab → Group | Base → 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/dashboardpuede 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
Panel completo en
/dashboard/invernadero, con los seis grupos del paso 2, alimentado por tus flujos de las sesiones 2 y 3.Actuadores con estado confirmado: los interruptores envían al punto único de comandos y muestran el estado real del Shelly.
Consignas editables y validadas en
global.umbrales, usadas por la decisión de riego (S1) y de ventilación (S2).Sin datos congelados: si una magnitud está en
problemas, su valor se distingue claramente en el panel. Por ejemplo:- el texto muestra
— sin datosen lugar del último valor; - o el gauge se oculta y aparece un aviso;
- o el widget cambia de aspecto enviándole
msg.classcon una clase CSS.
Elige una opción y justifícala en el informe.
- el texto muestra
Reto opcional (elige uno):
- Página de detalle: una segunda
ui-page,Sensores, con unaui-tablede todas las magnitudes deglobal.sensoresy la antigüedad de cada lectura en segundos. - Confirmar antes de actuar: al pulsar un interruptor, un
ui-notificationen 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?