How we connect our office lights to NPS, Twitter, and Slack

When you mention “@eShares” on Twitter.When you mention “@eShares” on twitter.

See more of our lights here. Like most tech companies, we measure everything and hope to find meaning in our data. The challenge of sharing that data with everyone at eShares led me to build these lights. Here’s how I did it.

Slack, Hue, & Raspberry Pi

Everything we do at eShares shows up in Slack via various bots and integrations listening to every service we use and even logging our production server events. We use that stream of information as an easy way to control our lights. The lights are out-of-the-box Philips Hues in a swanky chandelier. We control the system with a Slack bot running on a Raspberry Pi.

The Software
The Hardware
Example code

Here’s the code for the Python-rtmbot plugin we use.

I’m no software engineer. I only learned enough python to do this project, but here’s the code I put together with a bunch of handy comments to help you out.

Creating Your Own Slack Bot

For this tutorial you’ll need some familiarity with python and general programming — but not much.

Setting Up a Slackbot

Go to https://slack.com/apps/build while logged into Slack and choose to make a Custom integration just for your team. Give your bot a name and save it. This will get you access to your token. You’ll use this as a key to connect your code to Slack.

Your bot will appear as a user in your team’s slack that anybody can message. It can also listen in on channels that it has been invited to. Use /invite @mybotname in any channel to give your bot access.

Setting up Python-rtmbot

Python-rtmbot is a python library produced by Slack, it makes listening to messages in a Slack channel really easy. They have some great instructions on getting this setup. Follow their instructions to install dependencies and download rtmbot.
Make sure you’ve configured rtmbot with your API token.

Adding code

Your code will be run as a plugin to rtmbot. Put it in a folder with the same name as your python file in the ‘plugins’ folder inside of the rtm-bot folder. You could also just place a ‘.py’ file inside the plugins folder if your project is one file.

Here we use the name ‘lightBot.py’ inside of ‘lightBot’ in the plugins folder.Here we use the name lightBot.py inside of lightBot in the plugins folder.

Running rtmbot

From your rtmbot directory run:

$ python rtmbot.py

If everything is configured properly, rtmbot.py will find and run the code inside the plugins folder.

Listening to slack messages

Remember you must first invite your bot to a channel before it can see the messages in it. /invite @mybotname

Python-rtmbot plugins are based around the process_message() function. Python-rtmbot will call process_message(data) everytime it sees a new message with ‘data’ as the contents the message. Your bot must contain this function to run.

def process_message(data):  
    print data

For most messages in slack this code will output an object like this.

{u'text': u'Contents of the message', u'ts': u'1459879767.000842', u'user': u'U0XXXXXXX', u'team': u'T0XXXXXXX', u'type': u'message', u'channel': u'C0XXXXXXX'}

You can use data.get('text') to read the contents of a message or data.get('user') to get the person who sent the message. You can then call other functions based on the text and types of messages recived.

Note: Some integrations output messages as an array of attachments, so try data.get('attachments')[0].get('text') for those messages.

Example:

This is an rtmbot plugin which runs a function when it sees the string foo in any message:

def process_message(data):  
     if "foo" in data.get('text'): # finds if "foo" is in the message
          myFunctionToRun()

If you want to restrict the bot’s response to only respond to specific users or channels you can use the channel or user IDs:

# I like to save channel and user IDs as variables
generalChannelID = "C0XXXXXXX"

def process_message(data):  
     if "foo" in data.get('text') and data.get('channel') == generalChannelID:
          myFunctionToRun()
Controlling hue with python

For sending commands to the Hue lights we use the phue library. It will handle connecting to the Hue bridge and sending commands to the individual lights.

This will be quite a bit easier and more reliable if you can get your hue bridge a static IP address.

So go to https://github.com/studioimaginaire/phue, read the docs, and install phue in your project.

$ pip install phue
Connecting to the hue bridge

When connecting phue to your Slack bot, add this code the top of your Python-rtmbot plugin file.

from phue import Bridge  
# Imports the hue library

b = Bridge('ip_of_your_bridge')  
# Saves your bridge and its info to the variable 'b'

b.connect()  
# Attempts a connection, if this is your first time using this
# on a hue configuration you will be prompted to press the link
# button on your hue bridge.

print b.get_api()  
# This will return a huge dump of all the states of your hue lights.
# If this happens your hue is connected properly and you can remove
# this line of code.

The first run will require you to press the link button on the bridge within 30 second of attempting to connect. A warning will show up in your python console if this is required.

