UNPKG

node-unifi

Version:

NodeJS class for querying UniFi-Controller (www.ubnt.com)

3,200 lines 110 kB
/* eslint-disable max-params, camelcase */

/**
 *
 * UniFi controller class (NodeJS)
 *
 * This nodejs class provides functionality to query a UniFi controller (www.ubnt.com) through
 * its Web-API. The functionality implemented here had been gathered through different
 * souces, namely:
 *
 *   UniFi-API-client: https://github.com/Art-of-WiFi/UniFi-API-client/blob/master/src/Client.php
 *   UniFi-API sh: https://dl.ui.com/unifi/5.12.35/unifi_sh_api
 *   domwo: http://community.ubnt.com/t5/UniFi-Wireless/little-php-class-for-unifi-api/m-p/603051
 *   fbagnol: https://github.com/fbagnol/class.unifi.php
 *
 * The majority of the functions in here are actually based on the PHP UniFi-API-client class
 * which defines compatibility to UniFi-Controller versions v4 and v5+
 *
 * Based/Compatible to UniFi-API-client class: v1.1.80
 *
 * Copyright (c) 2017-2023 Jens Maus <mail@jens-maus.de>
 *
 * The source code is distributed under the MIT license
 *
 */
'use strict';

const EventEmitter = require('eventemitter2').EventEmitter2;
const WebSocket = require('ws');
const axios = require('axios');
const {CookieJar} = require('tough-cookie');
const {HttpCookieAgent, HttpsCookieAgent} = require('http-cookie-agent/http');

/// ///////////////////////////////////////////////////////////
// PUBLIC CLASS

class Controller extends EventEmitter {
  /** CONSTRUCTOR */
  constructor(options) {
    super({
      wildcard: true
    });

    // Parse opts
    this.opts = options || {};
    this.opts.host = (typeof (this.opts.host) === 'undefined' ? 'unifi' : this.opts.host);
    this.opts.port = (typeof (this.opts.port) === 'undefined' ? 8443 : this.opts.port);
    this.opts.username = (typeof (this.opts.username) === 'undefined' ? 'admin' : this.opts.username);
    this.opts.password = (typeof (this.opts.password) === 'undefined' ? 'ubnt' : this.opts.password);
    this.opts.token2FA = (typeof (this.opts.token2FA) === 'undefined' ? null : this.opts.token2FA);
    this.opts.site = (typeof (this.opts.site) === 'undefined' ? 'default' : this.opts.site);
    this.opts.sslverify = (typeof (this.opts.sslverify) === 'undefined' ? true : this.opts.sslverify);
    this.opts.timeout = (typeof (this.opts.timeout) === 'undefined' ? 5000 : this.opts.timeout);
    this.opts.rememberMe = (typeof (this.opts.rememberMe) === 'undefined' ? true : this.opts.rememberMe);

    this._baseurl = new URL(`https://${options.host}:${options.port}`);
    this._cookieJar = new CookieJar();
    this._unifios = false;
    this._isClosed = true;
    this._autoReconnectInterval = 5 * 1000; // Ms
    this._pingPongInterval = 3 * 1000; // Ms
    this._isInit = false;
  }

  /** PUBLIC METHODS */

  /**
   * Login to the UniFi controller - login()
   *
   * returns true upon success
   */
  async login(username = null, password = null, token2FA = null) {
    // Allows to override username+password
    if (username !== null) {
      this.opts.username = username;
    }

    if (password !== null) {
      this.opts.password = password;
    }

    if (token2FA !== null) {
      this.opts.token2FA = token2FA;
    }

    // Make sure init() was called
    const result = await this._init();

    // If init() was already called
    // resolve immediately
    if (result === 2) {
      return true;
    }

    let endpointUrl = `${this._baseurl.href}api/login`;
    if (this._unifios) {
      endpointUrl = `${this._baseurl.href}api/auth/login`;
    }

    // Prepare payload data
    const data = {
      username: this.opts.username,
      password: this.opts.password,
      rememberMe: this.opts.rememberMe
    };

    // Add 2FA token to payload
    if (this.opts.token2FA) {
      // On UniFiOS 2FA is in 'token' field, else in 'ubic_2fa_token'
      data[this._unifios ? 'token' : 'ubic_2fa_token'] = this.opts.token2FA;
    }

    // Perform the login to the Unifi controller
    const response = await this._instance.post(endpointUrl, data, {
      timeout: this.opts.timeout
    });

    // Catch x-csrf-token if supplied in response
    if (response.headers['x-csrf-token']) {
      this._xcsrftoken = response.headers['x-csrf-token'];
      this._instance.defaults.headers.common['x-csrf-token'] = this._xcsrftoken;
    }

    return true;
  }

  /**
   * Logout from the UniFi controller - logout()
   *
   * returns true upon success
   */
  async logout() {
    if (this._unifios === true) {
      return this._request('/api/auth/logout', null, 'POST');
    }

    return this._request('/logout');
  }

  /**
   * Authorize a client device - authorize_guest()
   *
   * required parameter <mac>     = client MAC address
   * optional parameter <minutes> = minutes (from now) until authorization expires
   * optional parameter <up>      = upload speed limit in kbps
   * optional parameter <down>    = download speed limit in kbps
   * optional parameter <megabytes>= data transfer limit in MB
   * optional parameter <ap_mac>  = AP MAC address to which client is connected, should result in faster authorization
   */
  authorizeGuest(mac, minutes = null, up = null, down = null, megabytes = null, ap_mac = null) {
    const payload = {cmd: 'authorize-guest', mac: mac.toLowerCase()};

    if (minutes !== null) {
      payload.minutes = minutes;
    }

    if (up !== null) {
      payload.up = up;
    }

    if (down !== null) {
      payload.down = down;
    }

    if (megabytes !== null) {
      payload.bytes = megabytes;
    }

    if (ap_mac !== null) {
      payload.ap_mac = ap_mac.toLowerCase();
    }

    return this._request('/api/s/<SITE>/cmd/stamgr', payload);
  }

  /**
   * Unauthorize a client device - unauthorize_guest()
   *
   * required parameter <mac>     = client MAC address
   */
  unauthorizeGuest(mac) {
    return this._request('/api/s/<SITE>/cmd/stamgr', {cmd: 'unauthorize-guest', mac: mac.toLowerCase()});
  }

  /**
   * Reconnect a client device - reconnect_sta()
   *
   * required parameter <mac>     = client MAC address
   */
  reconnectClient(mac) {
    return this._request('/api/s/<SITE>/cmd/stamgr', {cmd: 'kick-sta', mac: mac.toLowerCase()});
  }

  /**
   * Block a client device - block_sta()
   *
   * required parameter <mac>     = client MAC address
   */
  blockClient(mac) {
    return this._request('/api/s/<SITE>/cmd/stamgr', {cmd: 'block-sta', mac: mac.toLowerCase()});
  }

  /**
   * Unblock a client device - unblock_sta()
   *
   * required parameter <mac>     = client MAC address
   */
  unblockClient(mac) {
    return this._request('/api/s/<SITE>/cmd/stamgr', {cmd: 'unblock-sta', mac: mac.toLowerCase()});
  }

  /**
   * Forget one or more client devices - forget_sta()
   *
   * return true on success
   * required parameter <macs> = array of client MAC addresses
   *
   * NOTE:
   * only supported with controller versions 5.9.X and higher, can be
   * slow (up to 5 minutes) on larger controllers
   */
  forgetClient(macs) {
    return this._request('/api/s/<SITE>/cmd/stamgr', {cmd: 'forget-sta', macs});
  }

  /**
   * Create a new user/client-device - create_user()
   *
   * return an array with a single object containing details of the new user/client-device on success, else return false
   * required parameter <mac>           = client MAC address
   * required parameter <user_group_id> = _id value for the user group the new user/client-device should belong to which
   *                                      can be obtained from the output of list_usergroups()
   * optional parameter <name>          = name to be given to the new user/client-device
   * optional parameter <note>          = note to be applied to the new user/client-device
   * optional parameter <is_guest>      = boolean; defines whether the new user/client-device is a   guest or not
   * optional parameter <is_wired>      = boolean; defines whether the new user/client-device is wi  red or not
   */
  createUser(mac, user_group_id, name = null, note = null, is_guest = null, is_wired = null) {
    const new_user = {mac: mac.toLowerCase(),
      user_group_id
    };

    if (name !== null) {
      new_user.name = name;
    }

    if (note !== null) {
      new_user.note = note;
    }

    if (is_guest !== null) {
      new_user.is_guest = is_guest;
    }

    if (is_wired !== null) {
      new_user.is_wired = is_wired;
    }

    return this._request('/api/s/<SITE>/group/user', {objects: [{data: new_user}]});
  }

  /**
   * Add/modify/remove a client device note - set_sta_note()
   *
   * required parameter <user_id> = id of the client-device to be modified
   * optional parameter <note>    = note to be applied to the client-device
   *
   * NOTES:
   * - when note is empty or not set, the existing note for the client-device is removed and "noted" attribute set to false
   */
  setClientNote(user_id, note = '') {
    return this._request('/api/s/<SITE>/upd/user/' + user_id.trim(), {note});
  }

  /**
   * Add/modify/remove a client device name - set_sta_name()
   *
   * required parameter <user_id> = id of the client device to be modified
   * optional parameter <name>    = name to be applied to the client device
   *
   * NOTES:
   * - when name is empty or not set, the existing name for the client device is removed
   */
  setClientName(user_id, name = '') {
    return this._request('/api/s/<SITE>/upd/user/' + user_id.trim(), {name});
  }

  /**
   * Fetch 5 minutes site stats method - stat_5minutes_site()
   *
   * returns an array of 5-minute stats objects for the current site
   * optional parameter <start> = Unix timestamp in milliseconds
   * optional parameter <end>   = Unix timestamp in milliseconds
   *
   * NOTES:
   * - defaults to the past 12 hours
   * - this function/method is only supported on controller versions 5.5.* and later
   * - make sure that the retention policy for 5 minutes stats is set to the correct value in
   *   the controller settings
   */
  get5minSiteStats(start = null, end = null) {
    if (end === null) {
      end = Date.now();
    }

    if (start === null) {
      start = end - (12 * 3600 * 1000);
    }

    const payload = {attrs: ['bytes',
      'wan-tx_bytes',
      'wan-rx_bytes',
      'wan2-tx_bytes',
      'wan2-rx_bytes',
      'wlan_bytes',
      'num_sta',
      'lan-num_sta',
      'wlan-num_sta',
      'time'],
    start,
    end};

    return this._request('/api/s/<SITE>/stat/report/5minutes.site', payload);
  }

  /**
   * Fetch Hourly site stats method - stat_hourly_site()
   *
   * optional parameter <start> = Unix timestamp in milliseconds
   * optional parameter <end>   = Unix timestamp in milliseconds
   *
   * NOTES:
   * - defaults to the past 7*24 hours
   * - "bytes" are no longer returned with controller version 4.9.1 and later
   */
  getHourlySiteStats(start = null, end = null) {
    if (end === null) {
      end = Date.now();
    }

    if (start === null) {
      start = end - (7 * 24 * 3600 * 1000);
    }

    const payload = {attrs: ['bytes',
      'wan-tx_bytes',
      'wan-rx_bytes',
      'wan2-tx_bytes',
      'wan2-rx_bytes',
      'wlan_bytes',
      'num_sta',
      'lan-num_sta',
      'wlan-num_sta',
      'time'],
    start,
    end};

    return this._request('/api/s/<SITE>/stat/report/hourly.site', payload);
  }

  /**
   * Fetch Daily site stats method - stat_daily_site()
   *
   * optional parameter <start> = Unix timestamp in milliseconds
   * optional parameter <end>   = Unix timestamp in milliseconds
   *
   * NOTES:
   * - defaults to the past 52*7*24 hours
   * - "bytes" are no longer returned with controller version 4.9.1 and later
   */
  getDailySiteStats(start = null, end = null) {
    if (end === null) {
      end = Date.now();
    }

    if (start === null) {
      start = end - (52 * 7 * 24 * 3600 * 1000);
    }

    const payload = {attrs: ['bytes',
      'wan-tx_bytes',
      'wan-rx_bytes',
      'wan2-tx_bytes',
      'wan2-rx_bytes',
      'wlan_bytes',
      'num_sta',
      'lan-num_sta',
      'wlan-num_sta',
      'time'],
    start,
    end};

    return this._request('/api/s/<SITE>/stat/report/daily.site', payload);
  }

