Unifi Access API

Hello
I don’t know if someone is working on this. But unifi released a new API to control doors get events etc.

For the moment I had some success in this little python script for testing:


import requests
import json
import time

def fetch_system_logs(host, token, topic, page_num=1, page_size=25, since=None, until=None, actor_id=None):
    url = f"{host}/api/v1/developer/system/logs"
    headers = {
        "Authorization": f"Bearer {token}",
        "accept": "application/json",
        "content-type": "application/json"
    }
    query_params = {
        "page_num": page_num,
        "page_size": page_size
    }
    if since:
        query_params["since"] = since
    if until:
        query_params["until"] = until
    body = {
        "topic": topic
    }
    if actor_id:
        body["actor_id"] = actor_id
    try:
        response = requests.post(url, headers=headers, params=query_params, json=body, verify=False)
        if response.status_code == 200:
            return response.json()
        else:
            return {"error": f"Failed to fetch logs, HTTP Status Code: {response.status_code}"}
    except requests.exceptions.RequestException as e:
        return {"error": f"An error occurred: {e}"}

def extract_user_info_from_logs(logs):
    user_activity = []
    if 'data' in logs and 'hits' in logs['data']:
        for log_entry in logs['data']['hits']:
            source = log_entry.get('_source', {})
            timestamp = log_entry.get('@timestamp', 'N/A')
            actor = source.get('actor', {})
            event = source.get('event', {})
            actor_name = actor.get('display_name', 'N/A')
            event_type = event.get('type', 'N/A')
            event_result = event.get('result', 'N/A')
            user_activity.append({
                'Timestamp': timestamp,
                'Name': actor_name,
                'Event': event_type,
                'Result': event_result
            })
    return user_activity

if __name__ == "__main__":
    host = "https://192.168.1.1:12445"  # IP most likely the same
    token = "token_here"  # here your actual token from the security tab of unifi Access webinterface
    topic = "door_openings" 

    while True: 
        logs = fetch_system_logs(host, token, topic)
        user_activity = extract_user_info_from_logs(logs)
        for activity in user_activity:
            print(f"Timestamp: {activity['Timestamp']}, Name: {activity['Name']}, Event: {activity['Event']}, Result: {activity['Result']}")
        
        time.sleep(10)


Still reading on what is the best way to use openhab to poll this API to get almost real time events seeing I don’t find any webhooks references in this document:
api_reference.pdf (580.8 KB)

Is the http binding the right tool for this job ?

You’re not going to get near realtime through polling from OH. The only way you’ll get near realtime is if Unifi can push the status to OH. I see no mechanism for it to do that from this API.

Given that you have this working in Python already, the easiest would be to use the Exec binding to call this script. Another approach would be to update this script to run as a service and push state updates to OH either through OH’s REST API or MQTT.

If you want to implement this as a native OH rule, you’ll want to use the HTTP binding and/or the sendHttpXRequest actions.

Thank you rlkoshak
I already posted on the unifi forums the lack of webhooks maybe in two years we will get another update.
For now I already did the same for wallbox pulsar API to integrate it openhab using an python library used also by homeassistant so maybe I will stick with this test grow it and add mqtt. As for the real time update I have a cunning plan because I have knx connected to the doorbel output and also the lock I can poll the api only then so hey presto real time update(I think). I will work on a proof of concept this would be nice with my alarm diy projects if I can poll the pin code entered. I don’t need to worry about ssl for the moment because it’s all local.
But would be cool to have an example of someone doing API using openhab without having to deal with java binding and someone developing one for simple task like this.

I do something similar with my router. I use the http actions in a (JSScripting) rule instead of the http binding. I chose this method for one primary reason: for dynamic header information such as a bearer token that changes the http binding isn’t great; it’s much easier to handle the authentication in the script. There’s a secondary reason about how I process the data I get from the polling but that’s specific to my system.

How do you deal with ssl because I cannot find anything to ignore it in the JavaScript docs ?

The HTTP binding supports HTTPS but the rule actions do not. Also, the usual JavaScript HTTP stuff like XMLHttpRequest are not available. You’ll have to use the raw Java Objects. See Honeywell Home API with openHAB using just rules for an example though be forewarned it’s pretty hackey and never did work well.

I need time to process that. Just for fun what are the consequences if I modify /etc/openhab/runtime.cfg with this

-Dcom.sun.net.ssl.checkRevocation=false
-Djava.security.egd=file:/dev/./urandom

