Python sunset timer for Philips Hue

The Philips Hue has a nice API. It took me a few picnics to figure out how it works. So for your convenience I give you a few snippets of Python code.

First you have to figure out what your username is:

response = requests.post(self.ip + '/api', data=json.dumps({"devicetype": "casa"}))
print response.content

With this username you can check lights or turn them on and off:

r = requests.get('http://192.168.1.194/api/qKsJorcN2lYr-bFfmzyFNqrCe5sEV/lights/3')
print r.content

And now a wrapper function:

import requests
import time
import json
import math
import iot

def EnhanceColor(normalized):
    if normalized > 0.04045:
        return math.pow( (normalized + 0.055) / (1.0 + 0.055), 2.4)
    else:
        return normalized / 12.92

def xy(r, g, b):
    rNorm = r / 255.0
    gNorm = g / 255.0
    bNorm = b / 255.0

    rFinal = EnhanceColor(rNorm)
    gFinal = EnhanceColor(gNorm)
    bFinal = EnhanceColor(bNorm)
    
    X = rFinal * 0.649926 + gFinal * 0.103455 + bFinal * 0.197109
    Y = rFinal * 0.234327 + gFinal * 0.743075 + bFinal * 0.022598
    Z = rFinal * 0.000000 + gFinal * 0.053077 + bFinal * 1.035763

    if X + Y + Z == 0:
        return (0,0)
    else:
        xFinal = X / (X + Y + Z)
        yFinal = Y / (X + Y + Z)
        return (xFinal, yFinal)

class Bridge():
   def __init__(self):
      self.ip = '192.168.1.194'
      self.username = "qKsJorcBTlYr-bFfma30NqrCeprG5sEV"

   def put(self, action, d):
      s = 'http://' + self.ip + "/api/" + self.username + action
      print s
      r = requests.put(s,data=json.dumps(d))
      print r.content
      
   def kitchen_off(self):
      self.put('/groups/2/action/',{'on':False})
      iot.iot_send_string(50, 'Kitchen Off')

   def kitchen_night_light(self):
      self.put('/groups/2/action/',{'on':True, 'bri':1})
      iot.iot_send_string(50, 'Kitchen Night Light')
      
   def kitchen_dimmed(self):
      self.put('/groups/2/action/',{'on':True, 'bri':128})
      iot.iot_send_string(50, 'Kitchen Dimmed')

   def kitchen_bright(self):
      self.put('/groups/2/action/',{'on':True, 'bri':254})
      iot.iot_send_string(50, 'Kitchen Bright')

   def kitchen_blink(self):
      for i in range(2):
         self.put('/groups/2/action/',{'on':True, 'bri':254})
         time.sleep(1)
         self.put('/groups/2/action/',{'on':False})
         time.sleep(1)
      iot.iot_send_string(50, 'Kitchen Blink')

   def living_room_off(self):
      self.put('/groups/1/action/',{'on':False})
      iot.iot_send_string(50, 'Living Room Off')

   def living_room_night_light(self):
      self.put('/groups/1/action/',{'on':True, 'bri':1})
      iot.iot_send_string(50, 'Living Room Night Light')
      
   def living_room_dimmed(self):
      self.put('/groups/1/action/',{'on':True, 'bri':128})
      iot.iot_send_string(50, 'Living Room Dimmed')

   def living_room_bright(self):
      self.put('/groups/1/action/',{'on':True, 'bri':254})
      iot.iot_send_string(50, 'Living Room Bright')

   def bloom_color_loop(self):
      self.put('/lights/8/state',{'on':True, 'bri':254, 'effect':'colorloop'})
      iot.iot_send_string(50, 'Hue Bloom Color Loop')

   def bloom_set_rgb(self, r,g,b):
      self.put('/lights/8/state',{'on':True, 'bri':254, 'xy':xy(r,g,b)})
      iot.iot_send_string(50, 'Hue Bloom red=%d, green=%d, blue=%d' % (r,g,b))

The iot class is a homebrew event logger:

import urllib, urllib2, os, traceback, time

