As part of my home automation I wanted to emulate a Philips Hue bridge. The reason for that is that a lot of things provide out-of-the-box integration with Philips Hue. Aside from that, there’s a ton of apps and other cool things in the Hue ecosystem I wanted to unlock.

However, we use the IKEA Trådfri system at home, even though we do have a first generation Philips Hue bridge. The reason for switching to the IKEA one was:

  • Local-only API, no cloud involved at all
  • Bulbs are cheaper, especially at the time, making it feasible to equip the whole house with them without breaking the bank
  • You can observe/subscribe to events on the Trådfri gateway, which fits in more neatly with our usage of MQTT than the HTTP polling approach Philips took with the Hue
  • Security. Trådfri uses CoAP+DTLS with a random Pre-Shared Key printed on the bottom of the device. You need physical access to the device to get this key and all communication with the gateway is always encrypted

The rest of this post will take a look at what went into building a Philips Hue v2 (the square unit) emulator. I wanted to emulate the Hue v2 because it has a newer API with more capabilities. Only the v2 bridge supports the new Hue Entertainment API so I can sync the light bulbs to what I’m watching on my screen. The other requirement was that the emulated bridge had to be good enough to fool the official Philips Hue app.

API

The first thing needed to do is figure out all the API endpoints to implement, what their response types are etc. Philips does a fairly decent job of keeping the documentation on their Hue developer site up to date. This makes it possible to figure out what you need to do.

The Hue API documentation is not complete though, and many of the response examples show all possible keys and values for the JSON blob an endpoint can return. Many key/value pairs are dependent on some other condition, like the bulb type, or the group type etc. Figuring this out is largely trial-and-error though having a Hue v1 bridge to test against proved useful. You can also find many examples of Hue API responses in GitHub issues, Gists and pastebins.

The Hue API is also very peculiar in places. It always returns a 200 OK even on errors, but then has a body with the error information encoded as JSON. I don’t mind the body, it’s helpful, but would it have killed you to return a 4XX or 5XX status code too? There’s other oddities too. The /lights endpoint returns a map of all lights, where the keys are stringified incremental IDs (though they don’t have to be, turns out). Lots of things are strings in the API for which real types exist. Other funky shit includes the special /groups/0 which always returns all devices on the bridge but never seems to be used.

I went through a cycle of 😕 😲 🤯 😖 😵 🤮 😡 trying to faithfully reimplement the Hue API. What really bugs me is that when the Hue v2 bridge came out they had the opportunity to fix a lot of this, but didn’t. It would also be really nice if the Hue API came with an OpenAPI spec. That would drastically improve the docs and make it feasible to auto-generate most client implementations or scaffold a server.

Authentication

In order to be able to talk to the Hue API you need to register with it first. This is done by posting some JSON to /api which returns a username. This username is really a token, not so much a username, and any subsequent requests are done to /api/<username>/<endpoint>. Why oh why the token ends up being part of the URL is a mystery to me, it could have just been an HTTP header. At least the whole API is JSON over HTTP and returns correctly formatted JSON too.

When you post to /api to register, the request will be rejected if you haven’t pressed the link button in the past 30s. So gaining access to the bridge effectively means you need physical access to the device. This may seem perfectly secure so you might be wondering what that “security” bit was about in the introduction. We’ll get into that in the next section.

Security

There is not much point to the whole registration thing if the token flies over the network plain text. However, that’s exactly what happens with the Hue v1 bridge. As long as someone manages to capture a single HTTP exchange with the Hue bridge (the official Hue app polls a number of endpoints every 2 seconds) they’ll have access to your bridge. 🎉

They attempted to rectify this with the Hue v2 bridge, but the solution is still a bit dodgy. The Hue v2 API is accessible both over HTTP on port 80 and HTTPS on port 443. The way it works is like this:

  • The client hits /api/nouser/config, plain text. When the Hue app doesn’t a token yet it uses nouser. This returns some basic information about the bridge, like its IP, MAC, API and software version (but nothing about lights, groups etc.)
  • The client then talks to the rest of the endpoints over HTTPS, with the Hue bridge returning a self-signed cert with a subject of C=NL/O=Philips Hue/CN=$MAC-WITHOUT-COLONS and the serial being the integer representation of that same $MAC-WITHOUT-COLONS base 16 🙄

It’s trivial to MITM this, or create your own fake cert that the official Hue app will gladly accept. All you need to do is ensure that the mac returned by the config endpoint matches the certificate. It doesn’t even have to be the real MAC of the device.

This makes the Hue v2 bridge much less susceptible to eavesdropping and stealing of credentials. However the ease with which you can MITM this is still concerning. It might have been nicer to burn a cert in the bridge at the factory and pin that in the app. Short of being able to extract the keys from the device it would’ve become much harder to MITM it, or to fool the Hue app into talking with an emulated bridge. The IKEA approach is fairly elegant too, using a PSK instead. This also avoids the weird registration thing you need to do but of course once you know the PSK you can’t prevent someone from accessing it without replacing the physical gateway.