It looks like it may make it ignore bad certificates but it’s not going to suddenly convert HTTPS to HTTP. You’ll still have to do the handshake and set up an encrypted SSL/TLS channel, it just wont refuse to do so when the certificate is revoked, self signed, or otherwise untrusted.

Unifi has an event driven api based on websockets. It has been reversed engineered. I utilize this api for events in the unifi protect binding for detecting motions in cameras and when you push the doorbell button. Feel free to look at the code for inspiration and how to decode the websocket frames.

As far as i know, the same api is used with the other unifi products as well, it’s not only limited to unifi protect.

Best regards, s

Well I am not an developer and I do use your binding to get events from some unifi cameras that I have. I am still trying to grasp the ropes of scripting languages. But probably in the end I will have to start something and see what happens.
Keep in mind I am just a simple electrician :slight_smile:

Hi!
Yes, then I understand. If you want to start develop some java-binding yourself (given if you have interest) then I can recommend to take a look at all the current bindings, it is not that hard and you don’t have to come up with everything yourself.

/S

Hello
Well I asked on the unifi forums for webhooks and actually they said they are working on it


Hi @stamate,

Webhook functionality will be available at a later stage as it's still under development.

So I don’t think it’s available the same way as unifi protect. For the moment I tried setting up eclipse took a whole day for it to startup and I got millions of errors and don’t know what to do next also the bloody thing is heavy takes 70 percent of the memory of the computer.

Hi!

Yes, might still be a websocket API you can use. Webhooks is something else.
https://hookdeck.com/webhooks/guides/when-to-use-webhooks

/S

Hello everyone,

I know I revive an old topic but it seems exactly relevant to this discussion. In case it is not the right way, let me know and I’ll post a new topic.

Unifi Access now has an API. It is available in a document from the Unifi Access application.

It does support webhooks, and the description of the payload that will be passed is like below (using one example associated to the door unlock event).

I was wondering what was the best way to catch this and process this in openHab? Is it the “webhook” addon as per this topic : [webhook] New, very simple binding for listening incomming http requests

Or is it the HTTP Binding? But I had the impression that this binding was more for outgoing requests than for incoming events…

And yes, there is the possibility of starting from Seaside’s Unifi Protect Binding and see what needs to be adapted, but I have the impression that this is a longer path.

Thanks for your views.

Note that I’m mostly interested in “object” and “actor” sections at the end of the example of the payload… I’d “decode” the actor and update a LastSeen item for each of the actors.

#access.door.unlock
 {
    "event": "access.door.unlock",
    "event_object_id": "4a98adf6-dbb8-4312-9b8b-593f6eba8c8e",
    "data": {
        "location": {
            "id": "d2b87427-7efa-43c1-aa52-b00d40d99ecf",
            "location_type": "door",
            "name": "Door 3855",
            "up_id": "62ff3aa1-ae96-4b6b-8eb5-44aadfd4aabd",
            "extras": {
                "door_thumbnail": "/preview/reader_0418d6a2bb7a_d2b87427-7efa
43c1-aa52-b00d40d99ecf_1722913291.jpg",
                "door_thumbnail_last_update": 1722913291,
                "uah-input_state_dps": "on",
                "uah-wiring_state_dps-neg": "on",
                "uah-wiring_state_dps-pos": "on"
            },
            "device_ids": null
        },
        "device": {
            "name": "UA-HUB-3855",
            "alias": "Door 3855",
            "id": "7483c2773855",
            "ip": "192.168.1.132",
            "mac": "",
            "online": false,
            "device_type": "UAH",
            "connected_hub_id": "",
            "location_id": "d2b87427-7efa-43c1-aa52-b00d40d99ecf",
            "firmware": "v4.6.1.0",
            "version": "v4.6.129",
            "guid": "4a5e238f-4bae-48d5-84d7-dd2b0e919ab5",
            "start_time": 1721988528,
            "hw_type": "",
            "revision": "1722912520784126005",
            "cap": null
        },
        "actor": {
            "id": "d62e92fd-91aa-44c2-9b36-6d674a4b74d0",
            "name": "Hon***",
            "type": "user"
        },
        "object": {
            "authentication_type": "CALL", //Door opening method, 
NFC/PIN_CODE/Call For a DoorBell
            "authentication_value": "",
            "policy_id": "",
            "policy_name": "",
            "reader_id": "",
            "result": "Access Granted"
        }
    }
 }

Hey
i never had the drive to write an binding for it but i do have a python script with mqtt that pulls the name date and door and type of event and this has been running for a year already.
Just make sure you generate a token and change the mqtt settings and folder location for logs of this script.