def __iot_send(device_id, event = None):
   if os.name == 'nt':
      event = urllib.unquote(event).decode('utf8')
      print "%d: %s" % (device_id, event)
   else:
      try:
         url = 'https://www.picnicprojects.com/casa/log.php?d=' + str(device_id)
         if event <> None:
            url += '&v=' + event
         response = urllib2.urlopen(url)
      except:
         pass

def iot_send(device_id):
   __iot_send(device_id)

def iot_send_string(device_id, string):
   event = urllib.quote(string)
   __iot_send(device_id, event)

def iot_send_value(device_id, value):
   event = '%f'% value 
   __iot_send(device_id, event)

def iot_send_values(device_id, values):
   first = 1
   print values
   for v in values:
      print v
      if first == 1:
         event = '%f' % v
         first = 0
      else:
         event += ',%f' % v
   __iot_send(device_id, event)
   
def iot_send_trace():
   __iot_send(999, 'exception: ' + traceback.format_exc())

You can use crontab to set timers. But why not make it yourself 🙂

import threading, datetime, time, os, traceback
import iot, sun, hue

def hue_set_lights(action, group):
   try:
      iot.iot_send_string(200, 'Hue: %s %s' % (group, action))
      if action == 'on':
         if group == 'kitchen':
            bridge.kitchen_night_light()
            bridge.bloom_set_rgb(255,165,0) #orange
         elif group == 'livingroom':
            bridge.living_room_night_light()
      else:
         if group == 'kitchen':
            bridge.kitchen_off()
         elif group == 'livingroom':
            bridge.living_room_off()
   except:
      iot.iot_send_trace()

def seconds_to_string(dt):
   h = int(dt/3600)
   m = (dt - h*3600) / 60
   s = "%d s = %02d:%05.2f" % (dt, h, m)
   return s
   
def seconds_until_end_of_day(now):
   dt = ((24 - now.hour - 1) * 60 * 60) + ((60 - now.minute - 1) * 60) + (60 - now.second)
   return dt

def seconds_until_time(now, h, m, delta):
   t2 = now.replace(hour = h, minute = m, second = 0, microsecond = 0) + datetime.timedelta(hours=delta)
   dt = t2 - now
   dt = dt.total_seconds()
   return(dt)

def seconds_until_sunset(now):
   t  = sun.get_local_sunset_time(now.date())
   t  = t.replace(tzinfo=None)
   dt = datetime.datetime.combine(datetime.date.today(), t.time()) - datetime.datetime.combine(datetime.date.today(), now.time())
   dt = dt.total_seconds()
   return(dt)

def seconds_until_sunrise(now):
   t  = sun.get_local_sunrise_time(now.date())
   t  = t.replace(tzinfo=None)
   dt = datetime.datetime.combine(datetime.date.today(), t.time()) - datetime.datetime.combine(datetime.date.today(), now.time())
   dt = dt.total_seconds()
   return(dt)

def calculate_seconds_until_event(now, days, h, m, duration, use_sun):
   # ignore days
   dt_on  = -1
   dt_off = -1
   d = now.weekday()
   if ((str(d+1) in str(days)) or ('*' in str(days))):
      dt_on  = seconds_until_time(now, h, m, 0)
      dt_off = seconds_until_time(now, h, m, duration)
      if use_sun:
         if h < 12: 
            dt_sun = seconds_until_sunrise(now)
            if dt_sun < dt_on:
               dt_on  = -1
               dt_off = -1
            elif dt_sun < dt_off:
               dt_off = dt_sun
         else:
            dt_sun = seconds_until_sunset(now) - 2700
            if dt_sun > dt_off:
               dt_on  = -1
               dt_off = -1
            elif dt_sun > dt_on:
               dt_on = dt_sun
   return(dt_on, dt_off)

   
def log_dt(now, dt, action, group):
   if action == 1:
      a = 'on'
   else:
      a = 'off'
   s = "timer programmed at: %s %s %s (%s)"  % ((now + datetime.timedelta(seconds = dt)), group, a, seconds_to_string(dt))