  /**
   * Fetch monthly site stats - stat_monthly_site()
   *
   * @param  int   $start optional, Unix timestamp in milliseconds
   * @param  int   $end   optional, Unix timestamp in milliseconds
   * @return array        returns an array of monthly stats objects for the current site
   *
   * NOTES:
   * - defaults to the past 52 weeks (52*7*24 hours)
   * - "bytes" are no longer returned with controller version 4.9.1 and later
   */
  getMonthlySiteStats(start = null, end = null) {
    if (end === null) {
      end = Date.now();
    }

    if (start === null) {
      start = end - (52 * 7 * 24 * 3600 * 1000);
    }

    const payload = {attrs: ['bytes',
      'wan-tx_bytes',
      'wan-rx_bytes',
      'wan2-tx_bytes',
      'wan2-rx_bytes',
      'wlan_bytes',
      'num_sta',
      'lan-num_sta',
      'wlan-num_sta',
      'time'],
    start,
    end};

    return this._request('/api/s/<SITE>/stat/report/monthly.site', payload);
  }

  /**
   * Fetch 5 minutes stats method for a single access point or all access points - stat_5minutes_aps()
   *
   * returns an array of 5-minute stats objects
   * optional parameter <start> = Unix timestamp in milliseconds
   * optional parameter <end>   = Unix timestamp in milliseconds
   * optional parameter <mac>   = AP MAC address to return stats for
   *
   * NOTES:
   * - defaults to the past 12 hours
   * - this function/method is only supported on controller versions 5.5.* and later
   * - make sure that the retention policy for 5 minutes stats is set to the correct value in
   *   the controller settings
   */
  get5minApStats(start = null, end = null, mac = null) {
    if (end === null) {
      end = Date.now();
    }

    if (start === null) {
      start = end - (12 * 3600 * 1000);
    }

    const payload = {attrs: ['bytes',
      'num_sta',
      'time'],
    start,
    end};

    if (mac !== null) {
      payload.mac = mac.toLowerCase();
    }

    return this._request('/api/s/<SITE>/stat/report/5minutes.ap', payload);
  }

  /**
   * Fetch Hourly stats method for a single access point or all access points - stat_hourly_aps()
   *
   * optional parameter <start> = Unix timestamp in milliseconds
   * optional parameter <end>   = Unix timestamp in milliseconds
   * optional parameter <mac>   = AP MAC address to return stats for
   *
   * NOTES:
   * - defaults to the past 7*24 hours
   * - UniFi controller does not keep these stats longer than 5 hours with versions < 4.6.6
   */
  getHourlyApStats(start = null, end = null, mac = null) {
    if (end === null) {
      end = Date.now();
    }

    if (start === null) {
      start = end - (7 * 24 * 3600 * 1000);
    }

    const payload = {attrs: ['bytes',
      'num_sta',
      'time'],
    start,
    end};

    if (mac !== null) {
      payload.mac = mac.toLowerCase();
    }

    return this._request('/api/s/<SITE>/stat/report/hourly.ap', payload);
  }

  /**
   * Fetch Daily stats method for a single access point or all access points - stat_daily_aps()
   *
   * optional parameter <start> = Unix timestamp in milliseconds
   * optional parameter <end>   = Unix timestamp in milliseconds
   * optional parameter <mac>   = AP MAC address to return stats for
   *
   * NOTES:
   * - defaults to the past 7*24 hours
   * - UniFi controller does not keep these stats longer than 5 hours with versions < 4.6.6
   */
  getDailyApStats(start = null, end = null, mac = null) {
    if (end === null) {
      end = Date.now();
    }

    if (start === null) {
      start = end - (7 * 24 * 3600 * 1000);
    }

    const payload = {attrs: ['bytes',
      'num_sta',
      'time'],
    start,
    end};

    if (mac !== null) {
      payload.mac = mac.toLowerCase();
    }

    return this._request('/api/s/<SITE>/stat/report/daily.ap', payload);
  }

  /**
   * Fetch monthly stats for a single access point or all access points - stat_monthly_aps()
   *
   * NOTES:
   * - defaults to the past 52 weeks (52*7*24 hours)
   * - make sure that the retention policy for hourly stats is set to the correct value in
   *   the controller settings
   *
   * @param  int    $start optional, Unix timestamp in milliseconds
   * @param  int    $end   optional, Unix timestamp in milliseconds
   * @param  string $mac   optional, AP MAC address to return stats for, when empty,
   *                       stats for all APs are returned
   * @return array         returns an array of monthly stats objects
   */
  getMonthlyApStats(start = null, end = null, mac = null) {
    if (end === null) {
      end = Date.now();
    }

    if (start === null) {
      start = end - (52 * 7 * 24 * 3600 * 1000);
    }

    const payload = {attrs: ['bytes',
      'num_sta',
      'time'],
    start,
    end};

    if (mac !== null) {
      payload.mac = mac.toLowerCase();
    }

    return this._request('/api/s/<SITE>/stat/report/monthly.ap', payload);
  }

  /**
   * Fetch 5 minutes stats method for a single user/client device - stat_5minutes_user()
   *
   * returns an array of 5-minute stats objects
   * required parameter <mac>     = MAC address of user/client device to return stats for
   * optional parameter <start>   = Unix timestamp in milliseconds
   * optional parameter <end>     = Unix timestamp in milliseconds
   * optional parameter <attribs> = array containing attributes (strings) to be returned, valid values are:
   *                                rx_bytes, tx_bytes, signal, rx_rate, tx_rate, rx_retries, tx_retries, rx_packets, tx_packets
   *                                default is ['rx_bytes', 'tx_bytes']
   *
   * NOTES:
   * - defaults to the past 12 hours
   * - only supported with UniFi controller versions 5.8.X and higher
   * - make sure that the retention policy for 5 minutes stats is set to the correct value in
   *   the controller settings
   * - make sure that "Clients Historical Data" has been enabled in the UniFi controller settings in the Maintenance section
   */
  get5minUserStats(mac, start = null, end = null, attribs = null) {
    if (end === null) {
      end = Date.now();
    }

    if (start === null) {
      start = end - (12 * 3600 * 1000);
    }

    attribs = attribs === null ? ['time',
      'rx_bytes',
      'tx_bytes'] : ['time', ...attribs];

    const payload = {attrs: attribs,
      start,
      end,
      mac: mac.toLowerCase()};

    return this._request('/api/s/<SITE>/stat/report/5minutes.user', payload);
  }

  /**
   * Fetch Hourly stats method for a a single user/client device - stat_hourly_user()
   *
   * returns an array of hourly stats objects
   * required parameter <mac>     = MAC address of user/client device to return stats for
   * optional parameter <start>   = Unix timestamp in milliseconds
   * optional parameter <end>     = Unix timestamp in milliseconds
   * optional parameter <attribs> = array containing attributes (strings) to be returned, valid values are:
   *                                rx_bytes, tx_bytes, signal, rx_rate, tx_rate, rx_retries, tx_retries, rx_packets, tx_packets
   *                                default is ['rx_bytes', 'tx_bytes']
   *
   * NOTES:
   * - defaults to the past 7*24 hours
   * - only supported with UniFi controller versions 5.8.X and higher
   * - make sure that "Clients Historical Data" has been enabled in the UniFi controller settings in the Maintenance section
   */
  getHourlyUserStats(mac, start = null, end = null, attribs = null) {
    if (end === null) {
      end = Date.now();
    }

    if (start === null) {
      start = end - (7 * 24 * 3600 * 1000);
    }

    attribs = attribs === null ? ['time',
      'rx_bytes',
      'tx_bytes'] : ['time', ...attribs];

    const payload = {attrs: attribs,
      start,
      end,
      mac: mac.toLowerCase()};

    return this._request('/api/s/<SITE>/stat/report/hourly.user', payload);
  }

  /**
   * Fetch Daily stats method for a single user/client device - stat_daily_user()
   *
   * returns an array of daily stats objects
   * required parameter <mac>     = MAC address of user/client device to return stats for
   * optional parameter <start>   = Unix timestamp in milliseconds
   * optional parameter <end>     = Unix timestamp in milliseconds
   * optional parameter <attribs> = array containing attributes (strings) to be returned, valid values are:
   *                                rx_bytes, tx_bytes, signal, rx_rate, tx_rate, rx_retries, tx_retries, rx_packets, tx_packets
   *                                default is ['rx_bytes', 'tx_bytes']
   *
   * NOTES:
   * - defaults to the past 7*24 hours
   * - only supported with UniFi controller versions 5.8.X and higher
   * - make sure that the retention policy for daily stats is set to the correct value in
   *   the controller settings
   * - make sure that "Clients Historical Data" has been enabled in the UniFi controller settings in the Maintenance section
   */
  getDailyUserStats(mac, start = null, end = null, attribs = null) {
    if (end === null) {
      end = Date.now();
    }

    if (start === null) {
      start = end - (7 * 24 * 3600 * 1000);
    }

    attribs = attribs === null ? ['time',
      'rx_bytes',
      'tx_bytes'] : ['time', ...attribs];

    const payload = {attrs: attribs,
      start,
      end,
      mac: mac.toLowerCase()};

    return this._request('/api/s/<SITE>/stat/report/daily.user', payload);
  }

  /**
   * Fetch monthly stats for a single user/client device - stat_monthly_user()
   *
   * NOTES:
   * - defaults to the past 13 weeks (52*7*24 hours)
   * - only supported with UniFi controller versions 5.8.X and higher
   * - make sure that the retention policy for monthly stats is set to the correct value in
   *   the controller settings
   * - make sure that "Clients Historical Data" has been enabled in the UniFi controller settings in the Maintenance section
   *
   * @param  string $mac     MAC address of user/client device to return stats for
   * @param  int    $start   optional, Unix timestamp in milliseconds
   * @param  int    $end     optional, Unix timestamp in milliseconds
   * @param  array  $attribs array containing attributes (strings) to be returned, valid values are:
   *                         rx_bytes, tx_bytes, signal, rx_rate, tx_rate, rx_retries, tx_retries, rx_packets, tx_packets
   *                         default is ['rx_bytes', 'tx_bytes']
   * @return array           returns an array of monthly stats objects
   */
  getMonthlyUserStats(mac, start = null, end = null, attribs = null) {
    if (end === null) {
      end = Date.now();
    }

    if (start === null) {
      start = end - (13 * 7 * 24 * 3600 * 1000);
    }

    attribs = attribs === null ? ['time',
      'rx_bytes',
      'tx_bytes'] : ['time', ...attribs];

    const payload = {attrs: attribs,
      start,
      end,
      mac: mac.toLowerCase()};

    return this._request('/api/s/<SITE>/stat/report/monthly.user', payload);
  }

  /**
   * Fetch 5 minutes gateway stats method - stat_5minutes_gateway()
   *
   * returns an array of 5-minute stats objects for the gateway belonging to the current site
   * optional parameter <start> = Unix timestamp in milliseconds
   * optional parameter <end>   = Unix timestamp in milliseconds
   * optional parameter <attribs> = array containing attributes (strings) to be returned, valid val  ues are:
   *                                mem, cpu, loadavg_5, lan-rx_errors, lan-tx_errors, lan-rx_bytes  ,
   *                                lan-tx_bytes, lan-rx_packets, lan-tx_packets, lan-rx_dropped, l  an-tx_dropped
   *                                default is ['time', 'mem', 'cpu', 'loadavg_5']
   *
   * NOTES:
   * - defaults to the past 12 hours
   * - this function/method is only supported on controller versions 5.5.* and later
   * - make sure that the retention policy for 5 minutes stats is set to the correct value in
   *   the controller settings
   * - requires a USG
   */
  get5minGatewayStats(start = null, end = null, attribs = null) {
    if (end === null) {
      end = Date.now();
    }

    if (start === null) {
      start = end - (12 * 3600 * 1000);
    }

    attribs = attribs === null ? ['time',
      'mem',
      'cpu',
      'loadavg_5'] : ['time', ...attribs];

    const payload = {attrs: attribs,
      start,
      end};

    return this._request('/api/s/<SITE>/stat/report/5minutes.gw', payload);
  }

  /**
   * Fetch Hourly gateway stats method - stat_hourly_gateway()
   *
   * returns an array of hourly stats objects for the gateway belonging to the current site
   * optional parameter <start> = Unix timestamp in milliseconds
   * optional parameter <end>   = Unix timestamp in milliseconds
   * optional parameter <attribs> = array containing attributes (strings) to be returned, valid val  ues are:
   *                                mem, cpu, loadavg_5, lan-rx_errors, lan-tx_errors, lan-rx_bytes  ,
   *                                lan-tx_bytes, lan-rx_packets, lan-tx_packets, lan-rx_dropped, l  an-tx_dropped
   *                                default is ['time', 'mem', 'cpu', 'loadavg_5']
   *
   * NOTES:
   * - defaults to the past 7*24 hours
   * - requires a USG
   */
  getHourlyGatewayStats(start = null, end = null, attribs = null) {
    if (end === null) {
      end = Date.now();
    }

    if (start === null) {
      start = end - (7 * 24 * 3600 * 1000);
    }

    attribs = attribs === null ? ['time',
      'mem',
      'cpu',
      'loadavg_5'] : ['time', ...attribs];

    const payload = {attrs: attribs,
      start,
      end};

    return this._request('/api/s/<SITE>/stat/report/hourly.gw', payload);
  }