Note: if you’re having trouble authenticating to your hue bridge, or you need to reset your bridge configuration there’s a hidden config file in your home directory you can delete with this command.

$ rm ~/.python_hue
Controlling the bulbs

To change the state of the bulb use the b.set_light(lamp, settings) method. It takes in a lamp ID, and an object holding all properties of a lamp you want to set.

b.set_light(1 , # The lamp ID  
                {'hue': 43690, # A value from 0 to 65535 this is
                 'sat': 254, # A value from 0 to 254
                 'bri': 120, # A value from 0 to 254
                 'transitiontime': 3, # a number of deciseconds
                 'on': True # True of False
               })

The hue bulbs are are identified by a number roughly ordered by the time they were added to the network, but you can use the hue app to find a bulb’s lamp ID.

You can use the hue app to easily identify the lamp ID. It’s the number on the left.You can use the hue app to easily identify the lamp ID. It’s the number on the left.

Speaking the hue language

Philips Hue works different from how most devices handle color. It doesn’t support RGB, but rather a system similar to HSL. Philips provides some extensive documentation on their color handling, but here’s a summary.

On: the accepted values are a case-sensitive True or False.

b.set_light(1 , {'on': True }) # Sets a light on  
b.set_light(1 , {'on': False }) # Sets a light off  

Hue: a value from 0 to 65,535 going around the color wheel through red, yellow, blue, and back to red.

Hue

b.set_light(1 , {'hue': 0 }) # Sets light 1 to red  
b.set_light(1 , {'hue': 65535 }) # Sets light 1 also to red  
b.set_light(1 , {'hue': 49151 }) # Sets light 1 also to blue  
b.set_light(1 , {'hue': 16384 }) # Sets light 1 also to lime  

Saturation: a value from 0 to 254 (yes, 254). This is how white or saturated a bulb appears. If a saturation is 0, no matter the hue, it will appear white.

b.set_light(1 , {'sat': 0 }) # Sets light 1 to white  
b.set_light(1 , {'sat': 254 }) # Sets light 1 to fully saturated  

Brightness: a value from 0 to 254. 0 is not off, it’s just the dimmest setting a light can have while still being on, and it’s not really that dim. If a light is off, no matter the brightness, the light will still appear off.

b.set_light(1 , {'bri': 0 }) # Sets light 1 to dim  
b.set_light(1 , {'bri': 254 }) # Sets light 1 to full brightness  

Transition Time: a number of 1/10ths of a second the bulb will spend transitioning from the previous state to the next state. The default is 0.4 seconds and transitions will always happen at that speed unless otherwise defined.

b.set_light(1 , {'bri': 254, 'transitiontime': 5 }) # Sets light 1 to full brightness over half a second.

b.set_light(1 , {'hue': 49151, 'bri': 0, 'transitiontime': 0 }) # Sets light 1 to blue with no delay  
Color, gamut, and style!

The Hue bulbs, above all else, are lightbulbs, and unless they are showing a vivid color or changing, they will still look like lightbulbs. So here are some things to consider:

  • Colors that are very luminous, those with a lot of green in them, don’t stand out too much from white.

  • The brightness gamut is a quite narrow. The difference between the dimmest and brightness isn’t that notable in a well lit space.

  • Muted colors don’t show up very well, especially if they look lightbulb colored.

  • The closest thing to black is off.

  • The second closest thing to black is a ‘dark grey’ which looks white.

‘transitiontime’, speed, and delay

Each bulb can only process one transition at a time. When a signal is sent to a light, you must wait for the transition to be complete before sending the next signal or it will be dropped. The hue bridge is is also limited to only accept 30 messages a second.

The default transition time is 400ms, but even if the transition time is set to 0 there is a small delay. The fastest I’ve been able to get the same light to responsively change, without dropping any messages, is about a quarter of a second.

Delaying a signal

import time # you’ll need to import the time library

time.sleep(.25) # delays the program for a quarter of a second.  

Run it after your set_light() command to allow the light to finish transitioning before sending the next command.

b.set_light(1, {'hue' : 25574, 'transitiontime' : 2 })  
time.sleep(.25)  
b.set_light(1, {'hue' : 0, 'transitiontime' : 2 })  

Staggering transitions
While any single bulb can only run one transition at time, the bridge does support 30 messages a second. So if you have several bulbs in your space you can stagger transitions and animations across several lights for an appearance of smoother movement.

