Skip to content

Sesión 2 · Integración con APIs externas ​

Objetivos de la sesión ​

  • Consumir una API REST con http request y 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 in y http 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.

Arquitectura de la sesión 2: Node-RED consulta Open-Meteo, guarda la previsión en el contexto global, la combina con la temperatura interior para decidir si ventilar y expone el estado en GET /api/invernadero/estado

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 ​

ElementoEjemplo en esta sesión
MétodoGET: pedir información sin modificar nada
URLhttps://api.open-meteo.com/v1/forecast
Parámetros de consulta?latitude=38.38&longitude=-0.51&current=temperature_2m
CabecerasContent-Type: application/json en la respuesta
Código de estado200 bien · 400 petición incorrecta · 404 no existe · 429 demasiadas peticiones · 5xx fallo del servidor
CuerpoJSON 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ámetroCampo en la respuestaUso en el invernadero
current=temperature_2mcurrent.temperature_2m (ºC)¿Enfría ventilar?
current=relative_humidity_2mcurrent.relative_humidity_2m (%)Humedad del aire que entra al ventilar
hourly=precipitation_probabilityhourly.precipitation_probability[] (%)Aviso de lluvia (p. ej. cerrar ventanas)
hourly=et0_fao_evapotranspirationhourly.et0_fao_evapotranspiration[] (mm)Cuánta agua pierden las plantas: anticipar el riego
forecast_hours=6tamaño de los arrays hourlyPrevisió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 ​

NodoPara qué
http requestHace 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 inCrea un endpoint en Node-RED (GET /api/...). La petición llega en msg.req
http responseDevuelve la respuesta al cliente que llamó a http in
catchRecibe los errores que lanzan otros nodos
statusRecibe 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_LAT

Qué 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 docker aceptando reemplazar los ficheros (tu .env no se pierde: no va dentro del zip) y repite los dos comandos. docker compose up -d recrea (Recreated) node-red con las variables nuevas. Tus flujos no se pierden: están en el volumen node-red-data.
  • no configuration file provided o service "node-red" is not running: no estás dentro de la carpeta docker o 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&current=temperature_2m,relative_humidity_2m&hourly=precipitation_probability,precipitation,et0_fao_evapotranspiration&forecast_hours=6&timezone=Europe%2FMadrid

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

Respuesta de Open-Meteo en el navegador: JSON con latitude, longitude, timezone, current_units, current con time, temperature_2m y relative_humidity_2m, hourly_units y hourly con arrays time, precipitation_probability, precipitation y et0_fao_evapotranspiration

Localiza en la respuesta:

  • current: un valor por campo; por ejemplo, temperature_2m: 25.3 en la captura; tus valores serán los de ahora.
  • hourly: un array por campo, con un valor por hora, y el array time con la hora de cada posición.
  • current_units y hourly_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.

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

  2. Añade un inject, un http request (sección network) y un debug, y únelos en ese orden.

  3. Abre el http request:

    Diálogo Edit http request node: Method GET, URL vacía, Payload Ignore, varias casillas de opciones desmarcadas y, abajo, Return "a parsed JSON object"

    • 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.
  4. En el debug, Output complete msg object. Deploy y pulsa el inject.

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.

Pestaña debug con la respuesta del http request desplegada: payload como objeto, statusCode 200, headers, responseUrl

