Unifi API client with Node-RED integration

Table of Contents:

Intro

This is a follow-up to my previous article. If you’re in the home automation business like me, you strive for reliable solutions under personal controll and try to prevent half-baked solutions (aka “hacks”) where you need to have luck to get it working for the time being.

My requirement is the following: I have my Node-RED workflow for controlling my lights in my home. All I want is to know if a specific device (= MAC address) is connected to my Unifi WiFi and based on that information, automatically turn on/off specific lights. Some now say: that’s easy, just integrate your Unifi Controller / Network Application with Node-RED and read the corresponding state. Yeah, a possible way, but to my taste a little bit too much dependencies on external plugins and libraries. This should always be a set-and-forget setup, you don’t want to mess around with stuff if after 6 months someone decided to change small part of some code and breaks my setup.

The setup

So I decided to be more flexible and autonomous, at least a little bit, and build my own Unifi API client with which I can fetch the current device presence. This is done with a small Flask application running as a Docker container. All I need to do is to execute a GET request to the app and get a true or false, depending if the device is connected to the WiFi.

$ curl -s http://192.168.199.51 | jq .
{
  "philipp": true,
  "jessica": false
}

You see that I don’t mess around with displayed MAC addresses or other useless information - I just see if the (pre-defined) device of Jessica is connected or not. Great!

Build Flask app

I’ve built the following Flask app for my needs. The requirement was also to be as flexible as possible in terms of which devices are fetched from my Unifi Network Application.

requirements.txt (Yes, I’m still using Flask version 2)

Flask==2.2.2
pyunifi==2.21
urllib3==1.26.11
Werkzeug==2.2.2

app.py

from flask import Flask
from pyunifi.controller import Controller
import urllib3, json, os
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

app = Flask(__name__)

@app.route('/')
def example():
    # Parse the ENTITIES environment variable into a list of dictionaries
    entities = json.loads(os.environ.get('ENTITIES', '[]'))

    # Initialize the payload dictionary with False values for each entity
    payload = {entity['name']: False for entity in entities}

    # Initialize the controller connection
    c = Controller(
        host=os.environ.get('CONTROLLER_HOST'),
        port=os.environ.get('CONTROLLER_PORT'),
        username=os.environ.get('CONTROLLER_USERNAME'),
        password=os.environ.get('CONTROLLER_PASSWORD'),
        ssl_verify=False
    )

    # Fetch all currently connected clients
    clients = c.get_clients()
    
    # Check if the desired devices are part of the client list, if so change the entity value to `True`
    for client in clients:
        for entity in entities:
            if entity['mac'] in client['mac']:
                payload[entity['name']] = True
            
    return payload

from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)

if __name__ == '__main__':
    app.run(host="0.0.0.0", port=8000)

Let’s dig a little bit deeper into the code itself.

  • I’m using the pyunifi package from Caleb Dunn to connect to the Unifi Network Application
  • Connection information are not hardcoded, but passed via environment variables - which we’ll look into in a minute
  • The entities (= desired devices) are also passed via an environment variable

To build and run this app, define the following Dockerfile:

FROM python:3.11.1-slim-buster
WORKDIR /
COPY requirements.txt .
RUN pip3 install -r requirements.txt
COPY app.py /
CMD python3 /app.py

build the container locally:

docker build --no-cache --tag local/unifi-api-client:latest .

and run it:

docker run -d \
  --name unifi-api-client \
  -p 192.168.199.51:80:8000 \
  -e CONTROLLER_HOST=192.168.199.50 \
  -e CONTROLLER_PORT=8443 \
  -e CONTROLLER_USERNAME=admin \
  -e CONTROLLER_PASSWORD='ThisIsMySuperSecurePassword!' \
  -e ENTITIES='[{"name": "philipp", "mac": "5b:6c:23:6a:c2:bc"}, {"name": "jessica", "mac": "bd:19:35:c4:43:61"}]' \
  --restart unless-stopped \
  local/unifi-api-client:latest

All -e parameters are environment variables which are set for the container. You will need to adapt the IP address (if you want to specify it at all), the Unifi connection information and off course the ENTITIES setting, which are the devices/MAC addresses you want to look up. Since I’m using a single big Docker host with virtual IP interfaces, I specified a specific IP via the -p statement. This is optional, you can just use -p 80:8000 \.

If you now do a curl (or browse) to http://192.168.199.51 you should get a json dict as response (like the one above).

Integrate it with Node-RED

Integration is done with a HTTP request node and a switch node:

node-red-01

node-red-02

node-red-03

That’s it!

If you like posts like that and want to support me, some sats (see my about page) are highly appreciated 🧡