The Hue Entertainment API, which like IKEA takes the DTLS approach, uses the PSK strategy. When you register with the bridge, using the /api endpoint over HTTPS, you can additionally request a client key that is entirely separate from the username/token. That client key is then used as a PSK for the DTLS part.

Discovery

In order to talk to the bridge, you need to know its IP first. The Hue developer documentation informs us that you can discover the bridge in 3 ways:

  • Do an SSDP discover (UPnP)
  • Hit https://discovery.meethue.com/ (Philips cloud) that will return all internal IPs and MACs of all bridges that have the same external IP as the IP the request to the Hue discovery API came from. Effectively a little agent runs on each hardware bridge that publishes this information to Philips. Kinda like the old dynamic DNS clients you had
  • Find everything with port 80 open on the network

Once you got an IP out of one of those approaches you hit the /descripiton.xml endpoint, parse the response and check some fields to determine if it’s a Hue bridge.

Additionally the docs mention that you can use mDNS by looking for SRV records on _hue._tcp.local but the bridge doesn’t seem to do so nor does the Hue app use it as part of its discovery strategy. My emulator does have an mDNS responder though, just in case things do start to use it.

There is also an interesting discrepancy between the Philips Hue app and the previous Philips Hue (v1 bridge) app. The former does not seem to do SSDP discovery at all and immediately proceeds to port scan my network, whereas the old app does. I’ve confirmed this with packet captures but Philips Hue insists their current application does use SSDP. It might still be listening for SSDP notify/broadcasts but it does not do SSDP discovery. Vendors lie, or at the very least appear (wilfully) ignorant of their own products' behaviour. But you can’t hide from tcpdump 🤷.

Bulb emulation

My Hue emulator leverages the same protocol as explained in the home automation post. This means that it’s not really aware of the fact that it’s emulating IKEA bulbs, or that those bulbs actually come from the IKEA Trådfri gateway. Thanks to this is also exposes some light strips that are exposed by a controller directly attached to the strips running on a NodeMCU.

The emulator discovers all lightbulb accessories on the MQTT broker and looks for the hue, saturation, colorTemperature and brightness characteristics. If hue and saturation are found it fakes a Hue A19 Extended Color bulb. colorTemperature is exposed for bulbs that can display a range of whites, from roughly “hospital” to “sunset” and fakes a Hue White Ambiance bulb. Anything else, i.e a bulb for which we only found the brightness characteristic, is emulated as a regular Hue White bulb 💡.

The Hue A19 and the Hue White Ambiance are exposes as a “mixed” function bulb with the “sultanbulb” archetype. The only real thing this seems to affect is the icon that is used to represent the light. The Hue White gets the “classicbulb” archetype which gets us a different icon making them a bit easier to distinguish in the Hue app.

Groups and Rooms

The Hue app is rather stubborn and demands all your lights be assigned to a room. Since the bulbs we have on MQTT are in reality groups we simply create a room for each bulb and assign the bulb to it. Because we gave our groups names that match the hard coded list of rooms Hue supports we can infer the class of the room. By setting the group named “Kitchen” to the Kitchen class, case sensitive and capitalised I kid you not, we get a nice icon in the app, a cooking pot.

The insistence of having every light assigned to a room is a bit annoying though. It would be fine to let people not organise them in groups. It also seems that nowadays the Hue app doesn’t let you create a regular LightGroup group anymore, all groups are Rooms. This makes it a bit hard to organise lights in arbitrary constellations that might not map nicely to rooms. It’s not been a problem in real life though.

Sensors

I haven’t gotten around to supporting sensors yet, but I’d like to add least at support for our motion and contact sensors. They’re also exposed by the MQTT broker so this shouldn’t be hard to do. It’ll take some time to dig through the sensors API documentation and figure out which devices to emulate in each case.

Conclusion

The main challenge in building this bridge wasn’t recreating the API, but getting it to work with the official Hue app. The switchover to HTTPS for v2 isn’t really documented anywhere so for the longest time I couldn’t figure out why it never moved passed the initial queries to /description.xml and /config. A lot of this would probably have been a lot harder if not for the emulated Hue bridge in Home Assistant and the code of the diyHue projects. That latter one came in especially handy to figure out what I had to stuff in the TLS cert for the Hue app to be happy.

With a couple of evenings of work I have a pretty functional Hue bridge now that works with the official Hue app. I’ve yet to implement the functionality to control the bulbs through the Hue API so for now it’s read-only. I want to reorganise and have tests for this code before I add all of that.

Once that’s done I’m planning to add support for Hue Entertainment API. That will require adding a UDP listener, figuring out DTLS and adding support for the Hue Entertainment API packet format.

iot