[Guide]Unifi device tracker / presence detection with Node Red

Caution
Unifi controller is very buggy. It is not stable at all when checking events or what devices are connected to the network. It can work in one setting, but after a firmware upgrade it can break, or after changing some setting, adding a device, or the neighbour adds a stronger wireless network.
I see now that I can’t make a good enough solution to handle all these abnormalities, but this might work for you, just test it and see. (This also apply for the Unifi integration in home assistant)

Introduction
I tried the buildt in Unifi intergration, but I felt it was very messy and I did not want to spend time cleaning everything up.
So I made a Node Red flow that makes it a bit easier to track connected devices through Unifi.
I only use this for presence detection.

Prerequisites
-Node Red
-node-red-contrib-unifi palette in Node Red
-Unifi controller (unsure of version but something new)
-A person in Home Assistant that you want to track

Note
I chose not to use a admin account in Unifi to connect the palette in Node Red to Unifi. But this only works in some cases where you don’t need to write information to Unifi. And in this case we only read.

How it works
Ok, here is my flow. I also attached an image and explanation for others to easier understand. (For me it’s difficult to get an understanding just from the flow-code)

My thought was to get some presence detection that was using the alerts rather than what clients were on the network (of experience the reporting of what clients are on the network is not that good).
So I am polling the unifi controller every 30 seconds, but limiting the number of alerts to 10.
Then I have to split the payload since it is in two arrays.
Then I check and split on those users I am interested in.
Then I only allow new alerts to pass through, drop messages that has already been handled.
Then I check if the alert is about user connected or user disconnected from the network.
And lastly I update the person entity in home assistant.

I have written details on each node to help understanding.