while True:  
     b.set_light(1, {'hue' : 25574, 'transitiontime' : 1})
     time.sleep(.05)
     b.set_light(2, {'hue' : 25574, 'transitiontime' : 1})
     b.set_light(1, {'hue' : 0, 'transitiontime' : 1})
     time.sleep(.05)
     b.set_light(3, {'hue' : 25574, 'transitiontime' : 1})
     b.set_light(2, {'hue' : 0, 'transitiontime' : 1})
     time.sleep(.05)
     b.set_light(3, {'hue' : 0, 'transitiontime' : 1})
Connecting Hue to Slack
Group and Organize Lights in Meaningful Ways

At eShares our hue lights are in a chandelier and in several lamps around the office. There are also some bulbs on our network, which I want to ignore when iterating through a collection of lights.

It’s much easier to animate the lamps if they’re organized in a meaningful way. I created a couple lists which group the lights by where they are and ordered the lights in our chandelier by their position from bottom to top.

# light configuration
desks = [1,2,3,4]  
chandelier = [6,11,7,9,8,12,13,10]  

With the phue library you can group lights if you want to animate them at the same time. The create_group function takes in a name and an array of lights, so I used my light lists from above.

b.create_group('desks', desks)  
b.create_group('chandelier', chandelier)  

Groups can receive messages like an individual bulb. Just use set_group() instead of set_light().

myGroup = [1,2,3,4]  
b.create_group('myGroup', myGroup)  
# Create a group

b.set_group('myGroup' , {'hue': 0, 'bri': 0, 'transitiontime': 10})  
# Sets the group 'myGroup' to red, dim, with a transition of 1s
Define the Base State as a Function and Variable

Save the state you want your lights to be in while at rest so you can always return your lights to this state. I save an object of baseLightState and a function, baseLights(), which I use at the end of every pattern.

# Save the state of a neutral bulb
baseLightState = {'bri': 120, 'sat': 0, 'transitiontime' : 10, 'on' : True}

# Function to set all lights to the base state.
def baseLights():  
 b.set_group('chandelier', baseLightState)
 b.set_group('desks', baseLightState)
 time.sleep(1)
A simple example

Let’s put it all together!

from phue import Bridge  
import time

# Setup hue bridge
b = Bridge('192.168.1.100') # your hue bridge IP address.  
b.connect()

# Group the lights in a meaningful way
myLights = [1, 2, 3, 4]  
b.create_group('myLights', myLights)

# Base light state. Try matching your office lighting!
baseLightState = {'bri': 120, 'sat': 0, 'transitiontime' : 10, 'on' : True}

def baseLights():  
    b.set_group('myLights', baseLightState)

def flashLightsGreen():  
    # Turn light 1 to green and bright
    b.set_light(myLights[1], {'hue' : 25574, 'sat' : 254, 'bri': 254,
    'transitiontime' : 2, 'on' : True })

    #delay the next event by a quarter second
    time.sleep(.25)

# return lights to base state   
    baseLights()

# This is where rtmbot.py reads the messages
def process_message(data):

# Parse the message for the word "green"
    if "green" in data.get('text'):
         #run the flash lights green function
         flashLightsGreen()
Baking the Pi

Find a computer to run your bot. I used a Pi running raspbain, but most any spare computer would work fine. As long as it’s OS looks something like unix/linux, these instructions should work great.

Running the process on it’s own

We’ll use cron and flock to keep the process alive. Cron is a process scheduler. Flock (stands for file lock) is used here to check if a process is currently running and if it’s not, run it. We’ll set the cron job to run every minute, so if the bot ever crashes or the Pi is reset the program will be back in a minute.

Type this into the terminal to configure cron.

$ crontab -e

This opens up the cron config file. Add this line to the end of the file.

* * * * * flock -n /path/to/bot/lockfile python /path/to/bot/rtmbot.py --config /path/to/bot/rtmbot.conf
  • The first five “*”s are the notation which cron uses to indicate every minute.

  • flock is a utility which allows only one program to run at a time.

  • flock uses a lock file to test if a program is currently running and we indicate where that file will be stored. I keep it in the rtmbot directory.

  • Then, the path to the rtmbot.py file to execute.

  • Due to how rtmbot.py is being called, we need to call rtmbot while pointing it to the config file.

Now you can just let the pi be. If it ever gets unplugged, reset, or the program crashes, it will just start again the next minute. Ours has been running like this for months without any intervention.

A big thanks to Erik Hurkman, Josh Merrill, and the rest who helped get this running.

Thanks to James Seely and Brett Hardin.