sunrise with a loop icon on top of it

My Own Schema to Create Colorful Sunrise

Published on:

Control the colors of the LED based on a specific data structure. Define a sunrise easily.

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

Old Alarm Runner

To control my LED and simulate a sunrise I wrote a python script. The only way I could imagine my code that controls the light was pure code. Take a look at it:

# ...
class Alarm:
    def __init__(self) -> None:
        self.waitTime = 3.62
    # ...
    def run(self):
        cont = LedController()
        for i in range(3, 55, 1):
            cont.blue = i
            if not self.customSleep(self.waitTime):
                return
        # ...
        for val in [[45, 12, 8], [50, 50*.75, 0]]:
            for rgb in cont.transition(*val):
                cont.red = rgb[0]
                cont.green = rgb[1]
                cont.blue = rgb[2]
                if not self.customSleep(self.waitTime):
                    return
        for val in [100, 150]:
            for c in range(val-50, val+1):
                cont.red = val
                cont.green = val * .75
                if not self.customSleep(self.waitTime):
                    return
        while True:
            c += 1
            for part in [[0, 256, 5], [255, 0, -5]]:
                for i in range(*part):
                    cont.red = cont.green = cont.blue = i
                    if not self.customSleep((.05 if i < 150 else .02) if c < 9 else (.01 if i < 150 else .005)):
                        return
                if not self.customSleep(1 if c < 9 else .35):
                    return

It is flooded with for loops. It eases from one color to another. Change color then wait a little and repeat this till it reaches the final color. If I wanted to change something on it, I had to edit the code. This is not a good practice. I reworked it.

Schema

To solve it, I thought about a schema. A schema which would do the same things in the background, but it is easy to modify it. It works based on steps and inside that there can be a range or a transition. Read the documentation of it in the README.md of sunrise-pialarm.

The Goal

I have to parse the JSON and run the actions in it. If the alarm is stopped, it should stop and reset fully. What are the goals?

Implementation

Load Config

Load in the JSON, get main properties into separate variables. LedController is inited too.

from json import load
from led_controller import LedController

class AlarmRunner:
    def __init__(self, config_path="alarm.json"):
        self.config_path = config_path
        self.parsed_config = load(open(self.config_path, "r", encoding="UTF-8"))
        self.led_controller = LedController()
        self.load_config()

    def load_config(self):
        self.wait_time = self.parsed_config["wait_time"]
        self.when_yellow = self.parsed_config["when_yellow"]
        self.repeat_last = self.parsed_config.get("repeat_last") # use get because it is optional
        self.steps = self.parsed_config["steps"]

Modify Calculations

It can contain any arithmetic calculation, and it can contain some variables. To execute the calculation we could use eval, but that is dangerous. I browsed and found numexpr.evaulate which executes only these expressions, plus variables can be passed to it.

The function arguments are modify: what and how should be modified, and replace_vars: what variables can be in the calculations. In modify there are colors for example red and after running the calculation we have to set the led_controller’s red to the outcome, for this setattr is used where a variable can be accessed by name in string format.

from numexpr import evaluate as ne_eval
# ...
class AlarmRunner:
    # ...

    def run_modify(self, modify, replace_vars):
        for color, calc in modify.items():
            calculated_value = ne_eval(calc, local_dict=replace_vars)
            setattr(self.led_controller, color, calculated_value)

    # ...

Add a Custom Sleep

Because the alarm can be stopped anytime a custom sleep is required. What if it is time to sleep 10 seconds, and it can’t detect that it has been stopped? A PIALARM_RUNNING constant will be checked from the environment variables. The function will return a boolean if the alarm is up or not. Bigger sleeps will be broken into smaller periods.

CLEVER_SLEEP_SECS_SEGMENTS = 1
from time import sleep as time_sleep
from os import environ
# ...
class AlarmRunner:
    # ...

    def clever_sleep(self, val: float):
        PIALARM_RUNNING = environ.get("PIALARM_RUNNING", "True") # get environment variable
        if PIALARM_RUNNING == "False":
            self.led_controller.reset() # if not running reset the light, go fully dark
            return False
        else:
            try:
                if val > CLEVER_SLEEP_SECS_SEGMENTS: # break sleeps into smaller segments
                    time_sleep(CLEVER_SLEEP_SECS_SEGMENTS)
                    return self.clever_sleep(val-CLEVER_SLEEP_SECS_SEGMENTS) # recall this function
                else: time_sleep(val)
                return True
            except KeyboardInterrupt:
                self.led_controller.reset()
                return False

Run a Step

Run a simple sleep statement, a range or a transition.

Let’s start with the simple statement.

# ...
class AlarmRunner:
    # ...
    def run_step(self, replace_vars, step): # replace_vars required for modify calculations
        range_data = step.get("range")
        transition_data = step.get("transition")
        sleep_time = self.wait_time if step["sleep"] == "wait_time" else step["sleep"]
        if range_data:
            pass
        elif transition_data:
            pass
        else:
            self.run_modify(step.get("modify", {}), replace_vars) # run the modify calculations
            if not self.clever_sleep(step["sleep"]): return False # if sleep returns False, return so the step stops
    # ...

If range is define, we have to put this into a for loop. Also replace_vars should be extended with current i.

# ...
class AlarmRunner:
    # ...
    def run_step(self, replace_vars, step):
        # ...
        if range_data:
            for i in range(range_data.get("start", 0), range_data.get("stop", 10), range_data.get("step", 1)):
                self.run_modify(step.get("modify", {}), {**replace_vars, "i":i})
                if not self.clever_sleep(sleep_time): return False
        # ...

Transition is almost the same, but we will loop through a list.

# ...
class AlarmRunner:
    # ...
    def run_step(self, replace_vars, step):
        # ...
        elif transition_data:
            for rgb in self.led_controller.transition(transition_data.get("red"), transition_data.get("green"), transition_data.get("blue"), transition_data.get("steps", 255)):
                self.run_modify(step.get("modify", {}), {**replace_vars, "r":rgb[0], "g": rgb[1], "b": rgb[2]}) # call modify, with current rgb values in vars
                if not self.clever_sleep(sleep_time): return False
        # ...

Start

Initialize replace_vars with the wait_time and color values. Because the color values change we need to define them as lambdas, and we will call it to get the current value. Loop through steps and call self_run on them. Take care of repeat last too.

# ...
class AlarmRunner:
    # ...
    def start(self):
        replace_vars = {"wait_time": self.wait_time, "red": lambda: self.led_controller.red, "green": lambda: self.led_controller.green, "blue": lambda: self.led_controller.blue} # init replace_vars
        for step in self.steps: 
            if self.run_step(replace_vars, step) == False: return # if step stopped, stop the alarm
        if self.repeat_last:
            while True:
                for step in self.steps[-self.repeat_last:]: 
                    if self.run_step(replace_vars, step) == False: return # repeat last steps
    
    def run_modify(self, modify, replace_vars):
        for color, calc in modify.items():
            calculated_value = ne_eval(calc, local_dict={var_name:var_value if not callable(var_value) else var_value() for var_name, var_value in replace_vars.items()}) # call lambdas in replace_vars
            setattr(self.led_controller, color, calculated_value)

It is done. For a sample JSON, use GitHub - sunrise-pialarm/alarm.json.

Read more

a paper calendar, 31 days are on it

My 31 Days Passcode Based System for My Sunrise Alarm

a hand holding a remote control with a led strip in the background

Python Class to Control an RGB LED Strip on a Raspberry Pi