[SOLVED] UniFi: allow PoE switching of connected UniFi devices

The current UniFi integration adds switches for PoE connected devices as long as they are not UniFi network devices. Although these devices are discovered, there is no PoE switch added for controlling the power off these devices. I’d like to see PoE switches for connected UniFi devices (access points) as well.
The steps for controlling these devices are as followed:

  • log into the UniFi controller
  • get the sites registered under the controller with the endpoint: /api/stat/sites
  • get the mac address and device id from the switch you want to control using the sites object retrieved in the previous step
  • get the device settings from using the sites object and mac address with endpoint /api/s/<site>/stat/device/<device_mac>
  • extract the port_overrides settings object from the device settings retrieved in the previous step and update the poe_mode from the port(s) you wish to change
  • update the device settings using the endpoint /api/s/<site>/rest/device/<device_id> and the updated port_overrides settings object as payload (this is a PUT request).
  • log out of the controller

I have this working in nodejs but lack the Python knowledge that add it to the UniFi integration for Home Assistant.

[EDIT]
I created a Python script which can be used in automations for the time being. See [SOLVED] UniFi: allow PoE switching of connected UniFi devices - #3 by Phuturist

Also, basically a “poe_poweroff” “poe_poweron” and “poe_powercycle” action added to devices in HA that happen to have a matching MAC to something in the UniFi integration ? Like an ONVIF camera for example.

And maybe it could read the POE power usage for that device as well

To the best of my Python knowledge I have put together a Python script which I can use within my automations. Perhaps it’s useful for someone else until this gets integrated in the UniFi integration.

import requests
import json
import sys
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# usage examples
# python3 poe_access_points.py 192.168.0.123 8443 username password 'a1:b2:c3:d4:e5:f6' '999999999999999999' 6 auto
# python3 poe_access_points.py 192.168.0.123 8443 username password 'a1:b2:c3:d4:e5:f6' '999999999999999999' 6,7 off

# parameters
base_url = 'https://%s:%s/api' % (sys.argv[1], sys.argv[2])
login_endpoint = base_url + '/login'
logout_endpoint = base_url + '/logout'
login_data = '{"username":"%s","password":"%s"}' % (sys.argv[3], sys.argv[4])
get_device_settings_endpoint = base_url + '/s/default/stat/device/%s' % (sys.argv[5])
set_device_settings_endpoint = base_url + '/s/default/rest/device/%s' % (sys.argv[6])
ports_array = sys.argv[7].split(',')
desired_poe_state = sys.argv[8]

# Login and get CSRF token
login = requests.post(login_endpoint, data=str(login_data), verify=False)
cookies = {'csrf_token':login.cookies['csrf_token'], 'unifises':login.cookies['unifises']}

if (login.status_code == 200):

    # Get current port_overrides config for device
    r = requests.get(get_device_settings_endpoint, cookies=cookies, verify=False)
    device_json = r.json()
    port_overrides = device_json['data'][0]['port_overrides']

    # Update the port_overrides config with new settings
    for x in ports_array:
        for value in port_overrides:
            if value['port_idx'] == int(x):
                value['poe_mode'] = desired_poe_state

    # Set the updated port_overides config for device
    new_port_overrides = { 'port_overrides': port_overrides }
    update = requests.put(set_device_settings_endpoint, cookies=cookies, data=json.dumps(new_port_overrides), verify=False)

    #if (update.status_code == 200):
    #    print("Update succesfully")
    #else:
    #    print("Update failed")

    # Logout
    logout = requests.get(logout_endpoint, {}, verify=False)

    if (login.status_code == 200):
        sys.exit()
    else:
        print("Logout failed")
        sys.exit()

else:
    print("Login failed")
    sys.exit()

I just wrote a script to do this, here’s an example of how to do it:

this automation calls this shell command:

and finally, here’s the function that is getting called

Hi @jon102034050 for that a POE Unify is required right ? i have a Unify Controller and 2 AP Unify but my POE is not Ubiquiti

correct, this was built specifically for a unifi poe switch. But, as long as you can call via cli to your switch to cycle a port, it shouldn’t really matter. concepts would be the same, but it would need to be adjusted a bit

Taking @Phuturist’s script as base, I’ve extended and adapted it so it can be used now with the UniFiOS driven units (UniFi Dream Machine).

Took me some time to discover the X-CSRF-Token trick and I’m a layman in python.

import argparse
import json
import logging
import requests
import sys
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

parser = argparse.ArgumentParser(description='Change the PoE Mode of UniFi switches controlled by a UDM (Pro).')
parser.add_argument("controller", help="hostname or IP address of UniFi Controller")
parser.add_argument("username", help="username with admin rights on UniFi Controller")
parser.add_argument("password", help="corresponding password for admin user")
parser.add_argument("mac", help="MAC address (with or without colons) of switch")
parser.add_argument("ports", help="port numbers to acquire new state (list separated by comma, e.g., '5,6,7'")
parser.add_argument("state", help="desired state of PoE ports, e.g., 'auto' or 'off'")
parser.add_argument("-v", "--verbose", help="increase output verbosity", action="store_true")
args=parser.parse_args()

