Nightscout-Notifier feature image part one

How to Build a Nexmo Notifier with Nexmo Messages and Python – Part One

Published February 24, 2020 by Diana Rodriguez

blog spotlight banner

In this tutorial, we’ll be building Scout, an application created using Python with Flask. On the client side, we’ll use JavaScript for certain dynamic functionalities required for our app. This tutorial is split into two parts—in the first, we’ll set up Google auth, build a user interface, and implement a Firebase Firestore.

The final version of the code can be found here, if you’re curious to see it all put together!

Services Involved

Scout relies on four services for its operation:

  • Nightscout, an open-source project that supports cloud access to data from a variety of CGM (continuous glucose monitoring) devices.

nightscout

  • Nexmo for sending and receiving SMS messages.

  • Google auth: The API that allows us to use the Google authentication service for our web application.

  • Firebase/Firestore, to store our data in the cloud.

Application Features

Scout allows us to ping the Nightscout data of a user obtaining the last blood glucose level recorded. If levels are below 70mg/dL(3.9 mmol/L) or above 240mg/dL(13.3 mmol/L), the application will execute a call to the user’s mobile phone number, and the user will hear their current blood glucose level. If the user does not respond, a text message will be sent to the user’s preferred emergency contact and up to 5 additional numbers.

The ping frequency to Nightscout is set to one minute. To be exact, it will be done 30 seconds after each minute using the system clock. If a user’s glucose level remains out of the standard range during that time, the call will be made again.

If the Nightscout service does not respond for 1 hour, an SMS will be sent alerting the user that their service is offline.

The user can sign up/login using their Google account and configure the following information:

  • Nightscout API URL
  • Personal number
  • Preferred emergency contact and up to 5 additional phone numbers

dashboard of the app

Application Structure

Our application can be divided into two parts:

  • A Flask app that allows the user to log in and configure the application data
  • A Python Thread with a scheduler that runs with a given frequency, consulting the Nightscout dashboard of each user and sending alerts when necessary. A second scheduler that runs less frequently can be assigned to obtain fresh data.

Prerequisites

  • Python 3.x.x

  • An application directory:

  • Flask:

  • dotenv,requests, and google-auth Python libraries:

  • A unique key to store our session variables. It’s a binary that we have to handle discreetly. Generate with the following:

User Interface Development and Google Auth

In this section, we will start configuring Google auth. We will also create a simple interface for calling the Google auth API, a login view, and a persistent server-side session to keep us logged in until the user decides to log out.

Google Auth

To use Google auth, we have to obtain a client ID, which we will use to call the Google API sign in. Head to the Google Cloud dashboard, and create a new project.

  • Once the project has been created, click on the navigation menu (≡) and select APIs & Services> Credentials.
  • Click on Create Credentials> OAuth client ID.
  • Select Web.
  • In Authorized JavaScript sources and Authorized redirection URIs write the domain name to be used for this app, e.g. https://domain.ext/. In our case we will assume that / will be the endpoint that will consume our authentication service.
  • Click on Create and our client ID will be generated. It should be listed on OAuth 2.0 client IDs. Keep the client ID at hand, as we will use it later.

Diving into the Source Code

Once we are done with all our preparations, let’s open our favorite editor (for example, pyCharm or Visual Studio Code). Create a new file. You can name it whatever you want, in my case I chose notifier.py.

At the very beginning of the file we will import the following modules:

In the same way, we import some functions that will allow us to read the environment variables. A secure way to handle credentials is to make them available only in the scope of the operating system that runs the application.

Let’s include the modules for Google auth that will allow us to reconfirm the identity of the user from the backend. This will allow us to create a persistent session if the identity is valid.

And the requests module that allows us to request using the POST or GET methods. It’s similar to Axios.

Create a new file and name it .env (In the tutorial repo I named it .example-env. If using my repo, make sure you rename it!) Add the following lines:

Note: ReplaceYOUR_GOOGLE_AUTH_CLIENT_ID with the clientID generated by google, and YOUR_SITE_URL with the domain name you registered previously (https://domain.ext). Save the file!


Let’s go back to notifier.py and add the following lines:

We assigned the variable app to represent. our Flask application.app will create its own context to make only operations related to the requests made to the Flask application

Then we assign the secret_key attribute. Paste the value previously generated with python -c 'import os; print(os.urandom(16))'.

To access the environment variables defined in the .env file, we add the following:

Now, let’s define the get_session function that evaluates whether there is a specific key within the session variable, returning None in case it doesn’t exist, and the value of the key otherwise. It can be reused in different sections of the program:

In the following section we begin to define our Flask application with the controller for the endpoint /, which will be our landing page and will show us the Google login button:

The line @app.route('/,methods=['GET','POST']) indicates that every request, either GET or POST, will be directed to the home() handler. The home function evaluates whether the user session exists, then loads the home.html template if the user is authenticated. If the user is not authenticated, we load the login.html template, where the Google authentication interface will be displayed (we pass the value of GOOGLE_CLIENT_ID and SITE_URL previously defined in our .env file. The second parameter will be used for redirection).

Following the workflow, when the user first enters the site the user session variable will not exist, therefore login.html will be loaded. The next logical step would be to develop the home.html jinja template. But before doing that we need to do the following:

  • Create a new static directory within your main application directory:

  • Download Materialize, a framework for front-end development based on material design. Unzip the file and move the css andjs directories into the previously created static directory. Ideally, keep only the minified versions of the css and js files.

  • Download the Materialize icons. Once downloaded, create a new fonts directory within static, and move the font file there.

  • Create style.css in the static/css/ directory. Usually Materialize is more than enough to style an app, but sometimes an additional style file is necessary to control certain details not covered by Materialize. Let’s add some extra style:

Now, we are ready to create our app’s layout. We start by creating a parent template that defines blocks of content that are used by other child files. Officially, this will be our first jinja template 🎉. In order for this file to be recognized by Flask as a template, let’s create a templates directory inside static, and create layout.html. Let’s add the following code:

There are a couple of interesting details to highlight: The use of blocks and the use of the url_for function. Blocks are reserved sections for inserting code with jinja from child templates. The url_for function generates the URLs to the JavaScript and CSS resources in static.

The file structure that we have up to this point should be:

If everything looks proper, create login.html in the same templates directory. This file will be loaded when the user session variable does not exist (that is, the user is not logged in).

Notice that the first line for login.html is {% extends "layout.html"%}. This indicates that login.html inherits from layout.html. In other words, it is a child of layout.html. This means that the renderer will load layout.html with the code variants that we added in login.html. These variants are defined within the blocks allowed in layout. Within login.html we use the head and content blocks.

In the head block, we have:

The first line indicates that we will be using the Google API for the authentication process and the second is metadata used by Google to know our app’s clientID. Note that within the content attribute we have written {{client_id}}. When the jinja compiler evaluates this expression, it will print the value of the client_id variable that we pass to the template using the render_template function.

The next block is content, and in there, we present a message to the user indicating how the application works. Then we have a few lines of JavaScript. Basically, it is a function connected to the onSignIn event, which is used by Google to return the data of the user that was logged in using Google auth.

We obtain the user profile with googleUser.getBasicProfile(). If there is an ID, the authentication process was successful, and we can proceed to send some data to our server to make an identity reconfirmation with Google and create the session.

The previous lines connect us to our server using an AJAX request. Pay attention to the line xhr.open ('POST', '{{site_url|safe}}/login');. It indicates the method that the request will use. The URL {{site_url|safe}} will be replaced by the value of the site_url variable that we pass to the template. To do the reconfirmation, our Flask application only needs the id_token. However, we also pass the username and email since we will use them later for other operations.

Once the reconfirmation is done, our server will redirect us to /. If the reconfirmation was not successful, the user will have to try to log in again. Now, if we look carefully, we haven’t defined the /login endpoint yet. this endpoint will be responsible for reconfirming.

To create it, let’s go back to notifier.py and add the following lines:

As mentioned above, to reconfirm the identity of the user on the server, we only need the id_token obtained from Google auth and passed to /login using an AJAX POST request. Then we get client_id using os.getenv("GOOGLE_CLIENT_ID").

When we make the reconfirmation, we place our code within a try/except block for exception handling in case an error occurs at the time of making the request.

This verification is done using the verify_oauth2_token method and returns an infoid that must have a key iss, which is a reference to theissuer. If the value of the issuer does not match the domain we configured, we assume the verification returns an error and an exception will be generated. If, on the other hand, the response is valid, we proceed to create the persistent session on the server side, assigning user to the session object. Within this session, we store the userid, username, and email of the user.

Once this is done, our server returns the response and the xhr.onload event of our ajax request is triggered. Its function is to redirect us to /. In / our application evaluates if the user session exists, and if so, it will load the home.html template by passing the session['user'].

Following the logic of our application, the next step is to create home.html in thetemplates directory:

This template inherits from layout, and in our content block we will show the logged user and alogout link to close the session. The latter is not programmed—to complete the login experience we will define the endpoint logout in notifier.py. Let’s add:

Our logout endpoint deletes the user session and redirects us to home. Back at home it will evaluate the session, and if it doesn’t find any, it will render login.html.

Note: url_for uses the handler name def home() for redirection, not the endpoint (although it is also valid to use endpoints for redirects). In the case of url_for needing to generate a url in https, the line should be: url_for ('home', _external = True, _scheme = 'https'). The external parameter indicates the generation of an absolute URL and scheme defines the protocol we want to use.

At this point, we can test if Google auth works. To test locally, let’s run the following command in our terminal:

We are telling Flask to run notifier.py. However, it’s best to use a more robust server that allows for more efficient handling of our requests, thus improving the app performance. Therefore, we will use Gunicorn, an HTTP WSGI server written in Python and compatible with multiple frameworks (Flask included).

To install, let’s execute the following command in our terminal:

After installing, from the same terminal window and from our app’s root directory, type:

This command deploys our application to our local server and listens for requests using port 80. With this, we should be able to access our app and test if we can log in and log out.

Note: To stop the application, hit ctrl+c in the same terminal window where gunicorn is running.

Storing Nightscout Settings with Firebase/Firestore

In this section, we will build a simple interface where our user can add the following data:

  • nightscout_api: a valid Nightscout URL to obtain the glucose level data, (for example, https://domain.ext/api/v1/entries.json).
  • phone: the mobile number where alerts will be sent.
  • emerg_contact: preferred emergency contact (relative or close friend who can receive alerts).
  • extra_contacts: an optional array with up to 5 additional phone numbers.
  • email: The Google account email address to log in (we will use it as an external key to obtain the logs of a logged-in user).
  • username: Also obtained from the user’s Google account, we will use it for data presentation.

Firestore allows us to handle collections and documents in a similar fashion to mongodb. For this application, our collection will be called scouts. A document from our collection should look like this:

Adding Firebase Firestore to Our Project

  • Go to https://firebase.google.com/
  • Log in with your Google or GSuite account
  • Click on Go to Console
  • Click on Add project. If your previously created project used for Google auth does not appear on the list, click on Add project . We should see our project name listed on Enter the name of your project . Select it and click on Continue, Provide additional information for the next steps, and when finished click on Create project
  • On the Firebase console page, click on authentication and in the sign in method tab, enable Google.
  • Click on Database, and select FIRESTORE. Then Database > Tab Rules. Modify the existing rule as follows:

This is to make sure only logged in users have access to our application.

Connecting the Project with Firebase

Once our project is set up on the Firebase console, we have to generate a Firebase key:

  • Login to https://console.firebase.google.com/
  • Click on Project Overview> Project settings and select the Service account tab
  • Click on the generate new private key option
  • Save the JSON file in our application’s root directory. You can rename that file to whatever you want. I used: super_private_key.json

The next step is to add the name of your private key file to .env with the following line:

With these initial preparations in order, we are ready to connect our application to Firebase/Firestore. In the case of data queries for Firestore, the ideal scenario in Python would be to have a class that we can reuse in our application that allows us to add, modify, or query records (CRUD). This way we keep our code simple, organized, and much easier to understand.

Before we write that code, we need to install the Python modules that will allow us to perform these operations (back to our terminal!):

Let’s create the models.py file and add the following lines:

The previous lines indicate which modules we will be using in our models.py file. Among these, we can see firebase_admin, which we will use to connect to our Firebase project and perform operations (CRUD) on our scouts data collection. We will use dotenv to get the FIREBASE_PRIVATE_KEY variable from our .env file.

In the same file, add the following lines:

The first two lines are well known since we have previously used them in notifier.py to load our environment to extract the value of the FIREBASE_PRIVATE_KEY variable. The next two lines connect our application with Firebase using the private key we generated earlier. credits = credentials.Certificate (os.getenv (" FIREBASE_PRIVATE_KEY ")) extracts the private key from the file as well as other additional data, and firebase_admin.initialize_app (credits) authenticates our application to use Firebase and initializes it to perform operations.

Once the connection with Firebase is defined, we will proceed to define the case model. Normally, when we use technologies such as SQLAlchemy to work withflask ​​and sqlite, the models are classes where we define different attributes that are the fields of the database and use a set of native functions of the model to perform operations on the database.

In our case, we will create the model class as a “bridge” class that will allow us to use the firestore methods to perform database operations. In other words, model will function as a parent class from which other classes will inherit to return their methods. Then we add the code to the end of the models.py file:

Within the model class, we start defining our constructor. It will accept the key parameter representing the name of the collection inside Firestore. The builder also initializes the firestore client, self.db= firestore.client(), and the collection, self.db.collection(self.key).

The get_by method receives the name of the field and the value by which we want to filter data from our collection. The line docs= list(self.collection.where (field,u'==',u'{0}.format(value)).stream()) runs a query to our Firebase collection, self.collection, and is a reference to this collection defined in the constructor. In our collection, we use the where method to filter by field and value.

Pay special attention to the character u—this indicates that Python will send the field name and the value in unicode format. By using the code fragment u'{0}.format(value) we are telling Python that any value, regardless of type, should be formatted as unicode. The stream method, in turn, returns the flow of documents as a special type of data, so the list function is used to convert it into an array that can be traversed with Python.

Normally, when making a query to Firestore to obtain data from a collection, for each record in the collection we would obtain an object with two attributes: the document id and the to_dict() method that formats the document to the Python dictionary type of data (a format that has a structure similar to JSON and that makes it easy for us to access each field).

The get_by function evaluates whether the document exists. If it exists, it creates a consolidated item with item= docs[0].to_dict() to store the document in a variable. With item['id']= docs[0].id, we add the id to the document to have all the information at our disposal. Another important detail is that get_by returns the first document found. We leave this as it is in our case. Once our user logs in with Google, they will only have access to a document that will contain their data (one and only one).

We define the get_all method, which does not receive any parameters. Its function is to obtain all the documents in a collection, consolidate them by creating a dictionary for each item, and fill out an array with each consolidated document. This function returns an array of all the documents in the collection, or none in case there are no documents.

The add method receives the data and id parameters. id is optional, but if it exists it allows us to add a new document with a defined id. If the parameter id does not exist, the new document will be created with an id automatically generated by Firestore. The parameter data must be of type dictionary and will contain the data that we want to add to our collection.

Finally, we define the update method, which receives the parameters data and id, both of which are required. While data contains a dict indicating which fields will be altered with what values, id defines which document in the collection we will be modifying.

Next, we will add the scout class. The purpose of this class is to act as an interface that allows us to pass the data of our scout collection more directly without thinking of unnecessary formatting when adding new documents. Let’s add the following code to models.py:

Note: We will go into more detail on how this class will be used later.

Finally, let’s add the scouts class that inherits from themodel class to reuse its methods and in turn has its own methods to interact with the scout collection. Let’s add the code at the end of the models.py file:

The scouts class inherits from the model. In its constructor we call the parent constructor and pass it scouts, which is nothing more than the key that the model constructor expects to reference a collection in Firebase.

Then we find the get_by_email method, which obtains the first document from the scouts collection that matches the email provided. This method will be used to obtain the Nightscout data of each user connected using a Google account.

The method getby_personal_phone receives a phone parameter (the user’s personal telephone) and will return the document associated with that data. This method calls the get_by method of themodel class and it will be very useful to obtain user data when we are running the nexmo events webhook.

Finally, we have the add method. IF data is an instance of thescout class, we will convert its attributes to dictionary with data.__dict__. The id attribute is optional for this method. Pay attention that this method, in turn, calls the add method of the model class for reuse.

Don’t forget to send the file!!⭐️

Playing with the Python Console

A very practical (and maybe fun?) way to test what we have done is with the Python console. Before the fun begins, open the Firebase console in your browser and click on the Database option. Make sure to select Cloud Firestore in the upper left corner next to Database.

Let’s go to our terminal. From our project folder, execute the python command. This will take us to the Python console where we can run Python code. In the python console, we execute the following commands:

  • Import our previously created python module:

  • Create an instance of the scouts class called scout_firebase and add a document to Firebase. Review the Firebase console after executing the add method. In Firestore, a new document will be added with the data provided. Pay special attention to the add method—we pass an instance of the scout class with all the corresponding data. Internally the add method converts the instance of the class to dictionary:

  • Get all the documents from our scouts collection:

  • Update the document we added (in this case we only update the nightscout_api field). We can check the update in the Firebase console. Later, we obtain the document using the get_by_email method and print item to confirm that the field value was in effect updated:

To close the Python console just type quit() to go back to the terminal.

Create the User Data Configuration Interface

With the defined data models, the next step is to create the interface that will receive the data of the connected user, with Google auth. Our application will be in charge of using the model to store this information.

In notifier.py, just under the last import add the following lines:

This adds the models module to the notifier.py script and imports the models, scouts, and scout classes from the module to be able to use them. Later, before the lines that define the get_session function, add the code that initializes the scouts class:

Next, edit the home function, which controls the endpoint /. The function should be modified with the following workflow in mind: A user authenticates with Google auth to our application; If they are authenticating for the first time when / is loaded, an empty form will be presented with a new flag to indicate to the application that the user will insert a new document to Firebase. If the user who is connecting already exists before loading /, a query will be made to Firebase to bring the data related to that email, and the information will be shown on the form with the edit flag to indicate to the application that by submitting the form you will be modifying the document of an existing user.

Currently our home function is defined as follows:

With the additional code, it should look like this:

Basically, we’ve added a conditional that assesses if the method used to access / is POST. If so, we can assume that the request has been made from a form.

In this case, we would be talking about the user configuration form. If the method used is POST, we ask if the flag (in this case cmd) isnew. If so, the add method will be executed by adding the user’s new document.

Note: We get email and username directly from the session, as this is data obtained from Google auth.

If the flag detected is edit, the new values ​​of the form are received and the update method of the scouts class is executed to update the document of the connected user.

Note: email and username are not modified as they are exclusive data from Google.

Regardless of the method used, in render_template we pass all the data of the connected user using the variable scout = nightscouts.get_by_email(get_session("user")["email"]), to fill the form with the configuration information in if the user exists. The form fields will be empty.

Now let’s edit the home.html file. This jinja template is loaded only if the user has previously logged in. Currently, we only have one line of code within the block content indicating the connected user and the link for logout. Just below this, we will add the code that will receive the application data for the user.

The block should look like this:

If the scout variable has a value of None, the flag cmd will have the value new—otherwise the value will be edit. The values of the text fields receive {{scout.phone}}, so when scout is None jinja will print empty. When scout exists the id is received in a hidden type field. This field should not be modified since it is the unique identifier of the document in Firebase. The add_contact() JavaScript function is undefined.

Let’s add some more code in home.html, just after{% endblock %}. In this case, we will use the script block that we define in layout.html to define the necessary JavaScript functions:

In the script block we define three functions and the event listener of onload page. The validate function evaluates whether the value of {{scout.extra_contacts|safe}} passed by jinja is empty. If that’s the case, then validate returns an empty Array(), otherwise jinja returns the extra_contacts array.

If, when loading the page, extra_contacts contains information, the function add_contact is executed for each position of the extra_contacts array, passing the value of the phone number to the value attribute of the input.

The function add_contact() dynamically adds a text field where the user can type an additional telephone number, up to the five allowed. Each input will have an icon to be clicked on to eliminate the record. This same function evaluates whether the number of allowed contacts has been reached. In that case, M.toast of materialize is used to display an alert to the user indicating that they cannot add more than five contact numbers. This function is triggered when loading / and when clicking on the button to add telephone numbers.

The delete_contact() function removes the record created by add_contact(). The function is triggered from the onclick event of the delete icon added by add_contact for each input.

With these last details, we have concluded configuring Google auth login and Firebase/Firestore for storage and reading data.

At this point, we should be able to log in with Google, add our Nightscout configuration from the form, save our data in Firestore, modify our configuration, and properly log out.

To Be Continued!

The next step will be to set up/configure the app in Nexmo and to create a scheduler in Python for the Nightscout alerts. Check back in next week to read Part Two of this tutorial.

Leave a Reply

Your email address will not be published.

Get the latest posts from Nexmo’s next-generation communications blog delivered to your inbox.

By signing up to our communications blog, you accept our privacy policy , which sets out how we use your data and the rights you have in respect of your data. You can opt out of receiving our updates by clicking the unsubscribe link in the email or by emailing us at [email protected].