In the first article of this short series, I explained the reason for wanting to know whether my garage door was open. I also described in detail how to set up an ESP32 to detect the door state (open/closed), and how to send it to a simple back end. In this post, I will tell you how the back end is set up.

I've already disclosed that the back end consists of a Raspberry Pi (running in my cluster of four). It runs a Redis server and uses a Python Flask application as web server. An nginx reverse proxy enables us to reach it from the internet (one of the requirements).

Let's have another look at the project directory structure, specifically the web/ subdirectory:

garage-door-checker/
├── esp32
│   └── main
│       └── main.ino
├── README.md
└── web
    ├── app
    │   ├── app.py
    │   ├── requirements.txt
    │   └── templates
    │       ├── index.html
    │       └── status.html
    ├── deployments
    │   └── app
    │       └── Dockerfile
    └── docker-compose.yml

In it, you'll find the directory app/, which contains the actual Python application. In deployments/, you can find a Dockerfile that we'll use to build the application. There's also docker-compose.yml, with which we can run the entire application dockerised.

Let's look at docker-compose.yml first:

version: '3'

services:
    app:
        container_name: gd-app
        restart: unless-stopped
        build:
            context: ./
            dockerfile: deployments/app/Dockerfile
        ports:
            - "5000:5000"
        depends_on:
            - redis
    
    redis:
        image: redis:4
        restart: unless-stopped
        ports:
            - "6379:6379"

In it, we create two services. At the bottom, there's the Redis service, which we use pretty much as it comes. Above that, we define our own service, which I've called app for lack of a better name. It uses the aforementioned Dockerfile to create the actual service. It opens port 5000, as Flask is configured to use this port. It also mentions that it depends on redis.

The Dockerfile looks like this:

FROM python:3.7

ADD app/ /app
WORKDIR /app
RUN apt-get update
RUN pip install -r requirements.txt
CMD python app.py

Again, not very complicated. The image is based on python:3.7. We add the local directory app/, which will be accessible in the container as /app. We update the packages and install any requirements for the Flask app using requirements.txt. Finally, we start the app.

The requirements look like this:

flask
redis

We only need to install Flask and Redis.

This pretty much only leaves the app itself. Let's have a look at app.py, which contains the main functionality:

import logging
from flask import Flask, render_template, request
from redis import Redis

app = Flask(__name__)
redis = Redis(host='redis', port=6379, charset="utf-8", decode_responses=True)

app.logger.setLevel(logging.INFO)


@app.route("/")
def home():
    return render_template('index.html')


@app.route("/status1234")
def status():
    door_status = redis.hgetall('pazzzas-garage-door')
    return render_template('status.html', status=door_status)


if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0')

At the top, as usual, are the import statements. Then, we create the Flask app and a Redis object. We set the log level to INFO, but you may use whatever you want. We define only two routes. One is for the home page, which will simply show a message "Nothing to see here." or something along those lines. You don't necessarily need this, but I prefer to have something there.

The template for the home page looks like this:

<html>
    <style>
        body {
            margin-left: auto;
            margin-right: auto;
            text-align: center;
            width: 37em;
        }
    </style>
    <head>
        <title>Home</title>
    </head>
    <body>
        <h2>Nothing to see here.</h2>
    </body>
</html>

The second route is for the actual status page. I've included a "pin" number in the URL, which makes it harder to guess. If you're paranoid (even more than me), you can use, for instance, a query parameter (?pin=1234) or even authentication. For this purpose, I believe a bit of obfuscating is enough, and you can make it as funky as you like. The function that handles this route, status(), gets the actual door status from the Redis server. It then passes it on to the render_template function. The template for the status page looks like this:

<html>
    <style>
        body {
            color: #f5f5f5;
            margin-left: auto;
            margin-right: auto;
            text-align: center;
            width: 37em;
{% if status and status.status == "open" %}
            background-color: #660000;
{% else %}
            background-color: #006600;
{% endif %}
        }
    </style>
    <head>
        <title>Status</title>
    </head>
    <body>
{% if status %}
        <h1>The garage door is {{ status.status }}.</h1>
        <h2>Last updated at {{ status.updated_at }}.</h2>
        <h3>Source: {{ status.source }}</h3>
{% else %}
        <h2>The garage door is closed.</h2>
{% endif %}
    </body>
</html>

It uses a bit of 'template programming' (is that a thing?) to use the status variable that was passed in. If the information was not found, meaning Redis returned 'nil' (or None in Python), it will simply say the door is closed. However, if the status information was successfully retrieved, it will show the actual door state and the time it was last updated. It will even change the background colour. When the door is open, the background is dark red (like a "red flag"), otherwise the background is green ("all good"). This way, even on a small mobile device, you can see the door state at a glance.

This is it! From within the web/ directory, you can start the application: docker-compose up [-d]. Use the flag -d if you want it to run in the background. The app is now listening on port 5000, which is the default port for Flask.

Now, this application runs behind an nginx reverse proxy. There are plenty of tutorials online how to set up a reverse proxy with nginx, but I would like to show the configuration file for this site as an example:

upstream door {
	server			10.0.0.100:5000;
}

server {
	listen			80;
	server_name		door.example.com;
	return			301 https://$host$request_uri;
}

server {
	listen			443 ssl;
	server_name		door.example.com;

	include 		common.conf;
	include			/etc/nginx/ssl.conf;

	location / {
		proxy_pass	http://door;
		include		common_location.conf;
	}
}

Here, we assume your Flask application runs on a device with IP address 10.0.0.100 (taylor to your situation, of course). Your listening URL will obviously differ, as *.example.com is not usable. This particular configuration listens on port 80 (HTTP) too, but will redirect to the safer port 443 (HTTPS). Because the Flask application runs in a private network, I've allowed it to use HTTP, hence the proxy_pass http://door; in the configuration file.

But that's it! You now have all the components to remotely check whether your garage door is open or closed.

Happy hacking!