Building a custom presence light solution using Philips Hue lights and C#

Building a custom presence light solution using Philips Hue lights and C#

I’ve built several presence light solutions in my life, mostly to entertain myself and to learn something new. Back when OCS 2007 was still a hot commodity I remember receiving a USB-powered plastic cube, that would turn red when I was set to busy in the client.

The purpose of a presence light, or a free/busy light, has usually been to passively-aggressively tell your colleagues at the office that you are busy right now. Perhaps you’re working.

Now that I’m working from home 100% of the time, this isn’t useful. Yet, I might be delivering a webinar, recording a podcast, or I might be in a conference call with 50 attendees and have my video on. It’s nice to tell my family that I’m busy in a different way.

Many people have already built interesting presence light solutions. One of the great ones is PresenceLight from Isaac Levin. It uses Microsoft Graph, the one API to rule them all, to ping your current presence (see here) and reflects that on your smart home appliances such as Philips Hue or the LIFX lights.

Unfortunately, for my needs, this wasn’t good. Microsoft Graph’s Presence resource type gives me base presence information, that reflects with values such as Away, Busy, and BusyIdle. Unfortunately, this is mostly based on what I’ve set my presence to be in Microsoft Teams. I have other software also, that puts me into a busy mode – Zoom, recording software, Facebook Messenger, and similar. I also have multiple organizational accounts, and the Microsoft Graph only reflects one of those at a time.

One could argue, that if Teams shows me as busy, it doesn’t matter how I’m busy, but I’m still busy. Perhaps, yes, but what if I’m offline in Teams, and still busy?

So I set out to figure out how to resolve my presence without resorting to Microsoft Graph. Hold on to your IKEA chairs, this journey will take a while.

Figuring out the busy state

As I knew I couldn’t rely on just what Microsoft Graph relays to me for my presence, I started to look for a more local solution. First, I listed the activities what I feel make me busy, in terms of “please don’t come to the home office just now“:

  • When I’m recording something
  • When I have a meeting, often (almost always) with video turned on

Beyond these two states, I felt I’m not really busy and can be interrupted at any time. The mutual factor is the use of the microphone. Perhaps I could ping the default audio device, and if it’s active, declare that I’m busy?

I set out to dig for the Win32 APIs. Based on previous adventures, I knew this would probably yield some very suspicious and punctilious code in C++, which I wasn’t keen on using. I whipped together some sample classes in .NET Core and C# to try out some hacks, but I wasn’t fully satisfied with the results. It was error-prone, and I felt I was chasing shadows.

Next, I had a look at third-party packages. I found Audio Switcher, which looked very promising. Turns out, it’s the exact library I needed, as it exposes the complexities of the Win32 audio subsystem with very nice abstraction levels. To get the default communications device, only a single line of code is required:

CoreAudioDevice device = new CoreAudioController().DefaultCaptureCommunicationsDevice;

I tested the library against my hypothesis, that my presence would be busy, if the microphone (of a communications device on my PC) is in use. But if it’s in use, and muted, I should still be busy. And this library is clever enough to distinguish for that. I ended up using a simple if-clause, such as:

if (device.SessionController.ActiveSessions().Count() > 0)
{
// set status to busy
}

Connecting with Philips Hue

Then it was time to have a look at Philips Hue. It’s a great system, even if it’s not cheap. You deploy a Philips Hue Bridge on the network, and it then controls and bridges all the wireless lights together. One bridge can control up to 50 lights, in any room within the house. Assuming you have wireless connectivity in those areas.

My current Philips Hue system includes 7 or 8 lights, several dimmer switches for controlling them and one motion controller. The motion controller I use at the home office to turn on the lights when I walk in.

Philips has documented the API well, here is the getting started page. What I found very useful is the debug tool, that allows you to try out the REST API without any coding. It’s accessible on the bridge under /debug/clip.html.

To get started you’ll need to generate an authentication key. To do this, call the /api base URL, and post a body with

{"devicetype": "PresenceLight"}

This will generate a username, that you can then use for subsequent calls. What I also found useful was to call /api/<USERNAME>/lights to see all available lights, and their details. Here’s a snippet of one light and its details returned through the API:

