Consulta la ocupación de los parkings de San Sebastian en tiempo real con datos abiertos, Grafana y Node-Red
-
yayitazale
- May 11, 2024
![Consulta la ocupación de los parkings de San Sebastian en tiempo real con datos abiertos, Grafana y Node-Red](/_astro/sven-mieke-lOgR82HSQKM-unsplash_Z11X1a8.jpg)
Ahora que viene el verano es habitual que los parkings de la costa estén más concurridos que en invierno. A nadie le gusta llegar y tener que hacer cola para esperar a entrar a un parking por lo que sería ideal poder saber la ocupación de estos aparcamientos. En San Sebastian, hace algunos años el ayuntamiento creó un servicio para informar de la ocupación de los aparcamientos de la ciudad casi en tiempo real. Este servicio llamado GEODONOSTIA está alojado en la propia página web del ayuntamiento, el cual es mapa de representación GIS creado con Javascript que es bastante lento y que además, no ofrece la información a simple vista de cada aparcamiento sino que hay que hacer clic en cada uno de ellos y solo ofrece el nº de plazas totales:
![Mapa GIS aparcamientos San Sebastian](/images/posts/parkings/gis_donostia.png)
Este tipo de desarrollos son bastante habituales en la administración pública, donde aglutinan toda la información geoespacial de la ciudad en un pequeño monstruo pensado para representar datos estáticos y que solo sirve a los técnicos del ayuntamiento y es muy poco útil para la ciudadanía en general salvo para los casos en los que tienes un par de horas y necesitas saber la ubicación de los mojones que delimitan los límites administrativos de la ciudad:
![Mapa GIS mojones San Sebastian](/images/posts/parkings/mojones_donostia.png)
Por suerte, la propia web del ayuntamiento dispone de un apartado de datos abiertos en el que está disponible dentro del catalogo de datos que ofrece, los datos de ocupación actual de los parkings subterráneos en tiempo real. Estos datos están accesibles en forma de API JSON completamente abierto, por lo que vamos a jugar con estos datos y crearnos nuestro propio mapa interactivo que realmente nos sirva para poder consultar esta información de forma rápida desde cualquier lugar, en cualquier momento y con cualquier dispositivo.
(Foto de portada Sven Mieke en Unsplash)
Si quieres navegar de forma rápida, este es el TOC:
TOC
- Requisitos
- JSON del servicio del ayuntamiento
- Transformado los datos en nodered
- Publicando nuestro propia API JSON
- Dashboard en Grafana
Requisitos
-
Vamos a utilizar Node-Red para transformar los datos del JSON del ayuntamiento ya que el formato de alguno de los campos no es el adecuado para nuestra herramienta de visualización. Tienes disponibles una pequeña introducción a Node-Red y la guía para instalarlo en el mi post Autoriego inteligente de plantas mediante IoT, HomeAssistant, OpenWeatherMap y NodeRed.
-
Para la parte de visualización de los datos, vamos a hacer uso de Grafana. Tienes una introducción a esta herramienta y la guía de instalación en el mi post sobre Monitorización de servidores con Telegraf, InfluxDB y Grafana.
JSON del servicio del ayuntamiento
Para empezar, vamos a echar un vistazo en el formato del JSON del servicio web del ayuntamiento. Como es un poco grande, vamos a ver un ejemplo solo con un parking:
{
"features": [
{
"geometry": {
"coordinates": [
582435.5700000001,
4796617.12
],
"type": "Point"
},
"type": "Feature",
"properties": {
"tipo": "LIB",
"plazasRotatorias": 300,
"plazasResidentesLibres": 2,
"noteId": "A8C4DD6C19C157A2C1257D6B00310619",
"libres": "104",
"nombre": "San Martin",
"precios": [
"0,79",
"1,32",
"2,38",
"3,45",
"4,46",
"6,56",
"11,47",
"20,68",
"23,02",
"24,57"
],
"plazasResidentes": 300
}
}
],
"success": true,
"name": "parkings",
"count": 16,
"type": "FeatureCollection"
}
Podemos ver que los aparcamientos vienen listados dentro del objeto tipo array features. Dentro de cada objeto del array, tenemos tres elementos que lo definen:
- type: define el tipo de objeto, en este caso es siempre Feature, seguramente en otros juegos de datos del ayuntamiento existirán otros.
- Geometry: donde encontraremos los datos de geolocalización.
- Properties: donde tenemos los datos propiamente dichos de cada elemento.
Finalmente tenemos unos pocos campos en los que se define el array de objetos con un tipo FeatureCollection, el nº total de elementos Count, el nombre de la colección de datos y un success que nos indica si nos trae datos de la base de datos.
Cada objeto array, como hemos dicho, es un Parking subterráneo de la ciudad. Dentro del array de cada parking encontramos varios bloques de información.
En el bloque Geometry de nuevo tenemos una definición de tipo, en este caso siempre son Point (el ayuntamiento localiza cada parking en un punto específico del mapa, si fuera un barrio por ejemplo, sería un área) y sus coordenadas. Como podemos ver, las coordenadas son un poco especiales. En herramientas GIS es habitual encontrarse con un sistema de coordenadas de tipo UTM, comúnmente conocido como Mercator. Este sistema de coordenadas desarrollado por el cuerpo de ingenieros del ejercito de estados unidos es una proyección cilíndrica conforme. No voy a entrar en detalles técnicos, pero debemos saber que este sistema de no está soportado por la herramienta de representación GEOMAP de Grafana, por lo que deberemos hacer una transformación de UTM al sistema clásico de coordenadas geográficas antes de poder representarlo en dichos mapas. Esto lo veremos más adelante en Node-Red.
En la parte de Properties tenemos bastante información. Por un lado, está disponible el nº total de plazas rotatorias (que son las que nos interesan a nosotros, son las de utilización libre), el nombre del parking, el nº de plazas rotatorias libres y también información sobre las plazas de residentes y precios que en este caso vamos a descartar.
Por comentar un detalle técnico, que el nº de plazas libres venga con un formato String (Cadena de carácteres) en vez de un Integer (Entero) es bastante chapucero por parte del desarrollador de esta API, pero bueno, cosas que pasan hasta en las mejores casas. Además, si las coordenadas se oferecieran en formato geográfico como hemos dicho, podríamos directamente usarlo en Grafana como fuente de datos y nos podríamos saltar todo lo que indico en el siguiente punto.
Transformado los datos en nodered
Bien vamos a ello. Como he dicho como el sistema UTM no nos vale, lo que vamos a hacer en Node-Red es crear un flujo para extraer los datos de este JSON con una frecuencia fija (por ejemplo, cada minuto), después vamos a hacer la transformación de UTM a Lat/Lon en cada objeto del array de parkings, y ya que no son muchos datos, para evitar usar una base de datos con todo lo que ello implica, lo vamos a guardar en la memoria interna de Node-Red como una variable.
Después, vamos a crear y publicar un servicio web en Node-Red al que llamaremos desde Grafana y el cual devolverá los datos almacenados en dicha variable.
Estamos de suerte (gracias a la comunidad de Node-Red) ya que existe un pallete preparado ya para pasarle datos de UTM y pasarlos al sistema Lat/Lon. Esto parece algo sencillo, pero créeme que no lo es. Si tuviéramos que hacerlo con funciones matemáticas sería bastante más complicado.
El flujo, que puedes descargarte aquí, hace lo siguiente:
![Flujo de Node-Red de solicitud a la API](/images/posts/parkings/flujo_solicitud_api.png)
- Se acciona cada minuto.
- Hace un http request al endpoint del servidor donde se encuentra el JSON.
- Si la respuesta del request es correcta (statusCode = 200) sigue adelante, de esto hablaremos más adelante.
- Si el campo succes = true se queda con el array de parkings y descarta todo lo demás.
- Divide el array en objetos únicos para tratarlos uno a uno.
- De parking en parking, prepara las coordenadas de UTM y los deja en las propiedades que requiere el conversor de UTM a Lat/Lon.
- Ejecuta la conversión de UTM a Lat/Lon para cada parking.
- Guarda las nuevas coordenadas en dentro de Geometry/Coordinates y borra las UTM de cada parking.
- Junta de nuevo todo el array de parkings y lo convierte en un objeto único de tipo array como teníamos al principio.
- Guarda el objeto en una variable de Node-Red de tipo Flow.
Ahora, lanzamos el flujo manualmente (o esperamos un minuto), si vamos a la pestaña de Información de contexto de Node-Red y actualizamos la lista de Variables de Flujo, podremos ver que se nos ha creado una nueva variable llamada parking donde tenemos un objeto muy parecido al JSON del ayuntamiento, pero ya con las coordenadas Lat/Lon incluidas en cada objeto del array de parkings, dentro de geometry/coordinates de cada parking:
![Variable de contexto en nodered](/images/posts/parkings/variable_contexto_nodered.png )
Publicando nuestro propia API JSON
Ahora vamos a publicar el servicio. Como hemos visto en la llamada al servicio del ayuntamiento, lo primero que hemos hecho es mirar que tenemos un statusCode = 200.
Este 200 es una de las respuestas posibles de la lista de códigos de estado del estándar http. Un servicio como el que vamos a publicar nosotros debería (si queremos cumplir con el estándar) utilizar los códigos necesarios para poder informar al cliente del estado de su petición. Estos códigos son los típicos que nos encontramos cuando intentamos acceder a una web y no funciona que sería un 404 Not Found.
![Ejemplo de 404 Not Found de GitHub](/images/posts/parkings/github-404.png )
Los códigos son de tres dígitos, siendo el primer digito el que define el tipo de mensaje que estamos recibiendo:
- 1xx respuestas informativas: se ha recibido la solicitud, proceso en curso.
- 2xx Peticiones correctas: la solicitud ha sido recibida, comprendida y aceptada con éxito.
- 3xx Redirecciones: es necesario realizar más acciones para completar la solicitud.
- 4xx Errores del cliente: la solicitud contiene una sintaxis incorrecta o no puede completarse.
- 5xx Errores del servidor: el servidor no ha podido completar una solicitud aparentemente válida.
En nuestro caso, el servicio que vamos a crear va a ser de consumo en la red local y no lo vamos a publicar a internet, ya que el servidor de Grafana va a estar alojado en el mismo servidor que Node-red. Con esta premisa, el servicio no va a estar encriptado (será un http y no un https) pero si vamos a añadir un sistema de autenticación muy simple al menos para evitar que alguien con malas intenciones que consiga acceder a nuestra red pueda llevar a cabo un por ejemplo, un ataque DDoS realizando llamadas de forma reiterada hasta hacer caer el servicio por saturación.
El servicio va a comprobar primero, que la IP del que se realiza la llamada es de la red local (en mi caso, 192.168.1.0/24), vamos a comprobar que nos llega un token (lo vamos a generar a mano por ejemplo usando este servicio), y vamos a implementar un límite de 1 llamada por segundo para evitar que haciendo llamadas al dashboard de Grafana de forma muy repetida se pueda saturar el servicio. Las salidas de estos controles devolverán cada uno su statusCode correspondiente. Además, como el método que vamos a publicar es un GET, vamos a hacer que devuelva también un error si el endpoint es llamado con otro método:
![Flujo de publicación de API en Node-Red](/images/posts/parkings/flujo_api_propia.png )
El flujo de este servicio lo tienes descargable aquí para importarlo, solo deberás añadir un token.
Ahora, si hacemos la llamada a http://la-ip-de-nodered:puerto-de-nodered/parking, en mi caso, http://192.168.1.20:3880/parking desde nuestro navegador nos devolverá un 401 ya que no estaremos pasándole el token que espera. Usando el modo depuración del navegador, podemos añadir el token de forma manual y conseguiremos que nos devuelva el JSON tal y como lo hemos preparado.
![Respuesta API navegador](/images/posts/parkings/response_api.png )
Este sistema de token plano no es para nada lo más seguro ya que es “fácil” de interceptar, pero ya que se trata de un servicio local, vamos a dejarlo así ya que es sencillo de implementar. En caso de querer implementar un sistema más seguro de autenticación, podríamos generar un flujo de token con JWT por ejemplo donde el token va encriptado y firmado y solo sabiendo la base de encriptación usada y el secret (que solo el servidor como el cliente conocen) se puede descifrar para compararlo con el que se espera. Algo así sería imprescindible si fuesemos a publicar la API a internet y quisieramos controlar el acceso.
Dashboard en Grafana
Ahora vamos a crear el dashboard de Grafana. Lo primero que vamos a hacer es ir a Inicio -> Conexiones -> Orígenes de datos y vamos a añadir una nueva fuente de datos. Buscamos por JSON y agregamos un nuevo origen de este tipo:
![Agregar fuente de datos API JSON en Grafana](/images/posts/parkings/fuente_datos_JSON.png )
Rellenamos los datos de nombre, la URL y añadimos el header token con el token antes creado, le damos a save&test y vemos que va todo bien:
![Datos para fuente de datos API JSON en Grafana](/images/posts/parkings/fuente_datos_JSON_datos.png )
Ahora creamos un nuevo dashboard y añadimos un panel de tipo GEOMAP:
![Añadimos un panel de tipo GEOMAP en Grafana](/images/posts/parkings/add_geomap_panel.png )
Como query, apuntamos a la fuente de datos que acabamos de crear, lo cual hará que se llame a la API cada vez que carguemos el dashboard. Ahora, creamos Fields que será el parseado de los campos de los objetos del JSON a señales de Grafana. Vamos a parsear los campos de libres, nombre, plazas rotatorias, lat y lon:
![Query a la API en Grafana](/images/posts/parkings/query.png )
Como ves, estoy usando una expresión regular de JSON Path para definir que quiero coger los elementos de cada objeto del array:
$[*].properties.libres
$[*].properties.nombre
$[*].properties.plazasRotatorias
$[*].geometry.coordinates.lat
$[*].geometry.coordinates.lon
Fíjate que he definido como Cache Time en 5 segundos. Esto hará que aunque el dashboard se abra mile de veces por segundo, solo llame al servicio de Node-Red una vez cada 5 segundos. Durante esos 5 segundos se cacheara la respuesta y así limitamos muchísimo el tráfico y el nº de solicitudes.
Con esto podremos calcular el % de ocupación haciendo la regla de tres (libres / plazasRotatorias * 100):
![Cálculo de porcentaje de ocupación en Grafana](/images/posts/parkings/porcentaje_ocupacion.png )
Ahora, en la configuración del panel de GEOMAP, vamos a decirle que debe representar la query A y que los datos de geolocalización están en formato Auto (lat/lon):
![Mapear capas en Grafana GEOMAP](/images/posts/parkings/mapear_capas.png )
Hacemos que los puntos tengan colores en base a su %, aunque también podríamos variar su diámetro en base a este valor:
Además, podemos añadir la etiqueta del valor real de plazas libres al lado del puntito para que tengamos la info más importante a simple vista:
Finalmente, nos quedará poner a nuestro gusto el mapa base, el centrado y nivel de zoom de la vista etc.
Si queremos tener otro tipo de visualizaciones, como, por ejemplo, un gráfico de barras, podemos aprovechar la query y el cacheo de datos por lo que al crear el nuevo panel, haremos referencia a la query del otro panel:
Solo nos quedará transformar un poco esos datos al formato requerido por el panel, descartar los campos de lat y lon y tocar la parte estética del panel:
Con un poco de mimo, puede conseguir algo parecido a esto:
Si quieres, puedes bajarte el cuadro de mandos aquí para importarlo como base. También puedes directamente acceder a mi dashboard, ya que lo tengo en la parte pública de mi Grafana, y así no tendrás que hacer nada más que guardarlo entre tus páginas favoritas. Para agregarlo como APP a tu teléfono móvil te dejo aquí una guía publicada por Euskalmet que está muy bien explicada.
PD: algunos parkings llevan meses emitiendo un 0 en la variable de plazas libres…
Espero que te haya parecido interesante y hayas aprendido algo nuevo y recuerda que para cualquier comentario, duda o sugerencia tienes a tu disposición nuestro grupo de telegram.
Nota: algunos de los enlaces a productos o servicios pueden ser enlaces referidos con los que podemos obtener una comisión de venta.