Pi Alarm title, a time input with apply button next to it, a text input with stop button next to it, cobweb in the background

Finish Line, Web Interface of My Sunrise Alarm

Published on:

The script that joins all modules. Schedules the alarm, provides the web interface.

The source code of the script can be found at GitHub - sunrise-pialarm/main.py. This is part of a bigger project, which is fully open source. Check out GitHub - 11Firefox11/sunrise-pialarm.

Finish Line

This will be what connects all previously made modules. The main runner.

Schedule Based System

To save up compute power a scheduling system is required.

The flow is the following. Web server turns on, so the alarm can be set. The server turns off and schedules the alarm and the web server that can turn off the alarm. Web server turns on before a specified time of the alarm, so the sunrise can be turned off. The alarm starts. If alarm is stopped the next web server turn on must be scheduled, so the alarm can be set next time too.

For scheduling threading and apscheduler will be used. A helper environment variable named PIALARM_RUNNING will be used too.

There are python related settings which are stored in environment_variables.py, these will be used throughout the script.

There are few time related settings that are needed for this scheduling to work correctly.

Settings

SettingsManager

To handle these settings I made a SettingsManager class. I don’t want to go much into the details, but here are the key things to know about it:

Initialize Modules

if __name__ == "__main__" will be used, so the code will only run if it is run as main script. AlarmRunner, SettingsManager will be the two components.

from os import path
from environment_variables import PASSCODES_JSON_PATH, ALARM_JSON_PATH, SETTINGS_JSON_PATH, WEB_SERVER_IP, WEB_SERVER_PORT
from settings_manager import SettingsManager
from alarm_runner import AlarmRunner
# ...
if __name__ == "__main__":
    this_dir = path.dirname(path.realpath(__file__))
    generate_full_path = lambda p: path.join(this_dir, p)
    alarm = AlarmRunner(generate_full_path(ALARM_JSON_PATH))

Create Scheduler

As detailed above, lots of schedulers will be created. Pass in a function, parameters (that will be passed to function), a time when to run it. Automatically it should extend the time with the date because add_job requires a full datetime object.

from datetime import datetime, timedelta
from apscheduler.schedulers.background import BackgroundScheduler
# ...
def create_scheduler(func: callable, timeList: list, args: list = []):
    global settings
    sch = BackgroundScheduler()
    sch.configure()
    now = datetime.now()
    if len(timeList) == 2: # add seconds to list if needed
        timeList.append(0)
    to_run_at = datetime(now.year, now.month, now.day, *timeList)
    if settings.time_list_to_secs([now.hour, now.minute, now.second]) > settings.time_list_to_secs([timeList[0], timeList[1], timeList[2]]): # if the time passed today, add one day
        to_run_at = to_run_at + timedelta(days=1)
    sch.add_job(func, "date", args=args, run_date=to_run_at) # add the schedule
    sch.start()
    return sch
# ...

Web Interface

The web server will be implemented with Flask. 2 routes are required: one that serves the HTML file, one that handles the action of the user.

There are two very basic HTML files running.html and waiting.html. Both have a form and both submit data to / with a POST method.

At the GET route if the PIALARM_RUNNING is "True" then running.html will be served else waiting.html. At waiting the current alarm time is passed into the HTML.

At the POST route either alarm_at or code should be in the JSON that has been posted. If neither is in it, False status will be sent back. If the alarm is running and the given code is good then the alarm will be stopped. If new alarm_at provided, it will be saved.

This will be in a function because the web server will be turned on by a schedule.

