The Philips Hue logotype. It consists of the Philips brand name in a dark blue, the Hue brand in rainbow colours, and the slogan "personal wireless lighting".

Philips Hue Programming, Evolved (Part 2 of 3)

Device name selection

In my previous instalment, I explained how to use the curl command to control Philips Hue lights. Part of the exercise involved dumping out a huge glop of JSON data and finding a long, gobbledegook of random alphanumeric characters that were the ID of a light we could control.

The Philips Hue logotype. It consists of the Philips brand name in a dark blue, the Hue brand in rainbow colours, and the slogan "personal wireless lighting".I wanted to go a step further by actually parsing that JSON in a way that was more meaningful to me — the dumb human who likes pretty colours.

If you’re a fan of the Programming by Stealth podcast, then you’ll know where I’m going with this. Yes, the wonderfully powerful jq tool. If you’d like to learn about jq in great detail, I will direct you to PBS episode 155 and onward. For this exercise, I will be putting just a little of what I learned from PBS to work. There’s a little bit of learning from the Taming the Terminal bash episodes, too.

My goal was to provide the name I set in the Hue app and get back the service ID for controlling the light. For this I wrote what turned out to be a fairly short script. For those reading along I present the whole script here and will then describe the pieces.

#!/bin/zsh
filter='.data[] | select(.metadata.name == "'"$1"'") | .id'
dev_id=$(curl --request GET -ks --header "hue-application-key: Qn74cB7YlKursSzMYyPL4pr5oLWxayBqhKyjFD10" https://192.168.1.15/clip/v2/resource/light | jq -r "$filter")
echo "Device ID = $dev_id"

The first line is the usual shebang line. I defined my script as zsh because that’s my default shell in which I tested all of the pieces.

The next line defines a variable which will hold our jq filter string. It is a three part filter to get down to the value we need.

Part one simply selects the data[] array at the top level. All of the light information is contained in this.

Part two does the hard work of finding the light service we asked for. It selects the object whose metadata.name key matches the parameter we passed in.

Part three then returns what we’re after: the id of the selected object.

If you are following along very closely, and you also studied the previous instalment, you might be wondering how I got the id of the light service without having to dive into the services object. I’ll explain why in just a moment.

With my filter string defined in the variable, I then use a special shell construct to get the result of a curl command into another variable. My command will return the id value, so by wrapping it in the $(...) construct, I can fetch it straight into my $dev_id variable.

The command in question is very similar to the one I used in the previous instalment to fetch all of the device details, but with one important difference.

I am still performing a GET request, still providing the application key, and still addressing the Bridge. However the later part of the URL is different. It’s now /clip/v2/resource/light. In using this, I am asking only for information about light services from any devices on the network. This is why the top level id is the one we want; it’s the only one present.

One other minor difference is, in addition to the -k flag which ignores certificate errors, I also added the -s flag to tell curl not to write out progress information while fetching the contents of the URL.

The results of the curl command are piped to jq to apply its filter, as previously defined. I specify the -r flag here to get the raw output. I don’t want a JSON representation of the id, I just want the value as a string. -rdoes this for me.

The final line of my script simply echoes out the device ID so you can see it has worked. For real automation tasks, you’d use the ID to build URLs which you would then ask curl to action as in the previous instalment.

Setting colour

Next up is colour. I said in the first instalment that colour was difficult, because Philips have chosen to represent colours in a CIE colour space which is not trivial to convert to and from. But… I figured it out.

First, it should be said that colour science is a very broad and very complicated subject. As soon as I say “convert RGB to CIE” I have already grossly over-simplified things. What follows is a method for converting sRGB colours to a CIE colour space that, to my eyes at least, gets us in the ballpark for Hue bulbs.

Maybe not everyone is geeky enough to think, like I do, in RGB colours. If you say “yellow” I immediately know that’s red plus green. This understanding came from my early life where computers could only usefully display primary and secondary colours, and they were represented in binary — each of red, green, and blue was either on or off. All off was black. All on was white. Red and green was yellow, etc. In later years more control was available and I learned that having red full on and green half on gives orange.

If you are like me, what follows will make sense… at least in terms of the inputs. If you’re not, then at least you will be able to find RGB values for colours all over the internet and in many different bits of software.

So how do we convert from RGB to CIE? It’s complicated.

First, it is complicated by the fact that CIE has three components, X, Y, and Z, but Philips only require X and Y to set a colour. As best I can understand things, X and Y define a colour, and Z defines a brightness. When I found the conversion equations, I thought I might simply be able to plug the Z value into the Hue brightness setting, but it doesn’t seem to work like that. Suffice to say, I don’t understand what I’m missing here, but it turns out if we just ignore Z, we get in the ballpark.

Without further ado, for those reading along, here is the complete script. It expects three values which are between 0 and 1 for each of red, green, and blue.

#!/bin/zsh
r_srgb=$1
g_srgb=$2
b_srgb=$3

#Linearise RGB
if [ 1 -eq "$(echo "${r_srgb} < 0.04045" | bc)" ]
then
  r_lin=$( echo "${r_srgb}" | awk '{print $1 / 12.92};')
else
  r_lin=$( echo "${r_srgb}" | awk '{print (($1 + 0.055) / 1.055) ^ 2.4};' )
fi

if [ 1 -eq "$(echo "${g_srgb} < 0.04045" | bc)" ]
then
  g_lin=$( echo "${g_srgb}" | awk '{print $1 / 12.92};')
else
  g_lin=$( echo "${g_srgb}" | awk '{print (($1 + 0.055) / 1.055) ^ 2.4};' )
fi

if [ 1 -eq "$(echo "${b_srgb} < 0.04045" | bc)" ]
then
  b_lin=$( echo "${b_srgb}" | awk '{print $1 / 12.92};')
else
  b_lin=$( echo "${b_srgb}" | awk '{print (($1 + 0.055) / 1.055) ^ 2.4};' )
fi

#Calculate XYZ
x_cie=$( echo "${r_lin} ${g_lin} ${b_lin}" | awk '{print $1 * 0.4124 + $2 * 0.3576 + $3 * 0.1805};')
y_cie=$( echo "${r_lin} ${g_lin} ${b_lin}" | awk '{print $1 * 0.2126 + $2 * 0.7152 + $3 * 0.0722};')
z_cie=$( echo "${r_lin} ${g_lin} ${b_lin}" | awk '{print $1 * 0.0193 + $2 * 0.1192 + $3 * 0.9505};')

json="{\"color\":{\"xy\":{\"x\":$x_cie,\"y\":$y_cie}}}"
echo $json

As before, the shebang line defines the script to be run in the zsh shell.

Next, I created three variables to hold the passed parameters with more meaningful names. These are called r_srgb, g_srgb, and b_srgb. These values first need to be converted from sRGB to a form called “linear RGB”.

In the next section there are three identical blocks, one each for red, green, and blue. The if statement uses the bc command to perform a floating point compare. If the value supplied is less than 0.0405, then one calculation is used, else a different one. bc is required because none of the shells can cope with floating point arithmetic in comparisons.

The calculations performed use another tool, awk. This can perform floating point arithmetic, too. Why didn’t I use bc for this? Well, because bc can’t raise a number to a floating point exponent! And, I hear you ask, why didn’t I use awk for the comparison? Well because it was more complicated in that context. Hey, I worked it out so you don’t have to!

In asking awk to perform the calculations, we cannot directly use our shell variables. By echoing the variables we need first and then piping that to awk, we can then use the values in the calculations. When I echo "$r_srgb" the value of that becomes available to awk as $1. Note this is not what the shell considers $1 to be!

Once the calculations are performed, we now have a linear RGB triplet. We can convert these into the XYZ components of the CIE colour space.

The next section does this conversion. I’ve made the calculation for Z even though we won’t use it. One day I might figure out how to.

Each component is the result of a matrix multiplication. For instance X = R × 0.4124 + G × 0.3576 + B × 0.1805. Where did I get those matrix multipliers from? You might be sorry you asked. Check out the Wikipedia page on the sRGB colour space. There you can read about all the generalisations I have made and how to correct them. Please… be my guest!

Again, each conversion is performed using awk. This time I am passing the three linear values previously calculated, which are then referred to in the expression as $1, $2, and $3. The bc command would have been able to cope with these simpler expressions, but for consistency, I stuck to awk.

Finally, I use the calculated X and Y values to build up a JSON string that we can directly pass to the Hue API in the way we learned from the last instalment. I have simply echoed this to standard out for you to copy.

Building blocks

I don’t think it needs much imagination to see how these two scripts could be combined to create a single one that takes the name of a light and the desired colour and makes it happen. Furthermore, the calculation for colour temperature would be easily handled in the same way as those in the latter script, and of course brightness is straightforward.

In the third and final instalment, I’ll take this to the next level.

1 thought on “Philips Hue Programming, Evolved (Part 2 of 3)

  1. […] In the second instalment of this series, I left you with a couple of useful scripts — one to fetch the device ID of a light, given a name, and one to perform the conversion from RGB to CIE colour. I developed those two scripts as necessary parts of what comes next. […]

Leave a Reply

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

Scroll to top