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 thejq
andcurl
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 setcfg_logging = False
in the configuration section.
How to use this script
-
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. -
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. -
Set the script to be executable
You can do this in a number of ways, but the easiest way is just tochmod +x /config/scripts/unifi.py
(or wherever you put it). -
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
-
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()