  /**
   * Fetch Daily gateway stats method - stat_daily_gateway()
   *
   * returns an array of daily stats objects for the gateway belonging to the current site
   * optional parameter <start> = Unix timestamp in milliseconds
   * optional parameter <end>   = Unix timestamp in milliseconds
   * optional parameter <attribs> = array containing attributes (strings) to be returned, valid val  ues are:
   *                                mem, cpu, loadavg_5, lan-rx_errors, lan-tx_errors, lan-rx_bytes  ,
   *                                lan-tx_bytes, lan-rx_packets, lan-tx_packets, lan-rx_dropped, l  an-tx_dropped
   *                                default is ['time', 'mem', 'cpu', 'loadavg_5']
   *
   * NOTES:
   * - defaults to the past 52 weeks (52*7*24 hours)
   * - requires a USG
   */
  getDailyGatewayStats(start = null, end = null, attribs = null) {
    if (end === null) {
      end = Date.now();
    }

    if (start === null) {
      start = end - (52 * 7 * 24 * 3600 * 1000);
    }

    attribs = attribs === null ? ['time',
      'mem',
      'cpu',
      'loadavg_5'] : ['time', ...attribs];

    const payload = {attrs: attribs,
      start,
      end};

    return this._request('/api/s/<SITE>/stat/report/daily.gw', payload);
  }

  /**
   * Fetch monthly gateway stats - stat_monthly_gateway()
   *
   * NOTES:
   * - defaults to the past 52 weeks (52*7*24 hours)
   * - requires a USG
   *
   * @param  int   $start   optional, Unix timestamp in milliseconds
   * @param  int   $end     optional, Unix timestamp in milliseconds
   * @param  array $attribs array containing attributes (strings) to be returned, valid values are:
   *                        mem, cpu, loadavg_5, lan-rx_errors, lan-tx_errors, lan-rx_bytes,
   *                        lan-tx_bytes, lan-rx_packets, lan-tx_packets, lan-rx_dropped, lan-tx_dropped
   *                        default is ['time', 'mem', 'cpu', 'loadavg_5']
   * @return array          returns an array of monthly stats objects for the gateway belonging to the current site
   */
  getMonthlyGatewayStats(start = null, end = null, attribs = null) {
    if (end === null) {
      end = Date.now();
    }

    if (start === null) {
      start = end - (52 * 7 * 24 * 3600 * 1000);
    }

    attribs = attribs === null ? ['time',
      'mem',
      'cpu',
      'loadavg_5'] : ['time', ...attribs];

    const payload = {attrs: attribs,
      start,
      end};

    return this._request('/api/s/<SITE>/stat/report/monthly.gw', payload);
  }

  /**
   * Fetch speed test results method - stat_speedtest_results()
   *
   * returns an array of speed test result objects
   * optional parameter <start> = Unix timestamp in milliseconds
   * optional parameter <end>   = Unix timestamp in milliseconds
   *
   * NOTES:
   * - defaults to the past 24 hours
   * - requires a USG
   */
  getSpeedTestResults(start = null, end = null) {
    if (end === null) {
      end = Date.now();
    }

    if (start === null) {
      start = end - (24 * 3600 * 1000);
    }

    const payload = {
      attrs: ['xput_download',
        'xput_upload',
        'latency',
        'time'],
      start,
      end};

    return this._request('/api/s/<SITE>/stat/report/archive.speedtest', payload);
  }

  /**
   * Fetch IPS/IDS events methods - stat_ips_events
   *
   * returns an array of IPS/IDS event objects
   * optional parameter <start> = Unix timestamp in milliseconds
   * optional parameter <end>   = Unix timestamp in milliseconds
   * optional parameter <limit> = Maximum number of events to return, defaults to 10000
   *
   * NOTES:
   * - defaults to the past 24 hours
   * - requires a USG
   * - supported in UniFi controller versions 5.9.X and higher
   */
  getIPSEvents(start = null, end = null, limit = 10_000) {
    if (end === null) {
      end = Date.now();
    }

    if (start === null) {
      start = end - (24 * 3600 * 1000);
    }

    const payload = {start,
      end,
      _limit: limit};

    return this._request('/api/s/<SITE>/stat/ips/event', payload);
  }

  /**
   * Fetch login sessions - stat_sessions()
   *
   * returns an array of login session objects for all devices or a single device
   * optional parameter <start> = Unix timestamp in seconds
   * optional parameter <end>   = Unix timestamp in seconds
   * optional parameter <mac>   = client MAC address to return sessions for (can only be used when start and end are also provided)
   * optional parameter <type>  = client type to return sessions for, can be 'all', 'guest' or 'user'; default value is 'all'
   *
   * NOTES:
   * - defaults to the past 7*24 hours
   */
  getSessions(start = null, end = null, mac = null, type = 'all') {
    if (end === null) {
      end = Math.floor(Date.now() / 1000);
    }

    if (start === null) {
      start = end - (7 * 24 * 3600);
    }

    const payload = {type,
      start,
      end};

    if (mac !== null) {
      payload.mac = mac.toLowerCase();
    }

    return this._request('/api/s/<SITE>/stat/session', payload);
  }

  /**
   * Fetch latest 'n' login sessions for a single client device - stat_sta_sessions_latest()
   *
   * required parameter <mac>   = client MAC address
   * optional parameter <limit> = maximum number of sessions to get (defaults to 5)
   *
   */
  getLatestSessions(mac, limit = 5) {
    const payload = {mac: mac.toLowerCase(),
      _limit: limit,
      _sort: '-assoc_time'};

    return this._request('/api/s/<SITE>/stat/session', payload);
  }

  /**
   * Fetch authorizations - stat_auths()
   *
   * optional parameter <start> = Unix timestamp in seconds
   * optional parameter <end>   = Unix timestamp in seconds
   *
   * NOTES:
   * - defaults to the past 7*24 hours
   */
  getAllAuthorizations(start = null, end = null) {
    if (end === null) {
      end = Math.floor(Date.now() / 1000);
    }

    if (start === null) {
      start = end - (7 * 24 * 3600);
    }

    return this._request('/api/s/<SITE>/stat/authorization', {start, end});
  }

  /**
   * Fetch client devices that connected to the site within given timeframe - stat_allusers()
   *
   * optional parameter <historyhours> = hours to go back (default is 8760 hours or 1 year)
   *
   * NOTES:
   * - <historyhours> is only used to select clients that were online within that period,
   *    the returned stats per client are all-time totals, irrespective of the value of <historyhours>
   */
  getAllUsers(within = 8760) {
    const payload = {type: 'all',
      conn: 'all',
      within};

    return this._request('/api/s/<SITE>/stat/alluser', payload);
  }

  /**
   * List all blocked client devices ever connected to the site
   *
   * optional parameter <historyhours> = hours to go back (default is 8760 hours or 1 year)
   *
   * NOTES:
   * - <historyhours> is only used to select clients that were online within that period,
   *    the returned stats per client are all-time totals, irrespective of the value of <historyhours>
   */
  getBlockedUsers(within = 8760) {
    const payload = {type: 'blocked',
      conn: 'all',
      within};

    return this._request('/api/s/<SITE>/stat/alluser', payload);
  }

  /**
   * Fetch guest devices - list_guests()
   *
   * optional parameter <within> = time frame in hours to go back to list guests with valid access (default = 24*365 hours)
   *
   * NOTES:
   * - defaults to the past 7*24 hours
   *
   */
  getGuests(within = 8760) {
    return this._request('/api/s/<SITE>/stat/guest', {within});
  }

  /**
   * Fetch online client device(s) - list_clients()
   *
   * returns an array of online client device objects, or in case of a single device request, returns a single client device object
   *
   * optional parameter <client_mac> = the MAC address of a single online client device for which the call must be made
   */
  getClientDevices(client_mac = '') {
    return this._request('/api/s/<SITE>/stat/sta/' + client_mac.trim().toLowerCase());
  }

  /**
   * Fetch details for a single client device - stat_client()
   *
   * optional parameter <client_mac> = the MAC address of a single online client device for which the call must be made
   */
  getClientDevice(client_mac = '') {
    return this._request('/api/s/<SITE>/stat/user/' + client_mac.trim().toLowerCase());
  }

  /**
   * Assign client device to another group - set_usergroup()
   *
   * @param string $client_id  _id value of the client device to be modified
   * @param string $group_id   _id value of the user group to assign client device to
   * @return bool returns true upon success
   */
  setUserGroup(client_id, group_id) {
    return this._request('/api/s/<SITE>/upd/user/' + client_id.trim(), {usergroup_id: group_id});
  }

  /**
   * Update client fixedip (using REST) - edit_client_fixedip()
   *
   * returns an array containing a single object with attributes of the updated client on success
   * required parameter <client_id>   = _id of the client
   * required parameter <use_fixedip> = boolean defining whether if use_fixedip is true or false
   * optional parameter <network_id>  = _id value for the network where the ip belongs to
   * optional parameter <fixed_ip>    = value of client's fixed_ip field
   *
   */
  editClientFixedIP(client_id, use_fixedip, network_id = null, fixed_ip = null) {
    const payload = {_id: client_id,
      use_fixedip};

    if (use_fixedip === true) {
      if (network_id !== null) {
        payload.network_id = network_id;
      }

      if (fixed_ip !== null) {
        payload.fixed_ip = fixed_ip;
      }
    }

    return this._request('/api/s/<SITE>/rest/user/' + client_id.trim(), payload, 'PUT');
  }

  /**
   * Update client name (using REST) - edit_client_name()
   *
   * @param string $client_id   _id value for the client
   * @param bool   $name of the client
   * @return array|false returns an array containing a single object with attributes of the updated client on success
   */
  editClientName(client_id, name) {
    const payload = {_id: client_id,
      name};

    return this._request('/api/s/<SITE>/rest/user/' + client_id.trim(), payload, 'PUT');
  }

  /**
   * Fetch user groups - list_usergroups()
   *
   */
  getUserGroups() {
    return this._request('/api/s/<SITE>/list/usergroup');
  }

  /**
   * Create user group (using REST) - create_usergroup()
   *
   * returns an array containing a single object with attributes of the new usergroup ("_id", "name", "qos_rate_max_down", "qos_rate_max_up", "site_id") on success
   *
   * required parameter <group_name> = name of the user group
   * optional parameter <group_dn>   = limit download bandwidth in Kbps (default = -1, which sets bandwidth to unlimited)
   * optional parameter <group_up>   = limit upload bandwidth in Kbps (default = -1, which sets bandwidth to unlimited)
   *
   */
  createUserGroup(group_name, group_dn = -1, group_up = -1) {
    const payload = {name: group_name,
      qos_rate_max_down: group_dn,
      qos_rate_max_up: group_up};

    return this._request('/api/s/<SITE>/rest/usergroup', payload);
  }

  /**
   * Modify user group (using REST) - edit_usergroup()
   *
   * returns an array containing a single object with attributes of the updated usergroup on success
   *
   * required parameter <group_id>   = _id of the user group
   * required parameter <site_id>    = _id of the site
   * required parameter <group_name> = name of the user group
   * optional parameter <group_dn>   = limit download bandwidth in Kbps (default = -1, which sets bandwidth to unlimited)
   * optional parameter <group_up>   = limit upload bandwidth in Kbps (default = -1, which sets bandwidth to unlimited)
   *
   */
  editUserGroup(group_id, site_id, group_name, group_dn = -1, group_up = -1) {
    const payload = {_id: group_id,
      site_id,
      name: group_name,
      qos_rate_max_down: group_dn,
      qos_rate_max_up: group_up};

    return this._request('/api/s/<SITE>/rest/usergroup/' + group_id.trim(), payload, 'PUT');
  }

  /**
   * Delete user group (using REST) - delete_usergroup()
   *
   * returns true on success
   *
   * required parameter <group_id> = _id value of the user group to delete
   *
   */
  deleteUserGroup(group_id) {
    return this._request('/api/s/<SITE>/rest/usergroup/' + group_id.trim(), null, 'DELETE');
  }

  /**
   * Fetch AP groups - list_apgroups()
   *
   * @return array  containing the current AP groups on success
   */
  getAPGroups() {
    return this._request('/v2/api/site/<SITE>/apgroups');
  }

  /**
   * Create AP group - create_apgroup()
   *
   * @param  string $group_name  name to assign to the AP group
   * @param  array  $device_macs optional, array containing the MAC addresses (strings) of the APs to add to the new group
   * @return object              returns a single object with attributes of the new AP group on success
   *
   */
  createAPGroup(group_name, device_macs = []) {
    const payload = {device_macs,
      name: group_name
    };

    return this._request('/v2/api/site/<SITE>/apgroups', payload);
  }

