Frontal en React para estación meteorológica arduino, con backend en node.js

12 de agosto 2021ComentariosjavascriptDavid Poza SuárezComentarios

Este proyecto lo comencé hace ya un par de años cuando comencé con arduino, y a pesar de que ha estado funcionando desde entonces, no acababa de finalizarlo, asi que en estas vacaciones he aprovechado para darle un repaso.

En qué consiste

El objetivo de este proyecto era adentrarme en el mundo arduino y de paso practicar con el desarrollo de un frontal React con hooks y una api con node.js.

En la práctica, me apetecia tener controlada la temperatura de la casa y las diferentes habitaciones, asi como un pequeño gráfico de su evolución, algo que realmente es útil en plena ola de calor en Agosto. De este modo podemos hacer un uso más inteligente del aire acondicionado o podemos saber sencillamente en qué momento es mejor abrir o cerras las ventanas para evitar que se recaliente el interior.

Primeras fases

Inicialmente el proyecto pretendia funcionar con una unidad exterior con bateria, pero el arduino mkr1010 es demasiado tragón y tiene bugs en su modo deep sleep, lo que me obligó a descartar esa idea.

pruebas

También hubo otro cambio. En una de las primeras fases del proyecto la comunicación con el servidor donde está corriendo la api en node se hacía únicamente con la unidad interior, a la que se conectaban el resto de unidades via bluetooth. Esta idea funcionó bien durante algún tiempo pero no era 100% estable, asi que acabé haciendo la comunicación via wifi directamente desde cada unidad con el servidor.

Arquitectura de hardware

Tengo 4 sensores:

  • Un arduino mkr1010 en el exterior, dentro de una caja estanca y con una carcasa impresa en 3d

    • Para el sensor de temperatura, humedad y presión (un bme280).
    • Además de un anemómetro que también he impreso en 3d, basado en un sensor de efecto hall KY-024 e imanes de neodimio, que cuenta revoluciones y ajusta con una ecuación que he calibrado previamente. Funciona incluso con muy poco viento gracias a un rodamiento 608ZZ.
  • Unidad interior para el salón con otro mkr1010 pero esta vez en una carcasa y un display oled. modulo interior mkr1010
  • Otras dos unidades Xiaomi temperature and humidity sensor 2 que se conectan por bluetooth a la raspberry pi que actua como "repetidor" y captura la medición y la envia al servidor.
  • Servidor vps en https://www.hetzner.com/ sobre ubuntu 20.04, con una api en Node.js con express.js y una base de datos mongodb, montado todo en docker.
  • Webcam preparada para el exterior

exterior

Arquitectura de software

Todo gira en torno al servidor VPS en https://www.hetzner.com, Alemania, con una api en Node.js, montada sobre docker.

Usa una base de datos mongodb, que es idónea para este tipo de casos, pues no deja de ser un log, donde queremos tener centenares de miles de entradas a lo largo de los años. Para la autenticación uso tokens jwt.

Los sensores con el arduino llaman a esta api directamente via Wifi, sin embargo los sensores Xiaomi solo pueden comunicarse via bluetooth, asi que usan a la raspberry que tengo como nas para comunicarse con el VPS.

El frontal esta desarrollado en React, usando hooks, y manteniendo un contexto como estado global para el token jwt y los datos que se leen del sensor exterior y son compartidos entre widgets. El local storage se usa para persistir todo entre sesiones.

Para el despliegue tengo una pipeline en github.

Funcionalidad

Observando otras estaciones meteorológicas comerciales, parece que todas usan un diseño basado en una cuadrícula de widgets, asi que decidí usar la misma idea.

Modulo de temperatura

temperature widget

Este módulo es uno de los más útiles. Nos permite comparar la temperatura interior de la habitación principal con la exterior, además de la humedad.

Además nos permite conocer la tendencia de ambos valores, mostrando un símbolo con una flecha apuntando arriba, abajo o bien horizontal si se mantiene. Por último muestra el punto de rocío calculado.

He decidido mostrar un valor de sensación térmica, el típico "It feels", para saber cómo afectan el viento y la humedad a la sensación percibida. Su fórmula es la siguiente:

/**
 * Calculates THS
 * @param {number} temp celsius
 * @param {number} hum relative humidity %
 * @param {number} wind km/h
 */
export function calculateTHWIndex(temp, hum, wind) {
  const e = (hum / 100) * 6.105 * Math.exp((17.27 * temp) / (237.7 + temp));
  return (temp + 0.33 * e - 0.70 * (wind / 3.6) - 4.00).toFixed(2);
}

Otro valor muy interesante que muestro es la diferencia de temperatura respecto de la que teníamos hace 24h. Dado que guardo todo el histórico es fácil.

