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 continues to run through the lifetime of 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( "weekday_morning_wake_trigger", "06:30:00" ) self.weekend_morning_wake_trigger = AbsoluteTimeTrigger( "weekend_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...