  /**
   * Modify AP group - edit_apgroup()
   *
   * @param  string $group_id    _id value of the AP group to modify
   * @param  string $group_name  name to assign to the AP group
   * @param  array  $device_macs array containing the members of the AP group which overwrites the existing
   *                             group_members (passing an empty array clears the AP member list)
   * @return object              returns a single object with attributes of the updated AP group on success
   *
   */
  editAPGroup(group_id, group_name, device_macs) {
    const payload = {_id: group_id,
      attr_no_delete: false,
      name: group_name,
      device_macs};

    return this._request('/v2/api/site/<SITE>/apgroups/' + group_id.trim(), payload, 'PUT');
  }

  /**
   * Delete AP group - delete_apgroup()
   *
   * @param  string $group_id _id value of the AP group to delete
   * @return bool             returns true on success
   *
   */
  deleteAPGroup(group_id) {
    return this._request('/v2/api/site/<SITE>/apgroups/' + group_id.trim(), null, 'DELETE');
  }

  /**
   * List firewall groups (using REST) - list_firewallgroups()
   *
   * returns an array containing the current firewall groups or the selected firewall group on success
   * optional parameter <group_id> = _id value of the single firewall group to list
   */
  getFirewallGroups(group_id = '') {
    return this._request('/api/s/<SITE>/rest/firewallgroup/' + group_id.trim());
  }

  /**
   * Create firewall group (using REST) - create_firewallgroup()
   *
   * returns an array containing a single object with attributes of the new firewall group on succe  ss
   * required parameter <group_name>    = name to assign to the firewall group
   * required parameter <group_type>    = firewall group type; valid values are address-group, ipv6  -address-group, port-group
   * optional parameter <group_members> = array containing the members of the new group (IPv4 addre  sses, IPv6 addresses or port numbers)
   *                                      (default is an empty array)
   */
  createFirewallGroup(group_name, group_type, group_members = []) {
    const payload = {name: group_name,
      group_type,
      group_members};

    return this._request('/api/s/<SITE>/rest/firewallgroup', payload);
  }

  /**
   * Modify firewall group (using REST) - edit_firewallgroup
   *
   * returns an array containing a single object with attributes of the updated firewall group on s  uccess
   * required parameter <group_id>      = _id value of the firewall group to modify
   * required parameter <site_id>       = site_id value of the firewall group to modify
   * required parameter <group_name>    = name of the firewall group
   * required parameter <group_type>    = firewall group type; valid values are address-group, ipv6  -address-group, port-group,
   *                                      group_type cannot be changed for an existing firewall gro  up!
   * optional parameter <group_members> = array containing the members of the group (IPv4 addresses  , IPv6 addresses or port numbers)
   *                                      which overwrites the existing group_members (default   is an empty array)
   *
   *
   */
  editFirewallGroup(group_id, site_id, group_name, group_type, group_members = []) {
    const payload = {_id: group_id,
      name: group_name,
      group_type,
      group_members,
      site_id};

    return this._request('/api/s/<SITE>/rest/firewallgroup/' + group_id.trim(), payload, 'PUT');
  }

  /**
   * Delete firewall group (using REST) - delete_firewallgroup()
   *
   * returns true on success
   * required parameter <group_id> = _id value of the firewall group to delete
   */
  deleteFirewallGroup(group_id) {
    return this._request('/api/s/<SITE>/rest/firewallgroup/' + group_id.trim(), null, 'DELETE');
  }

  /**
   * List firewall rules (using REST) - list_firewallrules()
   *
   * returns an array containing the current firewall rules on success
   */
  getFirewallRules() {
    return this._request('/api/s/<SITE>/rest/firewallrule');
  }

  /**
   * List static routing settings (using REST) - list_routing()
   *
   * returns an array of static routes and their settings
   * optional parameter <route_id> = string; _id value of the static route to get settings for
   */
  getRouting(route_id = '') {
    return this._request('/api/s/<SITE>/rest/routing/' + route_id.trim());
  }

  /**
   * List health metrics - list_health()
   *
   */
  getHealth() {
    return this._request('/api/s/<SITE>/stat/health');
  }

  /**
   * List dashboard metrics - list_dashboard()
   *
   * returns an array of dashboard metric objects (available since controller version 4.9.1.alpha)
   * optional parameter <five_minutes> = boolean; if true, return stats based on 5 minute intervals,
   *                                     returns hourly stats by default (supported on controller versions 5.5.* and higher)
   */
  getDashboard(five_minutes = false) {
    const path_suffix = five_minutes === true ? '?scale=5minutes' : '';

    return this._request('/api/s/<SITE>/stat/dashboard' + path_suffix);
  }

  /**
   * List client devices - list_users()
   *
   * returns an array of known client device objects
   */
  getUsers() {
    return this._request('/api/s/<SITE>/list/user');
  }

  /**
   * List of site devices with a basic subset of fields (e.g., mac, state, adopted, disabled, type, model, name) - list_devices_basic()
   *
   * returns an array containing known UniFi device objects)
   */
  getAccessDevicesBasic() {
    return this._request('/api/s/<SITE>/stat/device-basic');
  }

  /**
   * List access points and other devices under management of the controller (USW and/or USG devices) - list_devices()
   *
   * optional paramater <device_mac> = the MAC address of a single device for which the call must be made
   */
  getAccessDevices(device_mac = '') {
    return this._request('/api/s/<SITE>/stat/device/' + device_mac.trim().toLowerCase());
  }

  /**
   * List (device) tags (using REST) - list_tags()
   *
   * returns an array of known device tag objects
   *
   * NOTES: this endpoint was introduced with controller versions 5.5.X
   */
  listTags() {
    return this._request('/api/s/<SITE>/rest/tag');
  }

  /**
   * List rogue/neighboring access points - list_rogueaps()
   *
   * returns an array of rogue/neighboring access point objects
   * optional parameter <within> = hours to go back to list discovered "rogue" access points (default = 24 hours)
   *
   */
  getRogueAccessPoints(within = 24) {
    return this._request('/api/s/<SITE>/stat/rogueap', {within});
  }

  /**
   * List known rogue access points - list_known_rogueaps()
   *
   * returns an array of known rogue access point objects
   */
  getKnownRogueAccessPoints() {
    return this._request('/api/s/<SITE>/rest/rogueknown');
  }

  /**
   * Generate a backup - generate_backup()
   *
   * returns a URL from where the backup file can be downloaded once generated
   *
   * NOTES: this is an experimental function, please do not use unless you know
   * exactly what you're doing
   */
  generateBackup() {
    return this._request('/api/s/<SITE>/cmd/backup', {cmd: 'backup'});
  }

  /**
   * List auto backups - list_backups()
   *
   * return an array containing objects with backup details on success
   */
  getBackups() {
    return this._request('/api/s/<SITE>/cmd/backup', {cmd: 'list-backups'});
  }

  /**
   * Generate a backup/export of the current site - generate_backup_site()
   *
   * NOTES: this is an experimental function, please do not use unless you know
   * exactly what you're doing
   */
  generateBackupSite() {
    return this._request('/api/s/<SITE>/cmd/backup', {cmd: 'export-site'});
  }

  /**
   * Delete a backup file
   *
   * return true on success
   * required parameter <filename> = string; filename of backup to delete
   */
  deleteBackup(filename) {
    return this._request('/api/s/<SITE>/cmd/backup', {cmd: 'delete-backup', filename});
  }

  /**
   * List sites
   *
   * calls callback function(err, result) with an array of the sites
   * registered to the UniFi controller
   */
  getSites() {
    return this._request('/api/self/sites');
  }

  /**
   * List sites stats
   *
   * calls callback function(err, result) with an array of sysinfo information
   * for all sites registered to the UniFi controller
   *
   * NOTES: endpoint was introduced with controller version 5.2.9
   */
  getSitesStats() {
    return this._request('/api/stat/sites');
  }

  /**
   * Create a site - create_site()
   *
   * required parameter <description> = the long name for the new site
   *
   * NOTES: immediately after being added, the new site is available in the output of the "list_sites" function
   */
  createSite(description) {
    const payload = {desc: description,
      cmd: 'add-site'};

    return this._request('/api/s/<SITE>/cmd/sitemgr', payload);
  }

  /**
   * Delete a site - delete_site()
   *
   * return true on success
   * required parameter <site_id> = 24 char string; _id value of the site to delete
   *
   */
  deleteSite(site_id) {
    const payload = {site: site_id,
      cmd: 'delete-site'};

    return this._request('/api/s/<SITE>/cmd/sitemgr', payload);
  }

  /**
   * Change the current site's name - set_site_name()
   *
   * return true on success
   * required parameter <site_name> = string; the new long name for the current site
   *
   * NOTES: immediately after being changed, the site is available in the output of the list_sites() function
   */
  setSiteName(site_name) {
    const payload = {desc: site_name,
      cmd: 'update-site'};

    return this._request('/api/s/<SITE>/cmd/sitemgr', payload);
  }

  /**
   * Set site country - set_site_country()
   *
   * required parameter <payload> = stdClass object or associative array containing the configuration to apply to the network, must be a (partial)
   *                                object structured in the same manner as is returned by list_settings() for the "country" key.
   *                                Valid country codes can be obtained using the list_country_codes() function/method.
   *                                Do not include the _id property, it is assigned by the controller and returned upon success.
   * return true on success
   */
  setSiteCountry(country_id, payload) {
    return this._request('/api/s/<SITE>/rest/setting/country/' + country_id.trim(), payload, 'PUT');
  }

  /**
   * Set site locale - set_site_locale()
   *
   * required parameter <payload> = stdClass object or associative array containing the configuration to apply to the network, must be a (partial)
   *                                object structured in the same manner as is returned by list_settings() for the "locale" key.
   *                                Do not include the _id property, it is assigned by the controller and returned upon success.
   * return true on success
   */
  setSiteLocale(locale_id, payload) {
    return this._request('/api/s/<SITE>/rest/setting/locale/' + locale_id.trim(), payload, 'PUT');
  }

  /**
   * Set site snmp - set_site_snmp()
   *
   * required parameter <payload> = stdClass object or associative array containing the configuration to apply to the network, must be a (partial)
   *                                object structured in the same manner as is returned by list_settings() for the "snmp" key.
   *                                Do not include the _id property, it is assigned by the controller and returned upon success.
   * return true on success
   */
  setSiteSNMP(snmp_id, payload) {
    return this._request('/api/s/<SITE>/rest/setting/snmp/' + snmp_id.trim(), payload, 'PUT');
  }

  /**
   * Set site mgmt - set_site_mgmt()
   *
   * required parameter <payload> = stdClass object or associative array containing the configuration to apply to the network, must be a (partial)
   *                                object structured in the same manner as is returned by list_settings() for the "mgmt" key.
   *                                Do not include the _id property, it is assigned by the controller and returned upon success.
   * return true on success
   */
  setSiteMgmt(mgmt_id, payload) {
    return this._request('/api/s/<SITE>/rest/setting/mgmt/' + mgmt_id.trim(), payload, 'PUT');
  }

  /**
   * Set site guest access - set_site_guest_access()
   *
   * required parameter <payload> = stdClass object or associative array containing the configuration to apply to the network, must be a (partial)
   *                                object structured in the same manner as is returned by list_settings() for the "guest_access" key.
   *                                Do not include the _id property, it is assigned by the controller and returned upon success.
   * return true on success
   */
  setSiteGuestAccess(guest_access_id, payload) {
    return this._request('/api/s/<SITE>/rest/setting/guest_access/' + guest_access_id.trim(), payload, 'PUT');
  }

  /**
   * Set site ntp - set_site_ntp()
   *
   * required parameter <payload> = stdClass object or associative array containing the configuration to apply to the network, must be a (partial)
   *                                object structured in the same manner as is returned by list_settings() for the "ntp" key.
   *                                Do not include the _id property, it is assigned by the controller and returned upon success.
   * return true on success
   */
  setSiteNTP(ntp_id, payload) {
    return this._request('/api/s/<SITE>/rest/setting/ntp/' + ntp_id.trim(), payload, 'PUT');
  }

  /**
   * Set site connectivity - set_site_connectivity()
   *
   * required parameter <payload> = stdClass object or associative array containing the configuration to apply to the network, must be a (partial)
   *                                object structured in the same manner as is returned by list_settings() for the "connectivity" key.
   *                                Do not include the _id property, it is assigned by the controller and returned upon success.
   * return true on success
   */
  setSiteConnectivity(connectivity_id, payload) {
    return this._request('/api/s/<SITE>/rest/setting/connectivity/' + connectivity_id.trim(), payload, 'PUT');
  }

  /**
   * Fetch admins - list_admins()
   *
   * @return array  containing administrator objects for selected site
   */
  listAdmins() {
    return this._request('/api/s/<SITE>/cmd/sitemgr', {cmd: 'get-admins'});
  }

  /**
   * Fetch all admins - list_all_admins()
   *
   * @return array  containing administrator objects for all sites
   */
  listAllAdmins() {
    return this._request('/api/stat/admin');
  }