def schedule_timers(days, hour, minute, duration, use_sun, group):
   now = datetime.datetime.now()
   dt_on, dt_off = calculate_seconds_until_event(now, days, hour, minute, duration, use_sun)
   if dt_on > 0:
      log_dt(now, dt_on, 1, group)
      t = threading.Timer(dt_on, hue_set_lights, args=('on',group))
      t.setDaemon(True)
      t.start()
   if dt_off > 0:
      log_dt(now, dt_off, 0, group)
      t = threading.Timer(dt_off, hue_set_lights, args=('off',group))
      t.setDaemon(True)
      t.start()
      
bridge = hue.Bridge()
sun = sun.Sun()

if __name__ == '__main__':
   while 1:
      try:
         if os.name == 'nt':
            fname = 'timers.txt'
         else:
            fname = '/mnt/nas/data/timers.txt'
         fid = open(fname, 'r')
         for line in fid:
            if '#' not in line:
               a = line.replace('\n','').split(',')
               # days, hour, minute, duration, use_sun, group
               schedule_timers(a[0], int(a[1]), int(a[2]), float(a[3]), int(a[4]), a[5])
               # time.sleep(10)
      except:
         iot.iot_send_trace()
      dt = 60 + seconds_until_end_of_day(datetime.datetime.now())
      time.sleep(dt)

You have to put all timers in timers.txt
Yes, I know, it looks a lot like crontab. This one is not better, but I made it myself 🙂

# days, hour, minute, duration, use_sun, group
1245,  5,30,  1,1,kitchen
12345, 7, 0,1.5,1,kitchen
67,    7, 5,3,  1,kitchen
*,    17,30,  6,1,kitchen
*,    18,15,  4,1,livingroom

And you need to know when the sun rises and sets. I didn’t write this class myself but I also forgot where I found it. Credits to the anonymous maker!

import math
import datetime
from dateutil import tz

class SunTimeException(Exception):

    def __init__(self, message):
        super(SunTimeException, self).__init__(message)