# parameters
base_url = 'https://%s' % args.controller
login_endpoint = base_url + '/api/auth/login'
logout_endpoint = base_url + '/api/auth/logout'

login_data = {
    'username': args.username,
    'password': args.password
}

get_device_settings_endpoint = base_url + '/proxy/network/api/s/default/stat/device/%s' % args.mac
set_device_settings_endpoint = base_url + '/proxy/network/api/s/default/rest/device/'
ports_array = args.ports.split(',')
desired_poe_state = args.state

if (args.verbose):
    loglevel=logging.DEBUG
else:
    loglevel=logging.INFO

logging.basicConfig(level=loglevel, format='%(asctime)s - %(levelname)s - %(message)s')

#
# Logout - this requires the X-CSRF-Token
#

def logout():
    logging.info("Logging out via %s.", logout_endpoint)
    logout = s.post(logout_endpoint, headers = {'x-csrf-token': login.headers['X-CSRF-Token']}, verify = False, timeout = 1)

    if (logout.status_code == 200):
        logging.debug("Success.")
        sys.exit()
    else:
        logging.debug("Failed with return code %s", logout)
        sys.exit()


# Login and get auth token from Cookies plus X-CSRF-Token from header
# 
# working call with cURL:  Login:
# curl -X POST --data 'username=user&password=pass' -c cookie.txt https://udm/api/auth/login
# Get status:
# curl -X GET -b cookie.txt https://udm/proxy/network/api/s/default/stat/device/abcdef012
# 

logging.info("Trying to login to %s with data %s", login_endpoint, str(login_data))

s = requests.Session()

headers = {
    "Accept": "application/json",
    "Content-Type": "application/json; charset=utf-8"
}

login = s.post(login_endpoint, headers = headers,  json = login_data , verify = False, timeout = 1)

if (login.status_code == 200):
    cookies = login.cookies
    logging.debug("Success. Cookies received:")
    for c in cookies:
        logging.debug("%s ==> %s", c.name, c.value)
    logging.debug("X-CSRF-Token: %s", login.headers['X-CSRF-Token'])

else:
    logging.debug("Login failed with return code %s", login.status_code)
    sys.exit()

# Get current port_overrides config for device
logging.info ("Read current settings from %s", get_device_settings_endpoint)

r = s.get(get_device_settings_endpoint, headers = headers, verify = False, timeout = 1)

if (r.status_code == 200):
    logging.debug("Success.")
else:
    logging.debug("Failed with return code %s", r)
    logout()

device_json = r.json()
port_overrides = device_json['data'][0]['port_overrides']
device_id = device_json['data'][0]['device_id']

set_device_settings_endpoint = set_device_settings_endpoint + device_id

# Update the port_overrides config with new settings
for x in ports_array:
    for value in port_overrides:
        if value['port_idx'] == int(x):
            if 'poe_mode' in value:
                if (value['poe_mode'] != desired_poe_state):
                    logging.info("Updating port_idx %s from %s to %s", value['port_idx'], value['poe_mode'], desired_poe_state)
                    value['poe_mode'] = desired_poe_state
                else:
                    logging.info("port_idx %s already set to %s", value['port_idx'], desired_poe_state)


# Set the updated port_overides config for device
new_port_overrides = { 'port_overrides': port_overrides }

logging.info("Trying to update port overrides on %s", set_device_settings_endpoint)

logging.debug("%s", json.dumps(new_port_overrides))

headers = {
    "Accept": "application/json",
    "Content-Type": "application/json; charset=utf-8",
    "x-csrf-token": login.headers['X-CSRF-Token']
}

update = s.put(set_device_settings_endpoint, headers = headers, data = json.dumps(new_port_overrides), verify = False, timeout = 1)

if (update.status_code == 200):
    logging.debug("Success.")
else:
    logging.debug("Failed with return code %s", update.status_code)

logout()

Does any one know if the native integration now supports this? i have 3 Unifi AP’s all powered from the same USW switch and i have a PoE switch in HA for one of these AP’s but not the other 2 AP’s.

No the PR hasn’t seen activity for a few weeks now.

Here is the PR: Expose unifi PoE ports as individual switches by Flameeyes · Pull Request #59194 · home-assistant/core · GitHub

2 Likes

I’ve refactored parts of the UniFi library and provided a generic PoE port control Expose UniFi PoE ports as individual switches by Kane610 · Pull Request #80566 · home-assistant/core · GitHub

Try it out in nightly once merged or for the GitHub repo

Feedback appreciated!