Bot Estaciones de Servicio

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 como favorita.

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 tuyo o 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 precios de las estaciones de servicio de España: 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 los diferentes 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, el parseo de un XML o un JSON es inmediato. Además cuenta con clases que permiten realizar el parseo de ficheros tan grandes 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 que realize este parseo y te deje preparados los datos de alguna forma más óptima. Por ejemplo una versión de este bot la hice en Google Cloud Run y aquí el tiempo de ejecución del bot es importante pues no puedes exceder de un tiempo determinado (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 el mantenerla 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 poder saber qué tipo de carburante usa así como la estación favorita y el precio último para poder avisarle de cambios en el mismo.

La persistencia de las sesiones en este caso es una simple carpeta de un volumen persistente. La versión Google Cloud Run usaba por ejemplo un Datastore de Google, pero también se podría usar otras soluciones de base de datos como 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 forma que los datos no se pierden aunque destruya el contenedor (para desplegar nuevas versiones por ejemplo). Así que en 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ún dato 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.

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

En este json viene o bien el texto que ha introducido el usuario o bien datos asociados a cada botón que hayamos mostrado 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 coordenadas y 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 procedemos a 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és para 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 puntos geográ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 la base de datos (una lista en memoria) en función de la distancia a las mismas de cada gasolinera y se queda con las n 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. Este endpoint puede recibir un id de sessión y realizar el chequeo sólo para esta (útil para el modo debug o si hubiera un 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 precio de 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 mismo bot 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 SAAS para desplegar pequeños proyectos la adaptación a un kubernetes sencillo fue muy fácil y así de paso aprendo algo de esta tecnología

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

Follow comments at Telegram channel

2019 - 2020 | Mixed with Bootstrap | Baked with JBake v2.6.4