├── .gitignore ├── LICENSE ├── README.md ├── setup.py ├── unifi-disconnect-client ├── unifi-log-roaming ├── unifi-low-snr-reconnect ├── unifi-ls-clients ├── unifi-save-statistics └── unifi ├── __init__.py └── controller.py /.gitignore: -------------------------------------------------------------------------------- 1 | MANIFEST 2 | build 3 | dist 4 | 5 | *.pyc 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 Jakob Borg 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | - The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This project is not actively maintained 2 | 3 | Issues and pull requests on this repository may not be acted on in a timely 4 | manner, or at all. You are of course welcome to use it anyway. You are even 5 | more welcome to fork it and maintain the results. 6 | 7 | ![Unmaintained](https://nym.se/img/unmaintained.jpg) 8 | 9 | unifi-api 10 | ========= 11 | 12 | --- 13 | 14 | A rewrite of https://github.com/unifi-hackers/unifi-lab in cleaner Python. 15 | 16 | Install 17 | ------- 18 | 19 | sudo pip install -U unifi 20 | 21 | Utilities 22 | --------- 23 | 24 | The following small utilities are bundled with the API: 25 | 26 | ### unifi-ls-clients 27 | 28 | Lists the currently active clients on the networks. Takes parameters for 29 | controller, username, password, controller version and site ID (UniFi >= 3.x) 30 | 31 | ``` 32 | jb@unifi:~ % unifi-ls-clients -c localhost -u admin -p p4ssw0rd -v v3 -s default 33 | NAME MAC AP CHAN RSSI RX TX 34 | client-kitchen 00:24:36:9a:0d:ab Study 100 51 300 216 35 | jborg-mbp 28:cf:da:d6:46:20 Study 100 45 300 300 36 | jb-iphone 48:60:bc:44:36:a4 Living Room 1 45 65 65 37 | jb-ipad 1c:ab:a7:af:05:65 Living Room 1 22 52 65 38 | ``` 39 | 40 | ### unifi-low-snr-reconnect 41 | 42 | Periodically checks all clients for low SNR values, and disconnects those who 43 | fall below the limit. The point being that these clients will then try to 44 | reassociate, hopefully finding a closer AP. Take the same parameters as above, 45 | plus settings for intervals and SNR threshold. Use `unifi-low-snr-reconnect -h` 46 | for an option summary. 47 | 48 | A good source of understanding for RSSI/SNR values is [this 49 | article](http://www.wireless-nets.com/resources/tutorials/define_SNR_values.html). 50 | According to that, an SNR of 15 dB seems like a good cutoff, and that's also 51 | the default value in the script. You can set a higher value for testing: 52 | 53 | ``` 54 | jb@unifi:~ % unifi-low-snr-reconnect -c localhost -u admin -p p4ssw0rd -v v3 -s default --minsnr 30 55 | 2012-11-15 11:23:01 INFO unifi-low-snr-reconnect: Disconnecting jb-ipad/1c:ab:a7:af:05:65@Study (SNR 22 dB < 30 dB) 56 | 2012-11-15 11:23:01 INFO unifi-low-snr-reconnect: Disconnecting Annas-Iphone/74:e2:f5:97:da:7e@Living Room (SNR 29 dB < 30 dB) 57 | ``` 58 | 59 | For production use, launching the script into the background is recommended... 60 | 61 | ### unifi-save-statistics 62 | 63 | Get a csv file with statistics 64 | 65 | ``` 66 | unifi-save-statistics -c localhost -u admin -p p4ssw0rd -v v3 -s default -f filename.csv 67 | ``` 68 | 69 | API Example 70 | ----------- 71 | 72 | ```python 73 | from unifi.controller import Controller 74 | c = Controller('192.168.1.99', 'admin', 'p4ssw0rd') 75 | for ap in c.get_aps(): 76 | print 'AP named %s with MAC %s' % (ap.get('name'), ap['mac']) 77 | ``` 78 | 79 | See also the scripts `unifi-ls-clients` and `unifi-low-rssi-reconnect` for more 80 | examples of how to use the API. 81 | 82 | UniFi v3 Compatibility and Migration 83 | ------------------------------------ 84 | With the release of v3, UniFi gained multisite support which requires some 85 | changes on how to interract with the API . Currently we assume v2 to be the 86 | default, thus: Updating the API WON'T BREAK existing code using this API. 87 | 88 | Though, for continued v2 usage we **recommend** you start explicitely 89 | instanciating your controller in v2 mode for the day the default assumption 90 | starts to be v3 or newer: 91 | 92 | ```python 93 | c = Controller('192.168.1.99', 'admin', 'p4ssw0rd', 'v2') 94 | ``` 95 | 96 | With UniFi v3, connecting to the first (`default`) site, is as easy as 97 | instanciating a controller in v3 mode: 98 | 99 | ```python 100 | c = Controller('192.168.1.99', 'admin', 'p4ssw0rd', 'v3') 101 | ``` 102 | 103 | Connecting to a site other than `default` requires indication of both version 104 | and the site ID: 105 | 106 | ```python 107 | c = Controller('192.168.1.99', 'admin', 'p4ssw0rd', 'v3', 'myothersite') 108 | ``` 109 | 110 | You can find about the site ID by selecting the site in the UniFi web interface, 111 | i.e. "My other site". Then you can find ia its URL (`https://localhost:8443/manage/s/foobar`) 112 | that the site ID is `myothersite`. 113 | 114 | API 115 | --- 116 | 117 | ### `class Controller` 118 | 119 | Interact with a UniFi controller. 120 | 121 | Uses the JSON interface on port 8443 (HTTPS) to communicate with a UniFi 122 | controller. Operations will raise unifi.controller.APIError on obvious 123 | problems (such as login failure), but many errors (such as disconnecting a 124 | nonexistant client) will go unreported. 125 | 126 | ### `__init__(self, host, username, password)` 127 | 128 | Create a Controller object. 129 | 130 | - `host` -- the address of the controller host; IP or name 131 | - `username` -- the username to log in with 132 | - `password` -- the password to log in with 133 | - `port` -- the port of the controller host 134 | - `version` -- the base version of the controller API [v2|v3] 135 | - `site_id` -- the site ID to connect to (UniFi >= 3.x) 136 | 137 | ### `block_client(self, mac)` 138 | 139 | Add a client to the block list. 140 | 141 | - `mac` -- the MAC address of the client to block. 142 | 143 | ### `disconnect_client(self, mac)` 144 | 145 | Disconnects a client, forcing them to reassociate. Useful when the 146 | connection is of bad quality to force a rescan. 147 | 148 | - `mac` -- the MAC address of the client to disconnect. 149 | 150 | ### `get_alerts(self)` 151 | 152 | Return a list of Alerts. 153 | 154 | ### `get_alerts_unarchived(self)` 155 | 156 | Return a list of unarchived Alerts. 157 | 158 | ### `get_events(self)` 159 | 160 | Return a list of Events. 161 | 162 | ### `get_aps(self)` 163 | 164 | Return a list of all AP:s, with significant information about each. 165 | 166 | ### `get_clients(self)` 167 | 168 | Return a list of all active clients, with significant information about each. 169 | 170 | ### `get_statistics_last_24h(self)` 171 | 172 | Return statistical data of the last 24h 173 | 174 | ### `get_statistics_24h(self, endtime)` 175 | 176 | Return statistical data last 24h from endtime 177 | 178 | - `endtime` -- the last time of statistics. 179 | 180 | ### `get_users(self)` 181 | 182 | Return a list of all known clients, with significant information about each. 183 | 184 | ### `get_user_groups(self)` 185 | 186 | Return a list of user groups with its rate limiting settings. 187 | 188 | ### `get_wlan_conf(self)` 189 | 190 | Return a list of configured WLANs with their configuration parameters. 191 | 192 | ### `restart_ap(self, mac)` 193 | 194 | Restart an access point (by MAC). 195 | 196 | - `mac` -- the MAC address of the AP to restart. 197 | 198 | ### `restart_ap_name(self, name)` 199 | 200 | Restart an access point (by name). 201 | 202 | - `name` -- the name address of the AP to restart. 203 | 204 | ### `unblock_client(self, mac)` 205 | 206 | Remove a client from the block list. 207 | 208 | - `mac` -- the MAC address of the client to unblock. 209 | 210 | ### `archive_all_alerts(self)` 211 | 212 | Archive all alerts of site. 213 | 214 | ### `create_backup(self)` 215 | 216 | Tells the controller to create a backup archive that can be downloaded with download_backup() and 217 | then be used to restore a controller on another machine. 218 | 219 | Remember that this puts significant load on a controller for some time (depending on the amount of users and managed APs). 220 | 221 | ### `get_backup(self, targetfile)` 222 | 223 | Tells the controller to create a backup archive and downloads it to a file. It should have a .unf extension for later restore. 224 | 225 | - `targetfile` -- the target file name, you can also use a full path. Default creates unifi-backup.unf in the current directoy. 226 | 227 | ### `authorize_guest(self, guest_mac, minutes, up_bandwidth=None, down_bandwidth=None, byte_quota=None, ap_mac=None)` 228 | 229 | Authorize a guest based on his MAC address. 230 | 231 | - `guest_mac` -- the guest MAC address : aa:bb:cc:dd:ee:ff 232 | - `minutes` -- duration of the authorization in minutes 233 | - `up_bandwith` -- up speed allowed in kbps (optional) 234 | - `down_bandwith` -- down speed allowed in kbps (optional) 235 | - `byte_quota` -- quantity of bytes allowed in MB (optional) 236 | - `ap_mac` -- access point MAC address (UniFi >= 3.x) (optional) 237 | 238 | ### `unauthorize_guest(self, guest_mac)` 239 | Unauthorize a guest based on his MAC address. 240 | 241 | - `guest_mac` -- the guest MAC address : aa:bb:cc:dd:ee:ff 242 | 243 | License 244 | ------- 245 | 246 | MIT 247 | 248 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | import sys 5 | 6 | if sys.version_info[0] == 2: 7 | from commands import getoutput 8 | elif sys.version_info[0] == 3: 9 | from subprocess import getoutput 10 | 11 | 12 | setup(name='unifi', 13 | version='1.2.5', 14 | description='API towards Ubiquity Networks UniFi controller', 15 | author='Jakob Borg', 16 | author_email='jakob@nym.se', 17 | url='https://github.com/calmh/unifi-api', 18 | packages=['unifi'], 19 | scripts=['unifi-low-snr-reconnect', 'unifi-ls-clients', 'unifi-save-statistics', 'unifi-log-roaming'], 20 | classifiers=['Development Status :: 4 - Beta', 21 | 'Intended Audience :: Developers', 22 | 'License :: OSI Approved :: MIT License', 23 | 'Topic :: Software Development :: Libraries', 24 | 'Topic :: System :: Networking'] 25 | ) 26 | -------------------------------------------------------------------------------- /unifi-disconnect-client: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | 5 | from unifi.controller import Controller 6 | 7 | parser = argparse.ArgumentParser() 8 | parser.add_argument('-c', '--controller', default='unifi', help='the controller address (default "unifi")') 9 | parser.add_argument('-u', '--username', default='admin', help='the controller username (default("admin")') 10 | parser.add_argument('-p', '--password', default='', help='the controller password') 11 | parser.add_argument('-b', '--port', default='8443', help='the controller port (default "8443")') 12 | parser.add_argument('-v', '--version', default='v2', help='the controller base version (default "v2")') 13 | parser.add_argument('-s', '--siteid', default='default', help='the site ID, UniFi >=3.x only (default "default")') 14 | parser.add_argument('-d', '--debug', help='enable debug output', action='store_true') 15 | parser.add_argument('macs', metavar='MAC', nargs='+', help='Client MAC address(es)') 16 | args = parser.parse_args() 17 | 18 | if args.debug: 19 | import logging 20 | logging.basicConfig(level=logging.DEBUG) 21 | 22 | c = Controller(args.controller, args.username, args.password, args.port, args.version, args.siteid) 23 | 24 | for mac in args.macs: 25 | c.disconnect_client(mac) 26 | -------------------------------------------------------------------------------- /unifi-log-roaming: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import print_function 4 | 5 | import argparse 6 | import time 7 | 8 | from unifi.controller import Controller 9 | 10 | parser = argparse.ArgumentParser() 11 | parser.add_argument('-c', '--controller', default='unifi', help='the controller address (default "unifi")') 12 | parser.add_argument('-u', '--username', default='admin', help='the controller username (default "admin")') 13 | parser.add_argument('-p', '--password', default='', help='the controller password') 14 | parser.add_argument('-b', '--port', default='8443', help='the controller port (default "8443")') 15 | parser.add_argument('-v', '--version', default='v2', help='the controller base version (default "v2")') 16 | parser.add_argument('-s', '--siteid', default='default', help='the site ID, UniFi >=3.x only (default "default")') 17 | args = parser.parse_args() 18 | 19 | c = Controller(args.controller, args.username, args.password, args.port, args.version, args.siteid) 20 | 21 | aps = c.get_aps() 22 | ap_names = dict([(ap['mac'], ap.get('name')) for ap in aps]) 23 | 24 | client_aps = {} 25 | while True: 26 | clients = c.get_clients() 27 | for client in clients: 28 | ap = ap_names[client['ap_mac']] 29 | mac = client['mac'] 30 | name = client['hostname'] or client['ip'] or client['mac'] 31 | channel = client['channel'] 32 | rssi = client['rssi'] 33 | key = '%s/%d' % (ap, channel) 34 | 35 | if mac in client_aps: 36 | if client_aps[mac] != key: 37 | print('%s roamed %s -> %s' % (name, client_aps[mac], key)) 38 | client_aps[mac] = key 39 | time.sleep(10) 40 | -------------------------------------------------------------------------------- /unifi-low-snr-reconnect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import print_function 4 | 5 | import argparse 6 | import time 7 | from collections import defaultdict 8 | 9 | from unifi.controller import Controller 10 | 11 | parser = argparse.ArgumentParser() 12 | parser.add_argument('-c', '--controller', default='unifi', help='the controller address (default "unifi")') 13 | parser.add_argument('-u', '--username', default='admin', help='the controller username (default "admin")') 14 | parser.add_argument('-p', '--password', default='', help='the controller password') 15 | parser.add_argument('-b', '--port', default='8443', help='the controller port (default "8443")') 16 | parser.add_argument('-v', '--version', default='v2', help='the controller base version (default "v2")') 17 | parser.add_argument('-s', '--siteid', default='default', help='the site ID, UniFi >=3.x only (default "default")') 18 | parser.add_argument('-m', '--minsnr', default=15, type=int, help='(dB) minimum required client SNR (default 15)') 19 | parser.add_argument('-i', '--checkintv', default=5, type=int, help='(s) check interval (default 5)') 20 | parser.add_argument('-o', '--holdintv', default=300, type=int, help='(s) holddown interval (default 300)') 21 | parser.add_argument('-d', '--debug', help='enable debug output', action='store_true') 22 | args = parser.parse_args() 23 | 24 | c = Controller(args.controller, args.username, args.password, args.port, args.version, args.siteid) 25 | 26 | all_aps = c.get_aps() 27 | ap_names = dict([(ap['mac'], ap.get('name')) for ap in all_aps]) 28 | 29 | kicks = defaultdict(int) 30 | held = defaultdict(bool) 31 | while True: 32 | res = c.get_clients() 33 | now = time.time() 34 | 35 | if args.debug: 36 | print('Got %d clients to check' % len(res)) 37 | for sta in res: 38 | name = sta.get('hostname','') 39 | snr = sta['rssi'] 40 | mac = sta['mac'] 41 | ap_mac = sta['ap_mac'] 42 | ap_name = ap_names[ap_mac] 43 | essid = sta['essid'] 44 | if args.debug: 45 | print('%s/%s@%s %d' % (name, mac, ap_name, snr)) 46 | if snr < args.minsnr: 47 | if now - kicks[name] > args.holdintv: 48 | print('Disconnecting %s/%s@%s (SNR %d dB < %d dB) on %s' % (name, mac, ap_name, snr, args.minsnr,essid)) 49 | c.disconnect_client(mac) 50 | kicks[name] = now 51 | held[name] = False 52 | elif not held[name]: 53 | held[name] = True 54 | if args.debug: 55 | print('Ignoring %s/%s@%s (SNR %d dB < %d dB) (holddown)' % (name, mac, ap_name, snr, args.minsnr)) 56 | 57 | time.sleep(args.checkintv) 58 | 59 | -------------------------------------------------------------------------------- /unifi-ls-clients: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | 5 | from unifi.controller import Controller 6 | 7 | parser = argparse.ArgumentParser() 8 | parser.add_argument('-c', '--controller', default='unifi', help='the controller address (default "unifi")') 9 | parser.add_argument('-u', '--username', default='admin', help='the controller username (default("admin")') 10 | parser.add_argument('-p', '--password', default='', help='the controller password') 11 | parser.add_argument('-b', '--port', default='8443', help='the controller port (default "8443")') 12 | parser.add_argument('-v', '--version', default='v2', help='the controller base version (default "v2")') 13 | parser.add_argument('-s', '--siteid', default='default', help='the site ID, UniFi >=3.x only (default "default")') 14 | args = parser.parse_args() 15 | 16 | c = Controller(args.controller, args.username, args.password, args.port, args.version, args.siteid) 17 | 18 | aps = c.get_aps() 19 | ap_names = dict([(ap['mac'], ap.get('name')) for ap in aps]) 20 | clients = c.get_clients() 21 | clients.sort(key=lambda x: -x['rssi']) 22 | 23 | FORMAT = '%-16s %18s %-12s %4s %4s %3s %3s' 24 | print(FORMAT % ('NAME', 'MAC', 'AP', 'CHAN', 'RSSI', 'RX', 'TX')) 25 | for client in clients: 26 | ap_name = ap_names[client['ap_mac']] 27 | name = client.get('hostname') or client.get('ip', 'Unknown') 28 | rssi = client['rssi'] 29 | mac = client['mac'] 30 | rx = int(client['rx_rate'] / 1000) 31 | tx = int(client['tx_rate'] / 1000) 32 | channel = client['channel'] 33 | 34 | print(FORMAT % (name, mac, ap_name, channel, rssi, rx, tx)) 35 | -------------------------------------------------------------------------------- /unifi-save-statistics: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import time 5 | 6 | from unifi.controller import Controller 7 | 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument('-c', '--controller', default='unifi', help='the controller address (default "unifi")') 10 | parser.add_argument('-u', '--username', default='admin', help='the controller username (default("admin")') 11 | parser.add_argument('-p', '--password', default='', help='the controller password') 12 | parser.add_argument('-b', '--port', default='8443', help='the controller port (default "8443")') 13 | parser.add_argument('-v', '--version', default='v2', help='the controller base version (default "v2")') 14 | parser.add_argument('-s', '--siteid', default='default', help='the site ID, UniFi >=3.x only (default "default")') 15 | parser.add_argument('-f', '--file', default='statistics-unifi.csv', help='the filename of write statistics') 16 | args = parser.parse_args() 17 | 18 | c = Controller(args.controller, args.username, args.password, args.port, args.version, args.siteid) 19 | 20 | statistics = c.get_statistics_last_24h() 21 | 22 | #open file 23 | fo = open(args.file, "wb") 24 | 25 | FORMAT_CSV = '%15s;%10s;%25s;%10s\n' 26 | fo.write(FORMAT_CSV % ('Bytes', 'num_sta', 'time', 'time int')); 27 | for stat in statistics: 28 | bytes = stat['bytes'] 29 | num_sta = stat['num_sta'] 30 | timeint = stat['time']/1000 31 | timestap = time.ctime(int(stat['time'])/1000) 32 | fo.write(FORMAT_CSV % (bytes, num_sta, timestap, timeint)); 33 | 34 | # Close file 35 | fo.close() 36 | 37 | # Print result of file 38 | print(open(args.file,"rb").read()) 39 | -------------------------------------------------------------------------------- /unifi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calmh/unifi-api/5562d9c7689ef3d08c2d2390fb83d66f65d1086e/unifi/__init__.py -------------------------------------------------------------------------------- /unifi/controller.py: -------------------------------------------------------------------------------- 1 | 2 | try: 3 | # Ugly hack to force SSLv3 and avoid 4 | # urllib2.URLError: 6 | import _ssl 7 | _ssl.PROTOCOL_SSLv23 = _ssl.PROTOCOL_TLSv1 8 | except: 9 | pass 10 | 11 | try: 12 | # Updated for python certificate validation 13 | import ssl 14 | ssl._create_default_https_context = ssl._create_unverified_context 15 | except: 16 | pass 17 | 18 | import sys 19 | PYTHON_VERSION = sys.version_info[0] 20 | 21 | if PYTHON_VERSION == 2: 22 | import cookielib 23 | import urllib2 24 | elif PYTHON_VERSION == 3: 25 | import http.cookiejar as cookielib 26 | import urllib3 27 | import ast 28 | 29 | import json 30 | import logging 31 | from time import time 32 | import urllib 33 | 34 | 35 | log = logging.getLogger(__name__) 36 | 37 | 38 | class APIError(Exception): 39 | pass 40 | 41 | 42 | class Controller: 43 | 44 | """Interact with a UniFi controller. 45 | 46 | Uses the JSON interface on port 8443 (HTTPS) to communicate with a UniFi 47 | controller. Operations will raise unifi.controller.APIError on obvious 48 | problems (such as login failure), but many errors (such as disconnecting a 49 | nonexistant client) will go unreported. 50 | 51 | >>> from unifi.controller import Controller 52 | >>> c = Controller('192.168.1.99', 'admin', 'p4ssw0rd') 53 | >>> for ap in c.get_aps(): 54 | ... print 'AP named %s with MAC %s' % (ap.get('name'), ap['mac']) 55 | ... 56 | AP named Study with MAC dc:9f:db:1a:59:07 57 | AP named Living Room with MAC dc:9f:db:1a:59:08 58 | AP named Garage with MAC dc:9f:db:1a:59:0b 59 | 60 | """ 61 | 62 | def __init__(self, host, username, password, port=8443, version='v2', site_id='default'): 63 | """Create a Controller object. 64 | 65 | Arguments: 66 | host -- the address of the controller host; IP or name 67 | username -- the username to log in with 68 | password -- the password to log in with 69 | port -- the port of the controller host 70 | version -- the base version of the controller API [v2|v3|v4|v5] 71 | site_id -- the site ID to connect to (UniFi >= 3.x) 72 | 73 | """ 74 | 75 | self.host = host 76 | self.port = port 77 | self.version = version 78 | self.username = username 79 | self.password = password 80 | self.site_id = site_id 81 | self.url = 'https://' + host + ':' + str(port) + '/' 82 | self.api_url = self.url + self._construct_api_path(version) 83 | 84 | log.debug('Controller for %s', self.url) 85 | 86 | cj = cookielib.CookieJar() 87 | if PYTHON_VERSION == 2: 88 | self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj)) 89 | elif PYTHON_VERSION == 3: 90 | self.opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj)) 91 | 92 | self._login(version) 93 | 94 | def __del__(self): 95 | if self.opener != None: 96 | self._logout() 97 | 98 | def _jsondec(self, data): 99 | if PYTHON_VERSION == 3: 100 | data = data.decode() 101 | obj = json.loads(data) 102 | if 'meta' in obj: 103 | if obj['meta']['rc'] != 'ok': 104 | raise APIError(obj['meta']['msg']) 105 | if 'data' in obj: 106 | return obj['data'] 107 | return obj 108 | 109 | def _read(self, url, params=None): 110 | if PYTHON_VERSION == 3: 111 | if params is not None: 112 | params = ast.literal_eval(params) 113 | #print (params) 114 | params = urllib.parse.urlencode(params) 115 | params = params.encode('utf-8') 116 | res = self.opener.open(url, params) 117 | else: 118 | res = self.opener.open(url) 119 | elif PYTHON_VERSION == 2: 120 | res = self.opener.open(url, params) 121 | return self._jsondec(res.read()) 122 | 123 | def _construct_api_path(self, version): 124 | """Returns valid base API path based on version given 125 | 126 | The base API path for the URL is different depending on UniFi server version. 127 | Default returns correct path for latest known stable working versions. 128 | 129 | """ 130 | 131 | V2_PATH = 'api/' 132 | V3_PATH = 'api/s/' + self.site_id + '/' 133 | 134 | if(version == 'v2'): 135 | return V2_PATH 136 | if(version == 'v3'): 137 | return V3_PATH 138 | if(version == 'v4'): 139 | return V3_PATH 140 | if(version == 'v5'): 141 | return V3_PATH 142 | else: 143 | return V2_PATH 144 | 145 | def _login(self, version): 146 | log.debug('login() as %s', self.username) 147 | 148 | params = {'username': self.username, 'password': self.password} 149 | login_url = self.url 150 | 151 | if version == 'v4' or version == 'v5': 152 | login_url += 'api/login' 153 | params = json.dumps(params) 154 | else: 155 | login_url += 'login' 156 | params.update({'login': 'login'}) 157 | if PYTHON_VERSION is 2: 158 | params = urllib.urlencode(params) 159 | elif PYTHON_VERSION is 3: 160 | params = urllib.parse.urlencode(params) 161 | 162 | if PYTHON_VERSION is 3: 163 | params = params.encode("UTF-8") 164 | 165 | self.opener.open(login_url, params).read() 166 | 167 | def _logout(self): 168 | log.debug('logout()') 169 | 170 | self.opener.open(self.url + 'logout').read() 171 | 172 | def get_alerts(self): 173 | """Return a list of all Alerts.""" 174 | 175 | return self._read(self.api_url + 'list/alarm') 176 | 177 | def get_alerts_unarchived(self): 178 | """Return a list of Alerts unarchived.""" 179 | 180 | js = json.dumps({'_sort': '-time', 'archived': False}) 181 | params = urllib.urlencode({'json': js}) 182 | return self._read(self.api_url + 'list/alarm', params) 183 | 184 | def get_statistics_last_24h(self): 185 | """Returns statistical data of the last 24h""" 186 | 187 | return self.get_statistics_24h(time()) 188 | 189 | def get_statistics_24h(self, endtime): 190 | """Return statistical data last 24h from time""" 191 | 192 | js = json.dumps( 193 | {'attrs': ["bytes", "num_sta", "time"], 'start': int(endtime - 86400) * 1000, 'end': int(endtime - 3600) * 1000}) 194 | params = urllib.urlencode({'json': js}) 195 | return self._read(self.api_url + 'stat/report/hourly.system', params) 196 | 197 | def get_events(self): 198 | """Return a list of all Events.""" 199 | 200 | return self._read(self.api_url + 'stat/event') 201 | 202 | def get_aps(self): 203 | """Return a list of all AP:s, with significant information about each.""" 204 | 205 | #Set test to 0 instead of NULL 206 | params = json.dumps({'_depth': 2, 'test': 0}) 207 | return self._read(self.api_url + 'stat/device', params) 208 | 209 | def get_clients(self): 210 | """Return a list of all active clients, with significant information about each.""" 211 | 212 | return self._read(self.api_url + 'stat/sta') 213 | 214 | def get_users(self): 215 | """Return a list of all known clients, with significant information about each.""" 216 | 217 | return self._read(self.api_url + 'list/user') 218 | 219 | def get_user_groups(self): 220 | """Return a list of user groups with its rate limiting settings.""" 221 | 222 | return self._read(self.api_url + 'list/usergroup') 223 | 224 | def get_wlan_conf(self): 225 | """Return a list of configured WLANs with their configuration parameters.""" 226 | 227 | return self._read(self.api_url + 'list/wlanconf') 228 | 229 | def _run_command(self, command, params={}, mgr='stamgr'): 230 | log.debug('_run_command(%s)', command) 231 | params.update({'cmd': command}) 232 | if PYTHON_VERSION == 2: 233 | return self._read(self.api_url + 'cmd/' + mgr, urllib.urlencode({'json': json.dumps(params)})) 234 | elif PYTHON_VERSION == 3: 235 | return self._read(self.api_url + 'cmd/' + mgr, urllib.parse.urlencode({'json': json.dumps(params)})) 236 | 237 | def _mac_cmd(self, target_mac, command, mgr='stamgr'): 238 | log.debug('_mac_cmd(%s, %s)', target_mac, command) 239 | params = {'mac': target_mac} 240 | self._run_command(command, params, mgr) 241 | 242 | def block_client(self, mac): 243 | """Add a client to the block list. 244 | 245 | Arguments: 246 | mac -- the MAC address of the client to block. 247 | 248 | """ 249 | 250 | self._mac_cmd(mac, 'block-sta') 251 | 252 | def unblock_client(self, mac): 253 | """Remove a client from the block list. 254 | 255 | Arguments: 256 | mac -- the MAC address of the client to unblock. 257 | 258 | """ 259 | 260 | self._mac_cmd(mac, 'unblock-sta') 261 | 262 | def disconnect_client(self, mac): 263 | """Disconnect a client. 264 | 265 | Disconnects a client, forcing them to reassociate. Useful when the 266 | connection is of bad quality to force a rescan. 267 | 268 | Arguments: 269 | mac -- the MAC address of the client to disconnect. 270 | 271 | """ 272 | 273 | self._mac_cmd(mac, 'kick-sta') 274 | 275 | def restart_ap(self, mac): 276 | """Restart an access point (by MAC). 277 | 278 | Arguments: 279 | mac -- the MAC address of the AP to restart. 280 | 281 | """ 282 | 283 | self._mac_cmd(mac, 'restart', 'devmgr') 284 | 285 | def restart_ap_name(self, name): 286 | """Restart an access point (by name). 287 | 288 | Arguments: 289 | name -- the name address of the AP to restart. 290 | 291 | """ 292 | 293 | if not name: 294 | raise APIError('%s is not a valid name' % str(name)) 295 | for ap in self.get_aps(): 296 | if ap.get('state', 0) == 1 and ap.get('name', None) == name: 297 | self.restart_ap(ap['mac']) 298 | 299 | def archive_all_alerts(self): 300 | """Archive all Alerts 301 | """ 302 | js = json.dumps({'cmd': 'archive-all-alarms'}) 303 | params = urllib.urlencode({'json': js}) 304 | answer = self._read(self.api_url + 'cmd/evtmgr', params) 305 | 306 | def create_backup(self): 307 | """Ask controller to create a backup archive file, response contains the path to the backup file. 308 | 309 | Warning: This process puts significant load on the controller may 310 | render it partially unresponsive for other requests. 311 | """ 312 | 313 | js = json.dumps({'cmd': 'backup'}) 314 | params = urllib.urlencode({'json': js}) 315 | answer = self._read(self.api_url + 'cmd/system', params) 316 | 317 | return answer[0].get('url') 318 | 319 | def get_backup(self, target_file='unifi-backup.unf'): 320 | """Get a backup archive from a controller. 321 | 322 | Arguments: 323 | target_file -- Filename or full path to download the backup archive to, should have .unf extension for restore. 324 | 325 | """ 326 | download_path = self.create_backup() 327 | 328 | opener = self.opener.open(self.url + download_path) 329 | unifi_archive = opener.read() 330 | 331 | backupfile = open(target_file, 'w') 332 | backupfile.write(unifi_archive) 333 | backupfile.close() 334 | 335 | def authorize_guest(self, guest_mac, minutes, up_bandwidth=None, down_bandwidth=None, byte_quota=None, ap_mac=None): 336 | """ 337 | Authorize a guest based on his MAC address. 338 | 339 | Arguments: 340 | guest_mac -- the guest MAC address : aa:bb:cc:dd:ee:ff 341 | minutes -- duration of the authorization in minutes 342 | up_bandwith -- up speed allowed in kbps (optional) 343 | down_bandwith -- down speed allowed in kbps (optional) 344 | byte_quota -- quantity of bytes allowed in MB (optional) 345 | ap_mac -- access point MAC address (UniFi >= 3.x) (optional) 346 | """ 347 | cmd = 'authorize-guest' 348 | js = {'mac': guest_mac, 'minutes': minutes} 349 | 350 | if up_bandwidth: 351 | js['up'] = up_bandwidth 352 | if down_bandwidth: 353 | js['down'] = down_bandwidth 354 | if byte_quota: 355 | js['bytes'] = byte_quota 356 | if ap_mac and self.version != 'v2': 357 | js['ap_mac'] = ap_mac 358 | 359 | return self._run_command(cmd, params=js) 360 | 361 | def unauthorize_guest(self, guest_mac): 362 | """ 363 | Unauthorize a guest based on his MAC address. 364 | 365 | Arguments: 366 | guest_mac -- the guest MAC address : aa:bb:cc:dd:ee:ff 367 | """ 368 | cmd = 'unauthorize-guest' 369 | js = {'mac': guest_mac} 370 | 371 | return self._run_command(cmd, params=js) 372 | --------------------------------------------------------------------------------