Módulo de webcam

webcam widget

Para este modulo he preparado un script complementario en node que va recopilando periódicamente las capturas de n webcams configuradas y genera un json para consultarlo a modo de api.

Gracias a esto puedo elaborar animaciones de tipo timelapse, de modo que este componente funciona como un reproductor de video.

Este es uno de los componentes que más interacción tiene, me ha resultado una buena práctica.

Módulo con tabs

  • salida y puesta de sol

Estos datos se consumen desde el json cacheado, pero los originales vienen de openweathermap.

  • calidad del aire

    airq widget

Para la calidad del aire he decidio usar el servicio tomorrow.io, que ofrece una capa gratuita. Si eres alérgico al polen es útil saber cómo se encuentran los niveles, para asi planificar tus actividades al aire libre. Es tan preciso que puedes ver cómo se ven afectados en tiempo real por factores ambientales como tormentas.

Además he elaborado un script para calcular el estado del protocolo de contaminación en Madrid, algo útil si te mueves en coche, para poder conocer las restricciones de circulación aplicadas en cada caso.

  • previsión hora a hora

    hourly widget

Openweathermap me ofrece una evolución de la temperatura y probabilidad de precipitaciones hora a hora. Estupendo para saber en qué momento exacto va a romper a llover.

Puede ser útil para programar el aire acondicionado, si sabes que a partir de las 4 de la mañana la temperatura va a bajar hasta unos valores agradables, entonces puedes programar que el AC se apague a esa hora.

  • estado de las nubes, viento, etc

    clouds widget

Aquí combino la velocidad del viento que obtengo con mi anemómetro con la dirección del viento que obtengo de la api de openweathermap.

Se puede observar la diferencia de las rachas de viento locales a mi tejado frente a las que registra la api, que obtiene la información de una estación de AEMET situada a unos 800m.

  • previsión versión texto

Esta previsión la ofrece AEMET a modo de api, siempre es interesante contar con un resumen rápido en texto. Es como escuchar el telediario jeje.

Módulo de gráfico de evolución de temperatura

temperature chart

Para las gráficas estoy usando la fantástica librería nivo.

Muestro como una línea cada uno de los sensores, de forma que podemos ver claramente cómo se comporta la temperatura en los diferentes ambientes a lo largo de las últimas 24h.

Módulo de gráfico de evolución de presión atmosférica

Reutilizando los mismos componentes he creado un gráfico para ver la evolución de la presión atmosférica, que nos sirve para detectar cambios bruscos de tiempo.

Módulo de gráfico de evolución de humedad

Es muy interesante observar cómo afectan a la humedad los aparatos de aire acondicionado o las tormentas.

Módulo pronóstico

forecast widget

Este módulo también se apoya en un script en node, ejecutado períodicamente con un cron, para generar los resultados en un json servido a modo de api.

Esta solución me permite combinar varias api gratuitas como openweathermap o tomorrow.io (para los alérgenos en el aire) y ademáss ahorrar en llamadas, pues en la práctica funcionan a modo "cache".

Además he preparado un custom hook useCachedFetch para cachear las llamadas con un ttl definido:

const CACHE = {};

export default function useCachedFetch(url, defaultValue = [], ttl = 120) {
  const [data, setData] = useState(defaultValue);
  const [isLoading, setLoading] = useState(true);
  useEffect(() => {
    const cacheID = url;
    if (CACHE[cacheID]?.data !== undefined && cacheID) {
      setData(CACHE[cacheID]?.data);
      setLoading(false);
    } else {
      setLoading(true);
      setData(defaultValue);
    }
    // fetch new data only if ttl has passed or if no previous data
    if (!CACHE[cacheID] || (new Date().getTime() - CACHE[cacheID]?.ts) > ttl * 1000) {
      CACHE[cacheID] = {
        ts: new Date().getTime(),
      };
      (async () => {
        const res = await fetch(url);
        const resData = await res.json();
        CACHE[cacheID].data = { url, data: resData };
        setData(resData);
        setLoading(false);
      })();
    }
  }, [url, defaultValue, ttl]);

  return [data, isLoading];
}

Módulo mapa Windy

windy widget

Quería disponer de un mapa donde poder observar cómo evolucionan las nubes, vientos y precipitaciones, y la verdad es que el servicio de https://windy.com es impresionante. Asi que he decidido embeberlo con un iframe.

Conclusión

El objetivo de este pequeño proyecto se ha cumplido, pues me ha servido para acercame al ecosistema arduino y para seguir ampliando mis conocimiento de React y Node, pero sobre todo, me ha divertido y entretenido muchísimo 😋.