class Sun:
    """
    Approximated calculation of sunrise and sunset datetimes. Adapted from:
    https://stackoverflow.com/questions/19615350/calculate-sunrise-and-sunset-times-for-a-given-gps-coordinate-within-postgresql
    """
    def __init__(self, lat=52.37,lon=4.90): # default Amsterdam
        self._lat = lat
        self._lon = lon

    def get_sunrise_time(self, date=datetime.date.today()):
        """
        Calculate the sunrise time for given date.
        :param lat: Latitude
        :param lon: Longitude
        :param date: Reference date
        :return: UTC sunrise datetime
        :raises: SunTimeException when there is no sunrise and sunset on given location and date
        """
        sr = self._calc_sun_time(date, True)
        if sr is None:
            raise SunTimeException('The sun never rises on this location (on the specified date)')
        else:
            return sr

    def get_local_sunrise_time(self, date=datetime.date.today(), local_time_zone=tz.tzlocal()):
        """
        Get sunrise time for local or custom time zone.
        :param date: Reference date
        :param local_time_zone: Local or custom time zone.
        :return: Local time zone sunrise datetime
        """
        sr = self._calc_sun_time(date, True)
        if sr is None:
            raise SunTimeException('The sun never rises on this location (on the specified date)')
        else:
            return sr.astimezone(local_time_zone)

    def get_sunset_time(self, date=datetime.date.today()):
        """
        Calculate the sunset time for given date.
        :param lat: Latitude
        :param lon: Longitude
        :param date: Reference date
        :return: UTC sunset datetime
        :raises: SunTimeException when there is no sunrise and sunset on given location and date.
        """
        ss = self._calc_sun_time(date, False)
        if ss is None:
            raise SunTimeException('The sun never sets on this location (on the specified date)')
        else:
            return ss

    def get_local_sunset_time(self, date=datetime.date.today(), local_time_zone=tz.tzlocal()):
        """
        Get sunset time for local or custom time zone.
        :param date: Reference date
        :param local_time_zone: Local or custom time zone.
        :return: Local time zone sunset datetime
        """
        ss = self._calc_sun_time(date, False)
        if ss is None:
            raise SunTimeException('The sun never sets on this location (on the specified date)')
        else:
            return ss.astimezone(local_time_zone)

    def _calc_sun_time(self, date=datetime.date.today(), isRiseTime=True, zenith=90.8):
        """
        Calculate sunrise or sunset date.
        :param date: Reference date
        :param isRiseTime: True if you want to calculate sunrise time.
        :param zenith: Sun reference zenith
        :return: UTC sunset or sunrise datetime
        :raises: SunTimeException when there is no sunrise and sunset on given location and date
        """
        # isRiseTime == False, returns sunsetTime
        day = date.day
        month = date.month
        year = date.year

        TO_RAD = math.pi/180.0

        # 1. first calculate the day of the year
        N1 = math.floor(275 * month / 9)
        N2 = math.floor((month + 9) / 12)
        N3 = (1 + math.floor((year - 4 * math.floor(year / 4) + 2) / 3))
        N = N1 - (N2 * N3) + day - 30

        # 2. convert the longitude to hour value and calculate an approximate time
        lngHour = self._lon / 15

        if isRiseTime:
            t = N + ((6 - lngHour) / 24)
        else: #sunset
            t = N + ((18 - lngHour) / 24)

        # 3. calculate the Sun's mean anomaly
        M = (0.9856 * t) - 3.289

        # 4. calculate the Sun's true longitude
        L = M + (1.916 * math.sin(TO_RAD*M)) + (0.020 * math.sin(TO_RAD * 2 * M)) + 282.634
        L = self._force_range(L, 360 ) #NOTE: L adjusted into the range [0,360)

        # 5a. calculate the Sun's right ascension

        RA = (1/TO_RAD) * math.atan(0.91764 * math.tan(TO_RAD*L))
        RA = self._force_range(RA, 360 ) #NOTE: RA adjusted into the range [0,360)

        # 5b. right ascension value needs to be in the same quadrant as L
        Lquadrant  = (math.floor( L/90)) * 90
        RAquadrant = (math.floor(RA/90)) * 90
        RA = RA + (Lquadrant - RAquadrant)

        # 5c. right ascension value needs to be converted into hours
        RA = RA / 15

        # 6. calculate the Sun's declination
        sinDec = 0.39782 * math.sin(TO_RAD*L)
        cosDec = math.cos(math.asin(sinDec))

        # 7a. calculate the Sun's local hour angle
        cosH = (math.cos(TO_RAD*zenith) - (sinDec * math.sin(TO_RAD*self._lat))) / (cosDec * math.cos(TO_RAD*self._lat))

        if cosH > 1:
            return None     # The sun never rises on this location (on the specified date)
        if cosH < -1:
            return None     # The sun never sets on this location (on the specified date)

        # 7b. finish calculating H and convert into hours

        if isRiseTime:
            H = 360 - (1/TO_RAD) * math.acos(cosH)
        else: #setting
            H = (1/TO_RAD) * math.acos(cosH)

        H = H / 15

        #8. calculate local mean time of rising/setting
        T = H + RA - (0.06571 * t) - 6.622

        #9. adjust back to UTC
        UT = T - lngHour
        UT = self._force_range(UT, 24)   # UTC time in decimal format (e.g. 23.23)

        #10. Return
        hr = self._force_range(int(UT), 24)
        min = round((UT - int(UT))*60, 0)
        if min == 60:
            hr += 1
            min = 0

        return datetime.datetime(year, month, day, hr, int(min), tzinfo=tz.tzutc())

    @staticmethod
    def _force_range(v, max):
        # force v to be >= 0 and < max
        if v < 0:
            return v + max
        elif v >= max:
            return v - max

        return v


if __name__ == '__main__':
    sun = Sun(85.0, 21.00)
    try:
        print(sun.get_local_sunrise_time())
        print(sun.get_local_sunset_time())

    # On a special date in UTC

        abd = datetime.date(2014, 1, 3)
        abd_sr = sun.get_local_sunrise_time(abd)
        abd_ss = sun.get_local_sunset_time(abd)
        print(abd_sr)
        print(abd_ss)
    except SunTimeException as e:
        print("Error: {0}".format(e))

Leave a Reply

Your email address will not be published. Required fields are marked *