Configure a UniFi SSID from Home Assistant [Python 3 Script]

What is this?

This is a Python 3 script that can be used to log into a UniFi controller (Cloud Key) and read and modify SSID settings. With this script, it’s easy to enable/disable a guest SSID for parties or family get togethers, or to quickly change your SSID’s password without having to log into the UniFi UI or phone app.
 

What inspired me to write this?

I have friends and family over often, and have a moderately easy to guess guest network password (for ease of access). That said, I wanted a way to quickly and easily set a new password or enable and disable the SSID itself. Previous work got me started and after being on the right path, I was able to hammer this out in a single night. @jcconnell, I want to first of all say thank you for the foundation. Without that, it would have been more research to get this working.
 

Changes in this script

  • Re-wrote this from the ground up, using Python 3
    It should be much easier to extend this script and for others to dive in and see how things work. Also, writing this in Python 3 removes the jq and curl requirements, and I’ve been careful to only use packages available in Home Assistant.
  • Added a function to get any configuration option
    This can be called directly, or programmatically of course. Additionally, you can call it with no SSID (it’ll output all of your SSIDs and all of their settings), an SSID but no config option (it’ll output all settings for just one SSID), or an SSID and a config option (it’ll output only that specific value). This makes it handy when you want to get other settings on the fly.
  • Added a way to get configuration settings by specifying SSID names
    Using the aforementioned function and some basic filtering, I’ve all but done away with having to manually look up those pesky IDs. Rejoice!
  • Added a password-setting command to the command line
    This allows you to easily set your SSID’s password from Home Assistant using something like an input_text - more on this later.
  • Added a log file for logging script execution
    Be warned that script executions will log your provided arguments (including passwords) in clear text. You may wish to disable the relevant bit of code that does this. It’s very easy to do so, just set cfg_logging = False in the configuration section.
     

How to use this script

  1. Add a new UniFi controller user
    You can search in the “New UI” settings for the term “new”, and go back to the “Old UI” to add a password-enabled user instead of a ui.com user. I don’t know why we still can’t do that. :flushed:
  2. Put this script in /config/scripts/ or somewhere similar
    But not in /config/python_scripts/ because that’s for different kinds of Python scripts. In short, Python scripts in /config/python_scripts/ can’t import other libraries and such, and are really only meant to interface with Home Assistant itself.
  3. Set the script to be executable
    You can do this in a number of ways, but the easiest way is just to chmod +x /config/scripts/unifi.py (or wherever you put it).
  4. Adjust the config details below
    cfg_host = 'my-host.mydomain.com' # This is *your* UniFi controller's hostname or IP address
    cfg_port = 8443                   # Similarly, the port to access your UniFi controller
    cfg_username = 'SOME_USERNAME'    # Put your username here
    cfg_password = 'SOME_PASSWORD'    # And your password here
    cfg_logging = True                # If you want to log calls to the script
    
  5. Set it up in Home Assistant
    See below for a couple examples of how to integrate this script into Home Assistant.
     

Sample Home Assistant configs

  • As a switch (enabling/disabling/seeing status)
    switch:
      - platform: command_line
        switches:
          guest_wifi:
            friendly_name: "Guest WiFi"
            command_on: "/config/scripts/unifi.py enable MyGuestSSID"
            command_off: "/config/scripts/unifi.py disable MyGuestSSID"
            command_state: "/config/scripts/unifi.py getconf MyGuestSSID enabled"
    
  • As an input_select, shell_command, and automation to tie them together (enabling/disabling)
    input_select:
      guest_network_enabled:
        name: Guest Network Enabled
        options:
          - Disabled
          - Enabled
        initial: Disabled
    
    shell_command:
      set_guest_network_enabled: "python3 /config/scripts/unifi.py {{ 'enable' if states('input_select.guest_network_enabled') == 'Enabled' else 'disable' }} \"MyGuestSSID\""
    
    automation:
      - alias: Guest Network Enabled
        description: ''
        trigger:
          - platform: state
            entity_id: input_select.guest_network_enabled
        condition: []
        action:
          - service: shell_command.set_guest_network_enabled
    
  • As an input_text, shell_command, and automation to tie them together (setting the password)
    input_text:
      guest_network_password:
        name: Guest Network Password
        initial: "ChangeMe"
        pattern: "[\\x20-\\x7E]{8,63}|[0-9a-fA-F]{64}"
    
    shell_command:
      set_guest_network_password: "python3 /config/scripts/unifi.py password \"MyGuestSSID\" \"{{ states('input_text.guest_network_password') }}\""
    
    automation:
      - alias: Guest Network Password
        description: ''
        trigger:
          - platform: state
            entity_id: input_text.guest_network_password
        condition: []
        action:
          - service: shell_command.set_guest_network_password
    

 

Running the script manually

You can also run the script manually. Change to the directory and run the script with relative paths, or absolute paths from somewhere else:

./unifi.py getconf                - Get all SSIDs config
./unifi.py getconf SSID [OPTION]  - Get SSID config
./unifi.py enable SSID            - Enable SSID
./unifi.py disable SSID           - Disable SSID
./unifi.py password SSID PASSWORD - Set SSID password

 

The script itself

#!/usr/bin/env python3

from datetime import datetime
import json
import os
import requests
import ssl
import sys
import time
from urllib3.exceptions import InsecureRequestWarning

