Tema
Sesión 2 · Integración con APIs externas
Objetivos de la sesión
- Consumir una API REST con
http requesty procesar su respuesta JSON. - Combinar un dato externo (temperatura exterior) con uno del invernadero (temperatura interior) en una decisión de control.
- Distinguir y tratar los dos tipos de fallo de una llamada: la respuesta con código de error y la ausencia de respuesta.
- Exponer el estado del invernadero como API propia con
http inyhttp response. - Leer configuración desde variables de entorno.
Conexión con el sistema del invernadero
Hasta ahora el sistema solo sabía lo que miden sus sensores. En esta sesión aprende lo que pasa fuera: consulta periódicamente la previsión meteorológica y la guarda para que cualquier flujo la use. Además, el sistema se abre hacia fuera: ofrece un endpoint que cualquier programa (tu script Python, un navegador, más adelante el agente de IA de la sesión 7) puede consultar.
Node-RED hace aquí dos papeles: arriba es cliente de una API ajena (http request) y abajo es servidor de una API propia (http in → http response).
Conceptos clave
Anatomía de una llamada REST
| Elemento | Ejemplo en esta sesión |
|---|---|
| Método | GET: pedir información sin modificar nada |
| URL | https://api.open-meteo.com/v1/forecast |
| Parámetros de consulta | ?latitude=38.38&longitude=-0.51¤t=temperature_2m |
| Cabeceras | Content-Type: application/json en la respuesta |
| Código de estado | 200 bien · 400 petición incorrecta · 404 no existe · 429 demasiadas peticiones · 5xx fallo del servidor |
| Cuerpo | JSON con los datos pedidos |
Frente a MQTT, HTTP es petición-respuesta: el cliente pregunta y espera. Nadie avisa cuando el dato cambia; si quieres el valor nuevo, vuelves a preguntar. Por eso la previsión se consulta periódicamente.
La API de Open-Meteo
Open-Meteo ofrece previsiones sin necesidad de registro ni clave para uso no comercial (hasta 10 000 llamadas al día). Los campos que usaremos:
| Parámetro | Campo en la respuesta | Uso en el invernadero |
|---|---|---|
current=temperature_2m | current.temperature_2m (ºC) | ¿Enfría ventilar? |
current=relative_humidity_2m | current.relative_humidity_2m (%) | Humedad del aire que entra al ventilar |
hourly=precipitation_probability | hourly.precipitation_probability[] (%) | Aviso de lluvia (p. ej. cerrar ventanas) |
hourly=et0_fao_evapotranspiration | hourly.et0_fao_evapotranspiration[] (mm) | Cuánta agua pierden las plantas: anticipar el riego |
forecast_hours=6 | tamaño de los arrays hourly | Previsión de las próximas 6 horas |
¿Y la lluvia?
El invernadero está cerrado: la lluvia de fuera no riega el cultivo. Lo que sí importa es la temperatura exterior (ventilar solo enfría si fuera hace más fresco) y la evapotranspiración (con ET₀ alta el suelo se seca antes).
Nodos de esta sesión
| Nodo | Para qué |
|---|---|
http request | Hace una petición HTTP. La URL puede venir en msg.url; la respuesta llega en msg.payload y el código en msg.statusCode |
http in | Crea un endpoint en Node-RED (GET /api/...). La petición llega en msg.req |
http response | Devuelve la respuesta al cliente que llamó a http in |
catch | Recibe los errores que lanzan otros nodos |
status | Recibe los cambios de estado (el indicador bajo el nodo) de otros nodos |
Documentación: HTTP requests en el Cookbook de Node-RED.
Práctica guiada paso a paso
Como en la sesión 1, cada paso indica para qué sirve, qué hacer, qué debes ver y si no funciona. Las operaciones básicas del editor (añadir y unir nodos, abrir un diálogo con doble clic, elegir el tipo de un valor en su desplegable, Done y Deploy) ya las conoces; aquí solo se explican las nuevas.
Paso 0 · Comprobar el entorno (5 min)
Para qué sirve. Node-RED necesita saber dónde está el invernadero para pedir su previsión. Las coordenadas se pasan como variables de entorno, igual que el host del broker. El entorno de la sesión 1 ya las trae (por defecto, las del campus de la Universidad de Alicante); este paso solo comprueba que tu contenedor las tiene.
Qué hacer. Arranca el entorno si no lo está y, en una terminal dentro de tu carpeta docker, pregunta al contenedor de Node-RED por la latitud:
bash
docker compose up -d
docker compose exec node-red printenv INVERNADERO_LATQué debes ver. El segundo comando escribe 38.3852. Entonces tu entorno está al día: pasa al paso 1.
Si no funciona.
- No escribe nada: tu entorno es de una versión anterior del curso. Descarga de nuevo entorno-docker.zip, descomprímelo encima de tu carpeta
dockeraceptando reemplazar los ficheros (tu.envno se pierde: no va dentro del zip) y repite los dos comandos.docker compose up -drecrea (Recreated)node-redcon las variables nuevas. Tus flujos no se pierden: están en el volumennode-red-data. no configuration file providedoservice "node-red" is not running: no estás dentro de la carpetadockero el entorno no ha arrancado. Revisa el paso 2 de la sesión 1.- No tienes el entorno (no hiciste la sesión 1): sigue el paso 2 de la sesión 1 antes de continuar.
Otro invernadero
Para trabajar con otra ubicación, añade a tu .env INVERNADERO_LAT=... e INVERNADERO_LON=... con sus coordenadas y ejecuta docker compose up -d.
Esta sesión necesita Internet
Conéctate a una red con salida a Internet (la de la universidad o la de casa). Sin ella verás errores ENOTFOUND o ETIMEDOUT en el paso 2: es justo el error que tratarás en el paso 6. Por eso esta sesión no se puede hacer con el invernadero real, cuya Wi-Fi no tiene Internet.
Paso 1 · Explorar la API desde el navegador (15 min)
Para qué sirve. Antes de programar nada, conviene ver qué devuelve la API: qué campos hay, dónde están y en qué unidades.
Qué hacer. Copia esta URL en la barra del navegador:
text
https://api.open-meteo.com/v1/forecast?latitude=38.3852&longitude=-0.5143¤t=temperature_2m,relative_humidity_2m&hourly=precipitation_probability,precipitation,et0_fao_evapotranspiration&forecast_hours=6&timezone=Europe%2FMadridLa URL es la petición completa: después de ? van los parámetros, separados por &. Si tu navegador tiene la opción Dar formato o Pretty print, actívala para leer mejor el JSON.
Qué debes ver.

