Sunday 17 January 2021

User Presence Detection using Node-Red and Unifi


A big challenge in Home Automation is implementing a reliable mechanism for presence detection - figuring out who's home, and tailoring logic accordingly.

Here's how I do it using Node-Red, Unifi and MQTT.

Many approaches to presence detection rely on interacting with the device itself, either by way of software running on the mobile handset, or having the mobile ping or poll an external service. None of these approaches really appealed to me as they are fiddly, rely on handset configuration & maintenance and need to be reset every time someone gets a new phone.

The approach I've taken here is to rely on the fact that all the families' mobile phones are configured to connect to home WiFi (to save on data charges!). This method interrogates the home network to see what phones are present - a good indication that the person is home.

In short, I have a Node-Red flow that polls the network on a schedule, looks for named devices, and updates presence status in MQTT based on that.

I could have used a database, or flat file storage for storing state, but as I have an MQTT broker running anyway, it's a convenient and flexible approach for me.

UNIFI

The first step is to configure the network to allow all of this to work. I run a Unifi set-up comprising multiple switches, a USG and 4 wireless access points. My Unifi controller lives as a docker on my unRaid server. For this to work, the Unifi controller must be running, but in my case it is 24/7, so box 1 is ticked.

Within Unifi, it's possible to list clients by connection type, so my wireless device list looks like this;



What's important here is that each mobile device to be used for presence detection has a unique name as this string is used later to identify the device.

This can be configured in the device 'general tab in Unifi;




NODE-RED

The first step here is to install the node-red-contrib-unifi node. This provides access to a number of interfaces with the Unifi system, particularly the 'ClientDevices' command, which lists all connected devices.

Here is the main flow in Node-red;



The inject node is configured to run every 5 minutes. This triggers the Unifi node which is configured to retrieve connected devices as follows;



This list is passed to a function node that parses the list for the named devices;





This simply compiles a list of devices to check for, checks the incoming list and sends out the topic string of the people array along with a true/false status indicating whether the associated device had been found.

Now, if users ever change their devices, or I need to add a new device, I can simply edit this code block and all will be well in the world.

Note that there is an extra little piece of code here dealing with timing. I picked this up off the web and figured I didn't need this as Unifi does a good job of keeping track of connected devices and dropping them off when not found.