import paho.mqtt.client as mqtt
import logging
import signal
import sys
import json
import websocket
import ssl
import time
from logging.handlers import TimedRotatingFileHandler
from datetime import datetime

# Initialize logging with rotation every 5 days
log_file = '/etc/openhab/misc/unifi/unifi_api.log'
handler = TimedRotatingFileHandler(log_file, when="D", interval=5, backupCount=5)
handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
handler.setFormatter(formatter)

logger = logging.getLogger("system_logs")
logger.setLevel(logging.INFO)
logger.addHandler(handler)

# MQTT settings
BROKER = "127.0.0.1" # ip of broker
USERNAME = "openhabian" # user broker
PASSWORD = "mysecretpassword" # password broker
CLIENT_ID = "system_logs_api"

# WebSocket settings
WS_HOST = "wss://192.168.1.1:12445/api/v1/developer/devices/notifications"
TOKEN = "yourtoken" # here place token generated in the unifi acess interface

# Create an MQTT client
logger.info("Initializing MQTT client.")
client = mqtt.Client(CLIENT_ID)
client.username_pw_set(USERNAME, PASSWORD)

def on_connect(client, userdata, flags, rc):
    if rc == 0:
        logger.info("Connected to MQTT broker.")
    else:
        logger.error(f"Failed to connect to MQTT broker, Code: {rc}")

client.on_connect = on_connect

# Connect to MQTT broker and start loop
client.connect(BROKER)
client.loop_start()
logger.info("MQTT client loop started.")

from datetime import datetime

from datetime import datetime, timezone

def extract_door_unlock_event(logs):
    if isinstance(logs, str) and (logs.strip() == '"Hello"' or not logs.startswith("{")):
        return  # Skip without logging

    door_unlock_events = []
    try:
        if 'data' in logs and '_source' in logs['data'] and 'event' in logs['data']['_source']:
            event_type = logs['data']['_source']['event'].get('type', '')
            if event_type == 'access.door.unlock':
                # Get the 'published' timestamp from the API in milliseconds and treat it as UTC
                timestamp_ms = logs['data']['_source']['event'].get('published', 0)
                if timestamp_ms:
                    # Convert the timestamp from milliseconds to seconds
                    timestamp_utc = datetime.utcfromtimestamp(timestamp_ms / 1000).replace(tzinfo=timezone.utc)

                    # Convert UTC to local time
                    timestamp_local = timestamp_utc.astimezone().strftime('%Y-%m-%d %H:%M:%S')
                else:
                    timestamp_local = 'N/A'

                event_data = {
                    'Timestamp': timestamp_local,
                    'Display_Name': logs['data']['_source']['actor'].get('display_name', 'N/A'),
                    'Result': logs['data']['_source']['event'].get('result', 'N/A'),
                    'Door': next((t['display_name'] for t in logs['data']['_source']['target'] if t['type'] == 'door'), 'N/A')
                }
                door_unlock_events.append(event_data)
            else:
                logger.info("Event type is not access.door.unlock.")
    except Exception as e:
        logger.error(f"Error extracting door unlock event: {e}")
    
    return door_unlock_events


def publish_with_retry(topic, payload, retries=3):
    attempt = 0
    while attempt < retries:
        result = client.publish(topic, payload)
        if result.rc == mqtt.MQTT_ERR_SUCCESS:
            return True
        else:
            logger.error(f"Failed to publish to {topic}, attempt {attempt + 1}/{retries}, result code: {result.rc}")
            attempt += 1
    return False

def publish_all_with_retry(event, retries=3):
    topics_payloads = [
        ("openhab/door_unlock/Timestamp", event['Timestamp']),
        ("openhab/door_unlock/Display_Name", event['Display_Name']),
        ("openhab/door_unlock/Door", event['Door']),
        ("openhab/door_unlock/Result", event['Result'])
    ]

    results = []
    for topic, payload in topics_payloads:
        result = client.publish(topic, payload)
        results.append((result, topic, payload))

    # Now check results and retry if necessary
    for attempt in range(retries):
        failed_publishes = []
        for result, topic, payload in results:
            if result.rc != mqtt.MQTT_ERR_SUCCESS:
                logger.error(f"Failed to publish to {topic}, attempt {attempt + 1}/{retries}, result code: {result.rc}")
                failed_publishes.append((topic, payload))

        # Retry only for failed messages
        if not failed_publishes:
            break

        results = []
        for topic, payload in failed_publishes:
            result = client.publish(topic, payload)
            results.append((result, topic, payload))

    # Final check after all retries
    for result, topic, payload in results:
        if result.rc != mqtt.MQTT_ERR_SUCCESS:
            logger.error(f"Failed to publish to {topic} after {retries} retries.")

