Alternative Methods for Loading Dynamic Data in HTML


Description

In a previous project we looked at one way to pass dynamic data into an HTML file. That method works fine as long as there aren’t a lot of parameters to pass and the data doesn’t need to be manipulated or processed in some way before being displayed or used in some other way.

In this project we’re going to look at two alternatives. The first is simply a way to handle many parameters being passed in a cleaner way, and the second looks at using Javascript and Ajax to load the data after the browser has loaded the HTML and supporting files.

This project builds on the previous project in which we added self-monitoring to our pi.

Parameters

This is some information about this project and the conditions under which it was done. If you try to replicate it in the future and it doesn’t work, you can evaluate these parameters to see if any changes between these and your configuration might have impacted your results. An example might be changes to future versions of Python or Flask.

  • Date: July 23, 2021
  • Skill: Beginner
  • Raspberry Pi Model(s): Zero-W, 3B+, 4B
  • OS: Raspberry Pi OS version 10 (Buster)
  • Python Version: 3.7.3
  • Flask Version: 1.0.2

Steps

  1. Lets start by opening the latest version of sample.py and make the following changes:
#!/usr/bin/env python3

from flask import Flask, render_template
from datetime import datetime
import io
import os
import subprocess

START_DATE = datetime.now().strftime('%m/%d/%y %I:%M:%S %p')
SERVER_NAME = 'Raspberry Pi Server'
PI_MODEL = 'Unknown'

proc = subprocess.Popen(["cat", "/sys/firmware/devicetree/base/model"], stdout=subprocess.PIPE)
(out, err) = proc.communicate()
if not err:
     PI_MODEL = out.decode('utf-8')
else:
    print("Error getting Pi Model: %s" % err)


app = Flask(__name__)

@app.route('/')
def index():
    serverData = {'serverName': SERVER_NAME, 'piModel': PI_MODEL}
    return render_template('index.html', serverData=serverData)

@app.route('/healthcheck')
def healthcheck():
    curDateTime = datetime.now().strftime('%m/%d/%y %-I:%M:%S %p')

    #Get ip.txt contents
    try:
        f = open('/home/pi/ip.txt', 'r')
        fdata = f.readlines()
        fcnt = 'Fail Cnt: %s' % (fdata[0].strip())
        rcnt = 'Reboot Cnt: %s' % (fdata[1].strip())
        f.close()
    except:
        fcnt = 'Fail Cnt: No Data'
        rcnt = 'Reboot Cnt: No Data'

    #Get reboot.txt contents
    try:
        f = open('/home/pi/reboot.txt', 'r')
        fdata = f.readlines()
        rhist = ''
        for x in range(len(fdata)):
            rhist += fdata[x]
        f.close()
    except:
        rhist = 'None'

    # Report available disk space
    stat = os.statvfs('/home/pi')
    gbFree = '%0.2f GB' % (stat.f_bfree*stat.f_bsize/1024/1024/1024)

    # Report CPU Temp
    try:
        tFile = open('/sys/class/thermal/thermal_zone0/temp')
        temp = float(tFile.read())
        tempC = '%0.1f C' % (temp/1000)
        tempF = '%0.1f F' % ((temp/1000) * 1.8 + 32)
    except:
        tempC = 'ERR'
        tempF = 'ERR'

    tFile.close()

    return render_template('health.html', curDate=curDateTime, startDate=START_DATE, gbFree=gbFree, tempC=tempC, tempF=tempF, fcnt=fcnt, rcnt=rcnt, rhist=rhist)

@app.route('/reboot')
def reboot():
    ret = os.system('sudo reboot')
    return 'Rebooting'

@app.route('/shutdown')
def shutdown():
    ret = os.system('sudo shutdown -h now')
    return 'Shutting down'

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000)

Save the file and restart the server if necessary.

Notice that we are giving our server a name now (SERVER_NAME = ‘Raspberry Pi Server’) and that we are making a system call to get the model of our Pi (proc = subprocess.Popen([“cat”, “/sys/firmware/devicetree/base/model”], stdout=subprocess.PIPE)). Then we are placing that information into a dictionary-type variable (serverData) and passing that as a parameter in render_template(), similarly to what we’ve seen before, but this time we’re only passing one parameter with all of the data elements we need in our HTML.

Flask uses a built-in templating engine known as Jinja2 to process these render_template() parameters and replaces the placeholders in the HTML, where we used the {{variable}} code, with the actual literal values before passing the HTML to the browser. That’s how we are able to place Python code inside the curly brackets ({{serverData[‘serverName’]}}) in our HTML file. If you inspect the HTML in the browser, you’ll see it has been replaced with the respective string value in our dictionary variable.

