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?
- Load config.
- Create a function that runs the
modify
calculations. - Add a custom sleep.
- Run a step. Sleep, range, transition.
- Start the steps.
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 lambda
s, 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.
Previous Part
My 31 Days Passcode Based System for My Sunrise Alarm
Next Part