def on_message(ws, message):
    # Suppress messages like "Hello" completely without logging
    if message == '"Hello"':
        return

    try:
        # Attempt to parse the message as JSON
        logs = json.loads(message)

        # Check if the message is a valid JSON object (dict)
        if not isinstance(logs, dict):
            # If not valid JSON, skip further processing without logging an error
            return

        # Process only if the event is "access.logs.add"
        if logs.get('event') == 'access.logs.add':
            logger.info(f"Processing access.logs.add event: {message}")
            door_unlock_events = extract_door_unlock_event(logs)

            if door_unlock_events:
                for event in door_unlock_events:
                    # Publish all fields together with retry mechanism
                    publish_all_with_retry(event)
                    logger.info(f"Published door unlock event: {event}")
            else:
                logger.info("No door unlock event found.")
        else:
            # Ignore all other events
            return

    except json.JSONDecodeError:
        # Silently ignore messages that are not valid JSON without logging an error
        pass
    except Exception as e:
        logger.error(f"Error processing message: {e}")




def on_error(ws, error):
    logger.error(f"WebSocket error: {error}")

def on_close(ws, close_status_code, close_msg):
    logger.info(f"WebSocket closed: {close_status_code}, {close_msg}")

def on_open(ws):
    logger.info("WebSocket connection opened.")


import time

def start_websocket():
    ws_url = WS_HOST
    headers = {
        "Authorization": f"Bearer {TOKEN}"
    }

    websocket.enableTrace(True)
    
    while True:  # Keep trying to reconnect in case of disconnection
        try:
            ws = websocket.WebSocketApp(
                ws_url,
                header=headers,
                on_open=on_open,
                on_message=on_message,
                on_error=on_error,
                on_close=on_close
            )

            # Disable SSL verification
            ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
        except Exception as e:
            logger.error(f"Error while establishing WebSocket connection: {e}")
        
        logger.info("WebSocket connection lost. Attempting to reconnect in 5 seconds...")
        time.sleep(5)  # Wait 5 seconds before attempting to reconnect


# Signal handling for graceful exit
def handle_exit(sig, frame):
    logger.info("Exiting...")
    client.disconnect()
    client.loop_stop()
    sys.exit(0)

# Register signal handler for clean exit
signal.signal(signal.SIGINT, handle_exit)

# Start WebSocket and listen for events (without manual loop)
start_websocket()

To make a service that starts automatically just

sudo nano /etc/systemd/system/unifi_log.service
[Unit]
Description=Unifi log MQTT Service
After=network.target

[Service]
ExecStart=/usr/bin/python3 /etc/openhab/misc/unifi/unifi_api.py
Restart=on-failure
User=openhab
Group=openhab

[Install]
WantedBy=multi-user.target

Then for openhab adapt this for the mqtt binding

Bridge mqtt:broker:system_logs_api "System Logs broker" [ host="127.0.0.1", secure=false, username="openhabian", password="mysecretpassword", clientID="openhab_system_logs" ] {
    Thing topic system_logs "System Logs topic" {
        Channels:
            Type string : timestamp "Timestamp" [ stateTopic="openhab/door_unlock/Timestamp" ]
            Type string : name "Name" [ stateTopic="openhab/door_unlock/Display_Name" ]
            Type string : event "Door" [ stateTopic="openhab/door_unlock/Door" ]
            Type string : result "Result" [ stateTopic="openhab/door_unlock/Result" ]
    }
}
//unifi_logs
String UserActivity_Timestamp "Timestamp [%s]" {channel="mqtt:topic:system_logs_api:system_logs:timestamp"}
String UserActivity_Name "Name [%s]" {channel="mqtt:topic:system_logs_api:system_logs:name"}
String UserActivity_Event "Event [%s]" {channel="mqtt:topic:system_logs_api:system_logs:event"}
String UserActivity_Result "Result [%s]" {channel="mqtt:topic:system_logs_api:system_logs:result"}

Thanks a lot @stamate_viorel for this great example of how you got to use the Unifi Access API and I’m going to go your way if I can not get a combination of the webhooks proposed by the Unifi Access API and the webhook binding.

If the webhook binding has the capability to extract the payload from a POST request, then I think we could have a “native” connection between the 2 worlds, no?

Read the earlier post of @Seaside webhooks is not the way to go. This python script is the closest you ever going to get to realtime updates from the unifi box without you asking her every second . Just make sure you have python and the packages via pip installed before attempting to run the script.