Localiza en la respuesta:
current: un valor por campo; por ejemplo,temperature_2m: 25.3en la captura; tus valores serán los de ahora.hourly: un array por campo, con un valor por hora, y el arraytimecon la hora de cada posición.current_unitsyhourly_units: las unidades de cada campo.
Cambia forecast_hours=6 por 12 y recarga: los arrays de hourly pasan de 6 a 12 elementos.
Paso 2 · Primera petición desde Node-RED (20 min)
Para qué sirve. Hacer la misma petición desde un flujo con el nodo http request, el equivalente en Node-RED a requests.get(...) de Python.
Qué hacer.
Crea una pestaña nueva con el botón + a la derecha de las pestañas. Doble clic en su nombre para renombrarla:
S2 · Meteorología y API.Añade un
inject, un http request (sección network) y undebug, y únelos en ese orden.Abre el
http request:
- Method:
GET. - URL: pega la URL del paso 1. En el paso 3 la quitarás.
- Return: a parsed JSON object. Así el nodo convierte el texto JSON en un objeto de JavaScript y no tendrás que hacerlo tú.
- Name (al final del diálogo):
Open-Meteo.
- Method:
En el
debug, Output complete msg object. Deploy y pulsa elinject.
Qué debes ver. Un mensaje en debug. Despliégalo con ▸: además de payload (el JSON ya convertido en objeto) aparecen statusCode: 200, headers (las cabeceras de la respuesta) y responseUrl.

