Previously I've shown how I've collected the data, set the Philips Hue bulbs, and written the weather readings to a SQLite database. Here in part 10 I start work on the web site that will show the readings.
The web server
Part of the rationale for this project is to leverage my burgeoning Python knowledge in as many places as possible. So it seemed sensible to me to try out one of the Python web frameworks for my website. I’ve tried Django before, and it seemed quite complicated for my needs, so I went with the simpler Flask to see if it could do what I wanted. And indeed it could.Setting up the web server with Flask consists of creating some simple initiator scripts. The file structure from earlier lists all the files we need to make. We’ll focus on the scripts here, and get to the templates and static resources in the next section.
./Project/pybloom
│
├── app.py
├── app/
│ ├── __init__.py
│ ├── content.py
│ ├── routes.py
│ ├── templates/
│ │ ├── base.html
│ │ ├── index.html
│ │ └── colours.html
│ └── static/
│ ├── favicon.ico
│ ├── brand.svg
│ ├── logo.svg
│ ├── bar_graph.svg
│ └── pie_chart.svg
The technologies
Flask
Advanced Python Scheduler
HTTP
The code
The app entityFlask looks for an app.py script in the root of the project directory to tell it where to get the rest of the files. (You can use a different filename, but then you need to declare that in a different .flaskenv variable - why bother.)
from app import app
That’s all you need, at least in our simple project.
A rudimentary CMS
We’ve generated our weather observation data and visualisations in the Python script, but we need a way of declaring them to the web pages. Rather than sprinkling references to filenames and file paths throughout the HTML, I decided to collect them in a content management script.
from db_utils import get_rows
We’re going to need our helpful database utility, so let’s import it first.
def graphs():
all_graphs = {
'lastday': 'last_day_bar.svg',
'lastweek': 'last_week_pie.svg',
'lastmonth': 'last_month_bar.svg'
}
return all_graphs
This function collects all the graphs in one dictionary.
def colours_table():
rows = get_rows('colours')
return rows
This second function fetches our temperature-colour lookup table.
We’re using functions to define the content instead of variables because I like to keep variables associated with a single namespace - to me it’s just cleaner and less prone to error.
Setting up the page routes in python
Flask takes care of responding to all the HTTP requests with the correct web pages (web serving) and calls them routes.
from flask import render_template
from app import app
from app import content
First let’s import all the modules that we’re going to need in this routing script.
CONTENT = content.graphs()
COLOURS_TABLE = content.colours_table()
Second, we have a couple of global variables that tell our pages where to get their data, defined in our CMS.
@app.route('/')
@app.route('/index')
def index():
return render_template('index.html', title='Home', content=CONTENT)
Flask uses function decorators to associate HTTP requests with HTML files. These statements are saying that if the web server receives a request for <IP address of server:5000>/ or <IP address of server:5000>/index it should render the index.html file.
Additionally, the render_template() method passes a couple of parameters into the file in order to complete its rendering, the title of the page and the graph content.
@app.route('/colours')
def colours():
return render_template('colours.html', title='Colours', rows=COLOURS_TABLE)
<IP address of server:5000>/colours takes us to the colours.html page. Again the render_template() method passes a couple of parameters as defined in the CMS.
@app.after_request
def after_request_func(response):
response.headers['Cache-Control'] = 'no-store'
return response
While testing, I noticed that the graphs were old, and that refreshing the page just re-showed the old graphs. I figured that the browser was serving a cached page, but as the data refreshes every 10 minutes, the page was old. Furthermore, clicking on the refresh button in modern browsers just re-fetches the same page from the browser cache. In order to force the browser to re-send its HTTP request, I need to make sure the HTTP response indicates that the browser should not store the page.
Let’s unpick this final decorator:
- @app.after_request : tells Flask that this function is to be called after every web page request is responded to
- after_request_func(response) : works on Flask’s internal response object, which normally you wouldn’t interact with directly
- response.headers['Cache-Control'] = 'no-store' : this adds a new header to the HTTP response which tells the browser not to cache the page
Initialising and running the code
We now have two processes that need to run simultaneously: the web server, and fetching the observations. There’s a library that makes the scheduling straightforward:
from apscheduler.schedulers.background import BackgroundScheduler
The Advanced Python Scheduler has a background scheduling function that is perfect for my needs.
schedule = BackgroundScheduler(daemon=True)
schedule.add_job(lambda: weather(), 'interval', minutes=10)
schedule.start()
We instantiate a schedule object as a daemon, which means it closes once it finishes (contrast with a thread that just keeps going). The job itself is a lambda function that calls the weather function every 10 minutes. (We should remember to import this function with from pybloom import weather at the top of the file.)
from flask import Flask
app = Flask(__name__)
from app import routes
The remaining initialisation command tells Flask what the app is, and where it can find the routing.
In dev, I’m using the same machine to run the server as to browse it, so the correct command is flask run. By default, this runs a server in the localhost, so isn’t accessible outside of the dev machine. However, in prod, I want the RPi to serve the site to other browsers to view it, so the correct command is:
flask run --host=0.0.0.0
To access the site, point the browser to http://<IP address of RPi>:5000/ (specify the port, otherwise the RPi will reject the connection to the browser).
Putting it together
app.py
from app import app
_init_py
from flask import Flask
app = Flask(__name__)
from app import routes
from apscheduler.schedulers.background import BackgroundScheduler
from pybloom import weather
schedule = BackgroundScheduler(daemon=True)
schedule.add_job(lambda: weather(), 'interval', minutes=10)
schedule.start()
routes.py
from flask import render_template
from app import app
from app import content
CONTENT = content.graphs()
COLOURS_TABLE = content.colours_table()
@app.route('/')
@app.route('/index')
def index():
return render_template('index.html', title='Home', content=CONTENT)
@app.route('/colours')
def colours():
return render_template('colours.html', title='Colours', rows=COLOURS_TABLE)
@app.after_request
def after_request_func(response):
response.headers['Cache-Control'] = 'no-store'
return response
content.py
from db_utils import get_rows
def graphs():
all_graphs = {
'lastday': 'last_day_bar.svg',
'lastweek': 'last_week_pie.svg',
'lastmonth': 'last_month_bar.svg'
}
return all_graphs
def colours_table():
rows = get_rows('colours')
return rows
No comments:
Post a Comment
It's always great to hear what you think. Please leave a comment, and start a conversation!