Sunrise, Sunset, Swifty Flow the Days

In my previous blog post, I introduced Time Triggers to demonstrate time based home automation.  Sometimes, however, pegging an action down to a specific time doesn't work: darkness falls at different times every evening as one season follows another.  How do you calculate sunset time?  It's complicated, but there are several Python packages that can do it: I chose Astral.

The Things Gateway doesn't know where it lives.   The Raspberry Pi distribution that includes the Things Gateway doesn't automatically know and understand your timezone when it is booted. Instead, it uses UTC, essentially Greenwich Mean Time, with none of those confounding Daylight Savings rules. Yet when viewing the Things Gateway from within a browser, the times in the GUI Rule System automatically reflect your local timezone. The presentation layer of the Web App served by the Things Gateway is responsible for showing you the correct time for your location.  Beware, when you travel and access your Things Gateway GUI rules remotely from a different timezone, any references to time will display in your remote timezone.  They'll still work properly at their appropriate times, but they will look weird during travel.

My own homegrown rule system uses a different tactic: it nails down a timezone for your Things Gateway.  In the configuration, you specify two timezones:  the timezone where your Things Gateway is physically located, local_timezone, and the timezone that is the default on the computer's clock running the the external rule system, system_timezone.  Here's two examples to show why both need to be specified.
  1. I generally run my rules on my Linux Workstation.  As this machine sits on my desk, its internal clock is set to reflect my local time.  I set both the local_timezone and the system_timezone to US/Pacific.  That tells my rule system that no time translations are required
  2. However, if were to instead run my Rule System on the Raspberry Pi that also runs the Things Gateway, I'd have have to specify the system_timezone as UTC.  My local_timezone remains US/Pacific.
These configuration parameters can be set in several ways.  You can create environment variables:
        
        $ export local_timezone=US/Pacific
        $ export system_timezone=US/Pacific
        $ ./my_rules.py --help

Or they can be command line parameters:
        
        $ ./my_rules.py --local_timezone=US/Pacific --system_timezone=US/Pacific

Or they can be in a configuration file:
        
        $ cat config.ini
        local_timezone=US/Pacific
        system_timezone=US/Pacific
        $ ./my_rules.py --admin.config=config.ini


My next blog post will cover more on how to set configuration and run this rule system on either the same Raspberry Pi that runs the Things Gateway or some other machine.

Meanwhile, let's talk about solar events.  Once the Rule System knows where the Things Gateway is, it can calculate sunrise, sunset  along with a host of other solar events that happen on a daily basis.  That's where the Python package Astral comes in.  Given the latitude, longitude, elevation and the local timezone, it will calculate the times for: blue_hour, dawn, daylight, dusk, golden_hour, night, rahukaalam, solar_midnight, solar_noon, sunrise, sunset, and twilight.

I created a trigger object that wraps Astral so it can be specified in the register_trigger method of the Rule System.  Here's a rule that will turn a porch light on ten minutes after sunset every day:
class EveningPorchLightRule(Rule):

    def register_triggers(self):
        self.sunset_trigger = DailySolarEventsTrigger(
            self.config,
            "sunset_trigger",
            ("sunset", ),
            (44.562951, -123.3535762),
            "US/Pacific",
            70.0,
            "10m"  # ten minutes
        )
        self.ten_pm_trigger = AbsoluteTimeTrigger(
            self.config,
            'ten_pm_trigger',
            '22:00:00'
        )
        return (self.sunset_trigger, self.ten_pm_trigger)

    def action(self, the_triggering_thing, *args):
        if the_triggering_thing is self.sunset_trigger:
            self.Philips_HUE_01.on = True
        else:
            self.Philips_HUE_01.on = False
(see this code in situ in the solar_event_rules.py file in the pywot rule system demo directory)

Like the other rules that I've created, I start by creating my trigger, in this case, an instance of the DailySolarEventsTrigger class.  It is given the name "sunset_trigger" and the solar event, "sunset".  The rule can trigger on multiple solar events, but in this case, since I want only one, "sunset" appears alone in a tuple. Next I specify the latitude and longitude of my home city, Corvallis, OR.  That's in the US/Pacific timezone and about 70 meters above sea level.  Finally, I specify a string representing 10 minutes.