Si no funciona.
payloades un texto largo entre comillas: Return está en a UTF-8 string.- Error
getaddrinfo ENOTFOUND: no hay salida a Internet. Revisa la red; lo tratarás en el paso 6.
Paso 3 · La URL desde variables de entorno (15 min)
Para qué sirve. Las coordenadas son configuración, no código: si mañana el invernadero se muda, no debería hacer falta tocar el flujo. Una función construirá la URL con las variables de entorno.
Qué hacer.
Inserta un
functionentre elinjecty elhttp request. Name:construir URL. En On Message pega:js// Coordenadas del invernadero desde variables de entorno del contenedor const lat = env.get("INVERNADERO_LAT") || "38.3852"; const lon = env.get("INVERNADERO_LON") || "-0.5143"; msg.url = "https://api.open-meteo.com/v1/forecast" + `?latitude=${lat}&longitude=${lon}` + "¤t=temperature_2m,relative_humidity_2m" + "&hourly=precipitation_probability,precipitation,et0_fao_evapotranspiration" + "&forecast_hours=6&timezone=Europe%2FMadrid"; msg.requestTimeout = 10000; // ms: sin respuesta en 10 s -> error return msg;Abre el
http requesty borra la URL: deja el campo vacío. Cuando está vacío, el nodo usa la demsg.url.Done, Deploy y pulsa el
inject.
Qué hace el código:
env.get("…")lee una variable de entorno: las que pusiste en.envy quedocker-compose.ymlpasa al contenedor. Si no existe,||usa el valor por defecto.- Las comillas invertidas
`…${lat}…`construyen un texto insertando variables. msg.requestTimeoutindica cuántos milisegundos esperará elhttp requestuna respuesta antes de dar error.
Qué debes ver. La misma respuesta que en el paso 2, con statusCode: 200. En responseUrl aparecen tus coordenadas.
Si no funciona. El http request se queda en requesting o da No url specified: hay algo escrito en su campo URL que tiene prioridad sobre msg.url, o el function no está conectado. Deja URL completamente vacío.
Paso 4 · Extraer lo útil y compartirlo (25 min)
Para qué sirve. La respuesta trae mucho más de lo que necesita el sistema. Vas a quedarte con cinco valores y guardarlos para usarlos después desde otros flujos.
Recuerda que cada pestaña del editor de Node-RED (S1 · …, S2 · …) es un flujo distinto. En el paso 7 de la sesión 1 guardaste los umbrales en el contexto de flujo, que solo ven los nodos de la pestaña donde se guardó. La previsión la consultas en la pestaña S2, pero también la necesitarán otras, como el panel de la sesión 4. Por eso la guardarás en el contexto global, que comparten todas las pestañas.
Qué hacer.
Añade un
functiondetrás delhttp request(quita eldebugde antes o déjalo en otra salida). Name:extraer previsión:js// Resume la respuesta en los campos que usa el sistema const c = msg.payload.current; const h = msg.payload.hourly; const suma = a => a.reduce((s, x) => s + (x ?? 0), 0); const meteo = { tempExterior: c.temperature_2m, humExterior: c.relative_humidity_2m, lluvia6h: Math.round(suma(h.precipitation) * 10) / 10, probLluviaMax: Math.max(...h.precipitation_probability), et0_6h: Math.round(suma(h.et0_fao_evapotranspiration) * 100) / 100, ts: Date.now() }; global.set("meteo", meteo); node.status({ fill: "green", shape: "dot", text: `${meteo.tempExterior} ºC fuera` }); msg.payload = meteo; return msg;Detrás, un
debugllamadoprevisión(en msg.payload). Deploy y pulsa elinject.
Qué hace el código:
sumarecorre un array y suma sus valores;x ?? 0trata un valor ausente como 0.Math.max(...array)devuelve el mayor elemento del array.global.set("meteo", …)guarda el resultado en el contexto global con el nombremeteo. Desde unfunctionde cualquier otra pestaña se lee conglobal.get("meteo").tsguarda cuándo se obtuvo la previsión: en el paso 8 servirá para saber si es demasiado antigua.Date.now()devuelve un entero, los milisegundos transcurridos desde el 1 de enero de 1970 (por ejemplo,1791193200000). Parece poco legible, pero es el formato cómodo para calcular: la antigüedad esDate.now() - meteo.ts, sin convertir fechas. Lo pasarás a texto legible solo al mostrarlo, como harás en el paso 9 contoISOString().
Qué debes ver. En debug, un objeto con los cinco campos y ts. Haz clic en el número de ts: la barra de depuración lo muestra como fecha en UTC (2026-10-05T09:40:00.000Z). Otro clic lo muestra en tu hora local; los siguientes, en hexadecimal y de nuevo como entero. Bajo el nodo, 27.4 ºC fuera (con tu temperatura). En Context Data, sección Global, pulsa ↻: aparece meteo.

