Philips Hue Getting Started
Let’s Play: Philips Hue with Python
Introduction
This article offers a quick ride along with me as I change the colour of a Philips hue light by sending commands over a home network from a Python shell on my home computer. Since all the action in today’s example is based on simple HTTP messages, you could do the things shown here in any programming language. I use Python because it’s simple and clean, and I think it’s readable even to non-Pythonic developers.
I recommend opening this document as a Jupyter notebook
I use Jupyter Notebook because it lets me write documents while also trying code I’m talking about, with output captured as part of the document. The code samples assume you’ll be using [Jupyter Notebook], too. If you have a hue hub, I recommend opening this document as a Jupyter notebook, so you can edit the code blocks and run them in place.
Running these code samples without Jupyter Notebook
If you’re not using Jupyter Notebook, here’s a quick workaround to make the examples work in plain Python.
Execute this statement before any code sample that uses the display
function:
display = print
Getting started with the official Getting Started document
Taking a look at the Getting started document on developers.meethue.com, I see that the first task is getting on the same network as the Hue hub and confirming its connection and ours. Since I’ve confirmed my hub is already connected and I can control lights with my phone, I can skip the first step in Getting started.
Finding the Philips hue bridge
My choices are:
- Use a UPnP discovery app to find Philips hue in your network.
- Use our broker server discover process by visiting www.meethue.com/api/nupnp
- Log into your wireless router and look Philips hue up in the DHCP table.
- Hue App method: Download the official Philips hue app. Connect your phone to the network the hue bridge is on. Start the hue app(iOS described here). Push link connect to the bridge. Use the app to find the bridge and try controlling lights. All working – Go to the settings menu in the app. Go to My Bridge. Go to Network settings. Switch off the DHCP toggle. The ip address of the bridge will show. Note the ip address, then switch DHCP back on
Since I want to do this automatically in the future, I’ll opt for using the broker server (option 2).
"""Use the broker server at meethue.com to locate any Philips hue bridges on the local network."""
URL_FIND_BRIDGE_NUPNP = 'https://www.meethue.com/api/nupnp' # https://developers.meethue.com/documentation/hue-bridge-discovery
import urllib.request
I’ll be getting JSON back, but urllib
doesn’t deal with that,
so let me import the json
module and wrap urlopen
in json.load
,
for my convenience.
import json
def request_json(*args):
"""Returns parsed JSON from server"""
return json.load(urllib.request.urlopen(*args))
For the sake of readability, I’d rather have a well named function so we don’t have to think about URLs and serialization formats.
def request_bridges():
return request_json(URL_FIND_BRIDGE_NUPNP)
Now I can call request_bridges()
to get a list:
my_bridges = request_bridges()
My bridge is the first bridge, ‘cause I have only one.
my_bridge = my_bridges[0]
Let’s take a look at that.
display(my_bridge) # (display can be mapped to print, if you're not using Jupyter)
{'id': '001788fffe24d54e', 'internalipaddress': '10.0.1.4'}
I can see the IP address as the internalipaddress
attribute.
I can do things with it, such as use it in strings.
display(f'My bridge is at {my_bridge["internalipaddress"]}.')
'My bridge is at 10.0.1.4.'
If the IP address represents a hub, we can confirm it’s online and connected
by calling the config
API, as suggested in Hue bridge discovery.
If we’ll be using lots of URLs, we should make a quick template to avoid repetition.
api_url = lambda path: f'http://{my_bridge["internalipaddress"]}{path}'
I’m going to go a little further and assume it might be handy
to keep a copy of the data we’re about to retrieve.
I’ll store the data as an attribute in my_bridge
.
my_bridge[api_config_path] = request_json(api_url('/api/config'))
Let’s see how that turned out:
display(my_bridge)
{'/api/config': {'apiversion': '1.20.0',
'bridgeid': '001788FFFE24D54E',
'datastoreversion': '63',
'factorynew': False,
'mac': '00:17:88:24:d5:4e',
'modelid': 'BSB002',
'name': 'RingoTree',
'replacesbridgeid': None,
'starterkitid': '',
'swversion': '1707040932'},
'id': '001788fffe24d54e',
'internalipaddress': '10.0.1.4'}
Great! Let’s continue to Create user in the Philips hue API.
Create user
POST /api
Creates a new user. The link button on the bridge must be pressed and this command executed within 30 seconds.
And what should we expect as a response?
Contains a list with a single item that details whether the user was added successfully along with the username parameter. If successful the username should be stored for future API calls.
A list. So, we can use our request_json
function for this.
def create_user():
return request_json(api_url('/api'), bytes(json.dumps(dict(devicetype='jupyter-notebook#DevvynPM')), encoding="utf-8"))
create_user()
[{'error': {'address': '',
'description': 'link button not pressed',
'type': 101}}]
The link button on the bridge must have been recently pressed for the command to execute successfully. If the link button has not been pressed a 101 error will be returned.
def get_me():
try:
response = create_user()
yield from (user['success'] for user in response)
except KeyError as e:
raise Exception(response)
display(list(get_me()))
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-64-a0ef0c68afb1> in get_me()
3 response = create_user()
----> 4 yield from (user['success'] for user in response)
5 except KeyError as e:
<ipython-input-64-a0ef0c68afb1> in <genexpr>(.0)
3 response = create_user()
----> 4 yield from (user['success'] for user in response)
5 except KeyError as e:
KeyError: 'success'
During handling of the above exception, another exception occurred:
Exception Traceback (most recent call last)
<ipython-input-65-ca2d7d19cfa3> in <module>()
----> 1 display(list(get_me()))
<ipython-input-64-a0ef0c68afb1> in get_me()
4 yield from (user['success'] for user in response)
5 except KeyError as e:
----> 6 raise Exception(response)
Exception: [{'error': {'type': 101, 'address': '', 'description': 'link button not pressed'}}]
I’ve pressed the button. I’ll try again.
me = next(get_me())
display(me)
{'username': 'cV4mAIgcdlY4vgXfpCjlk0eS9gqvYTThwZfugxQO'}
Splendid!
Interacting with a light
Can I turn my light strip orange? We’ll need to find a way to identify it. Let’s get list of lights, and a basic filter search, by name.
class Lights(object):
def __init__(self, username):
self.url = api_url(f'/api/{username}/lights')
def get_all(self):
return request_json(self.url)
@property
def lights(self):
return self.get_all()
def get_by_name(self, search_string):
return {key: light for key, light in self.lights.items() if search_string in light['name']}
lights = Lights(me['username'])
search_results = lights.get_by_name('Light strip')
display(search_results)
{'6': {'manufacturername': 'Philips',
'modelid': 'LST002',
'name': 'Light strip',
'state': {'alert': 'none',
'bri': 108,
'colormode': 'xy',
'ct': 500,
'effect': 'none',
'hue': 5999,
'on': True,
'reachable': True,
'sat': 239,
'xy': [0.5581, 0.3987]},
'swupdate': {'lastinstall': None, 'state': 'noupdates'},
'swversion': '5.50.2.19072',
'type': 'Extended color light',
'uniqueid': '00:17:88:01:01:1c:fa:7a-0b'}}
It feels like time to make a Light
class, but let’s revisit that after doing a quick test.
request_json(api_url(f'/api/{me["username"]}/lights/6'))
{'manufacturername': 'Philips',
'modelid': 'LST002',
'name': 'Light strip',
'state': {'alert': 'none',
'bri': 108,
'colormode': 'xy',
'ct': 500,
'effect': 'none',
'hue': 5999,
'on': True,
'reachable': True,
'sat': 239,
'xy': [0.5581, 0.3987]},
'swupdate': {'lastinstall': None, 'state': 'noupdates'},
'swversion': '5.50.2.19072',
'type': 'Extended color light',
'uniqueid': '00:17:88:01:01:1c:fa:7a-0b'}
Ugh, it looks like urllib.request.urlopen
doesn’t support PUT
requests. I’ll just do it by hand. At least we still have the api_url
function.
data = bytes(json.dumps(dict(on=True, bri=255, hue=int(65535*0.09))), encoding='utf-8')
request = urllib.request.Request(api_url(f'/api/{me["username"]}/lights/6/state'), data=data, method='PUT')
response = urllib.request.urlopen(request)
display(json.loads(response.read().decode('utf-8')))
[{'success': {'/lights/6/state/on': True}},
{'success': {'/lights/6/state/hue': 5898}},
{'success': {'/lights/6/state/bri': 254}}]
Well, it wasn’t very easy to get the right colour, and we couldn’t re-use the request_json
function, but I saw the light turn on and it looked orange to me. Success?
Conclusions
The good
The question was, can I turn my light strip orange? The light reacted to a REST command, turning orange! The question has been answered.
The bad
We ended up with a function that isn’t as useful as we expected, as it can only “post” and read, but not “put”.
Next steps
It’s probably time to get serious about re-usability. Nobody wants to have to re-use snippets of code for simple actions. Ultimately, we’ll want a REST request method that’s useful for all HTTP verbs, and it should also encode our arguments automatically. Once we go beyond individual lights, we’ll probably find it useful to have shorthand methods for everything relevant.
What would be relevant for our intended needs depends entirely on what we want to do. We should formalize our intentions, now that we know Jupyter Notebook is a decent place to tinker. A good idea might be to create an interactive dashboard. It may be helpful to see if any libraries have already been written to interface with Philips hue in Python, so we don’t have te re-build classes as we go through exercises in this narrative fashion.
We also don’t want our “username” lingering in the open in our examples, as this is an access token and it’s considered security faux pas to prioritize convenience over secrecy when handling private access tokens. It’s probably prudent to find a way of passing that token invisibly through our environment, or encrypting it at the very least. Environment variables are usually a good way to pass strings without storing them in distributed sources.