I recommend you read more about Jinja2 here to get a better understanding of the flexibility it can provide. It even gives you access to if/else logic and for-loops, among other powerful features, to help you build your HTML to meet your needs.

  1. Next open index.html and make the following changes and save them:
<!DOCTYPE html>
<html>

<head>
  <style>
    body {
      background-color: lightblue;
      color: blue;
    }
  </style>
</head>

<body>
  <h1>{{serverData['serverName']}}</h1>
  <h2> [{{serverData['piModel']}}]</h2>
  <p><a href='/healthcheck'>Health Check</a></p>
  <p><a href='/reboot'>Reboot</a></p>
  <p><a href='/shutdown'>Shutdown</a></p>
</body>
</html>

Again, notice the Python variable references. These are being processed and replaced with their literal values by Jinja2.

Now load the home/index route in your browser. It should look something like this:

So now you’ve seen the first alternative to passing many parameters in the render_template() call. We simply place all of the values we want to pass inside of a dictionary-type variable so we are only passing one parameter, and then accessing the elements from there as needed.

  1. Now we’ll look at using Ajax to load the data as a json object in Javascript and then placing the display items into their respective HTML elements. This code is running after the HTML is loaded in the browser versus the previous example in which it ran before.

Open sample.py again and make the following changes:

#!/usr/bin/env python3

from flask import Flask, render_template
from datetime import datetime
import io
import os
import subprocess
import json

START_DATE = datetime.now().strftime('%m/%d/%y %I:%M:%S %p')
SERVER_NAME = 'Raspberry Pi Server'
PI_MODEL = 'Unknown'

proc = subprocess.Popen(["cat", "/sys/firmware/devicetree/base/model"], stdout=subprocess.PIPE)
(out, err) = proc.communicate()
if not err:
     PI_MODEL = out.decode('utf-8')
else:
    print("Error get Pi Model: %s" % err)


app = Flask(__name__)

@app.route('/')
def index():
    serverData = {'serverName': SERVER_NAME, 'piModel': PI_MODEL}
    return render_template('index.html', serverData=serverData)

@app.route('/healthcheck')
def healthcheck():
    return render_template('health.html')

@app.route('/health-data')
def healthData():
    curDateTime = datetime.now().strftime('%m/%d/%y %-I:%M:%S %p')

    resp = {'serverName': SERVER_NAME, 'piModel': PI_MODEL, 'startDate': START_DATE, 'curDate': curDateTime}

    #Get ip.txt contents
    try:
        f = open('/home/pi/ip.txt', 'r')
        fdata = f.readlines()
        fcnt = 'Fail Cnt: %s' % (fdata[0].strip())
        rcnt = 'Reboot Cnt: %s' % (fdata[1].strip())
        f.close()
    except:
        fcnt = 'Fail Cnt: No Data'
        rcnt = 'Reboot Cnt: No Data'
    resp['fcnt'] = fcnt
    resp['rcnt'] = rcnt

    #Get reboot.txt contents
    try:
        f = open('/home/pi/reboot.txt', 'r')
        fdata = f.readlines()
        rhist = ''
        for x in range(len(fdata)):
            rhist += fdata[x]
        f.close()
    except:
        rhist = 'None'
    resp['rhist'] = rhist

    # Report available disk space
    stat = os.statvfs('/home/pi')
    gbFree = '%0.2f GB' % (stat.f_bfree*stat.f_bsize/1024/1024/1024)
    resp['gbFree'] = gbFree

    # Report CPU Temp
    try:
        tFile = open('/sys/class/thermal/thermal_zone0/temp')
        temp = float(tFile.read())
        tempC = '%0.1f C' % (temp/1000)
        tempF = '%0.1f F' % ((temp/1000) * 1.8 + 32)
    except:
        tempC = 'ERR'
        tempF = 'ERR'
    resp['tempC'] = tempC
    resp['tempF'] = tempF

    tFile.close()

    return json.dumps(resp)

@app.route('/reboot')
def reboot():
    ret = os.system('sudo reboot')
    return 'Rebooting'

@app.route('/shutdown')
def shutdown():
    ret = os.system('sudo shutdown -h now')
    return 'Shutting down'

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000)