Si no funciona. Error Cannot read properties of undefined (reading 'temperature_2m'): la respuesta no trae current. Pasa si la petición ha fallado (el paso siguiente lo resuelve) o si la URL no pide current=….
Paso 5 · Comprobar el código de estado (15 min)
Para qué sirve. Una respuesta no siempre es una buena respuesta. Si la petición está mal formada, Open-Meteo responde con el código 400 y un JSON que explica el motivo. extraer previsión no debe recibir esas respuestas.
Qué hacer.
Inserta un
switchentre elhttp requestyextraer previsión. Name:¿200 OK?.- Property: cambia
payloadporstatusCode, para que quedemsg.statusCode. - Regla 1: ==, tipo number (icono 0-9), valor
200. - +add, regla 2: otherwise.

- Property: cambia
Une la salida 1 a
extraer previsión. En la salida 2 pon unfunctionllamadorespuesta inesperada:js// La API ha respondido, pero no con 200 (p. ej. 400 por parámetros incorrectos) // Sin red, el http request también envía el mensaje con statusCode = "ENOTFOUND", // "ETIMEDOUT"...: esos casos ya los trata el catch if (typeof msg.statusCode !== "number") return null; const motivo = msg.payload && msg.payload.reason ? msg.payload.reason : "sin detalle"; node.status({ fill: "red", shape: "ring", text: `HTTP ${msg.statusCode}` }); msg.payload = { origen: "Open-Meteo", statusCode: msg.statusCode, motivo }; return msg;Detrás, un
debugllamadoerrores.Un caso de prueba del error. Añade un
injectllamadoprobar error 400. En sus propiedades, borramsg.payloadymsg.topiccon su × y añade con +add una fila msg.url, de tipo string, con una latitud imposible:texthttps://api.open-meteo.com/v1/forecast?latitude=999&longitude=-0.5143¤t=temperature_2m
Une este
injectdirectamente a la entrada delhttp request: así prueba el error sin tocarconstruir URL.Deploy y pulsa
probar error 400.
Qué debes ver. El mensaje sale por la salida 2. En el debug errores, statusCode: 400 y motivo: "Latitude must be in range of -90 to 90°. Given: 999.0.". Bajo respuesta inesperada, un anillo rojo con HTTP 400.
Paso 6 · Cuando no hay respuesta: catch (20 min)
Para qué sirve. Si no hay red, el DNS falla o el servidor no contesta a tiempo, no llega ninguna respuesta: ni siquiera un statusCode. El http request lanza un error, y ese error no sale por ningún cable. Para recogerlo existe el nodo catch.
Qué hacer.
Añade un catch (sección common, rojo). No tiene entrada: no hace falta cablearlo a nada. Ábrelo:

- Catch errors from: elige selected nodes.
- Aparece la lista de nodos de la pestaña, cada uno con una casilla: marca Open-Meteo y extraer previsión (el buscador de encima filtra la lista). También puedes pulsar Select nodes..., hacer clic en esos nodos en el lienzo y confirmar.
- Name:
errores de red / código.
Une el
catcha unfunctionllamadoregistrar errory este al debugerrores:js// Sin conexión, timeout, JSON mal formado o fallo en "extraer previsión" node.status({ fill: "red", shape: "dot", text: msg.error.message.slice(0, 40) }); msg.payload = { origen: msg.error.source.name, error: msg.error.message }; return msg;El
catchentrega un mensaje conmsg.error:message(qué ha pasado) ysource(qué nodo ha fallado).Caso de prueba: otro
inject,probar sin red, igual que el del paso 5 pero conmsg.url=https://api.open-meteo.invalid/v1/forecast(el dominio.invalidno existe nunca). Únelo a la entrada delhttp request.Deploy y pulsa
probar sin red.
Qué debes ver. En el debug errores, un mensaje del catch con error: "RequestError: getaddrinfo ENOTFOUND api.open-meteo.invalid" (el texto exacto puede variar). Con los dos casos de prueba, la depuración queda así:

| Situación | ¿Llega respuesta? | Dónde se trata |
|---|---|---|
| Todo bien | Sí, 200 | extraer previsión |
| Parámetros incorrectos, límite de uso superado | Sí, 4xx/5xx | Salida 2 de ¿200 OK? |
| Sin red, DNS, timeout | No: error | catch |
| La API cambia el formato y falta un campo | Sí, 200, pero extraer previsión falla | catch |
Un detalle del http request: cuando falla la conexión, además de lanzar el error al catch, también envía el mensaje por su salida, con msg.statusCode igual al código del error ("ENOTFOUND", "ETIMEDOUT"…), que es un texto y no un número. Por eso respuesta inesperada empieza con if (typeof msg.statusCode !== "number") return null;: sin esa línea, el mismo fallo se trataría dos veces.
Si no funciona. El error aparece en debug en rojo pero no llega al catch: el http request no está marcado en su lista de nodos.
Ver el estado de la petición: status
Para qué sirve. El catch solo se entera de los errores. El indicador que aparece bajo el http request (punto azul mientras pide, anillo rojo con ENOTFOUND si falla) también se puede capturar como un mensaje, con el nodo status. Así puedes ver cuánto tarda la petición o avisar cuando un nodo cambia de estado. En la sesión 3 lo usarás para detectar que el broker MQTT se desconecta.
Qué hacer.
- Añade un status (sección common, azul claro, con un icono de pulso). Igual que el
catch, no tiene entrada. Ábrelo:- Report status from: elige selected nodes y marca solo Open-Meteo.
- Name:
estado de la petición.
- Únelo a un
debugnuevo con Nameestado httpy, en Output,msg.status(borrapayloady escribestatus). - Deploy y pulsa primero
cada 15 miny despuésprobar sin red.
Qué debes ver. El status no trae payload: todo va en msg.status, con fill, shape, text y source (el nodo que ha cambiado). En el debug estado http aparecen cuatro mensajes:
| Acción | msg.status | Significado |
|---|---|---|
cada 15 min | { fill: "blue", shape: "dot", text: "httpin.status.requesting", source: {…} } | Petición en curso |
{ source: {…} } | Ha llegado la respuesta y el nodo borra su indicador | |
probar sin red | { fill: "blue", shape: "dot", text: "httpin.status.requesting", source: {…} } | Petición en curso |
{ fill: "red", shape: "ring", text: "ENOTFOUND", source: {…} } | Fallo de conexión: el indicador se queda en rojo |
source vale { id: "…", type: "http request", name: "Open-Meteo" }. El texto httpin.status.requesting es la clave de traducción: el editor la muestra traducida bajo el nodo, pero al status le llega sin traducir. Para decidir algo, compara fill o shape, no text.
Cuando lo hayas visto, desactiva el debug estado http con el cuadrado de su derecha: con la consulta cada 15 minutos llena la depuración de mensajes que no aportan nada. En el flujo de referencia viene desactivado.
Si no funciona. El debug no muestra nada: en el status no está marcado Open-Meteo, o el debug está desactivado. Solo ves { source: … }: es el mensaje de borrado; sube en la depuración para ver el anterior.
Paso 7 · Consulta periódica (5 min)
Para qué sirve. La previsión cambia despacio. Basta con pedirla cada 15 minutos: más a menudo solo gasta cuota de la API.
Qué hacer. Abre el inject que va a construir URL:
- Name:
cada 15 min. - Marca Inject once after
2seconds, then, para tener datos nada más desplegar. - Repeat: elige interval y escribe
every 15 minutes.