"1": {
		"state": {
			"on": false,
			"bri": 254,
			"ct": 343,
			"alert": "none",
			"colormode": "ct",
			"mode": "homeautomation",
			"reachable": true
		},
		"swupdate": {
			"state": "noupdates",
			"lastinstall": "2020-03-10T12:14:02"
		},
		"type": "Color temperature light",
		"name": "Dining room",
		"modelid": "LTP003",
		"manufacturername": "Signify Netherlands B.V.",
		"productname": "Hue ambiance pendant",
		"capabilities": {
			"certified": true,
			"control": {
				"mindimlevel": 100,
				"maxlumen": 3000,
				"ct": {
					"min": 153,
					"max": 454
				}
			},
			"streaming": {
				"renderer": false,
				"proxy": false
			}
		},
		"config": {
			"archetype": "pendantround",
			"function": "functional",
			"direction": "downwards",
			"startup": {
				"mode": "safety",
				"configured": true
			}
		},
		"uniqueid": "id",
		"swversion": "1.50.2_r30933",
		"swconfigid": "id",
		"productid": "ENA_LTP003_1_FairPendant_v1"
	}

That’s a pendant light hanging over the dining room table. The Dutch sure know how to build proper home automation devices!

Thankfully, a third-party package already exists for managing the Hue lights. It’s called Q42.HueApi, and it’s easy to use.

Choosing the environment

I now have a way to detect when I’m busy, and I have an approach to reflect that via the Philips Hue. I spent several hours on this next task in choosing the environment. The options I fiddled with included

  • A custom REST API running in Azure
  • A custom REST API running locally, with control logic in Azure
  • A WPF app
  • A command-line tool
  • A Win32 (WinForms) app

I was initially drawn to the idea of having all possible logic in Azure, pinging my local Windows 10 box to detect if a microphone was being used. This way I could control the Philips Hue from the cloud, instead of running everything locally.

I build a custom REST API for this, and got it working. I spend an extra 30 minutes fiddling with IIS, identities, deployment issues and .NET Core issues. In the end I was unable to reliably detect the correct identity within the IIS Application Pool, and even then it always incorrectly detected the microphone.

So, I scratched the REST API approach altogether. Too many moving parts for something simple I needed. A WPF app sounded interesting, but having just finished my temperature WPF app, I knew that scheduling this would be painful. Updating the status every 30 minutes would not suffice, as I wanted to ping the microphone every 5 seconds to be as near realtime as needed.

I then first built a command-line tool, and converted that to a Win32 (WinForms) app.

This is how the app looks:

It’s running locally in the background, and takes about 36 MB of RAM:

It’s very barebones, and perhaps I’ll need to polish it more in the future. For now, what the app will do is:

  • Ping the active audio device every 5 seconds
  • If it’s in use (any active sessions?), connect with Philips Hue Bridge
  • Set a specific light to red
  • Once the session ends, turn off the light

Writing the Win32 app

In order for the Win32 app to have a loop, often called a game loop, I needed to employ the BackgroundWorker. This ensures that my app is not blocking the UI thread, thus I can ping the microphone without freezing my app or other logic.

In my main form, I’ll initialize the BackgroundWorker first:

BackgroundWorker backgroundWorker1 = new BackgroundWorker();

backgroundWorker1.WorkerReportsProgress = true;
backgroundWorker1.WorkerSupportsCancellation = true;
backgroundWorker1.DoWork +=
               new DoWorkEventHandler(backgroundWorker1_DoWork);
backgroundWorker1.RunWorkerAsync();

Key here is backgroundWorker1_DoWork(), which will implement the actual logic of the solution. Let’s take a look at this next.

Within this method, I’ll instantiate a worker thread, and connect to my audio device and the Philips Hue. It’s possible to locate the bridge automatically, but I’m connecting to it directly as it has a fixed IP in my network:

CoreAudioDevice device = new CoreAudioController().DefaultCaptureCommunicationsDevice;
LocalHueClient client = new LocalHueClient("IP.ADDRESS.GOES.HERE");
client.Initialize("USER");

