Up until a few weeks ago, i’ve been using IFTTT for controlling various automated light tasks.
The lights i want to control are mostly outdoor lights, turning on at dusk, and off again at sunrise. I also automatically tone the light down and into a slight more red color in the kids rooms around bedtime, and in the living room a wee bit later :)
IFTTT works, but is not very punctual. Sometimes the receipt is run 25-30 minutes late. Not a big deal, just rather annoying on a personal level.
I decided to “do something” about it, so i wrote a small python script for automating it from home.
First i started analyzing what my needs really were :
- Turn on/off at a specific time
- Set brightness
- Set light color
Sounds simple enough, right ? A basic crontab could easily take care of it, but that doesn’t sound “engineered” enough :)
I translated the demands above into a data structure like the following :
rules = {
'Outdoor lights on':{
'lights':['Front Door','Back door'],
'status':'on',
'when':'dusk',
'brightness':254,
'xy':[0.4643, 0.4115]
},
'Lights out at sunrise':{
'lights':['all'],
'status':'off',
'when':'sunrise',
'brightness':254,
},
'Lights down - kids room':{
'lights':['Kids room 1','Kids room 2'],
'status':'on',
'when':'18:45',
'brightness':10,
'xy':[0.51, 0.4148]
}
}'
With the basic rules in place, it was time to start implementing it. First obstacle was choosing a python Hue library. It seems there are a lot of them, so i settled on BeatifulHue which seems to be up-to-date.
For calculating dusk/dawn, sunset/sunrise times i used the excellent Astral module.
In order to use the Hue api, you need to register a user first, BeautifulHue has a nice description on how to accomplish this.
Here’s what i came up with Gist:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import time
import astral
import logging
import datetime
from dateutil import parser
from threading import Thread
from beautifulhue.api import Bridge
rules = {
'Outdoor lights on':{
'lights':['Front Door','Back door'],
'status':'on',
'when':'dusk',
'brightness':254,
'xy':[0.4643, 0.4115]
},
'Lights out at sunrise':{
'lights':['all'],
'status':'off',
'when':'sunrise',
'brightness':254,
},
'Lights down - kids room':{
'lights':['Kids room 1','Kids room 2'],
'status':'on',
'when':'18:45',
'brightness':10,
'xy':[0.51, 0.4148]
}
}
class LightScheduler(Thread):
def __init__(self):
super(LightScheduler, self).__init__()
self.daemon=True
self.bridge = Bridge(device={'ip':'hue'},user={'name':'huetestuser'})
def run(self):
while True:
next_time,next_rules = self.next_activation()
logging.info("next activation : "+str(self.next_activation()))
now = datetime.datetime.now()
waittime = (next_time-now).total_seconds()
logging.info('Executing rules '+str(next_rules)+' in '+str(waittime)+' seconds')
time.sleep(waittime)
try:
for r in next_rules:
self.run_rule(r)
except Exception as e:
logging.exception(e)
def when_to_absolute(self,when):
if when in ['dusk','dawn','sunrise','sunset']:
a = astral.Astral()
a.solar_depression = 'civil'
sun = a['Copenhagen'].sun(datetime.datetime.now(),local=True)
d = sun[when].replace(tzinfo=None)
if d < datetime.datetime.now():
logging.info("using tomorrows date for "+when)
sun = a['Copenhagen'].sun(datetime.date.today() + datetime.timedelta(days=1),local=True)
return sun[when].replace(tzinfo=None)
try:
absolute = parser.parse(when)
if absolute < datetime.datetime.now():
absolute += datetime.timedelta(days=1)
return absolute.replace(tzinfo=None)
except Exception as e:
logging.exception(e)
raise 'Unable to parse activation time \"'+when+'\"'
def next_activation(self):
now = datetime.datetime.time(datetime.datetime.now())
next_time = None
next_rule = []
for r in rules:
rule = rules[r]
when = self.when_to_absolute(rule['when'])
if next_time is None or when < next_time:
next_time = when
next_rule = [r]
elif when == next_time:
next_rule.append(r)
return next_time,next_rule
def run_rule(self,rule_name):
r = rules[rule_name]
logging.info('Running rule :'+rule_name+':'+str(r))
for l in r['lights']:
status = r['status']
brightness = r['brightness']
xy = None
if 'xy' in r:
xy = r['xy']
self.update_light(l,status,brightness,xy)
def update_light(self,name='all',state=False,brightness=254, xy=None):
resource = {'which':'all'}
if state == 'on':
to_state = True
else:
to_state = False
lights = self.bridge.light.get({'which':'all'})
for light in lights['resource']:
l = self.bridge.light.get({'which':light['id']})
l = l['resource']
if l['name'] == name or name == 'all':
resource = {'which':light['id'],'data':{'state':{'on':to_state, 'ct':brightness}}}
if xy is not None:
resource['data']['state']['xy'] = xy
self.bridge.light.update(resource)
def main():
logging.basicConfig(filename='hue_automation.log',level=logging.DEBUG)
t = LightScheduler()
t.start()
while True:
time.sleep(1)
if __name__ == '__main__':
main()
Now when i run the above, i get a nice log like the following :
INFO:root:using tomorrows date for sunrise
INFO:root:using tomorrows date for sunrise
INFO:root:next activation : (datetime.datetime(2015, 11, 13, 16, 48, 47), ['Outdoor lights on'])
INFO:root:Executing rules ['Outdoor lights on'] in 9907.367849 seconds
INFO:root:using tomorrows date for sunrise
INFO:root:using tomorrows date for sunrise
INFO:root:next activation : (datetime.datetime(2015, 11, 13, 14, 12), ['Lights down - kids room'])
INFO:root:Executing rules ['Lights down - kids room'] in 12.125967 seconds
INFO:root:Running rule :Lights down - kids room:{'status': 'on', 'lights': ['Kids room 1', 'Kids room 2'], 'xy': [0.51, 0.4148], 'when': '14:12', 'brightness': 10}
INFO:root:using tomorrows date for sunrise
INFO:root:using tomorrows date for sunrise
INFO:root:next activation : (datetime.datetime(2015, 11, 13, 16, 48, 47), ['Outdoor lights on'])
INFO:root:Executing rules ['Outdoor lights on'] in 9406.924245 seconds
Time taken: a little over 2 hours, and i now have a automated solution that runs on time :)