In my last posting, I talked about creating an External Rule System for the Things Gateway from Mozilla. This is a key component of the Automation part of a Smart Home system. Of course, the Things Gateway already has a rule system of its own. However, because it is GUI based, it has a complexity ceiling that is rather low by the standards of programmers.
My External Rule System provides an alternative for more sophisticated rules that leverage the full power and readability of the Python programming language. However, I must ensure the capabilities are a proper superset of the built in Thing Gateway capabilities. The built in GUI Rule System has a special object called the "Clock" that can trigger a rule every day at a specific time. This is for the classic "turn the porch light on in the evening" home automation idea. My External Rule System needs the same capabilities, but as you'll see, it is easy to extend beyond basic time of day idea.
We'll start with the simplest example.
class MorningWakeRule(Rule):
def register_triggers(self):
morning_wake_trigger = AbsoluteTimeTrigger("morning_wake_trigger", "06:30:00")
return (morning_wake_trigger,)
def action(self, *args):
self.Bedside_Ikea_Light.on = True
(see this code in situ in the morning_wake_rule.py file in the pywot rule system demo directory)Having only two parts, a trigger and an action, this rule is about as terse as a rule can be. In the
register_triggers method, I defined an
AbsoluteTimeTrigger that will fire every day at 6:30am. That means that everyday at my wake up alarm time, the
action method will run. The body of that method is to set the "
on" property of my bedside Ikea light to
True. That turns it on.
There are a number of triggers in the
pywot.rule_triggers module. It is useful to understand how they work. The code that runs the
AbsoluteTimeTrigger consists of two parts: the constructor and the
trigger_detection_loop. The constructor takes the time for the alarm in the form of a string. The
trigger_detection_loop method is run when the enclosing
RuleSystem is started.
class AbsoluteTimeTrigger(TimeBasedTrigger):
def __init__(
self,
name,
# time_of_day_str should be in the 24Hr form "HH:MM:SS"
time_of_day_str,
):
super(AbsoluteTimeTrigger, self).__init__(name)
self.trigger_time = datetime.strptime(time_of_day_str, '%H:%M:%S').time()
async def trigger_detection_loop(self):
logging.debug('Starting timer %s', self.trigger_time)
while True:
time_until_trigger_in_seconds = self.time_difference_in_seconds(
self.trigger_time,
datetime.now().time()
)
logging.debug('timer triggers in %sS', time_until_trigger_in_seconds)
await asyncio.sleep(time_until_trigger_in_seconds)
self._apply_rules('activated', True)
await asyncio.sleep(1)
(see this code in situ in the rule_triggers.py file in the pywot directory)The
trigger_detection_loop is an infinite loop than can only be stopped by killing the program. Within the loop, it calculates the number of seconds until the alarm is to go off. It then sleeps the requisite number of seconds. A trigger object like this can participate in more than one rule, so it keeps an internal list of all the rules that included it using the
Rule.register_triggers method. When the alarm is to fire, the call to
_apply_rules will iterate over all the participating Rules and call their
action methods. In the case of
MorningWakeRule above, that will turn on the light.
With the
AbsoluteTimeTrigger, I've duplicated the capabilities of the GUI Rule System in regards to time. Let's add more features.
Even though my sleep doctor says a consistent wake time throughout the week is best, I let myself sleep in on weekends. I don't want the light to come on at 6:30am on Saturday and Sunday. Let's modify the rule to take the day of the week into account.
class MorningWakeRule(Rule):
@property
def today_is_a_weekday(self):
weekday = datetime.now().date().weekday() # M0 T1 W2 T3 F4 S5 S6
return weekday in range(5)
@property
def today_is_a_weekend_day(self):
return not self.today_is_a_weekday
def register_triggers(self):
self.weekday_morning_wake_trigger = AbsoluteTimeTrigger(
"morning_wake_trigger", "06:30:00"
)
self.weekend_morning_wake_trigger = AbsoluteTimeTrigger(
"morning_wake_trigger", "07:30:00"
)
return (self.weekday_morning_wake_trigger, self.weekend_morning_wake_trigger)
def action(self, the_changed_thing, *args):
if the_changed_thing is self.weekday_morning_wake_trigger:
if self.today_is_a_weekday:
self.Bedside_Ikea_Light.on = True
elif the_changed_thing is self.weekend_morning_wake_trigger:
if self.today_is_a_weekend_day:
self.Bedside_Ikea_Light.on = True
(see this code in situ in the morning_wake_rule_02.py file in the pywot rule system demo directory)In this code, I've added a couple properties to detect the day of the week and judge if it is a weekday or weekend day. The
register_triggers method has changed to include two instances of
AbsoluteTimeTrigger. The first has my weekday wake time and the second has the weekend wake time. Both triggers will call the
action method everyday, but that method will ignore the one that is triggering on an inappropriate day.
Have you ever used a bedside table light as a morning alarm? It's a rather rude way to wake up to have the light suddenly come on at full brightness when it is still dark in the bedroom. How about changing it so the light slowly increases from off to full brightness over twenty minutes before the alarm time?
class MorningWakeRule(Rule):
@property
def today_is_a_weekday(self):
weekday = datetime.now().date().weekday() # M0 T1 W2 T3 F4 S5 S6
return weekday in range(5)
@property
def today_is_a_weekend_day(self):
return not self.today_is_a_weekday
def register_triggers(self):
self.weekday_morning_wake_trigger = AbsoluteTimeTrigger(
"weekday_morning_wake_trigger", "06:10:00"
)
self.weekend_morning_wake_trigger = AbsoluteTimeTrigger(
"weekend_morning_wake_trigger", "07:10:00"
)
return (self.weekday_morning_wake_trigger, self.weekend_morning_wake_trigger)
def action(self, the_changed_thing, *args):
if the_changed_thing is self.weekday_morning_wake_trigger:
if self.today_is_a_weekday:
asyncio.ensure_future(self._off_to_full())
elif the_changed_thing is self.weekend_morning_wake_trigger:
if self.today_is_a_weekend_day:
asyncio.ensure_future(self._off_to_full())
async def _off_to_full(self):
for i in range(20):
new_level = (i + 1) * 5
self.Bedside_Ikea_Light.on = True
self.Bedside_Ikea_Light.level = new_level
await asyncio.sleep(60)
(see this code in situ in the morning_wake_rule_03.py file in the pywot rule system demo directory)This example is a little more complicated because it involves a bit of asynchronous programming. I wrote the asynchronous method,
_off_to_full, to slowly increase the brightness of the light. At the designated time, instead of turning the light on, the
action method instead will fire off the
_off_to_full method asynchronously. The
action method ends, but
_off_to_full runs on for the next twenty minutes raising the brightness of the bulb one level each minute. When the bulb is at full brightness, the
_off_to_full method falls off the end of its loop and silently quits.
Controlling lights based on time criterion is a basic feature of any Home Automation System. Absolute time rules are the starting point. Next time, I hope to show using the
Python Package Astral to enable controlling lights with concepts like Dusk, Sunset, Dawn, Sunrise, the Golden Hour, the Blue Hour or phases of the moon. We could even make a Philips HUE bulb show warning during the
inauspicious Rahukaal part of the day.
In a future posting, I'll introduce the concept of RuleThings. These are versions of my rule system that are also Things to add to the Things Gateway. This will enable three great features:
- the ability to enable or disable external rules from within the Things Gateway GUI
- the ability to set and adjust an external alarm time from within the GUI
- the ability for my rules system to interact with the GUI Rule System
Stay tuned, I'm just getting started...
http://www.twobraids.com/2018/10/the-things-gateway-its-all-about-timing.html