To debug this, especially with threading, I created a very small helper-method that allows me to log entries to the app:

private void log(string msg)
{
	if (textBox1.InvokeRequired == true)
	{
		textBox1.Invoke((MethodInvoker)delegate { textBox1.Text += DateTime.Now.ToString() + ": " + msg + Environment.NewLine; });
	}
}

I also added a button to just turn everything off, in case things fail:

private void bOff_Click(object sender, EventArgs e)
{
	LocalHueClient client = new LocalHueClient("IP.ADDRESS.HERE");
	client.Initialize("USER");

	var command = new LightCommand();
	command.On = true;
	command.TurnOff();
	client.SendCommandAsync(command, new List<string> { "8" });

	setColor(Color.White); 
}

You can see from this method that I’m instantiating a LightCommand, which allows me to fiddle with the lights.

I also added a setColor() helper method, again to fight the threading issue, that let’s me specify what color I want to set the Philips Hue lights:

private void setColor(Color color)
{
	if (pLight.InvokeRequired == true)
	{
		pLight.Invoke((MethodInvoker)delegate { pLight.BackColor = color; });
	}
}

Philips Hue has an.. interesting way of specifying the colors. I found out later that Q42 has a separate package for figuring that out, too. But I’m fine with just one color – red.

Next, in my main loop, I define a boolean to check if I did set the light on previously. This saves me one extra cycle when I don’t need to ping the Hue Bridge.

bool red = false;

And finally, here’s the main loop that runs within the app:

while (true)
{
	if (device.IsDefaultCommunicationsDevice)
	{
		if (device.SessionController.ActiveSessions().Count() > 0)
		{
			//log("Active session found: " + device.SessionController.ActiveSessions().Count());

			if (!red)
			{
				log("  Turning busylight ON");
				var command = new LightCommand();
				command.On = true;
				command.TurnOn().SetColor(1, 0);
				client.SendCommandAsync(command, new List<string> { "8" });
				red = true;

				setColor(Color.Red);
			}
			else
			{
				//log("  Busylight already turned ON -- no actions taken");
				setColor(Color.Red);
			}
		}
		else
		{
			if (red)
			{
				log("  Turning busylight OFF");

				var command = new LightCommand();
				command.On = true;
				command.TurnOff();
				client.SendCommandAsync(command, new List<string> { "8" });

				red = false;
				setColor(Color.Green);
			}
		}

		Thread.Sleep(5000);
	}
	else
	{
		setColor(Color.Green);
	}

It’s not too complex, and I later realized it has a bug, too. But more on that in a sec. It first check that the primary communications device is found (device.IsDefaultCommunicationsDevice). Then, if there are active sessions (device.SessionController.ActiveSessions().Count()), we know that we should be busy.

If the light hasn’t been set to red (!red), we’ll simply create a LightCommand, and call one of our lights. For me, that light is number 8.

If there are no active sessions (else-loop), we clean up by turning off the light. Finally, we’ll put the thread to sleep for 5 seconds.

So, what’s the bug? The issue is with instantiating the active device before the loop. If I then change the device to something else – say, from a headset to a proper mic, the app doesn’t detect that change. This is something I’ll fix at some point along with other changes.

Selecting the Philips Hue light

I had plenty of Philips Hue lights already, mostly lightbulbs. I wanted to have a light turn red close to my home office door so that my family can easily see if I’m busy. Philips has a lot of lights to choose from, and finally, I settled on the LightStrip Plus. You can buy one, and then extend that with 1-meter extension pieces. The default length is 2 meters. The cost for the LightStrip Plus and the 1-meter extension is about 76 € ($85) together.

I installed the light on one of the doors to my home office. This is the light when it’s turned off:

And when I’m set to busy:

Outside:

In conclusion

This was yet another fun little project, that scratched my own itch. I felt a bit bad about not utilizing Azure here, as I was really looking forward to hosting a custom API that connects back to Azure. Then, reflecting on this omission I realized that since all components of this solution reside within my office, that Azure brings very little value in the end to this.