To give a better idea of what this looks like, heres the debug messaging generated. Two devices are home, two are not. (This is accurate; right now, I'm home typing this and my son is enjoying a lazy Sunday lie-in. My wife is out walking the dog and my daughter is at work).



These outputs are structured as topics as they are pushed to MQTT. The rbe node prevents updates if status is unchanged;


While the mqtt node updates the topic in the broker;


And that's fundamentally it. Any application can now query or receive updates from MQTT with individual presence status. I use this in Node-Red to update a Dashboard;


This takes the topic update from MQTT, uses a change node to set the formatting based on true/false status, and pushes the update to the dashboard.




Et voila - reliable presence detection, or as reliable as I need. I had read that iPhones in particular were tricky for this as they go into a low power mode, but the setup as described has no trouble keeping track of the status of Apple devices just as well as android.

As an aside, in the time I've been writing this, wife and dog returned from their walk and the dashboard updated pretty much immediately;




Side Note: In writing this post, I realised that my son's recently acquired iWatch is now also showing up, so I'll need to enhance this to take account of that device. But it poses a dilemma - should be be home if both phone and watch are present, or rely on one device only?


UPDATE

As requested in comments, here's an anonymised version of the flows. Note that there's a dependency on having an MQTT broker. This could be re-worked to function entirely in Node-Red, but MQTT allows for other applications and services to access the user states as well;



[{"id":"3d6e9c4a.9cdc04","type":"Unifi","z":"e0922890.07e998","name":"get active devices","ip":"192.168.1.199","port":8443,"site":"default","command":"20","x":870,"y":240,"wires":[["ee630cd6.2aba8"]]},{"id":"b8015ccc.430c9","type":"inject","z":"e0922890.07e998","name":"run every 5 mins","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"300","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":650,"y":240,"wires":[["3d6e9c4a.9cdc04"]]},{"id":"ee630cd6.2aba8","type":"function","z":"e0922890.07e998","name":"check presence","func":"const lastSeenSeconds = 20;\nlet presenceCutoff = (new Date() - (lastSeenSeconds * 1000)) / 1000; \nconst people = {\n    \"presence/user1\": \"user1iPhone\",\n    \"presence/user2\": \"user2Xperia\",\n     \"presence/user3\": \"user3Xperia\",\n    \"presence/user4\": \"Duser4iPhone\",\n};\n\n\nreturn  Object.keys(people).map(function(topic) {\n    //let devices = msg.payload[0].filter(device => device.name === people[topic] && device.last_seen > presenceCutoff);\n    let devices = msg.payload[0].filter(device => device.name === people[topic]);\n\n    return {\n      topic: topic,\n      retain: true,\n      payload: devices.length > 0\n    };\n});\n\n","outputs":4,"noerr":0,"initialize":"","finalize":"","x":1080,"y":240,"wires":[["dbe23e68.45c3","7934121e.fc3aac"],["dbe23e68.45c3","7934121e.fc3aac"],["dbe23e68.45c3","7934121e.fc3aac"],["dbe23e68.45c3","7934121e.fc3aac"]],"outputLabels":["joep presence","monique presence","",""]},{"id":"e0c8176d.7410d8","type":"mqtt out","z":"e0922890.07e998","name":"mqtt: change presence","topic":"","qos":"0","retain":"true","broker":"fee361e0.b1e17","x":1450,"y":240,"wires":[]},{"id":"96c68931.446538","type":"comment","z":"e0922890.07e998","name":"check unifi for presence of phones","info":"","x":680,"y":200,"wires":[]},{"id":"7934121e.fc3aac","type":"rbe","z":"e0922890.07e998","name":"","func":"rbe","gap":"","start":"","inout":"out","property":"payload","x":1250,"y":240,"wires":[["e0c8176d.7410d8"]]},{"id":"dbe23e68.45c3","type":"debug","z":"e0922890.07e998","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":1230,"y":140,"wires":[]},{"id":"439195c3.4c403c","type":"ui_text","z":"e0922890.07e998","group":"d86c0487.5aa878","order":0,"width":0,"height":0,"name":"","label":"User1","format":"{{msg.payload}}","layout":"row-left","x":1030,"y":360,"wires":[]},{"id":"56e90f9d.dee72","type":"mqtt in","z":"e0922890.07e998","name":"presence/user1","topic":"presence/user1","qos":"0","datatype":"auto","broker":"fee361e0.b1e17","x":650,"y":360,"wires":[["8527e24b.d15f8"]]},{"id":"25404c61.4c78d4","type":"mqtt in","z":"e0922890.07e998","name":"presence/user2","topic":"presence/user2","qos":"0","datatype":"auto","broker":"fee361e0.b1e17","x":660,"y":420,"wires":[["ad98ccd8.78574"]]},{"id":"f48d4f0e.53dd","type":"mqtt in","z":"e0922890.07e998","name":"presence/user3","topic":"presence/user3","qos":"0","datatype":"auto","broker":"fee361e0.b1e17","x":680,"y":480,"wires":[["bb002127.c3376"]]},{"id":"92794687.d5e118","type":"mqtt in","z":"e0922890.07e998","name":"presence/user4","topic":"presence/user4","qos":"0","datatype":"auto","broker":"fee361e0.b1e17","x":690,"y":540,"wires":[["134da60c.97337a"]]},{"id":"9f217ff4.0b68c","type":"ui_text","z":"e0922890.07e998","group":"d86c0487.5aa878","order":0,"width":0,"height":0,"name":"","label":"User2","format":"{{msg.payload}}","layout":"row-left","x":1030,"y":420,"wires":[]},{"id":"e42147d3.0ecf08","type":"ui_text","z":"e0922890.07e998","group":"d86c0487.5aa878","order":0,"width":0,"height":0,"name":"","label":"User3","format":"{{msg.payload}}","layout":"row-left","x":1050,"y":480,"wires":[]},{"id":"44adb680.16ba08","type":"ui_text","z":"e0922890.07e998","group":"d86c0487.5aa878","order":0,"width":0,"height":0,"name":"","label":"User4","format":"{{msg.payload}}","layout":"row-left","x":1070,"y":540,"wires":[]},{"id":"8527e24b.d15f8","type":"change","z":"e0922890.07e998","name":"","rules":[{"t":"change","p":"payload","pt":"msg","from":"true","fromt":"str","to":"<font color = \"green\" i  class=\"fa fa-check\"></i>","tot":"str"},{"t":"change","p":"payload","pt":"msg","from":"false","fromt":"str","to":"<font color = \"red\" i  class=\"fa fa-times\"></i>","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":840,"y":360,"wires":[["439195c3.4c403c"]]},{"id":"ad98ccd8.78574","type":"change","z":"e0922890.07e998","name":"","rules":[{"t":"change","p":"payload","pt":"msg","from":"true","fromt":"str","to":"<font color = \"green\" i  class=\"fa fa-check\"></i>","tot":"str"},{"t":"change","p":"payload","pt":"msg","from":"false","fromt":"str","to":"<font color = \"red\" i  class=\"fa fa-times\"></i>","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":860,"y":420,"wires":[["9f217ff4.0b68c"]]},{"id":"bb002127.c3376","type":"change","z":"e0922890.07e998","name":"","rules":[{"t":"change","p":"payload","pt":"msg","from":"true","fromt":"str","to":"<font color = \"green\" i  class=\"fa fa-check\"></i>","tot":"str"},{"t":"change","p":"payload","pt":"msg","from":"false","fromt":"str","to":"<font color = \"red\" i  class=\"fa fa-times\"></i>","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":880,"y":480,"wires":[["e42147d3.0ecf08"]]},{"id":"134da60c.97337a","type":"change","z":"e0922890.07e998","name":"","rules":[{"t":"change","p":"payload","pt":"msg","from":"true","fromt":"str","to":"<font color = \"green\" i  class=\"fa fa-check\"></i>","tot":"str"},{"t":"change","p":"payload","pt":"msg","from":"false","fromt":"str","to":"<font color = \"red\" i  class=\"fa fa-times\"></i>","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":900,"y":540,"wires":[["44adb680.16ba08"]]},{"id":"fee361e0.b1e17","type":"mqtt-broker","name":"mqtt local broker","broker":"192.168.1.199","port":"1883","clientid":"presencedetector","usetls":false,"compatmode":true,"keepalive":"60","cleansession":false,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"d86c0487.5aa878","type":"ui_group","name":"Who's Home?","tab":"a404e75d.99def8","disp":true,"width":"6","collapse":true},{"id":"a404e75d.99def8","type":"ui_tab","name":"Home","icon":"dashboard","disabled":false,"hidden":false}]


6 comments:

Unknown said...

This is great! Would you be so kink as to post the flow?

Unknown said...

Please post you node-red flow, this is great!

Unknown said...

Very thankful for you write-up and providing the flow! thxxxxxx!!!

BigWheels said...

Could this also be re-worked to identify which AP you were connected to (in the case of multiple APs) and then use this in a home automation scenario to automate turning on/off devices?

MediaServer8 said...

@BigWheels, That should be possible. I cannot check right now, though, as my automation is broken. I've updated to UniFi v7 and the node I'm using only supports up to v6. I'm getting an api error :-(

I've asked on github if there are any plans to support v7. If it gets fixed, I'll add clarification on your query.

Findit said...

Using Node Red and Unifi to do presence detection has been quite frustrating.

I'm using the latest version of the Node Red, the Unifi controller (via Docker), and node-red-contrib-unifi.


After some troubleshooting I was able to get information out of all the functions, BUT:

- `AllUsers` gives back all devices that have ever been seen, but it's `last_seen` is wrong
- `ClientDevices` gives back all devices that are currently connected and the `last_seen` is correct, HOWEVER devices drop off this list the moment they disconnect.

That means that neither option is viable if you want a buffer of time between `present` and `not present`, and unfortunately as I've found out: many phones constantly disconnect from wifi for a few seconds to save power and you therefore need that buffer for reliable presence detection.