Bot Estaciones de Servicio
Tags: micronaut, telegram, bot
Published: 2020-08-15 19:39:35

---
title: Bot Estaciones de Servicio
published: true
date: 2020-08-06 00:00:00 UTC
tags: micronaut,telegram,bot
canonical_url: https://jorge.aguilera.soy/blog/2020/estaciones-servicio-bot.html
---

El bot "EstacionesServico" es un bot de Telegram que te permite saber el precio de la gasolinera más cercana, asícomo ubicarla en un mapa además de recibir una notificación si cambia el precio de la gasolinera que marques comofavorita.

En este post voy a destripar (a grandes rasgos) cómo funciona el bot por si te da alguna idea de cómo hacer el tuyoo algún otro caso de uso de los datos.

## Open Data

El bot se basa en un conjunto de datos abiertos del Ministerio de Industria y Consumo donde se muestran los preciosde las estaciones de servicio de España: [https://sedeaplicaciones.minetur.gob.es/ServiciosRESTCarburantes/PreciosCarburantes/EstacionesTerrestres/](https://sedeaplicaciones.minetur.gob.es/ServiciosRESTCarburantes/PreciosCarburantes/EstacionesTerrestres/)

En este xml (o json) se listan todas las estaciones con su nombre, dirección, marca, geoposición así como losdiferentes precios para gasolina 95, 98, los diferentes tipos de diesel, bios, etc.

Estos precios se actualizan al menos una vez al día (realmente no recuerdo donde leí cúando se realiza ni la periodicidad)

```
<PreciosEESSTerrestres xmlns="http://schemas.datacontract.org/2004/07/ServiciosCarburantes" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<Fecha>06/08/2020 21:31:14</Fecha>
<ListaEESSPrecio>
    <EESSPrecio>
        <C.P.>02250</C.P.>
        <Dirección>AVENIDA CASTILLA LA MANCHA, 26</Dirección>
        <Horario>L-D: 07:00-22:00</Horario>
        <Latitud>39,211417</Latitud>
        <Localidad>ABENGIBRE</Localidad>
        <Longitud_x0020 __x0028_WGS84_x0029_>-1,539167</Longitud_x0020__ x0028_WGS84_x0029_>
        <Margen>D</Margen>
        <Municipio>Abengibre</Municipio>
        <Precio_x0020_Biodiesel/>
        <Precio_x0020_Bioetanol/>
        <Precio_x0020_Gas_x0020_Natural_x0020_Comprimido/>
        <Precio_x0020_Gas_x0020_Natural_x0020_Licuado/>
        <Precio_x0020_Gases_x0020_licuados_x0020_del_x0020_petróleo/>
        <Precio_x0020_Gasoleo_x0020_A>1,039</Precio_x0020_Gasoleo_x0020_A>
        <Precio_x0020_Gasoleo_x0020_B>0,569</Precio_x0020_Gasoleo_x0020_B>
        <Precio_x0020_Gasoleo_x0020_Premium/>
        <Precio_x0020_Gasolina_x0020_95_x0020_E10/>
        <Precio_x0020_Gasolina_x0020_95_x0020_E5>1,149</Precio_x0020_Gasolina_x0020_95_x0020_E5>
        <Precio_x0020_Gasolina_x0020_95_x0020_E5_x0020_Premium i:nil="true"/>
        <Precio_x0020_Gasolina_x0020_98_x0020_E10/>
        <Precio_x0020_Gasolina_x0020_98_x0020_E5/>
        <Precio_x0020_Hidrogeno/>
        <Provincia>ALBACETE</Provincia>
        <Remisión>dm</Remisión>
        <Rótulo>Nº 10.935</Rótulo>
        <Tipo_x0020_Venta>P</Tipo_x0020_Venta>
        <_x0025 __x0020_BioEtanol>0,0</_x0025__ x0020_BioEtanol>
        <_x0025 __x0020_Éster_x0020_metílico>0,0</_x0025__ x0020_Éster_x0020_metílico>
        <IDEESS>4375</IDEESS>
        <IDMunicipio>52</IDMunicipio>
        <IDProvincia>02</IDProvincia>
        <IDCCAA>07</IDCCAA>
    </EESSPrecio>

    <!-- unas 10500 estaciones-->
</ListaEESSPrecio>
```

Como puedes ver hay bastante información pero la que le interesa al bot son básicamente los diferentes precios,el nombre y la latitud+longitud

## Parseo

El bot al arrancar se descarga el xml y lo parsea. Como utilizo Groovy, elparseo de un XML o un JSON es inmediato. Además cuenta con clases que permiten realizar el parseo de ficheros tangrandes sin consumir muchos recursos. Así mismo, cada hora, realiza una actualización del fichero volviendoselo a bajar.

Para poder disponer de los precios de forma inmediata se mantiene una lista en memoria con los precios de cada estación.

Esta parte es fundamental según diseñes tu bot. Si diseñas un bot stateless deberás contar con algún proceso querealize este parseo y te deje preparados los datos de alguna forma más óptima. Por ejemplo una versión de este botla hice en Google Cloud Run y aquí el tiempo de ejecución del bot es importante pues no puedes exceder de un tiempodeterminado (y además si tu bot tiene muchas visitas podrias sobrepasar la capa generosa grautita de Google)

En mi caso, como el bot va a estar desplegado en un servicio que me deja correr la aplicación 24h puedo permitirme elmantenerla en memoria.

## Sesión

Otra de las características de este bot es que mantiene una sesión por cada usuario con el que dialoga para podersaber qué tipo de carburante usa así como la estación favorita y el precio último para poder avisarle de cambiosen el mismo.

La persistencia de las sesiones en este caso es una simple carpeta de un volumen persistente. La versión GoogleCloud Run usaba por ejemplo un Datastore de Google, pero también se podría usar otras soluciones de base de datoscomo un mysql u otros servicios con capa gratuita como FaunaDB.

Sin embargo una vez más el servicio donde se encuentra desplegado me permite crear volúmenes persistentes de tal formaque los datos no se pierden aunque destruya el contenedor (para desplegar nuevas versiones por ejemplo). Así queen este caso la persistencia es un fichero por cada chat con el que el bot dialoga

| | Aunque Telegram ofrece en el api conocer algunos datos del usuario en este caso prefiero NO guardar ningúndato que no sea imprescindible y que mantenga el ananimato del usuario. Así simplemente guardo el id del chat(que no del usuario) así como el id de la gasolinera, el tipo y el precio del carburante elegido. |

## Diálogo

Cada vez que el usuario interacciona con el bot a través de Telegram, recibimos un json mediante un POST al controllerque hayamos configurado.

En este json viene o bien el texto que ha introducido el usuario o bien datos asociados a cada botón que hayamosmostrado o bien un mensaje con la localización del usuario (cuando este la envía usando el clip del móvil)

Básicamente los comandos que acepta el bot son:

- /start, inicio del diálogo. Preparamos un fichero `chat_id` para mantener la sesion

- /carburante , enviamos un teclado con los diferentes tipos de carburante admitidos

- /favorita, muestra el precio actual de la estación guardadas en la sesion

Así mismo mediante los datos asociados a los botones de teclado podemos recibir del usuario:

- info\_estacion xxxxx, cuando el usuario ha seleccionado la estación XXXX la buscamos y mostramos su precio

- send\_localizacion xxxxx, cuando el usuario selecciona "ver en mapa" la estaacion XXXXX buscamos sus coordenadasy le indicamos al movil que abra un mapa con las mismas

- save\_estacaion xxxx, actualizamos la sesion del usuario con el id de la estacion

- algunas otras como `back` y `cancel` para gestionar los menús emergentes

Por su parte cuando el usuario nos envia su localización, lo recibimos en una estructura del mensaje y procedemosa actualizar la sesion con estas coordenadas.

Así cuando el bot recibe un mensaje y dispone de una estación o localizacion así como un carburante de interéspara el usuario puede realizar la búsqueda y filtrar aquellas estaciones de interés

## Búsqueda y filtrado

La búsqueda de estaciones de interes es realmente fácil si sabemos cómo calcular la distancia esntre dos puntosgeográficos:

```
static float metersTo(float lat1, float lng1, float lat2, float lng2) {
    double radioTierra = 6371;
    double dLat = Math.toRadians(lat2 - lat1);
    double dLng = Math.toRadians(lng2 - lng1);
    double sindLat = Math.sin(dLat / 2);
    double sindLng = Math.sin(dLng / 2);
    double va1 = Math.pow(sindLat, 2) + Math.pow(sindLng, 2) * Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2));
    double va2 = 2 * Math.atan2(Math.sqrt(va1), Math.sqrt(1 - va1));
    double meters = radioTierra * va2;
    meters as float;
}
```

Básicamente el servicio de búsqueda recibe unas coordenadas de donde se encuentra el usuario así que ordena labase de datos (una lista en memoria) en función de la distancia a las mismas de cada gasolinera y se queda con lasn primeras a las que ordena por el precio de interés más barato

## Chequeo y notificación

Para realizar un chequeo diario se podía haber creado un Job en la propia aplicación que se ejecutara una vez al día.Sin embargo me pareció más interesante tener un `endpoint` al que invocar para que realizar la comprobación. Esteendpoint puede recibir un id de sessión y realizar el chequeo sólo para esta (útil para el modo debug o si hubieraun plan premium por ejemplo) o si no recibe sesion realiza el chequeo para todas.

El chequeo es simplemente para cada estación comprobar el último precio guardado en cada sesioón con el preciode la base de datos y enviar un mensaje al chat del usuario

## Tecnología

Para este bot he usado

- groovy, como lenguaje de programación. Soy un fanático de este lenguaje y la facilidad de parsear xml es un plus.La performance y el compilado estático que tienen generan una aplicación bastante ligera y funcional.

- Micronaut, como framework de desarrollo. La facilidad para crear controllers, services, etc es increíble. El mismobot lo he desplegado en Heroku, Google AppEngine (Flex), Kubernetes o Google Cloud Run (con ligeras adaptaciones)

- Kubernetes. Las primeras versiones eran un simple Docker corriendo en Heroku pero tras descubrir Okteto y su SAASpara desplegar pequeños proyectos la adaptación a un kubernetes sencillo fue muy fácil y así de paso aprendo algode esta tecnología

Obviamente, estas son las herramientas que yo he elegido por mis motivos, pero no son las únicas.