Si no funciona.

  • payload es 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.

  1. Inserta un function entre el inject y el http 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}`
        + "&current=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;
  2. Abre el http request y borra la URL: deja el campo vacío. Cuando está vacío, el nodo usa la de msg.url.

  3. Done, Deploy y pulsa el inject.

Qué hace el código:

  • env.get("…") lee una variable de entorno: las que pusiste en .env y que docker-compose.yml pasa al contenedor. Si no existe, || usa el valor por defecto.
  • Las comillas invertidas `…${lat}…` construyen un texto insertando variables.
  • msg.requestTimeout indica cuántos milisegundos esperará el http request una 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.

  1. Añade un function detrás del http request (quita el debug de 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;
  2. Detrás, un debug llamado previsión (en msg.payload). Deploy y pulsa el inject.

Qué hace el código:

  • suma recorre un array y suma sus valores; x ?? 0 trata 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 nombre meteo. Desde un function de cualquier otra pestaña se lee con global.get("meteo").
  • ts guarda 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 es Date.now() - meteo.ts, sin convertir fechas. Lo pasarás a texto legible solo al mostrarlo, como harás en el paso 9 con toISOString().

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.

Pestaña Context Data, sección Global: meteo desplegado con tempExterior, humExterior, lluvia6h, probLluviaMax, et0_6h y ts

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.

  1. Inserta un switch entre el http request y extraer previsión. Name: ¿200 OK?.

    • Property: cambia payload por statusCode, para que quede msg.statusCode.
    • Regla 1: ==, tipo number (icono 0-9), valor 200.
    • +add, regla 2: otherwise.

    Diálogo Edit switch node: Name "¿200 OK?", Property msg.statusCode, regla == 200 de tipo número hacia la salida 1 y otherwise hacia la salida 2

  2. Une la salida 1 a extraer previsión. En la salida 2 pon un function llamado respuesta 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 debug llamado errores.

  3. Un caso de prueba del error. Añade un inject llamado probar error 400. En sus propiedades, borra msg.payload y msg.topic con su × y añade con +add una fila msg. url, de tipo string, con una latitud imposible:

    text
    https://api.open-meteo.com/v1/forecast?latitude=999&longitude=-0.5143&current=temperature_2m

    Diálogo Edit inject node: Name "probar error 400" y una única propiedad msg.url de tipo texto con la URL de latitud 999

    Une este inject directamente a la entrada del http request: así prueba el error sin tocar construir URL.

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

  1. Añade un catch (sección common, rojo). No tiene entrada: no hace falta cablearlo a nada. Ábrelo:

    Diálogo Edit catch node: Catch errors from "selected nodes" y la lista de nodos de la pestaña con las casillas de Open-Meteo y extraer previsión marcadas

    • 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.
  2. Une el catch a un function llamado registrar error y este al debug errores:

    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 catch entrega un mensaje con msg.error: message (qué ha pasado) y source (qué nodo ha fallado).

  3. Caso de prueba: otro inject, probar sin red, igual que el del paso 5 pero con msg.url = https://api.open-meteo.invalid/v1/forecast (el dominio .invalid no existe nunca). Únelo a la entrada del http request.

  4. 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í:

Pestaña debug con dos mensajes del nodo errores: uno de respuesta inesperada con statusCode 400 y el motivo de la latitud, y otro de registrar error con origen Open-Meteo y el error ENOTFOUND api.open-meteo.invalid

Situación¿Llega respuesta?Dónde se trata
Todo bienSí, 200extraer previsión
Parámetros incorrectos, límite de uso superadoSí, 4xx/5xxSalida 2 de ¿200 OK?
Sin red, DNS, timeoutNo: errorcatch
La API cambia el formato y falta un campoSí, 200, pero extraer previsión fallacatch

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.

  1. 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.
  2. Únelo a un debug nuevo con Name estado http y, en Output, msg. status (borra payload y escribe status).
  3. Deploy y pulsa primero cada 15 min y después probar 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ónmsg.statusSignificado
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:

  1. Name: cada 15 min.
  2. Marca Inject once after 2 seconds, then, para tener datos nada más desplegar.
  3. Repeat: elige interval y escribe every 15 minutes.

Detalle del diálogo del inject: Inject once after 2 seconds marcado, Repeat interval 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.

  1. Umbrales. Un inject llamado cargar umbrales, con Inject once after marcado, msg.payload de tipo JSON:

    json
    { "temperatura": { "aviso": 30 }, "margenExterior": 2 }

    y un change con Set flow.umbrales to the value msg.payload, como en la sesión 1.

  2. Temperatura interior. Un mqtt in con Server Broker invernadero (elígelo en el desplegable: ya existe), Topic invernadero/dht22/temperatura y Output a String. Detrás, un change texto → número con Set msg.payload a la expresión $number(payload).

  3. Decisión. Un function llamado decidir ventilación:

    js
    const 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 debug decisión ventilación.

  4. Casos de prueba: tres inject con msg.payload string 25, 31 y 35, unidos a la entrada del change texto → número.

La lógica, en orden:

CondiciónDecisión
Interior por debajo del avisoNo ventilar: temperatura correcta
Calor dentro, pero no hay previsión recienteVentilar (regla simple de la sesión 1)
Calor dentro y fuera hace al menos 2 ºC menosVentilar
Calor dentro, pero fuera hace calorNo 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.

  1. Añade un http in (sección network). Ábrelo:

    Diálogo Edit http in node: Method GET, URL /api/invernadero/estado

    • Method: GET.
    • URL: /api/invernadero/estado. Es la ruta dentro de Node-RED: la dirección completa será http://localhost:1880/api/invernadero/estado.
  2. Detrás, un function componer estado:

    js
    const 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;

    ?? null devuelve null si el valor no existe todavía, para que la respuesta tenga siempre los mismos campos.

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

    Diálogo Edit http response node: Status code vacío y una cabecera content-type con el valor application/json

    Esa cabecera le dice al cliente que la respuesta es JSON.

  4. Deploy y pruébalo de tres formas:

    text
    http://localhost:1880/api/invernadero/estado
    bash
    curl -i http://localhost:1880/api/invernadero/estado
    python
    import 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:

Navegador mostrando la respuesta de http://localhost:1880/api/invernadero/estado: JSON con ts, ventilacion (interior, exterior, ventilar, motivo), 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 del http in no 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 S2 en el editor: previsión cada 15 minutos con construir URL, Open-Meteo, ¿200 OK?, extraer previsión y respuesta inesperada; los inject probar error 400 y probar sin red; el catch con registrar error; el status estado de la petición con el debug estado http; la ventilación con mqtt in, texto a número y decidir ventilación; y la API con http in, componer estado y http response

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 on

Si 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 ​

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:

NodoSe activa cuando…Uso típico
catchun nodo lanza un errorRegistrar el fallo, reintentar, avisar
statuscambia el indicador de estado de un nodoDetectar que el broker MQTT se desconecta (msg.status.text)
completeun nodo termina de procesar un mensajeMedir 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óndeCómo
En un functionenv.get("INVERNADERO_LAT")
En cualquier campo de un nodoEscribe ${MQTT_HOST} como valor completo del campo (lo usaste en el broker en la sesión 1)
Solo para una pestaña o un grupoPestañ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 ​

  1. 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.
  2. Decisión integrada. Amplía tu decisión conjunta de la sesión 1 (regar y ventilar) para que ventilar tenga 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 en global.decision.
  3. API del invernadero. GET /api/invernadero/estado devuelve JSON con:
    • las últimas lecturas de humedad del suelo y temperatura interior;
    • la decisión actual (regar, ventilar y su motivo);
    • la previsión y su antigüedad en minutos.

Reto opcional (elige uno):

  • Riego anticipado: si el suelo está en aviso y et0_6h supera un umbral configurable, adelanta el riego.
  • Cambiar umbrales por API: POST /api/invernadero/umbrales recibe JSON, lo valida y actualiza los umbrales. Si el JSON no es válido, responde 400 con el motivo.

Cómo se comprueba ​

  • Con la API accesible, /api/invernadero/estado devuelve 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 el catch. 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 400 y un error de red.
  • [ ] Captura de Context Data → Global con meteo y decision.
  • [ ] Respuesta de /api/invernadero/estado en el navegador o curl -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?

Recursos adicionales ​