# Configuration
cfg_host = 'my-host.mydomain.com'
cfg_port = 8443
cfg_username = 'SOME_USERNAME'
cfg_password = 'SOME_PASSWORD'
cfg_logging = True

# API endpoints
api_base = f'https://{cfg_host}:{cfg_port}'
api_login = f'{api_base}/api/login'
api_logout = f'{api_base}/logout'
api_wlanconf = f'{api_base}/api/s/default/rest/wlanconf'

# Suppress insecure request warning
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)

# Cookie jar
cookie_jar = None

# Log the timestamp and passed arguments
if cfg_logging:
    with open(os.path.join(os.path.dirname(__file__), "unifi.py.log"), "a") as fh:
        now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        fh.write(f"{now_str} {json.dumps(sys.argv)}\n")

# Log into UniFi controller with specified username and password
def login(username, password):
    # Input validation
    if None in [username, password]:
        raise Exception("Username or password not defined")
    data = { 'username': username, 'password': password }
    # Submit a POST request to log in
    response = requests.post(api_login, json.dumps(data), verify=False)
    # Check the status code and JSON output for errors
    if not response.ok:
        raise Exception(f"HTTP {response.status_code}: {response.text}")
    if response.json()['meta']['rc'] != 'ok':
        raise Exception(f"HTTP {response.status_code}: {response.json()}")
    # Update the cookie jar
    global cookie_jar
    cookie_jar = response.cookies

# Log out of UniFi controller
def logout():
    # Submit a POST request to log out
    response = requests.post(api_logout, verify=False)
    # Check status code for errors
    if not response.ok:
        raise Exception(f"HTTP {response.status_code}: {response.text}")

# Get WLAN configuration
def getconf(ssid=None):
    # Submit a GET request to get WLAN configuration
    response = requests.get(api_wlanconf, data=None, cookies=cookie_jar, verify=False)
    # Check the status code and JSON output for errors
    if not response.ok:
        raise Exception(f"HTTP {response.status_code}: {response.text}")
    if response.json()['meta']['rc'] != 'ok':
        raise Exception(f"HTTP {response.status_code}: {response.json()}")
    # Hold the JSON output for parsing and returning
    json_data = response.json()['data']
    # If SSID is provided, filter response_json['data'] and return the first item
    if ssid is not None:
        json_data = list(filter(lambda x: x['name'] == ssid, json_data))
        if len(json_data) == 1:
            json_data = json_data[0]
    return json_data

# Set WLAN configuration
def setconf(ssid, option, value):
    data = json.dumps({ option: value })
    # Get SSID's config and WLAN ID
    conf = getconf(ssid)
    if not conf:
        raise Exception(f"Failed to fetch configuration for SSID {ssid}")
    # Ensure the config has an _id
    if not "_id" in conf:
        raise Exception(f"No _id in configuration for SSID {ssid}")
    wlan_id = conf['_id']
    # Submit a PUT request to set WLAN configuration
    response = requests.put(f"{api_wlanconf}/{wlan_id}", data, cookies=cookie_jar, verify=False)
    # Check the status code and JSON output for errors
    if not response.ok:
        raise Exception(f"HTTP {response.status_code}: {response.text}")
    if response.json()['meta']['rc'] != 'ok':
        raise Exception(f"HTTP {response.status_code}: {response.json()}")

# Main        
def main():
    try:
        # Log in
        login(cfg_username, cfg_password)
        # Parse command
        args = sys.argv[1:]
        if len(args) == 0:
            raise Exception("Command not provided\n"+
                            f" {sys.argv[0]} getconf                - Get all SSIDs config\n"+
                            f" {sys.argv[0]} getconf SSID [OPTION]  - Get SSID config\n"+
                            f" {sys.argv[0]} enable SSID            - Enable SSID\n"+
                            f" {sys.argv[0]} disable SSID           - Disable SSID\n"+
                            f" {sys.argv[0]} password SSID PASSWORD - Set SSID password")
        command = args[0]
        # Get SSID config
        if command == "getconf":
            ssid = args[1] if len(args) >= 2 else None
            option = args[2] if len(args) >= 3 else None
            conf = getconf(ssid)
            # If the config is empty, return code 2
            if conf == []:
                sys.exit(2)
            # If an option was not specified, print the entire config
            elif option is None:
                print(json.dumps(conf, indent=4, sort_keys=True))
            # If the specified option does not exist in the config, return code 3
            elif option not in conf:
                sys.exit(3)
            # Else if the specified option exists in the config,
            else:
                print(conf[option])
                # If the value is not True, return code 4
                if conf[option] is not True:
                    sys.exit(4)
        # Enable / disable SSID
        elif command in ["enable", "disable"]:
            if not len(args) >= 2:
                raise Exception("SSID not provided")
            ssid = args[1]
            enabled = command == "enable"
            setconf(ssid, "enabled", enabled)
        # Set SSID password
        elif command == "password":
            ssid = args[1] if len(args) >= 2 else None
            password = args[2] if len(args) >= 3 else None
            setconf(ssid, "x_passphrase", password)
    except Exception as ex:
        print(ex)
        sys.exit(1)
    finally:
        # Log out
        try:
            logout()
        except Exception as ex:
            print(ex)
            sys.exit(1)

if __name__ == "__main__":
    main()
3 Likes

Nice work! I’ll give this a shot and report back.