  /**
   * Invite a new admin for access to the current site - invite_admin()
   *
   * returns true on success
   * required parameter <name>           = string, name to assign to the new admin user
   * required parameter <email>          = email address to assign to the new admin user
   * optional parameter <enable_sso>     = boolean, whether or not SSO is allowed for the new admin
   *                                       default value is true which enables the SSO capability
   * optional parameter <readonly>       = boolean, whether or not the new admin has readonly
   *                                       permissions, default value is false which gives the new admin
   *                                       Administrator permissions
   * optional parameter <device_adopt>   = boolean, whether or not the new admin has permissions to
   *                                       adopt devices, default value is false. With versions < 5.9.X this only applies
   *                                       when readonly is true.
   * optional parameter <device_restart> = boolean, whether or not the new admin has permissions to
   *                                       restart devices, default value is false. With versions < 5.9.X this only applies
   *                                       when readonly is true.
   *
   * NOTES:
   * - after issuing a valid request, an invite is sent to the email address provided
   * - issuing this command against an existing admin triggers a "re-invite"
   */
  inviteAdmin(name, email, enable_sso = true, readonly = false, device_adopt = false, device_restart = false) {
    const payload = {name: name.trim(),
      email: email.trim(),
      for_sso: enable_sso,
      cmd: 'invite-admin',
      role: 'admin'
    };

    if (readonly === true) {
      payload.role = 'readonly';
    }

    const permissions = [];
    if (device_adopt === true) {
      permissions.push('API_DEVICE_ADOPT');
    }

    if (device_restart === true) {
      permissions.push('API_DEVICE_RESTART');
    }

    payload.permissions = permissions;

    return this._request('/api/s/<SITE>/cmd/sitemgr', payload);
  }

  /**
   * Assign an existing admin to the current site - assign_existing_admin()
   *
   * returns true on success
   * required parameter <admin_id>       = 24 char string; _id value of the admin user to assign, can be obtained using the
   *                                       list_all_admins() method/function
   * optional parameter <readonly>       = boolean, whether or not the new admin has readonly
   *                                       permissions, default value is false which gives the new admin
   *                                       Administrator permissions
   * optional parameter <device_adopt>   = boolean, whether or not the new admin has permissions to
   *                                       adopt devices, default value is false. With versions < 5.9.X this only applies
   *                                       when readonly is true.
   * optional parameter <device_restart> = boolean, whether or not the new admin has permissions to
   *                                       restart devices, default value is false. With versions < 5.9.X this only applies
   *                                       when readonly is true.
   */
  assignExistingAdmin(admin_id, readonly = false, device_adopt = false, device_restart = false) {
    const payload = {cmd: 'grant-admin',
      admin: admin_id.trim(),
      role: 'admin'
    };

    if (readonly === true) {
      payload.role = 'readonly';
    }

    const permissions = [];
    if (device_adopt === true) {
      permissions.push('API_DEVICE_ADOPT');
    }

    if (device_restart === true) {
      permissions.push('API_DEVICE_RESTART');
    }

    payload.permissions = permissions;

    return this._request('/api/s/<SITE>/cmd/sitemgr', payload);
  }

  /**
   * Revoke an admin from the current site - revoke_admin()
   *
   * returns true on success
   * required parameter <admin_id> = _id value of the admin to revoke, can be obtained using the
   *                                 list_all_admins() method/function
   *
   * NOTES:
   * only non-superadmin accounts can be revoked
   */
  revokeAdmin(admin_id) {
    return this._request('/api/s/<SITE>/cmd/sitemgr', {cmd: 'revoke-admin', admin: admin_id});
  }

  /**
   * Fetch wlan_groups - list_wlan_groups()
   *
   * @return array  containing known wlan_groups
   */
  getWLanGroups() {
    return this._request('/api/s/<SITE>/list/wlangroup');
  }

  /**
   * Fetch sysinfo - stat_sysinfo()
   *
   * @return array  containing known sysinfo data
   */
  getSiteSysinfo() {
    return this._request('/api/s/<SITE>/stat/sysinfo');
  }

  /**
   * Fetch controller status - stat_status()
   *
   * NOTES:
   * login not required
   *
   * @return bool true upon success (controller is online)
   */
  getStatus() {
    return this._request('/status', {});
  }

  /**
   * Fetch full controller status - stat_full_status()
   *
   * NOTES:
   * login not required
   *
   * @return bool|array  staus array upon success, false upon failure
   */
  async getFullStatus() {
    const result = await this._request('/status', {}, null, true);
    if (result === null) {
      throw new Error('false');
    } else {
      return result;
    }
  }

  /**
   * Fetch device name mappings - list_device_name_mappings()
   *
   * NOTES:
   * login not required
   *
   * @return bool|array  mappings array upon success, false upon failure
   */
  async getDeviceNameMappings() {
    const result = await this._request('/dl/firmware/bundles.json', {}, null, true);
    if (result === null) {
      throw new Error('false');
    } else {
      return result;
    }
  }

  /**
   * Fetch self - list_self()
   *
   * @return array  containing information about the logged in user
   */
  getSelf() {
    return this._request('/api/s/<SITE>/self');
  }

  /**
   * Fetch vouchers - stat_voucher()
   *
   * @param  int   $create_time optional, create time of the vouchers to fetch in Unix timestamp in seconds
   * @return array              containing hotspot voucher objects
   */
  getVouchers(create_time = null) {
    const payload = {};
    if (create_time !== null) {
      payload.create_time = create_time;
    }

    return this._request('/api/s/<SITE>/stat/voucher', payload);
  }

  /**
   * List payments - stat_payment()
   *
   * returns an array of hotspot payments
   */
  getPayments(within = null) {
    within = within === null ? '' : '?within=' + within.trim();

    return this._request('/api/s/<SITE>/stat/payment' + within);
  }

  /**
   * Create hotspot operator (using REST) - create_hotspotop()
   *
   * required parameter <name>       = name for the hotspot operator
   * required parameter <x_password> = clear text password for the hotspot operator
   * optional parameter <note>       = note to attach to the hotspot operator
   */
  createHotspotOperator(name, x_password, note = null) {
    const payload = {name,
      x_password};

    if (note !== null) {
      payload.note = note.trim();
    }

    return this._request('/api/s/<SITE>/rest/hotspotop', payload);
  }

  /**
   * Fetch hotspot operators (using REST) - list_hotspotop()
   *
   * @return array  containing hotspot operators
   */
  getHotspotOperators() {
    return this._request('/api/s/<SITE>/rest/hotspotop');
  }

  /**
   * Create voucher(s) - create_voucher()
   *
   * returns an array containing a single object which contains the create_time(stamp) of the voucher(s) created
   *
   * required parameter <minutes> = minutes the voucher is valid after activation (expiration time)
   * optional parameter <count>   = number of vouchers to create, default value is 1
   * optional parameter <quota>   = single-use or multi-use vouchers, value '0' is for multi-use, '1' is for single-use,
   *                                'n' is for multi-use n times
   * optional parameter <note>    = note text to add to voucher when printing
   * optional parameter <up>      = upload speed limit in kbps
   * optional parameter <down>    = download speed limit in kbps
   * optional parameter <megabytes>= data transfer limit in MB
   *
   * NOTES: please use the stat_voucher() method/function to retrieve the newly created voucher(s) by create_time
   */
  createVouchers(minutes, count = 1, quota = 0, note = null, up = null, down = null, megabytes = null) {
    const payload = {cmd: 'create-voucher',
      expire: minutes,
      n: count,
      quota};

    if (note !== null) {
      payload.note = note.trim();
    }

    if (up !== null) {
      payload.up = up;
    }

    if (down !== null) {
      payload.down = down;
    }

    if (megabytes !== null) {
      payload.bytes = megabytes;
    }

    return this._request('/api/s/<SITE>/cmd/hotspot', payload);
  }

  /**
   * Revoke voucher - revoke_voucher()
   *
   * return TRUE on success
   *
   * required parameter <voucher_id> = 24 char string; _id value of the voucher to revoke
   */
  revokeVoucher(voucher_id) {
    const payload = {cmd: 'delete-voucher',
      _id: voucher_id};

    return this._request('/api/s/<SITE>/cmd/hotspot', payload);
  }

  /**
   * Extend guest validity - extend_guest_validity()
   *
   * return TRUE on success
   *
   * required parameter <guest_id> = 24 char string; _id value of the guest to extend validity
   */
  extendGuestValidity(guest_id) {
    const payload = {cmd: 'extend',
      _id: guest_id};

    return this._request('/api/s/<SITE>/cmd/hotspot', payload);
  }

  /**
   * Fetch port forwarding stats - list_portforward_stats()
   *
   * @return array  containing port forwarding stats
   */
  getPortForwardingStats() {
    return this._request('/api/s/<SITE>/stat/portforward');
  }

  /**
   * Fetch DPI stats - list_dpi_stats()
   *
   * @return array  containing DPI stats
   */
  getDPIStats() {
    return this._request('/api/s/<SITE>/stat/dpi');
  }

  /**
   * List filtered DPI stats
   *
   * returns an array of fileterd DPI stats
   * optional parameter <type>       = whether to returns stats by app or by category, valid values  :
   *                                   'by_cat' or 'by_app'
   * optional parameter <cat_filter> = an array containing numeric category ids to filter by,
   *                                   only to be combined with a "by_app" value for $type
   */
  getFilteredDPIStats(type = 'by_cat', cat_filter = null) {
    const payload = {type};

    if (cat_filter !== null && type === 'by_app') {
      payload.cats = cat_filter;
    }

    return this._request('/api/s/<SITE>/stat/sitedpi', payload);
  }

  /**
   * Clear DPI stats
   *
   * clears stats of DPI
   */
  clearDPIStatus() {
    return this._request('/api/s/<SITE>/cmd/stat', {cmd: 'clear-dpi'});
  }

  /**
   * Fetch current channels - list_current_channels()
   *
   * @return array  containing currently allowed channels
   */
  getCurrentChannels() {
    return this._request('/api/s/<SITE>/stat/current-channel');
  }

  /**
   * Fetch country codes - list_country_codes()
   *
   * NOTES:
   * these codes following the ISO standard:
   * https://en.wikipedia.org/wiki/ISO_3166-1_numeric
   *
   * @return array  containing available country codes
   */
  getCountryCodes() {
    return this._request('/api/s/<SITE>/stat/ccode');
  }

  /**
   * Fetch port forwarding settings - list_portforwarding()
   *
   * @return array  containing port forwarding settings
   */
  getPortForwarding() {
    return this._request('/api/s/<SITE>/list/portforward');
  }

  /**
   * Set port forwarding rule - set_port_forwarding()
   *
   * required parameter <rule_id> = id of port forwarding rule retrieved by getPortForwarding
   * required parameter <enable>  = enable (true) or disable (false) rule
   *
   * @return true on success
   */
  setPortForwarding(rule_id, enable) {
    return this._request('/api/s/<SITE>/rest/portforward/' + rule_id.trim(), {enabled: enable}, 'PUT');
  }

  /**
   * Fetch dynamic DNS settings - list_dynamicdns()
   *
   * @return array  containing dynamic DNS settings
   */
  getDynamicDNS() {
    return this._request('/api/s/<SITE>/list/dynamicdns');
  }

  /**
   * Create dynamic DNS settings, base (using REST) - create_dynamicdns()
   *
   * return true on success
   * required parameter <payload> = stdClass object or associative array containing the configuration to apply to the site, must be a
   *                                (partial) object/array structured in the same manner as is returned by list_dynamicdns() for the site.
   */
  createDynamicDNS(payload) {
    return this._request('/api/s/<SITE>/rest/dynamicdns', payload);
  }

  /**
   * Update site dynamic DNS, base (using REST) - set_dynamicdns
   *
   * return true on success
   * required parameter <dynamicdns_id> = 24 char string; _id of the settings which can be found with the list_dynamicdns() function
   * required parameter <payload> = stdClass object or associative array containing the configuration to apply to the site, must be a
   *                                (partial) object/array structured in the same manner as is returned by list_dynamicdns() for the site.
   */
  setDynamicDNS(dynamicdns_id, payload) {
    return this._request('/api/s/<SITE>/rest/dynamicdns/' + dynamicdns_id.trim(), payload, 'PUT');
  }

  /**
   * Fetch port configurations - list_portconf()
   *
   * @return array  containing port configurations
   */
  getPortConfig() {
    return this._request('/api/s/<SITE>/list/portconf');
  }

  /**
   * Fetch VoIP extensions - list_extensions()
   *
   * @return array  containing VoIP extensions
   */
  getVoipExtensions() {
    return this._request('/api/s/<SITE>/list/extension');
  }

  /**
   * Fetch site settings - list_settings()
   *
   * @return array  containing site configuration settings
   */
  getSiteSettings() {
    return this._request('/api/s/<SITE>/get/setting');
  }

  /**
   * Adopt a device to the selected site - adopt_device()
   *
   * required parameter <mac> = device MAC address
   */
  adoptDevice(mac) {
    const payload = {cmd: 'adopt',
      mac: mac.toLowerCase()};

    return this._request('/api/s/<SITE>/cmd/devmgr', payload);
  }