from os import path, environ
from flask import render_template, request, jsonify
# ...
def start_server():
    app = Flask(__name__)

    @app.route("/", methods=["GET"])
    def main_get():
        global alarm
        return render_template(("waiting" if not environ["PIALARM_RUNNING"] == "True" else "running") + ".html", alarm_at=settings.time_list_to_str(settings.alarm_at))

    @app.route("/", methods=["POST"])
    def main_post():
        global settings, alarm
        data = dict(request.json)
        if not data or (environ["PIALARM_RUNNING"] == "False" and "alarm_at" not in data) or (environ["PIALARM_RUNNING"] == "True" and "code" not in data):
            return jsonify({"status": False})
        if environ["PIALARM_RUNNING"] == "True":
            if not settings.stop_alarm(data["code"]):
                return jsonify({"status": False})
            environ["PIALARM_RUNNING"] = "False"
        else:
            settings.set_alarm(data["alarm_at"])
        return jsonify({"status": True})
# ...

2 more things are needed to complete the flask server: to actually start it, to stop it.

Threading

A thread is needed for the web server. The recommended solution is an extended Thread class. Overwrite run and shutdown and that is it.

from threading import Thread
from werkzeug.serving import make_server
# ...
class ServerThread(Thread):

    def __init__(self, app):
        Thread.__init__(self)
        self.server = make_server(WEB_SERVER_IP, WEB_SERVER_PORT, app) # create the actual WSGI server
        self.ctx = app.app_context()
        self.ctx.push()

    def run(self):
        self.server.serve_forever()

    def shutdown(self):
        self.server.shutdown()
# ...
def start_server():
    global server
    # ...
    server = ServerThread(app)
    server.start()

def stop_server():
    global server
    server.shutdown()

More Schedule

Actually develop the schedule cycle. Everything will be built around the flask server.

class flaskManager:

    def __init__(self) -> None:
        self.alarm_schedule = None
        environ["PIALARM_RUNNING"] = "False"

    def start(self, night=False):
        global manager, settings, app
        if night:
            create_scheduler(manager.stop, settings.turn_off_at[0], [True]) # schedule the shutdown of web server
        else:
            environ["PIALARM_RUNNING"] = "True" # not night means the alarm is running
        start_server()

    def stop(self, night=False):
        global settings, alarm
        stop_server()
        if night: # if it stops at night that means that it has to prepare for next morning
            settings.load_config() # reload config
            self.alarm_schedule = create_scheduler(alarm.start, settings.realalarm_at) # start the alarm
            create_scheduler(self.start, settings.turn_on_at[1]) # start the server
        else:
            create_scheduler(self.start, settings.turn_on_at[0], [True]) # prepare for night
# ...

def start_server():
    # ...
    @app.route("/", methods=["POST"])
    def main_post():
        global settings, manager, alarm
        # ...
        if environ["PIALARM_RUNNING"] == "True":
            if not settings.stop_alarm(data["code"]):
                return jsonify({"status": False})
            environ["PIALARM_RUNNING"] = "False"
            manager.alarm_schedule.shutdown(wait=False) # stop actually the schedule
            to_stop_at = datetime.now() + timedelta(seconds=1)
            create_scheduler(manager.stop, [to_stop_at.hour, to_stop_at.minute, to_stop_at.second]) # stop the flask (one second after because we still need to respond to request)
    # ...

if __name__ == "__main__":
    # ...
    manager = flaskManager()

Last step is to handle the schedules when the script starts. Plus we need to run an infinite sleep, so the script keeps running.

if __name__ == "__main__":
    # ...
    now = datetime.now()
    now = settings.time_list_to_secs([now.hour, now.minute])
    if ((settings.time_list_to_secs(settings.turn_on_at[0]) < now and now < settings.time_list_to_secs(settings.turn_off_at[0]))): # check if the web server should run now
        manager.start(True)
    else:
        create_scheduler(manager.start, settings.turn_on_at[0], [True]) # else schedule the next web server start
    while True:
        sleep(5)

The project is fully done! Thank you for reading.

Read more

sunrise with a loop icon on top of it

My Own Schema to Create Colorful Sunrise

a paper calendar, 31 days are on it

My 31 Days Passcode Based System for My Sunrise Alarm