After the DailySolarEventsTrigger, I create another trigger, AbsoluteTimeTrigger to handle turning the light off at 10pm.  I could have created a second rule to do this, but a single rule to handle both ends of the operation seemed more satisfying.

In the action part of the rule, I needed to differentiate from the two types of actions.  Both triggers will call the action function, each identifying itself as the value of the parameter, the_triggering_thing.  If it was the sunset_trigger calling action, it turns the porch light on.  If it was the ten_pm_trigger calling action, the light gets turned off.  I think the implementation begs for a better dispatch method, but Python doesn't help much with that.


Some of the solar events are not just a single instant in time like a sunset, some represent periods during the day.  One example is Rahukaalam.  According to a Wikipedia article, in the realm of Vedic Astrology, Rahukaalam is an "inauspicious time" during which it is unwise to embark on new endeavors.  It's  based on dividing the daylight hours into eight periods.  One of the periods is marked as the inauspicious one based on the day of the week.  For example, it's the fourth period on Fridays and the seventh on Tuesdays.  Since the length of the daylight hours changes every day, the lengths of the periods change and it gets hard to remember when it's okay to start something new.

Here's a rule that will control a warning light.  The light being on indicates that the current time is within the start and end of the Rahukaalam time for the given global location and elevation

class RahukaalamRule(Rule):

    def register_triggers(self):
        rahukaalam_trigger = DailySolarEventsTrigger(
            self.config,
            "rahukaalam_trigger",
            ("rahukaalam_start", "rahukaalam_end", ),
            (44.562951, -123.3535762),
            "US/Pacific",
            70.0,
        )
        return (rahukaalam_trigger,)

    def action(self, the_triggering_thing, the_trigger, *args):
        if the_trigger == "rahukaalam_start":
            logging.info('%s starts', self.name)
            self.Philips_HUE_02.on = True
            self.Philips_HUE_02.color = "#FF9900"
        else:
            logging.info('%s ends', self.name)
            self.Philips_HUE_02.on = False

(this code is not part of the demo scripts, however, the next very similar script is.)

Like all rules, it has two parts: registering triggers and responding to trigger actions.  In register_trigger, I subscribe to the rahukaalam_start and rahukaalam_end solar events for my town location, timezone and elevation.  In the action, I just look to the_trigger to see which of the two possible triggers fired.  It results in a suitably cautious orange light illuminating during the Rahukaalam period.

Could we make the light blink for the first 30 seconds, just so we ensure that we notice the warning?  Sure we can!
class RahukaalamRule(Rule):

    def register_triggers(self):
        rahukaalam_trigger = DailySolarEventsTrigger(
            self.config,
            "rahukaalam_trigger",
            ("rahukaalam_start", "rahukaalam_end", ),
            (44.562951, -123.3535762),
            "US/Pacific",
            70.0,
            "-2250s"
        )
        return (rahukaalam_trigger,)

    async def blink(self, number_of_seconds):
        number_of_blinks = number_of_seconds / 3
        for i in range(int(number_of_blinks)):
            self.Philips_HUE_02.on = True
            await asyncio.sleep(2)
            self.Philips_HUE_02.on = False
            await asyncio.sleep(1)
        self.Philips_HUE_02.on = True

    def action(self, the_triggering_thing, the_trigger, *args):
        if the_trigger == "rahukaalam_start":
            logging.info('%s starts', self.name)
            self.Philips_HUE_02.on = True
            self.Philips_HUE_02.color = "#FF9900"
            asyncio.ensure_future(self.blink(30))
        else:
            logging.info('%s ends', self.name)
            self.Philips_HUE_02.on = False

(see this code in situ in the solar_event_rules.py file in the pywot rule system demo directory)

Here I created an async method that will turn the lamp on for two seconds and off for a second as many times as it can in 30 seconds.  The method is fired off by the action method when the light is initially turned on.  Once the blink routine has finished blinking, it silently quits.

Perhaps one of the best things about the Things Gateway is that the Things Framework allows nearly any programming language to participate. 

Thanks this week goes to the authors and contributors to the Python package Astral.  Readily available fun packages like Astral contribute immensely to the sheer joy of Open Source programming.

Now it looks like I've only got twenty minutes to publish this before I enter an inauspicious time...  D'oh, too late...