  /**
   * Adopt a device using custom SSH credentials - advanced_adopt_device()
   *
   * @param string $mac            device MAC address
   * @param string $ip             IP to use for SSH connection
   * @param string $username       SSH username
   * @param string $password       SSH password
   * @param string $url            inform URL to point the device to
   * @param int    $port           optional, SSH port
   * @param bool   $ssh_key_verify optional, whether to verify device SSH key
   * @return bool true on success
   */
  advancedAdoptDevice(mac, ip, username, password, url, port = 22, ssh_key_verify = true) {
    const payload = {cmd: 'adv-adopt',
      mac: mac.toLowerCase(),
      ip,
      username,
      password,
      url,
      port,
      sshKeyVerify: ssh_key_verify};

    return this._request('/api/s/<SITE>/cmd/devmgr', payload);
  }

  /**
   * Reboot a device - restart_device()
   *
   * @param string $mac         device MAC address
   * @param string $reboot_type optional, two options: 'soft' or 'hard', defaults to soft
   *                            soft can be used for all devices, requests a plain restart of that device
   *                            hard is special for PoE switches and besides the restart also requests a
   *                            power cycle on all PoE capable ports. Keep in mind that a 'hard' reboot
   *                            does *NOT* trigger a factory-reset.
   * @return bool true on success
   */
  restartDevice(mac, reboot_type = 'soft') {
    const payload = {cmd: 'restart',
      mac: mac.toLowerCase(),
      reboot_type: reboot_type.toLowerCase()
    };

    return this._request('/api/s/<SITE>/cmd/devmgr', payload);
  }

  /**
   * Force provision of a device - force_provision()
   *
   * return true on success
   * required parameter <mac> = device MAC address
   */
  forceProvision(mac) {
    const payload = {cmd: 'force-provision',
      mac: mac.toLowerCase()};

    return this._request('/api/s/<SITE>/cmd/devmgr', payload);
  }

  /**
   * Reboot a UniFi CloudKey - reboot_cloudkey()
   *
   * return true on success
   *
   * This API call does nothing on UniFi controllers *not* running on a UniFi CloudKey device
   */
  rebootCloudKey() {
    return this._request('/api/s/<SITE>/cmd/system', {cmd: 'reboot'});
  }

  /**
   * Disable/enable an access point (using REST) - disable_ap()
   *
   * required parameter <ap_id>   = 24 char string; value of _id for the access point which can be obtained from the device list
   * required parameter <disable> = boolean; TRUE disables the device, FALSE enables the device
   *
   * NOTES:
   * - a disabled device is excluded from the dashboard status and device count and its LED and WLAN is turned off
   * - appears to only be supported for access points
   * - available since controller versions 5.2.X
   */
  disableAccessPoint(ap_id, disable) {
    return this._request('/api/s/<SITE>/rest/device/' + ap_id.trim(), {disabled: disable}, 'PUT');
  }

  /**
   * Override LED mode for a device (using REST) - led_override()
   *
   * required parameter <device_id>     = 24 char string; value of _id for the device which can be obtained from the device list
   * required parameter <override_mode> = string, off/on/default; "off" disables the LED of the device,
   *                                      "on" enables the LED of the device,
   *                                      "default" applies the site-wide setting for device LEDs
   */
  setLEDOverride(device_id, override_mode) {
    return this._request('/api/s/<SITE>/rest/device/' + device_id.trim(), {led_override: override_mode}, 'PUT');
  }

  /**
   * Toggle flashing LED of an access point for locating purposes - locate_ap()
   *
   * required parameter <mac> = device MAC address
   * required parameter <enable> = boolean; true enables flashing LED, false disables
   */
  setLocateAccessPoint(mac, enable) {
    const payload = {cmd: enable === true ? 'set-locate' : 'unset-locate',
      mac: mac.toLowerCase()};

    return this._request('/api/s/<SITE>/cmd/devmgr', payload);
  }

  /**
   * Toggle LEDs of all the access points ON or OFF - site_leds()
   *
   * required parameter <enable> = boolean; true switches LEDs of all the access points ON, false switches them OFF
   */
  setSiteLEDs(enable) {
    return this._request('/api/s/<SITE>/set/setting/mgmt', {led_enabled: enable});
  }

  /**
   * Update access point radio settings - set_ap_radiosettings()
   *
   * required parameter <ap_id>   = value of _id for the access point which can be obtained from the device list
   * required parameter <radio>   = (default=ng)
   * required parameter <channel>
   * required parameter <ht>      = (default=20)
   * required parameter <tx_power_mode>
   * required parameter <tx_power>= (default=0)
   *
   * NOTES:
   * - only supported on pre-5.X.X controller versions
   */
  setAccessPointRadioSettings(ap_id, radio, channel, ht, tx_power_mode, tx_power) {
    const payload = {radio_table: [{radio,
      channel,
      ht,
      tx_power_mode,
      tx_power}]};

    return this._request('/api/s/<SITE>/upd/device/' + ap_id.trim(), payload);
  }

  /**
   * Assign access point to another WLAN group - set_ap_wlangroup()
   *
   * return true on success
   * required parameter <type_id>   = string; WLAN type, can be either 'ng' (for WLANs 2G (11n/b/g)  ) or 'na' (WLANs 5G (11n/a/ac))
   * required parameter <device_id> = string; _id value of the access point to be modified
   * required parameter <group_id>  = string; _id value of the WLAN group to assign device to
   */
  setAccessPointWLanGroup(type_id, device_id, group_id) {
    const payload = {wlan_overrides: {}};

    if (type_id === 'ng') {
      payload.wlangroup_id_ng = group_id;
    } else if (type_id === 'na') {
      payload.wlangroup_id_na = group_id;
    }

    return this._request('/api/s/<SITE>/upd/device/' + device_id.trim(), payload);
  }

  /**
   * Update guest login settings - set_guestlogin_settings()
   *
   * return true on success
   * required parameter <portal_enabled>    = boolean; enable/disable the captive portal
   * required parameter <portal_customized> = boolean; enable/disable captive portal customizations
   * required parameter <redirect_enabled>  = boolean; enable/disable captive portal redirect
   * required parameter <redirect_url>      = string; url to redirect to, must include the http/https prefix, no trailing slashes
   * required parameter <x_password>        = string; the captive portal (simple) password
   * required parameter <expire_number>     = numeric; number of units for the authorization expiry
   * required parameter <expire_unit>       = numeric; number of minutes within a unit (a value 60 is required for hours)
   * required parameter <section_id>        = 24 char string; value of _id for the site settings section where key = "guest_access", settings can be obtained
   *                                          using the list_settings() function
   *
   * NOTES:
   * - both portal parameters are set to the same value!
   *
   */
  setGuestLoginSettings(portal_enabled, portal_customized, redirect_enabled, redirect_url, x_password, expire_number, expire_unit, section_id) {
    const payload = {portal_enabled,
      portal_customized,
      redirect_enabled,
      redirect_url,
      x_password,
      expire_number,
      expire_unit,
      _id: section_id};

    return this._request('/api/s/<SITE>/set/setting/guest_access/' + section_id.trim(), payload);
  }

  /**
   * Update guest login settings, base - set_guestlogin_settings_base()
   *
   * return true on success
   * required parameter <payload> = stdClass object or associative array containing the configuration to apply to the guest login, must be a (partial)
   *                                object/array structured in the same manner as is returned by list_settings() for the "guest_access" section.
   */
  setGuestLoginSettingsBase(payload, section_id = '') {
    if (section_id !== '') {
      section_id = '/' + section_id;
    }

    return this._request('/api/s/<SITE>/set/setting/guest_access' + section_id.trim(), payload);
  }

  /**
   * Update IPS/IDS settings, base - set_ips_settings_base()
   *
   * return true on success
   * required parameter <payload> = stdClass object or associative array containing the IPS/IDS settings to apply, must be a (partial)
   *                                object/array structured in the same manner as is returned by list_settings() for the "ips" section.
   */
  setIPSSettingsBase(payload) {
    return this._request('/api/s/<SITE>/set/setting/ips', payload);
  }

  /**
   * Update "Super Management" settings, base - set_super_mgmt_settings_base()
   *
   * return true on success
   * required parameter <settings_id> = 24 char string; value of _id for the site settings section where key = "super_mgmt", settings can be obtained
   *                                    using the list_settings() function
   * required parameter <payload>     = stdClass object or associative array containing the "Super Management" settings to apply, must be a (partial)
   *                                    object/array structured in the same manner as is returned by list_settings() for the "super_mgmt" section.
   */
  setSuperMgmtSettingsBase(settings_id, payload) {
    return this._request('/api/s/<SITE>/set/setting/super_mgmt/' + settings_id.trim(), payload);
  }

  /**
   * Update "Super SMTP" settings, base - set_super_smtp_settings_base()
   *
   * return true on success
   * required parameter <settings_id> = 24 char string; value of _id for the site settings section where key = "super_smtp", settings can be obtained
   *                                    using the list_settings() function
   * required parameter <payload>     = stdClass object or associative array containing the "Super SMTP" settings to apply, must be a (partial)
   *                                    object/array structured in the same manner as is returned by list_settings() for the "super_smtp" section.
   */
  setSuperSMTPSettingsBase(settings_id, payload) {
    return this._request('/api/s/<SITE>/set/setting/super_smtp/' + settings_id.trim(), payload);
  }

  /**
   * Update "Super Controller Identity" settings, base - set_super_identity_settings_base()
   *
   * return true on success
   * required parameter <settings_id> = 24 char string; value of _id for the site settings section where key = "super_identity", settings can be obtained
   *                                    using the list_settings() function
   * required parameter <payload>     = stdClass object or associative array containing the "Super Controller Identity" settings to apply, must be a (partial)
   *                                    object/array structured in the same manner as is returned by list_settings() for the "super_identity" section.
   */
  setSuperIdentitySettingsBase(settings_id, payload) {
    return this._request('/api/s/<SITE>/set/setting/super_identity/' + settings_id.trim(), payload);
  }

  /**
   * Rename access point - rename_ap()
   *
   * required parameter <ap_id>  = value of _id for the access point which can be obtained from the device list
   * required parameter <apname> = New name
   *
   */
  renameAccessPoint(ap_id, apname) {
    return this._request('/api/s/<SITE>/upd/device/' + ap_id.trim(), {name: apname});
  }

  /**
   * Move a device to another site - move_device()
   *
   * return true on success
   * required parameter <mac>     = string; MAC address of the device to move
   * required parameter <site_id> = 24 char string; _id of the site to move the device to
   */
  moveDevice(mac, site_id) {
    const payload = {site: site_id,
      mac: mac.toLowerCase(),
      cmd: 'move-device'
    };

    return this._request('/api/s/<SITE>/cmd/sitemgr', payload);
  }

  /**
   * Delete a device from the current site - delete_device()
   *
   * return true on success
   * required parameter <mac>     = string; MAC address of the device to move
   */
  deleteDevice(mac) {
    const payload = {mac: mac.toLowerCase(),
      cmd: 'delete-device'
    };

    return this._request('/api/s/<SITE>/cmd/sitemgr', payload);
  }

  /**
   * List network settings (using REST) - list_networkconf()
   *
   * returns an array of (non-wireless) networks and their settings
   * optional parameter <network_id> = string; _id value of the network to get settings for
   */
  getNetworkConf(network_id = null) {
    if (network_id === null) {
      network_id = '';
    }

    return this._request('/api/s/<SITE>/rest/networkconf/' + network_id.trim());
  }

  /**
   * Create a network (using REST) - create_network()
   *
   * return an array with a single object containing details of the new network on success, else return false
   * required parameter <payload> = stdClass object or associative array containing the configuration to apply to the network, must be a (partial)
   *                                object structured in the same manner as is returned by list_networkconf() for the specific network type.
   *                                Do not include the _id property, it is assigned by the controller and returned upon success.
   */
  createNetwork(payload) {
    return this._request('/api/s/<SITE>/rest/networkconf', payload);
  }

  /**
   * Update network settings, base (using REST) - set_networksettings_base()
   *
   * return true on success
   * required parameter <network_id> = the "_id" value for the network you wish to update
   * required parameter <payload>    = stdClass object or associative array containing the configuration to apply to the network, must be a (partial)
   *                                   object/array structured in the same manner as is returned by list_networkconf() for the network.
   */
  setNetworkSettingsBase(network_id, payload) {
    return this._request('/api/s/<SITE>/rest/networkconf/' + network_id.trim(), payload, 'PUT');
  }

  /**
   * Delete a network (using REST) - delete_network()
   *
   * return true on success
   * required parameter <network_id> = 24 char string; _id value of the network which can be found with the list_networkconf() function
   */
  deleteNetwork(network_id) {
    return this._request('/api/s/<SITE>/rest/networkconf/' + network_id.trim(), 'DELETE');
  }