Notice that we first imported the json library so we could work with json structures. Then we removed all of the code within the healthcheck route and now we are simply returning the health.html file to the browser. And finally we created a new health-data route which gathers all of the same information as before, plus the server name and pi model, places it into a json structure, and then returns that to the browser. This is the route that will be called from the Javascript we will be adding to health.html in the next step.

  1. Next we’ll update health.html. Open the file and make the following changes:
<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='utf-8'>
  <meta name='viewport' content='width=device-width, initial-scale=1'>
  <title>Healthcheck</title>

  <style>
    body {
      font-size: 28px;
      background-color: lightblue;
    }
    table, th, td {
      border: 1px solid black;
      border-collapse: collapse;
      padding: 15px;
    }
    th {
      text-align: right;
      background-color: skyblue;
    }
    td {
      background-color: white;
    }
  </style>

</head>

<body>
  <h1 id="serverName">Server Name</h1>
  <h2>Health Check</h2>
  
  <table>
    <tr>
      <th>Raspberry Pi Model:</th>
      <td id="piModel">Pi Model</td>
    </tr>
    <tr>
      <th>Health Status as of:</th>
      <td id="curDate">Current Date</td>
    </tr>
    <tr>
      <th>Running Since:</th>
      <td id="startDate">Start Date</td>
    </tr>
    <tr>
      <th>Available Disk Space:</th>
      <td id="gbFree">Free Space</td>
    </tr>
    <tr>
      <th>CPU Temperature:</th>
      <td id="temps">Temperatures</td>
    </tr>
  </table>
  <p id="fcnt">Fail Count</p>
  <p id="rcnt">Reboot Count</p>
  <p>Reboot History:<pre id="rhist">Reboot History</pre></p>
  <p><a href='/'>Index</a></p>

  <script>
      function updateDisplay(resp) {
          document.querySelector("#serverName").textContent = resp.serverName;
          document.querySelector("#piModel").textContent = resp.piModel;
          document.querySelector("#curDate").textContent = resp.curDate;
          document.querySelector("#startDate").textContent = resp.startDate;
          document.querySelector("#gbFree").textContent = resp.gbFree;
          document.querySelector("#temps").textContent = resp.tempC + " (" + resp.tempF + ")";
          document.querySelector("#fcnt").textContent = resp.fcnt;
          document.querySelector("#rcnt").textContent = resp.rcnt;
          document.querySelector("#rhist").textContent = resp.rhist;
      }

      function loadHealthData() {
			var furl = "/health-data"

			var xhttp=new XMLHttpRequest();
			xhttp.onreadystatechange = function() {
				if (this.readyState == 4 && this.status == 200) {
					console.log(this.response);
          var resp = JSON.parse(this.response);
          updateDisplay(resp);
				}
			};
			xhttp.open("GET", furl);
			xhttp.send();
		}
    
    loadHealthData();
  
  </script>
</body>

</html>

Notice first that we added our server name and Pi model to the display. You’ll also notice that we replaced all of our Jinja2 placeholders ({{variable}}) with simple placeholder text and that we gave each of those HTML elements ID’s so we could access them from our Javascript. I like to enter placeholder text in most cases so that it becomes obvious if an element wasn’t updated properly.

Finally, you can see that Javascript code that was added to the bottom of the body within the <script></script> tags. Generally you would put your CSS and Javascript into separate files and then import them into your HTML file, but this way makes it easier to demonstrate what is happening.

Here we simply define two functions, updateDisplay() and loadHealthData(), and then we call loadHealthData() at the end.

updateDisplay() is simply placing the strings from the json object into their respective elements.

loadHealthData() is making an Ajax call back to our server to get the json data we need to build our display. You can see it’s a fairly simple function and that it doesn’t run until after the HTML is loaded. You could even create a timer function if you wanted and have this call made at specific intervals without ever having to manually reload the page.

Load the healthcheck route in your browser now. It should looking something like this:

This method of loading and displaying data is especially useful if you need to run it more than once, such as on a timer, or if you needed to manipulate or process the data in some way before displaying. For example, if our HTML was displaying weather data from a third-party web service, the only way to get it from the json or xml format that is returned by the API into our display would be through a Javascript function.

Summary

Hopefully this project gave you a little more insight into the power of Jinja2 as well as an intro into how to make Ajax calls in Javascript. I have used both of these methods and as you get more experience, you will begin to recognize when one might be a better solution for your needs than the other.

Learn More

Raspberry Pi / Raspberry Pi OS

Python / HTML / CSS

Flask

Jinja2

Leave a Comment

Your email address will not be published. Required fields are marked *