flow
[
    {
        "id": "26e3101e.d5bb6",
        "type": "tab",
        "label": "Presence detection",
        "disabled": false,
        "info": ""
    },
    {
        "id": "846b66f2.1abbd8",
        "type": "Unifi",
        "z": "26e3101e.d5bb6",
        "name": "Unifi controller",
        "ip": "192.168.1.15",
        "port": 8443,
        "site": "7yxxx4",
        "command": "90",
        "unifios": false,
        "x": 480,
        "y": 60,
        "wires": [
            [
                "4cb5c6dd.e5bb88"
            ]
        ],
        "info": "Polls the unifi controller to get the events."
    },
    {
        "id": "240ce31b.ff926c",
        "type": "inject",
        "z": "26e3101e.d5bb6",
        "name": "",
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "repeat": "30",
        "crontab": "",
        "once": true,
        "onceDelay": "10",
        "x": 150,
        "y": 60,
        "wires": [
            [
                "4894226c.0c0cfc"
            ]
        ],
        "info": "**Change this depending on how quickly you want to update the presence detection. Be aware that the next node (limit node) might need to be changed.**\n\nDictates how often the unifi controller should be polled to get events.\n"
    },
    {
        "id": "4cb5c6dd.e5bb88",
        "type": "split",
        "z": "26e3101e.d5bb6",
        "name": "split payload",
        "splt": "\\n",
        "spltType": "str",
        "arraySplt": 1,
        "arraySpltType": "len",
        "stream": false,
        "addname": "",
        "x": 130,
        "y": 140,
        "wires": [
            [
                "25fd3a0c.ef9e16"
            ]
        ],
        "info": "Splits the payload since it comes baked in two arrays."
    },
    {
        "id": "c5d30dcd.8f1ad",
        "type": "ha-api",
        "z": "26e3101e.d5bb6",
        "name": "Jay Home",
        "server": "d30490a3.29bd",
        "debugenabled": false,
        "protocol": "http",
        "method": "post",
        "path": "/api/states/person.jay",
        "data": "{\"state\":\"home\"}",
        "dataType": "json",
        "location": "payload",
        "locationType": "msg",
        "responseType": "json",
        "x": 510,
        "y": 320,
        "wires": [
            []
        ],
        "info": "Update the entity with the new state."
    },
    {
        "id": "946fda79.25d1b8",
        "type": "ha-api",
        "z": "26e3101e.d5bb6",
        "name": "Jay Away",
        "server": "d30490a3.29bd",
        "debugenabled": false,
        "protocol": "http",
        "method": "post",
        "path": "/api/states/person.jay",
        "data": "{\"state\":\"not_home\"}",
        "dataType": "json",
        "location": "payload",
        "locationType": "msg",
        "responseType": "json",
        "x": 510,
        "y": 380,
        "wires": [
            []
        ],
        "info": "Update the entity with the new state."
    },
    {
        "id": "a8bb2c5.ddafbd",
        "type": "ha-api",
        "z": "26e3101e.d5bb6",
        "name": "bob Home",
        "server": "d30490a3.29bd",
        "debugenabled": false,
        "protocol": "http",
        "method": "post",
        "path": "/api/states/bob",
        "data": "{\"state\":\"home\"}",
        "dataType": "json",
        "location": "payload",
        "locationType": "msg",
        "responseType": "json",
        "x": 510,
        "y": 440,
        "wires": [
            []
        ],
        "info": "Update the entity with the new state."
    },
    {
        "id": "3f372fc4.a8c7b",
        "type": "ha-api",
        "z": "26e3101e.d5bb6",
        "name": "bob Away",
        "server": "d30490a3.29bd",
        "debugenabled": false,
        "protocol": "http",
        "method": "post",
        "path": "/api/states/bob",
        "data": "{\"state\":\"not_home\"}",
        "dataType": "json",
        "location": "payload",
        "locationType": "msg",
        "responseType": "json",
        "x": 510,
        "y": 500,
        "wires": [
            []
        ],
        "info": "Update the entity with the new state."
    },
    {
        "id": "25fd3a0c.ef9e16",
        "type": "split",
        "z": "26e3101e.d5bb6",
        "name": "split payload",
        "splt": "\\n",
        "spltType": "str",
        "arraySplt": 1,
        "arraySpltType": "len",
        "stream": false,
        "addname": "",
        "x": 290,
        "y": 140,
        "wires": [
            [
                "b844443d.ab9398"
            ]
        ],
        "info": "Splits the payload since it comes baked in two arrays."
    },
    {
        "id": "7be51d21.e7a8f4",
        "type": "switch",
        "z": "26e3101e.d5bb6",
        "name": "Connected",
        "property": "payload.key",
        "propertyType": "msg",
        "rules": [
            {
                "t": "eq",
                "v": "EVT_WU_Connected",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "EVT_WU_Roam",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "EVT_WU_Disconnected",
                "vt": "str"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 3,
        "x": 130,
        "y": 360,
        "wires": [
            [
                "a4031b6d.90bf08"
            ],
            [
                "a4031b6d.90bf08"
            ],
            [
                "93896440.70e028"
            ]
        ],
        "info": "Checks if the message says the user is connected or now. Also checks for roaming since sometimes the unifi controller doesn't give a event about connecting, just roaming."
    },
    {
        "id": "2df48fcc.c284e",
        "type": "switch",
        "z": "26e3101e.d5bb6",
        "name": "Connected",
        "property": "payload.key",
        "propertyType": "msg",
        "rules": [
            {
                "t": "eq",
                "v": "EVT_WU_Connected",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "EVT_WU_Roam",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "EVT_WU_Disconnected",
                "vt": "str"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 3,
        "x": 130,
        "y": 420,
        "wires": [
            [
                "f0fa7c22.952a8"
            ],
            [
                "f0fa7c22.952a8"
            ],
            [
                "d58f027a.3b668"
            ]
        ],
        "info": "Checks if the message says the user is connected or now. Also checks for roaming since sometimes the unifi controller doesn't give a event about connecting, just roaming."
    },
    {
        "id": "4894226c.0c0cfc",
        "type": "function",
        "z": "26e3101e.d5bb6",
        "name": "Limit 10",
        "func": "msg.payload = { command: \"events\", limit: \"10\"}; \nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 300,
        "y": 60,
        "wires": [
            [
                "846b66f2.1abbd8"
            ]
        ],
        "info": "**Change the number to something reasonable relative to how quickly you poll the unifi controller and how often new events are created on the unifi controller**\n\nManipulates the poll request that will be done to the unifi controller so we only get a limited number of events from the controller.\n\nThis is very important to reduce the load on both the unifi controller and the flow.\n\n"
    },
    {
        "id": "8651b9bc.134828",
        "type": "delay",
        "z": "26e3101e.d5bb6",
        "name": "only newest",
        "pauseType": "rate",
        "timeout": "5",
        "timeoutUnits": "seconds",
        "rate": "1",
        "nbRateUnits": "2",
        "rateUnits": "second",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": true,
        "x": 310,
        "y": 200,
        "wires": [
            [
                "3a0a83e1.4f2c5c"
            ]
        ],
        "info": "Limit rates the incoming messages, allows only the first (newest) message to pass, then drops all the older messages."
    },
    {
        "id": "76bb5eda.f792c",
        "type": "delay",
        "z": "26e3101e.d5bb6",
        "name": "only newest",
        "pauseType": "rate",
        "timeout": "5",
        "timeoutUnits": "seconds",
        "rate": "1",
        "nbRateUnits": "2",
        "rateUnits": "second",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": true,
        "x": 310,
        "y": 240,
        "wires": [
            [
                "58dd994.d073f68"
            ]
        ],
        "info": "Limit rates the incoming messages, allows only the first (newest) message to pass, then drops all the older messages."
    },
    {
        "id": "b844443d.ab9398",
        "type": "switch",
        "z": "26e3101e.d5bb6",
        "name": "Users mac",
        "property": "payload.user",
        "propertyType": "msg",
        "rules": [
            {
                "t": "eq",
                "v": "xxx",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "xxx",
                "vt": "str"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 2,
        "x": 130,
        "y": 220,
        "wires": [
            [
                "8651b9bc.134828"
            ],
            [
                "76bb5eda.f792c"
            ]
        ],
        "info": "**Change this to reflect what users you want to track.**\n\nSplits and filters the messages depending on what user (mac address) you want to use later in the flow."
    },
    {
        "id": "a4031b6d.90bf08",
        "type": "api-current-state",
        "z": "26e3101e.d5bb6",
        "name": "Already home",
        "server": "d30490a3.29bd",
        "version": 1,
        "outputs": 2,
        "halt_if": "home",
        "halt_if_type": "str",
        "halt_if_compare": "is",
        "override_topic": false,
        "entity_id": "person.jay",
        "state_type": "str",
        "state_location": "payload",
        "override_payload": "msg",
        "entity_location": "data",
        "override_data": "msg",
        "blockInputOverrides": false,
        "x": 320,
        "y": 320,
        "wires": [
            [],
            [
                "c5d30dcd.8f1ad"
            ]
        ],
        "info": "Don't bother to update the entity if it already has the correct state."
    },
    {
        "id": "f0fa7c22.952a8",
        "type": "api-current-state",
        "z": "26e3101e.d5bb6",
        "name": "Already home",
        "server": "d30490a3.29bd",
        "version": 1,
        "outputs": 2,
        "halt_if": "home",
        "halt_if_type": "str",
        "halt_if_compare": "is",
        "override_topic": false,
        "entity_id": "person.bob",
        "state_type": "str",
        "state_location": "payload",
        "override_payload": "msg",
        "entity_location": "data",
        "override_data": "msg",
        "blockInputOverrides": false,
        "x": 320,
        "y": 440,
        "wires": [
            [],
            [
                "a8bb2c5.ddafbd"
            ]
        ],
        "info": "Don't bother to update the entity if it already has the correct state."
    },
    {
        "id": "93896440.70e028",
        "type": "api-current-state",
        "z": "26e3101e.d5bb6",
        "name": "Already away",
        "server": "d30490a3.29bd",
        "version": 1,
        "outputs": 2,
        "halt_if": "not_home",
        "halt_if_type": "str",
        "halt_if_compare": "is",
        "override_topic": false,
        "entity_id": "person.jay",
        "state_type": "str",
        "state_location": "payload",
        "override_payload": "msg",
        "entity_location": "data",
        "override_data": "msg",
        "blockInputOverrides": false,
        "x": 310,
        "y": 380,
        "wires": [
            [],
            [
                "946fda79.25d1b8"
            ]
        ],
        "info": "Don't bother to update the entity if it already has the correct state."
    },
    {
        "id": "d58f027a.3b668",
        "type": "api-current-state",
        "z": "26e3101e.d5bb6",
        "name": "Already away",
        "server": "d30490a3.29bd",
        "version": 1,
        "outputs": 2,
        "halt_if": "not_home",
        "halt_if_type": "str",
        "halt_if_compare": "is",
        "override_topic": false,
        "entity_id": "person.bob",
        "state_type": "str",
        "state_location": "payload",
        "override_payload": "msg",
        "entity_location": "data",
        "override_data": "msg",
        "blockInputOverrides": false,
        "x": 310,
        "y": 500,
        "wires": [
            [],
            [
                "3f372fc4.a8c7b"
            ]
        ],
        "info": "Don't bother to update the entity if it already has the correct state."
    },
    {
        "id": "3a0a83e1.4f2c5c",
        "type": "rbe",
        "z": "26e3101e.d5bb6",
        "name": "drop processed",
        "func": "deadband",
        "gap": "1",
        "start": "",
        "inout": "in",
        "property": "payload.time",
        "x": 520,
        "y": 200,
        "wires": [
            [
                "7be51d21.e7a8f4"
            ]
        ],
        "info": "Lets the first message come through, but if the next message is the same it drops this."
    },
    {
        "id": "58dd994.d073f68",
        "type": "rbe",
        "z": "26e3101e.d5bb6",
        "name": "drop processed",
        "func": "deadband",
        "gap": "1",
        "start": "",
        "inout": "in",
        "property": "payload.time",
        "x": 520,
        "y": 240,
        "wires": [
            [
                "2df48fcc.c284e"
            ]
        ],
        "info": "Lets the first message come through, but if the next message is the same it drops this."
    },
    {
        "id": "d30490a3.29bd",
        "type": "server",
        "z": "",
        "name": "Home Assistant"
    }
]

This is working quite good, the update time is under a minute (from the device connects to the wireless network).
Also in my flows, I choose to use nodes instead of code. I know how to write code, but I feel it looks cooler with the nodes, and it can also be easier for non-coding people to understand.

Let me know if something is unclear (I’m also open for suggestions for improvement)

Edit reason: Found some new way to improve it.
Edit reason: Unifi is buggy

3 Likes

Thanks for this flow. Started having issues with the integration not detecting state changes. I had to increase my event limit as I had a few things trigger causing it to miss the required devices within that 30 second window.

1 Like

Yeah it is not very stable at detecting. Hope it worked better when you increased the event limit.
I just had a problem where the unifi controller were giving disconnect/connect events instead of roaming. And that issue is only fixed with one of the latest beta firmwares.

Unifi integration started spazzing out over the last few weeks and this was incredibly easy to implement. So far, it has been rock solid. Thanks so much for developing and sharing!

1 Like

Hello everyone,
I upgraded to the new version HA , and after the update the automation does not work, can someone have a suitable solution?

is it possible to link the Unifi Insight neighboring wifi scanner to be used in Home Assistant??? It would be an excellent security tool to detect unknown people entering or passing by a wifi coverage area, for example near farms or industrial estates.
One could make automation for example alert when a new wifi device (person) detected during the night near a farm or factory

Hi.
I get an “ERR_BAD_REQUEST” from the Unifi Node when calling my UDM-PRO.
I have tried to find any helpful information on the library + error code but to no avail.
Any ideas?

I’m running UniFi OS UDM Pro 2.5.17
Thanks.