  /**
   * List wlan settings (using REST) - list_wlanconf()
   *
   * returns an array of wireless networks and their settings, or an array containing a single wireless network when using
   * the <wlan_id> parameter
   * optional parameter <wlan_id> = 24 char string; _id value of the wlan to fetch the settings for
   */
  getWLanSettings(wlan_id = null) {
    if (wlan_id === null) {
      wlan_id = '';
    }

    return this._request('/api/s/<SITE>/rest/wlanconf/' + wlan_id.trim());
  }

  /**
   * Create a wlan - create_wlan()
   *
   * @param  string  $name             SSID
   * @param  string  $x_passphrase     new pre-shared key, minimal length is 8 characters, maximum length is 63,
   *                                         assign a value of null when security = 'open'
   * @param  string  $usergroup_id     user group id that can be found using the list_usergroups() function
   * @param  string  $wlangroup_id     wlan group id that can be found using the list_wlan_groups() function
   * @param  boolean $enabled          optional, enable/disable wlan
   * @param  boolean $hide_ssid        optional, hide/unhide wlan SSID
   * @param  boolean $is_guest         optional, apply guest policies or not
   * @param  string  $security         optional, security type (open, wep, wpapsk, wpaeap)
   * @param  string  $wpa_mode         optional, wpa mode (wpa, wpa2, ..)
   * @param  string  $wpa_enc          optional, encryption (auto, ccmp)
   * @param  boolean $vlan_enabled     optional, enable/disable vlan for this wlan
   * @param  int     $vlan             optional, vlan id
   * @param  boolean $uapsd_enabled    optional, enable/disable Unscheduled Automatic Power Save Delivery
   * @param  boolean $schedule_enabled optional, enable/disable wlan schedule
   * @param  array   $schedule         optional, schedule rules
   * @param  array   $ap_group_ids     optional, array of ap group ids, required for UniFi controller versions 6.0.X and higher
   * @return bool                      true on success
   */
  createWLan(name, x_passphrase, usergroup_id, wlangroup_id,
    enabled = true, hide_ssid = false, is_guest = false, security = 'open', wpa_mode = 'wpa2', wpa_enc = 'ccmp', vlan_enabled = false, vlan = null, uapsd_enabled = false, schedule_enabled = false, schedule = {}, ap_group_ids = null) {
    const payload = {name,
      usergroup_id,
      wlangroup_id,
      enabled,
      hide_ssid,
      is_guest,
      security,
      wpa_mode,
      wpa_enc,
      vlan_enabled,
      uapsd_enabled,
      schedule_enabled,
      schedule
    };

    if (vlan !== null && vlan_enabled === true) {
      payload.vlan = vlan;
    }

    if (x_passphrase !== '' && security !== 'open') {
      payload.x_passphrase = x_passphrase;
    }

    if (ap_group_ids !== null) {
      payload.ap_group_ids = ap_group_ids;
    }

    return this._request('/api/s/<SITE>/add/wlanconf/', payload);
  }

  /**
   * Update wlan settings, base (using REST) - set_wlansettings_base()
   *
   * return true on success
   * required parameter <wlan_id> = the "_id" value for the WLAN you wish to update
   * required parameter <payload> = stdClass object or associative array containing the configuration to apply to the wlan, must be a
   *                                (partial) object/array structured in the same manner as is returned by list_wlanconf() for the wlan.
   */
  setWLanSettingsBase(wlan_id, payload) {
    return this._request('/api/s/<SITE>/rest/wlanconf/' + wlan_id.trim(), payload, 'PUT');
  }

  /**
   * Update basic wlan settings - set_wlansettings()
   *
   * required parameter <wlan_id>
   * optional parameter <x_passphrase> = new pre-shared key, minimal length is 8 characters, maximum length is 63,
   *                                     is ignored if set to null
   * optional parameter <name>
   *
   */
  setWLanSettings(wlan_id, x_passphrase = null, name = null) {
    const payload = {};

    if (x_passphrase !== null) {
      payload.x_passphrase = x_passphrase.trim();
    }

    if (name !== null) {
      payload.name = name.trim();
    }

    return this.setWLanSettingsBase(wlan_id, payload);
  }

  /**
   * Disable/Enable wlan - disable_wlan()
   *
   * required parameter <wlan_id>
   * required parameter <disable> = boolean; true disables the wlan, false enables it
   *
   */
  disableWLan(wlan_id, disable) {
    const payload = {enabled: disable !== true};

    return this.setWLanSettingsBase(wlan_id, payload);
  }

  /**
   * Delete a wlan (using REST) - delete_wlan()
   *
   * required parameter <wlan_id> = 24 char string; _id of the wlan that can be found with the list_wlanconf() function
   */
  deleteWLan(wlan_id) {
    return this._request('/api/s/<SITE>/rest/wlanconf/' + wlan_id.trim(), {}, 'DELETE');
  }

  /**
   * Update MAC filter for a wlan - set_wlan_mac_filter()
   *
   * required parameter <wlan_id>            = the "_id" value for the WLAN you wish to update
   * required parameter <mac_filter_policy>  = string, "allow" or "deny"; default MAC policy to apply
   * required parameter <mac_filter_enabled> = boolean; true enables the policy, false disables it
   * required parameter <macs>               = array; must contain valid MAC strings to be placed in the MAC filter list,
   *                                           replacing existing values. Existing MAC filter list can be obtained
   *                                           through list_wlanconf().
   *
   */
  setWLanMacFilter(wlan_id, mac_filter_policy, mac_filter_enabled, macs) {
    const payload = {mac_filter_enabled,
      mac_filter_policy,
      mac_filter_list: macs
    };

    return this.setWLanSettingsBase(wlan_id, payload);
  }

  /**
   * List events - list_events()
   *
   * optional parameter <historyhours> = hours to go back, default value is 720 hours
   * optional parameter <start>        = which event number to start with (useful for paging of results), default value is 0
   * optional parameter <limit>        = number of events to return, default value is 3000
   */
  getEvents(historyhours = 720, start = 0, limit = 3000) {
    const payload = {_sort: '-time',
      within: historyhours,
      type: null,
      _start: start,
      _limit: limit
    };

    return this._request('/api/s/<SITE>/stat/event', payload);
  }

  /**
   * List alarms - list_alarms()
   *
   * optional parameter <payload> = json payload of flags to filter by
   *                                Example: {archived: 'false', key: 'EVT_GW_WANTransition'}
   *                                return only unarchived for a specific key
   */
  getAlarms(payload = null) {
    return this._request('/api/s/<SITE>/stat/alarm', payload);
  }

  /**
   * Count alarms - count_alarms()
   *
   * @param  bool  $archived optional, if true all alarms are counted, if false only non-archived (active) alarms are counted,
   *                         by default all alarms are counted
   * @return array           containing the alarm count
   */
  countAlarms(archived = true) {
    return this._request('/api/s/<SITE>/cnt/alarm' + (archived === true ? '' : '?archived=false'));
  }

  /**
   * Archive alarms(s) - archive_alarm()
   *
   * @param  string $alarm_id optional, _id of the alarm to archive which can be found with the list_alarms() function,
   *                          by default all alarms are archived
   * @return bool             true on success
   */
  archiveAlarms(alarm_id = null) {
    const payload = {};
    if (alarm_id === null) {
      payload.cmd = 'archive-all-alarms';
    } else {
      payload.cmd = 'archive-alarm';
      payload._id = alarm_id;
    }

    return this._request('/api/s/<SITE>/cmd/evtmgr', payload);
  }

  /**
   * Check controller update - check_controller_update()
   *
   * NOTE:
   * triggers an update of the controller cached known latest version.
   *
   * @return array returns an array with a single object containing details of the current known latest controller version info
   *               on success, else returns false
   */
  checkControllerUpdate() {
    return this._request('/api/s/<SITE>/stat/fwupdate/latest-version');
  }

  /**
   * Check firmware update - check_firmware_update()
   *
   * NOTE:
   * triggers a Device Firmware Update in Classic Settings > System settings > Maintenance
   *
   * @return bool returns true upon success
   */
  checkFirmwareUpdate() {
    return this._request('/api/s/<SITE>/cmd/productinfo', {cmd: 'check-firmware-update'});
  }

  /**
   * Upgrade a device to the latest firmware - upgrade_device()
   *
   * return true on success
   * required parameter <device_mac> = MAC address of the device to upgrade
   *
   * NOTES:
   * - updates the device to the latest firmware known to the controller
   */
  upgradeDevice(device_mac) {
    return this._request('/api/s/<SITE>/cmd/devmgr/upgrade', {mac: device_mac.toLowerCase()});
  }

  /**
   * Upgrade a device to a specific firmware file - upgrade_device_external()
   *
   * return true on success
   * required parameter <firmware_url> = URL for the firmware file to upgrade the device to
   * required parameter <device_mac>   = MAC address of the device to upgrade
   *
   * NOTES:
   * - updates the device to the firmware file at the given URL
   * - please take great care to select a valid firmware file for the device!
   */
  upgradeDeviceExternal(firmware_url, device_mac) {
    return this._request('/api/s/<SITE>/cmd/devmgr/upgrade-external', {url: firmware_url, mac: device_mac.toLowerCase()});
  }

  /**
   * Start rolling upgrade - start_rolling_upgrade()
   *
   * return true on success
   *
   * NOTES:
   * - updates all access points to the latest firmware known to the controller in a
   *   staggered/rolling fashion
   */
  startRollingUpgrade() {
    return this._request('/api/s/<SITE>/cmd/devmgr', {cmd: 'set-rollupgrade'});
  }

  /**
   * Cancel rolling upgrade - cancel_rolling_upgrade()
   *
   * return true on success
   */
  cancelRollingUpgrade() {
    return this._request('/api/s/<SITE>/cmd/devmgr', {cmd: 'unset-rollupgrade'});
  }

  /**
   * Fetch firmware versions - list_firmware()
   *
   * @param  string $type optional, "available" or "cached", determines which firmware types to return,
   *                      default value is "available"
   * @return array        containing firmware versions
   */
  getFirmware(type = 'available') {
    return this._request('/api/s/<SITE>/cmd/firmware', {cmd: 'list-' + type});
  }

  /**
   * Power-cycle the PoE output of a switch port - power_cycle_switch_port()
   *
   * return true on success
   * required parameter <switch_mac> = string; main MAC address of the switch
   * required parameter <port_idx>   = integer; port number/index of the port to be affected
   *
   * NOTES:
   * - only applies to switches and their PoE ports...
   * - port must be actually providing power
   */
  powerCycleSwitchPort(switch_mac, port_idx) {
    const payload = {mac: switch_mac.toLowerCase(),
      port_idx,
      cmd: 'power-cycle'
    };

    return this._request('/api/s/<SITE>/cmd/devmgr', payload);
  }

  /**
   * Trigger an RF scan by an AP
   *
   * return true on success
   * required parameter <ap_mac> = MAC address of the AP
   */
  runSpectrumScan(ap_mac) {
    return this._request('/api/s/<SITE>/cmd/devmgr', {cmd: 'spectrum-scan', mac: ap_mac.toLowerCase()});
  }

  /**
   * Trigger a speedtest on a USG
   *
   * return true on success
   */
  runSpeedTest() {
    return this._request('/api/s/<SITE>/cmd/devmgr', {cmd: 'speedtest'});
  }

  /**
   * Get the current state of a running speedtest on a USG
   *
   * returns status of speedtest
   */
  getSpeedTestStatus() {
    return this._request('/api/s/<SITE>/cmd/devmgr', {cmd: 'speedtest-status'});
  }

  /**
   * Check the RF scanning state of an AP - spectrum_scan_state()
   *
   * returns an object with relevant information (results if available) regarding the RF scanning state of the AP
   * required parameter <ap_mac> = MAC address of the AP
   */
  getSpectrumScanState(ap_mac) {
    return this._request('/api/s/<SITE>/stat/spectrum-scan/' + ap_mac.trim().toLowerCase());
  }

  /**
   * Update device settings, base (using REST) - set_device_settings_base()
   *
   * required parameter <device_id> = 24 char string; _id of the device which can be found with the list_devices() function
   * required parameter <payload>   = stdClass object or associative array containing the configuration to apply to the device, must be a
   *                                  (partial) object/array structured in the same manner as is returned by list_devices() for the device.
   */
  setDeviceSettingsBase(device_id, payload) {
    return this._request('/api/s/<SITE>/rest/device/' + device_id.trim(), payload, 'PUT');
  }

  /**
   * List Radius profiles (using REST) - list_radius_profiles()
   *
   * returns an array of objects containing all Radius profiles for the current site
	 *
   * NOTES:
   * - this function/method is only supported on controller versions 5.5.19 and later
   */
  listRadiusProfiles() {
    return this._request('/api/s/<SITE>/rest/radiusprofile');
  }