Qué debes ver. Tras Deploy, a los 2 s aparece una previsión en debug sin pulsar nada. El nodo muestra un icono ↻ junto a su nombre, que indica que se repite.
Paso 8 · Decidir la ventilación (30 min)
Para qué sirve. Aquí el dato externo se combina con uno del invernadero en una decisión: ventilar solo enfría si fuera hace más fresco que dentro.
Qué hacer.
Umbrales. Un
injectllamadocargar umbrales, con Inject once after marcado, msg.payload de tipo JSON:json{ "temperatura": { "aviso": 30 }, "margenExterior": 2 }y un
changecon Setflow.umbralesto the valuemsg.payload, como en la sesión 1.Temperatura interior. Un
mqtt incon ServerBroker invernadero(elígelo en el desplegable: ya existe), Topicinvernadero/dht22/temperaturay Output a String. Detrás, unchangetexto → númerocon Setmsg.payloada la expresión$number(payload).Decisión. Un
functionllamadodecidir ventilación:jsconst interior = msg.payload; const u = flow.get("umbrales"); if (!u) { // Al arrancar pueden llegar lecturas retenidas antes que los umbrales node.warn("Lectura ignorada: los umbrales aún no están cargados"); return null; } let meteo = global.get("meteo"); // Una previsión de hace más de 1 h no es fiable if (meteo && Date.now() - meteo.ts > 60 * 60 * 1000) meteo = null; let ventilar = false; let motivo; if (interior < u.temperatura.aviso) { motivo = "temperatura interior correcta"; } else if (!meteo) { ventilar = true; motivo = "calor dentro; sin datos meteorológicos recientes"; } else if (meteo.tempExterior <= interior - u.margenExterior) { ventilar = true; motivo = `fuera hace ${meteo.tempExterior} ºC`; } else { motivo = `ventilar no enfría: fuera hace ${meteo.tempExterior} ºC`; } msg.payload = { interior, exterior: meteo ? meteo.tempExterior : null, ventilar, motivo }; global.set("ventilacion", msg.payload); node.status({ fill: ventilar ? "blue" : "grey", shape: "dot", text: ventilar ? "ventilar" : "no ventilar" }); return msg;y un
debugdecisión ventilación.Casos de prueba: tres
injectcon msg.payload string25,31y35, unidos a la entrada delchangetexto → número.
La lógica, en orden:
| Condición | Decisión |
|---|---|
| Interior por debajo del aviso | No ventilar: temperatura correcta |
| Calor dentro, pero no hay previsión reciente | Ventilar (regla simple de la sesión 1) |
| Calor dentro y fuera hace al menos 2 ºC menos | Ventilar |
| Calor dentro, pero fuera hace calor | No ventilar: no enfriaría |
Qué debes ver. Con 25: ventilar: false, "temperatura interior correcta". Con 31 y 35, depende de la temperatura exterior actual: si es menor que la interior menos 2, ventilar: true. Para forzar el caso "ventilar no enfría", baja temporalmente aviso en cargar umbrales por debajo de la temperatura exterior, Deploy, y prueba con un valor entre las dos.
Fíjate en la rama !meteo: si la API falla, el sistema sigue funcionando con la regla simple. Un sistema de control no debe quedarse bloqueado porque falle un servicio externo.
Si no funciona. Aparece Lectura ignorada…: justo después de un Deploy es normal (ver la condición de carrera de la sesión 1). Si no desaparece, falta el inject de umbrales o no tiene Inject once after.
Paso 9 · Tu propia API (30 min)
Para qué sirve. Hasta ahora Node-RED ha sido cliente de una API ajena. Ahora será servidor: ofrecerá el estado del invernadero en una dirección web que cualquier programa (tu script Python, un navegador, el agente de IA de la sesión 7) pueda consultar.
Qué hacer.
Añade un http in (sección network). Ábrelo:

- Method:
GET. - URL:
/api/invernadero/estado. Es la ruta dentro de Node-RED: la dirección completa seráhttp://localhost:1880/api/invernadero/estado.
- Method:
Detrás, un
functioncomponer estado:jsconst meteo = global.get("meteo"); msg.payload = { ts: new Date().toISOString(), ventilacion: global.get("ventilacion") ?? null, meteo: meteo ?? null, antiguedadMeteoMin: meteo ? Math.round((Date.now() - meteo.ts) / 60000) : null }; return msg;?? nulldevuelvenullsi el valor no existe todavía, para que la respuesta tenga siempre los mismos campos.Detrás, un http response (sección network). Ábrelo y, en Headers, pulsa +add: en el desplegable de la izquierda elige Content-Type y en el de la derecha application/json.

Esa cabecera le dice al cliente que la respuesta es JSON.
Deploy y pruébalo de tres formas:
texthttp://localhost:1880/api/invernadero/estadobashcurl -i http://localhost:1880/api/invernadero/estadopythonimport requests # pip install requests r = requests.get("http://localhost:1880/api/invernadero/estado", timeout=5) print(r.status_code) estado = r.json() print("¿Ventilar?", estado["ventilacion"]["ventilar"], "-", estado["ventilacion"]["motivo"])
Qué debes ver. En el navegador, el JSON con ts, ventilacion, meteo y antiguedadMeteoMin:

Con curl -i verás además la primera línea HTTP/1.1 200 OK y la cabecera content-type: application/json.
Si no funciona.
Cannot GET /api/invernadero/estado: la URL delhttp inno coincide (revisa la barra inicial y las mayúsculas) o falta Deploy.- El navegador se queda cargando: el mensaje no llega al
http response.
Un http in sin http response
Si el flujo no llega a un http response (por un error o un switch que descarta el mensaje), el cliente se queda esperando hasta agotar su tiempo. Todo camino que salga de un http in debe terminar en un http response, también los de error.
El flujo completo de la práctica guiada queda así:

Flujo de referencia: s2-meteo-api.json. Al importarlo, Node-RED avisará de que el nodo Broker invernadero ya existe si importaste el de la sesión 1: elige reutilizarlo. Si no, se crea un broker nuevo sin contraseña (las credenciales no se exportan) y sus nodos MQTT se quedan en connecting: escríbela en su pestaña Security o cambia esos nodos a tu broker.
Paso 10 · Ver reaccionar al invernadero (con el simulador, 10 min)
Para qué sirve. Comprobar que tu decisión reacciona a un cambio real de temperatura.
Qué hacer. El simulador te deja encender el ventilador (Shelly shellyplug-s-SIM002). Desde la carpeta docker/:
bash
docker compose exec mosquitto mosquitto_pub -u alumno -P alumno -t invernadero/shelly/shellyplug-s-SIM002/relay/0/command -m onSi has cambiado MQTT_ALUMNO_PASS, usa tu contraseña. Cambia on por off para apagarlo.
Qué debes ver. En unos minutos (tiempo simulado acelerado), la temperatura interior baja en el debug decisión ventilación y en /api/invernadero/estado. Si estaba por encima del aviso, la decisión pasa a temperatura interior correcta.
Node-RED como plataforma
link in / link out: cables invisibles
Cuando un flujo crece, los cables largos lo hacen ilegible. Un link out envía el mensaje a uno o varios link in, incluso en otra pestaña.
Ejercicio: conecta la salida de decidir ventilación a un link out y crea en otra zona del lienzo un link in → debug. En la sesión 3 usarás este patrón para separar la "lógica" de las "salidas".
Grupos
Selecciona varios nodos y pulsa Ctrl+Shift+G para agruparlos con un marco y un título. Agrupa el flujo de esta sesión en tres bloques: previsión, ventilación y API. Un grupo también puede tener sus propias variables de entorno.
catch, status y complete
Tres nodos que observan a otros sin estar en su cable:
| Nodo | Se activa cuando… | Uso típico |
|---|---|---|
catch | un nodo lanza un error | Registrar el fallo, reintentar, avisar |
status | cambia el indicador de estado de un nodo | Detectar que el broker MQTT se desconecta (msg.status.text) |
complete | un nodo termina de procesar un mensaje | Medir tiempos o encadenar acciones tras nodos sin salida |
Los tres permiten elegir su ámbito: todos los nodos de la pestaña o solo los seleccionados. Acota siempre el ámbito: un catch que recoge todos los errores de la pestaña mezcla problemas que no tienen nada que ver.
En la práctica has usado catch y status (paso 6). Ejercicio: añade un complete con ámbito el debug previsión y únelo a otro debug: recibirás el mismo mensaje justo después de que previsión lo muestre. Así se encadena una acción tras un nodo sin salida, como un debug o un mqtt out.
Variables de entorno
| Dónde | Cómo |
|---|---|
En un function | env.get("INVERNADERO_LAT") |
| En cualquier campo de un nodo | Escribe ${MQTT_HOST} como valor completo del campo (lo usaste en el broker en la sesión 1) |
| Solo para una pestaña o un grupo | Pestaña → Edit properties → Environment variables |
Regla práctica: lo que cambia entre instalaciones (hosts, coordenadas, identificadores) va en variables de entorno, y lo que ajusta el comportamiento del control (umbrales) va en el contexto.
Entregable y evidencias para el informe
Qué debe funcionar
- Previsión robusta. Consulta a Open-Meteo cada 15 minutos que trate los tres casos del paso 6: respuesta correcta, código de error y ausencia de respuesta. La previsión se guarda en
global.meteo. - Decisión integrada. Amplía tu decisión conjunta de la sesión 1 (
regaryventilar) para queventilartenga en cuenta la temperatura exterior, como en el paso 8. Si la previsión falta o tiene más de una hora, se aplica la regla de la sesión 1. Guarda la decisión englobal.decision. - API del invernadero.
GET /api/invernadero/estadodevuelve JSON con:- las últimas lecturas de humedad del suelo y temperatura interior;
- la decisión actual (
regar,ventilary su motivo); - la previsión y su antigüedad en minutos.
Reto opcional (elige uno):
- Riego anticipado: si el suelo está en
avisoyet0_6hsupera un umbral configurable, adelanta el riego. - Cambiar umbrales por API:
POST /api/invernadero/umbralesrecibe JSON, lo valida y actualiza los umbrales. Si el JSON no es válido, responde400con el motivo.
Cómo se comprueba
- Con la API accesible,
/api/invernadero/estadodevuelve datos coherentes con los sensores y con la previsión. - Con
INVERNADERO_LAT=999, el fallo sale por la rama del código de estado; con un dominio inexistente, por elcatch. En ambos casos la decisión se sigue tomando. - El script Python obtiene el estado y muestra la decisión.
Criterios de calidad: coordenadas en variables de entorno, ningún camino de http in sin http response, ámbito del catch acotado, nodos con nombre y flujo organizado en grupos.
Evidencias que debes guardar
- [ ] Captura del flujo completo.
- [ ] Captura de la depuración con una previsión correcta, un error
400y un error de red. - [ ] Captura de Context Data → Global con
meteoydecision. - [ ] Respuesta de
/api/invernadero/estadoen el navegador ocurl -i, y salida de tu script Python. - [ ] Flujo exportado:
s2-<tu-apellido>.json. - [ ] En el informe, respuesta breve a:
- ¿Qué diferencia hay entre recibir la temperatura por MQTT y la previsión por HTTP? ¿Por qué no se usa el mismo mecanismo para las dos?
- ¿Qué hace tu sistema si Open-Meteo deja de responder durante dos horas?
- ¿Por qué la previsión va en el contexto global y los umbrales en el de flujo?