  /**
   * List Radius user accounts (using REST) - list_radius_accounts()
   *
   * returns an array of objects containing all Radius accounts for the current site
	 *
   * NOTES:
   * - this function/method is only supported on controller versions 5.5.19 and later
   */
  listRadiusAccounts() {
    return this._request('/api/s/<SITE>/rest/account');
  }

  /**
   * Create a Radius user account (using REST) - create_radius_account()
   *
   * NOTES:
   * - this function/method is only supported on controller versions 5.5.19 and later
   *
   * @param  string $name               name for the new account
   * @param  string $x_password         password for the new account
   * @param  int    $tunnel_type        optional, must be one of the following values:
   *                                    1      Point-to-Point Tunneling Protocol (PPTP)
   *                                    2      Layer Two Forwarding (L2F)
   *                                    3      Layer Two Tunneling Protocol (L2TP)
   *                                    4      Ascend Tunnel Management Protocol (ATMP)
   *                                    5      Virtual Tunneling Protocol (VTP)
   *                                    6      IP Authentication Header in the Tunnel-mode (AH)
   *                                    7      IP-in-IP Encapsulation (IP-IP)
   *                                    8      Minimal IP-in-IP Encapsulation (MIN-IP-IP)
   *                                    9      IP Encapsulating Security Payload in the Tunnel-mode (ESP)
   *                                    10     Generic Route Encapsulation (GRE)
   *                                    11     Bay Dial Virtual Services (DVS)
   *                                    12     IP-in-IP Tunneling
   *                                    13     Virtual LANs (VLAN)
   * @param  int    $tunnel_medium_type optional, must be one of the following values:
   *                                    1      IPv4 (IP version 4)
   *                                    2      IPv6 (IP version 6)
   *                                    3      NSAP
   *                                    4      HDLC (8-bit multidrop)
   *                                    5      BBN 1822
   *                                    6      802 (includes all 802 media plus Ethernet "canonical format")
   *                                    7      E.163 (POTS)
   *                                    8      E.164 (SMDS, Frame Relay, ATM)
   *                                    9      F.69 (Telex)
   *                                    10     X.121 (X.25, Frame Relay)
   *                                    11     IPX
   *                                    12     Appletalk
   *                                    13     Decnet IV
   *                                    14     Banyan Vines
   *                                    15     E.164 with NSAP format subaddress
   * @param  int    $vlan               optional, VLAN to assign to the account
   * @return array                      containing a single object for the newly created account upon success, else returns false
   * @return bool|array                 containing a single object for the newly created account upon success, else returns false
   */
  createRadiusAccount(name, x_password, tunnel_type = null, tunnel_medium_type = null, vlan = null) {
    const payload = {name,
      x_password
    };

    if (tunnel_type !== null) {
      payload.tunnel_type = tunnel_type;
    }

    if (tunnel_medium_type !== null) {
      payload.tunnel_medium_type = tunnel_medium_type;
    }

    if (vlan !== null) {
      payload.vlan = vlan;
    }

    return this._request('/api/s/<SITE>/rest/account', payload);
  }

  /**
   * Update Radius account, base (using REST) - set_radius_account_base()
   *
   * return true on success
   * required parameter <account_id> = 24 char string; _id of the account which can be found with the list_radius_accounts() function
   * required parameter <payload>    = stdClass object or associative array containing the new profile to apply to the account, must be a (partial)
   *                                   object/array structured in the same manner as is returned by list_radius_accounts() for the account.
   *
   * NOTES:
   * - this function/method is only supported on controller versions 5.5.19 and later
   */
  setRadiusAccountBase(account_id, payload) {
    return this._request('/api/s/<SITE>/rest/account/' + account_id.trim(), payload, 'PUT');
  }

  /**
   * Delete a Radius account (using REST) - delete_radius_account()
   *
   * return true on success
   * required parameter <account_id> = 24 char string; _id of the account which can be found with the list_radius_accounts() function
   *
   * NOTES:
   * - this function/method is only supported on controller versions 5.5.19 and later
   */
  deleteRadiusAccount(account_id) {
    return this._request('/api/s/<SITE>/rest/account/' + account_id.trim(), null, 'DELETE');
  }

  /**
   * Execute specific stats command - cmd_stat()
   *
   * return true on success
   * required parameter <command>  = string; command to execute, known valid values
   *                                 'reset-dpi': reset all DPI counters for the current site
   */
  cmdStat(command) {
    const payload = {cmd: command.trim()};

    return this._request('/api/s/<SITE>/cmd/stat', payload);
  }

  /**
   * Toggle Element Adoption ON or OFF - set_element_adoption()
   *
   * return true on success
   * required parameter <enable> = boolean; true enables Element Adoption, false disables Element Adoption
   */
  setElementAdoption(enable) {
    const payload = {enabled: enable};

    return this._request('/api/s/<SITE>/set/setting/element_adopt', payload);
  }

  /**
   * List device states - list_device_states()
   *
   * NOTE:
   * this function returns a partial implementation of the codes listed here
   * https://help.ui.com/hc/en-us/articles/205231710-UniFi-UAP-Status-Meaning-Definitions
   *
   * @return array containing translations of UniFi device "state" values to humanized form
   */
  async getDeviceStates() {
    return {deviceState: {
      0: 'offline',
      1: 'connected',
      2: 'pending adoption',
      4: 'updating',
      5: 'provisioning',
      6: 'unreachable',
      7: 'adopting',
      9: 'adoption error',
      11: 'isolated'}
    };
  }

  /**
   * Upgrade External Firmware (5.4.9+)
   *
   * required parameter <mac>          = device MAC address
   * required parameter <firmware_url> = external URL to firmware data
   *
   */
  upgradeExternalFirmware(mac, firmware_url) {
    const payload = {url: firmware_url,
      mac: mac.toLowerCase()};

    return this._request('/api/s/<SITE>/cmd/devmgr/upgrade-external', payload);
  }

  /**
   * Custom API request - custom_api_request()
   *
   * NOTE:
   * Only use this method when you fully understand the behavior of the UniFi controller API. No input validation is performed, to be used with care!
   *
   * @param  string       $path           suffix of the URL (following the port number) to pass request to, *must* start with a "/" character
   * @param  string       $method         optional, HTTP request type, can be GET (default), POST, PUT, PATCH, or DELETE
   * @param  object|array $payload        optional, stdClass object or associative array containing the payload to pass
   * @param  string       $return         optional, string; determines how to return results, when "boolean" the method must return a
   *                                      boolean result (true/false) or "array" when the method must return an array
   * @return bool|array                   returns results as requested, returns false on incorrect parameters
   */
  customApiRequest(path, method = null, payload = null) {
    return this._request(path, payload, method);
  }

  /**
   * WebSocket listen function
   */
  async listen() {
    const cookies = await this._cookieJar.getCookieString(this._baseurl.href);

    let eventsUrl = new URL(`wss://${this._baseurl.host}/wss/s/${this.opts.site}/events`);
    if (this._unifios) {
      eventsUrl = new URL(`wss://${this._baseurl.host}/proxy/network/wss/s/${this.opts.site}/events`);
    }

    // Make sure we use clients=v2 URL parameter for the
    // more advanced version of the UniFi Websocket support
    eventsUrl.searchParams.set('clients', 'v2');

    // Create WebSocket
    this._ws = new WebSocket(eventsUrl.href, {
      perMessageDeflate: false,
      rejectUnauthorized: this.opts.sslverify,
      headers: {
        Cookie: cookies
      }
    });

    const pingpong = setInterval(() => {
      this._ws.send('ping');
    }, this._pingPongInterval);

    this._ws.on('open', () => {
      this._isReconnecting = false;
      this.emit('ctrl.connect');
    });

    this._ws.on('message', (data, isBinary) => {
      const message = isBinary ? data : data.toString();
      if (message === 'pong') {
        this.emit('ctrl.pong');
        return;
      }

      try {
        const parsed = JSON.parse(message);
        if ('meta' in parsed && Array.isArray(parsed.data)) {
          for (const entry of parsed.data) {
            this._event(parsed.meta, entry);
          }
        }
      } catch (error) {
        this.emit('ctrl.error', error);
      }
    });

    this._ws.on('close', () => {
      this.emit('ctrl.close');
      clearInterval(pingpong);
      this._reconnect();
    });

    this._ws.on('error', error => {
      this.emit('ctrl.error', error);
      clearInterval(pingpong);
      this._reconnect();
    });

    return (true);
  }

  /** PRIVATE METHODS */

  /**
   * Init
   */
  async _init() {
    if (this._isInit === true) {
      return 2;
    }

    const jar = this._cookieJar;
    this._instance = axios.create({
      httpAgent: new HttpCookieAgent({cookies: {jar}}),
      httpsAgent: new HttpsCookieAgent({cookies: {jar}, rejectUnauthorized: this.opts.sslverify, requestCert: true})
    });

    // Identify if this is UniFiOS or not by calling the baseURL without
    // any path and then check for the return code, etc.
    const response = await this._instance.get(this._baseurl.toString(), {
      timeout: this.opts.timeout,
      maxRedirects: 0,
      validateStatus: () => true
    });

    // Check for UniFiOS
    if (response.status === 302 && response.headers.location === '/manage') {
      this._unifios = false;
    } else if (response.status === 200) {
      this._unifios = true;
      if (response.headers['x-csrf-token']) {
        this._xcsrftoken = response.headers['x-csrf-token'];
        this._instance.defaults.headers.common['x-csrf-token'] = this._xcsrftoken;
      }
    } else {
      throw new Error('failed to detect UniFiOS status');
    }

    // DEBUG
    /*
      this._instance.interceptors.request.use(request => {
        console.dir({ 'Starting Request': request }, { depth: null })
        return request
      })
      this._instance.interceptors.response.use(response => {
        console.dir({ 'Response:': response }, { depth: null })
        return response
      })
      */

    this._isInit = true;
    try {
      this._isClosed = false;
      await this.login(null, null, null);

      return 1;
    } catch (error) {
      this._isInit = false;
      throw error;
    }
  }

  _reconnect() {
    if (this._isReconnecting === false && this._isClosed === false) {
      this._isReconnecting = true;
      setTimeout(async () => {
        this.emit('ctrl.reconnect');
        this._isReconnecting = false;
        try {
          await this.listen();
        } catch (error) {
          console.dir('_reconnect() encountered an error: ' + error);
        }
      }, this._autoReconnectInterval);
    }
  }

  /**
   * Private function to send out a generic URL request to a UniFi-Controller
   */
  async _request(path, payload = null, method = null, raw = false) {
    // Ensure that login() was used already
    await this._ensureLoggedIn();

    // Identify which request method we are using (GET, POST, PUT, DELETE) based
    // on the json data supplied and the overriding method
    if (payload !== null) {
      method = method === 'PUT' ? 'PUT' : 'POST';
    } else if (method === null) {
      method = 'GET';
    }

    // Perform HTTP request
    const response = await this._instance.request({
      url: this._url(path),
      method,
      data: payload,
      timeout: this.opts.timeout
    });

    const body = response.data;
    if (response.headers['x-csrf-token']) {
      this._xcsrftoken = response.headers['x-csrf-token'];
      this._instance.defaults.headers.common['x-csrf-token'] = this._xcsrftoken;
    }

    if (body !== null && typeof (body) !== 'undefined') {
      if (typeof (body.meta) !== 'undefined') {
        if (response.status >= 200 && response.status < 400 && body.meta.rc === 'ok') {
          if (raw === true) {
            return body;
          }

          return body.data;
        }

        const error = typeof (body.meta.msg) === 'undefined' ? new Error('generic error') : new Error(body.meta.msg);
        throw error;
      } else if (response.status >= 200 && response.status < 400) {
        return body;
      } else {
        throw new Error('invalid status return');
      }
    } else {
      throw new Error('empty response data');
    }
  }

  _close() {
    this._isClosed = true;
    this._ws.site.close();
    this._ws.super.close();
    this._ws.system.close();
  }

  _event(meta, data) {
    if (meta && data && meta.message && meta.rc === 'ok') {
      const messageType = meta.message.toLowerCase();
      let keyType = 'generic';
      if (data.key) {
        keyType = data.key.toLowerCase();
      }

      // Emit the event
      this.emit([messageType, keyType].join('.'), [data, meta]);
    }
  }

  async _ensureLoggedIn() {
    if (typeof (this._instance) === 'undefined') {
      await this._init();
      return true;
    }

    try {
      await this._instance.get(`${this._baseurl.href}api/${this._unifios ? 'users/' : ''}self`, {
        timeout: this.opts.timeout
      });
      return true;
    } catch {
      await this.login();
      return true;
    }
  }

  _url(path) {
    if (this._unifios === true &&
        path.endsWith('/logout') === false &&
        path.endsWith('/login') === false) {
      return `${this._baseurl.href}proxy/network${path.replace('<SITE>', this.opts.site)}`;
    }

    return `${this._baseurl.href}${path.replace('<SITE>', this.opts.site).slice(1)}`;
  }
}

module.exports = {Controller};