UNPKG

@jw988/unifi-access

Version:

A soon-to-be-almost-complete implementation of the UniFi Access API.

917 lines 40.7 kB
/* Copyright(C) 2019-2025, HJD (https://github.com/hjdhjd). All rights reserved. * * access-api.ts: Our UniFi Access API implementation. */ import { ACCESS_API_ERROR_LIMIT, ACCESS_API_RETRY_INTERVAL, ACCESS_API_TIMEOUT } from "./settings.js"; import { AbortError, FetchError, Headers, context, timeoutSignal } from "@adobe/fetch"; import { EventEmitter } from "node:events"; import WebSocket from "ws"; import util from "node:util"; /** * The direct UniFi Access API is partially documented through an officially supported public API that Ubiquiti has released. However, this API also has certain * constraints and limitations such as lacking the ability to change the settings on an Access device. The full native API has been reverse engineered mostly through * trail and error with the Access web interface as well as insight from the public API. * * Here's how the UniFi Access API works: * * 1. {@link login | Login} to the UniFi Access controller and acquire security credentials for further calls to the API. * * 2. Enumerate the list of UniFi Access devices by calling the {@link bootstrap} property. This contains everything you would want to know about the devices attached to * this particular UniFi Access controller. Information about the Access controller can be accessed through the {@link controller} property. * * 3. Listen for `message` events emitted by {@link AccessApi} containing all Access controller events, in realtime. They are delivered as * {@link access-types.AccessEventPacket} packets, containing the event-specific details. * * Those are the basics that gets us up and running. */ export class AccessApi extends EventEmitter { _bootstrap; _controller; _devices; _doors; _floors; address; apiErrorCount; apiLastSuccess; events; eventsTimer; fetch; headers; _isAdminUser; log; password; username; apikey; /** * Create an instance of the UniFi Access API. * * @param log - Logging functions to use. * * @defaultValue `none` - Logging will be done to stdout and stderr. */ // Initialize this instance with our login information. constructor(log) { // Initialize our parent. super(); // If we didn't get passed a logging parameter, by default we log to the console. if (!log) { log = { /* eslint-disable no-console */ // eslint-disable-next-line @typescript-eslint/no-unused-vars debug: (message, ...parameters) => { }, error: (message, ...parameters) => console.error(message, ...parameters), info: (message, ...parameters) => console.log(message, ...parameters), warn: (message, ...parameters) => console.log(message, ...parameters) /* eslint-enable no-console */ }; } this._bootstrap = null; this._controller = null; this._devices = null; this._doors = null; this._floors = null; this.events = null; this.eventsTimer = null; this.log = { debug: (message, ...parameters) => log?.debug(this.name + ": " + message, ...parameters), error: (message, ...parameters) => log?.error(this.name + ": API error: " + message, ...parameters), info: (message, ...parameters) => log?.info(this.name + ": " + message, ...parameters), warn: (message, ...parameters) => log?.warn(this.name + ": " + message, ...parameters) }; this.apiErrorCount = 0; this.apiLastSuccess = 0; this.fetch = context({ alpnProtocols: ["h2" /* ALPNProtocol.ALPN_HTTP2 */], rejectUnauthorized: false, userAgent: "unifi-access" }).fetch; this.headers = new Headers(); this._isAdminUser = false; this.address = ""; this.username = ""; this.password = ""; this.apikey = ""; } /** * Execute a login attempt to the UniFi Access API. * * @param address - Address of the UniFi Access controller, expressed as an FQDN or IP address. * @param username - Username to use when logging into the controller. * @param password - Password to use when logging into the controller. * * @returns Returns a promise that will resolve to `true` if successful and `false` otherwise. * * @remarks A `login` event will be emitted each time this method is called, with the result of the attempt as an argument. * * @example * Login to the Access controller. You can selectively choose to either `await` the promise that is returned by `login`, or subscribe to the `login` event. * * ```ts * import { AccessApi } from "unifi-access"; * * // Create a new Access API instance. * const ufp = new AccessApi(); * * // Set a listener to wait for the login event to occur. * ufp.once("login", (successfulLogin: boolean) => { * * // Indicate if we are successful. * if(successfulLogin) { * * console.log("Logged in successfully."); * process.exit(0); * } * }); * * // Login to the Access controller. * if(!(await ufa.login("access-controller.local", "username", "password"))) { * * console.log("Invalid login credentials."); * process.exit(0); * }; * ``` */ // Login to the Access controller and terminate any existing login we might have. async login(address, username, password) { this.logout(); this.address = address; this.username = username; this.password = password; // Let's attempt to login. const loginSuccess = await this.loginController(); // Publish the result to our listeners this.emit("login", loginSuccess); // Return the status of our login attempt. return loginSuccess; } // Login to the UniFi Access API. async loginController() { // If we're already logged in, we're done. if (this.headers.has("Cookie") && this.headers.has("X-CSRF-Token")) { return true; } // If we're using apikey, we're done. if (this.apikey) { return true; } // Acquire a CSRF token, if needed. We only need to do this if we aren't already logged in, or we don't already have a token. if (!this.headers.has("X-CSRF-Token")) { // UniFi OS has cross-site request forgery protection built into it's web management UI. We retrieve the CSRF token, if available, by connecting to the Access // controller and checking the headers for it. const response = await this.retrieve("https://" + this.address, { method: "GET" }, false); if (response?.ok) { const csrfToken = response.headers.get("X-CSRF-Token"); // Preserve the CSRF token, if found, for future API calls. if (csrfToken) { this.headers.set("X-CSRF-Token", csrfToken); } } } // Log us in. const response = await this.retrieve(this.getApiEndpoint("login"), { body: JSON.stringify({ password: this.password, rememberMe: true, token: "", username: this.username }), method: "POST" }); // Something went wrong with the login call, possibly a controller reboot or failure. if (!response?.ok) { this.logout(); return false; } // We're logged in. Let's configure our headers. const csrfToken = response.headers.get("X-Updated-CSRF-Token") ?? response.headers.get("X-CSRF-Token"); const cookie = response.headers.get("Set-Cookie"); // Save the refreshed cookie and CSRF token for future API calls and we're done. if (csrfToken && cookie) { // Only preserve the token element of the cookie and not the superfluous information that's been added to it. this.headers.set("Cookie", cookie.split(";")[0]); // Save the CSRF token. this.headers.set("X-CSRF-Token", csrfToken); return true; } // Clear out our login credentials. this.logout(); return false; } /** * Execute a login attempt to the UniFi Access API. * * @param address - Address of the UniFi Access controller, expressed as an FQDN or IP address. * @param apikey - Apikey for the controller. * * @returns Returns a promise that will resolve to `true` if successful and `false` otherwise. * * @remarks A `login` event will be emitted each time this method is called, with the result of the attempt as an argument. * * @example * Login to the Access controller. You can selectively choose to either `await` the promise that is returned by `login`, or subscribe to the `login` event. * * ```ts * import { AccessApi } from "unifi-access"; * * // Create a new Access API instance. * const ufp = new AccessApi(); * * // Set a listener to wait for the login event to occur. * ufp.once("login", (successfulLogin: boolean) => { * * // Indicate if we are successful. * if(successfulLogin) { * * console.log("Logged in successfully."); * process.exit(0); * } * }); * * // Login to the Access controller. * if(!(await ufa.login("access-controller.local", "username", "password"))) { * * console.log("Invalid login credentials."); * process.exit(0); * }; * ``` */ // Login to the Access controller and terminate any existing login we might have. async loginapi(address, apikey) { this.logout(); this.address = address; this.apikey = apikey; // Let's attempt to login. const loginSuccess = await this.loginController(); // Publish the result to our listeners this.emit("login", loginSuccess); // Return the status of our login attempt. return loginSuccess; } // Attempt to retrieve the bootstrap configuration from the Access controller. async bootstrapController(retry) { // Log us in if needed. if (!(await this.loginController())) { return retry ? this.bootstrapController(false) : false; } // Utility to retrieve and parise API responses, with error handling. const retrieveEndpoint = async (endpoint) => { // Retrieve the endpoint from the controller. const response = await this.retrieve(this.getApiEndpoint(endpoint)); // Something went wrong. Retry the bootstrap attempt once, and then we're done. if (!response?.ok) { this.logRetry("Unable to retrieve the UniFi Access " + endpoint + " configuration.", retry); return null; } let data = null; try { data = await response.json(); // @eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { data = null; this.log.error("Unable to parse response from UniFi Access. Will retry again later."); } return data; }; // Retrieve the controller configuration. this._controller = (await retrieveEndpoint("controller"))?.data ?? null; if (!this._controller && retry) { return this.bootstrapController(false); } // Next, retrieve the bootstrap configuration. const data = (await retrieveEndpoint("bootstrap"))?.data; this._bootstrap = data ? data[0] : null; if (!this._bootstrap && retry) { return this.bootstrapController(false); } // Retrieve the list of doors from all the floors the user has configured. this._doors = this._bootstrap?.floors?.flatMap(floor => floor.doors)?.filter(Boolean) ?? null; // In case we end up with an empty floors array due to changes in the Access API, we can conceivably end up with an empty array here. this._doors = this._doors?.length ? this._doors : null; // Retrieve the list of devices from all the doors the user has configured. this._devices = this._doors?.map(x => x.device_groups ?? []).flat(2).filter(Boolean) ?? null; // In case we end up with an empty floors array due to changes in the Access API, we can conceivably end up with an empty array here. this._devices = this._devices?.length ? this._devices : null; // Account for Enterprise Access Hubs. What we do here is append to the devices array a transformed version of each extension (which in the case of an EAH amounts to // the equivalent of a hub / lock) attached to it. We transform the configuration to make it appear like it's a typical UAH for our purposes, and we map the name and // unlock location accordingly. /* eslint-disable camelcase */ const eahDevices = this._bootstrap?.device_groups?.flat().filter(device => device.device_type === "UAH-Ent"). flatMap(({ extensions = [], alias, display_model, location_id, ...device }) => extensions. map(({ target_name, target_value, ...extension }) => ({ ...device, ...extension, alias: target_name ?? "Unknown", display_model: display_model + ((extension.source_id !== undefined) ? " " + extension.source_id.replace(/(^\w|\s+\w)/g, match => match.toUpperCase()) : ""), location_id: target_value ?? location_id, name: alias }))); /* eslint-enable camelcase */ // Add EAH devices, if we have them. if (eahDevices?.length) { this._devices ??= []; this._devices.push(...eahDevices); } // Set the list of floors as a convenience. this._floors = this._bootstrap?.floors ?? null; // If we're bootstrapped, connect to the event listener API. Otherwise, we're done. if (!this._bootstrap || !(await this.launchEventsWs())) { return retry ? this.bootstrapController(false) : false; } // Notify our users. this.emit("bootstrap", this._bootstrap); // We're bootstrapped and connected to the events API. return true; } // Connect to the realtime events API. async launchEventsWs() { // Log us in if needed. if (!(await this.loginController())) { return false; } // If we already have a listener, we're already all set. if (this.events) { return true; } try { const ws = new WebSocket("wss://" + this.address + "/proxy/access/api/v2/ws/notification", { headers: { Cookie: this.headers.get("Cookie") ?? "", ...(this.apikey ? { "X-API-KEY": this.apikey } : null) }, rejectUnauthorized: false }); if (!ws) { this.log.error("Unable to connect to the realtime events API. Will retry again later."); if (this.eventsTimer) { clearTimeout(this.eventsTimer); this.eventsTimer = null; } this.events = null; return false; } let messageHandler; // Cleanup after ourselves if our websocket closes for some resaon. ws.once("close", () => { if (this.eventsTimer) { clearTimeout(this.eventsTimer); this.eventsTimer = null; } this.events = null; if (messageHandler) { ws.removeListener("message", messageHandler); messageHandler = null; } }); // Handle any websocket errors. ws.once("error", (error) => { // If we're closing before fully established it's because we're shutting down the API - ignore it. if (error.message !== "WebSocket was closed before the connection was established") { this.log.error(error.toString()); } ws.terminate(); }); // Process messages as they come in. ws.on("message", messageHandler = (event) => { if (!event) { this.log.error("Unable to process message from the realtime events API."); ws.terminate(); return; } // The Access events API seems to send a heartbeat every five seconds. if (event.toString() === "\"Hello\"\n") { // Heartbeat. if (this.eventsTimer) { clearTimeout(this.eventsTimer); } this.eventsTimer = setTimeout(() => { this.log.error("Failed to detect heartbeat from the events API. Resetting the connection."); this.reset(); this.launchEventsWs().catch(() => { }); }, 1000 * 10); return; } // Parse the JSON from the events API. let json; try { json = JSON.parse(event.toString()); } catch (error) { this.log.error("Error processing message from the events API: %s", error); return; } // Emit the decoded packet for users. this.emit("message", json); }); // Make the websocket available, and then we're done. this.events = ws; // Establish our heartbeat. this.eventsTimer = setTimeout(() => { this.log.error("Failed to detect heartbeat from the events API. Resetting the connection."); this.reset(); this.launchEventsWs().catch(() => { }); }, 1000 * 10); } catch (error) { this.log.error("Error connecting to the realtime update events API: %s", error); } return true; } /** * Retrieve the bootstrap JSON from a UniFi Access controller. * * @returns Returns a promise that will resolve to `true` if successful and `false` otherwise. * * @remarks A `bootstrap` event will be emitted each time this method is successfully called, with the AccessToplogyConfig JSON as an argument. As a * convenience, the {@link devices}, {@link doors}, and {@link floors} properties will be populated as well. * * @example * Retrieve the bootstrap JSON. You can selectively choose to either `await` the promise that is returned by `getBootstrap`, or subscribe to the `bootstrap` event. * * ```ts * import { AccessApi, AccessDeviceConfig } from "unifi-access"; * import util from "node:util"; * * // Create a new Access API instance. * const ufa = new AccessApi(); * * // Set a listener to wait for the bootstrap event to occur. * ufa.once("bootstrap", (bootstrapJSON: AccessDeviceConfig) => { * * // Once we've bootstrapped the Access controller, output the bootstrap JSON and we're done. * process.stdout.write(util.inspect(bootstrapJSON, { colors: true, depth: null, sorted: true }) + "\n", () => process.exit(0)); * }); * * // Login to the Access controller. * if(!(await ufa.login("access-controller.local", "username", "password"))) { * * console.log("Invalid login credentials."); * process.exit(0); * }; * * // Bootstrap the controller. It will emit a message once it's received the bootstrap JSON, or you can alternatively wait for the promise to resolve. * if(!(await ufa.getBootstrap())) { * * console.log("Unable to bootstrap the Access controller."); * process.exit(0); * } * ``` * * Alternatively, you can access the bootstrap JSON directly through the {@link bootstrap} accessor: * * ```ts * import { AccessApi } from "unifi-access"; * import util from "node:util"; * * // Create a new Access API instance. * const ufa = new AccessApi(); * * // Login to the Access controller. * if(!(await ufa.login("access-controller.local", "username", "password"))) { * * console.log("Invalid login credentials."); * process.exit(0); * }; * * // Bootstrap the controller. * if(!(await ufa.getBootstrap())) { * * console.log("Unable to bootstrap the Access controller."); * process.exit(0); * } * * // Once we've bootstrapped the Access controller, access the bootstrap JSON through the bootstrap accessor and we're done. * process.stdout.write(util.inspect(ufa.bootstrap, { colors: true, depth: null, sorted: true }) + "\n", () => process.exit(0)); * ``` * */ // Get our UniFi Access configuration. async getBootstrap() { // Bootstrap the controller, and attempt to retry the bootstrap if it fails. return this.bootstrapController(true); } /** * Send an unlock command to the Access controller. * * @param device - Access device. * @param duration - Unlock interval in minutes. * * @returns Returns `true` if successful, `false` otherwise. * * @remarks If `duration` is not specified, a standard unlock request will be sent to the Access controller which will unlock for 2 seconds. Valid values for duration * are `Infinity` - remain unlocked until reset, `0` - reset lock to secure state, `duration` - number of minutes. */ // Send an unlock command to a hub. async unlock(device, duration) { // No device object, we're done. if (!device) { return false; } // Unlocking only works on hubs. if (!device.capabilities?.includes("is_hub")) { return false; } // Default to the standard unlock endpoint. let action = "unlock"; let endpoint = this.getApiEndpoint("location") + "/" + device.location_id + "/unlock"; let payload = {}; // If we've specified a duration, let's specify that. if (duration !== undefined) { // eslint-disable-next-line camelcase const params = new URLSearchParams({ get_result: "true" }); endpoint = this.getApiEndpoint("device") + "/" + device.unique_id + "/lock_rule?" + params.toString(); // Safety check for out of bounds values. if (duration < 0) { duration = 0; } switch (duration) { case 0: action = "lock"; payload = { type: "reset" }; break; case Infinity: payload = { type: "keep_unlock" }; break; default: payload = { interval: Math.trunc(duration), type: "custom" }; } } // Request the unlock from Access. const response = await this.retrieve(endpoint, { body: payload, method: "PUT" }); if (!response?.ok) { this.log.error("%s: Unable to %s the %s: %s.", this.getFullName(device), action, device.display_model, response?.status); return false; } // Get our status. const status = await response.json(); if (status.codeS === "SUCCESS") { return true; } // We failed - let's log what we know. this.log.error("%s: Error %sing the %s: \n%s", this.getFullName(device), action, device.display_model, util.inspect(status, { colors: false, depth: null, sorted: true })); return false; } /** * Update an Access device's configuration on the UniFi Access controller. * * @typeParam DeviceType - Generic for any known Access device type. * * @param device - Access device. * @param payload - Device configuration payload to upload, usually a subset of the device-specific configuration JSON. * * @returns Returns a promise that will resolve to the updated device-specific configuration JSON if successful, and `null` otherwise. * * @remarks Use this method to change the configuration of a given Access device or controller. It requires the credentials used to login to the Access API * to have administrative privileges for most settings. */ // Update an Access device object. async updateDevice(device, payload) { // No device object, we're done. if (!device) { return null; } // Log us in if needed. if (!(await this.loginController())) { return null; } // Only admin users can update JSON objects. if (!this.isAdminUser) { return null; } this.log.debug("%s: %s", this.getFullName(device), util.inspect(payload, { colors: true, depth: null, sorted: true })); // Update Access with the new configuration. const response = await this.retrieve(this.getApiEndpoint("device") + "/" + device.unique_id + (device.capabilities?.includes("is_hub") ? "configs" : "settings"), { body: JSON.stringify(payload), method: "PUT" }); if (!response?.ok) { this.log.error("%s: Unable to configure the %s: %s.", this.getFullName(device), device.device_type, response?.status); return null; } // We successfully set the message, return the updated device object. return await response.json(); } /** * Utility method that generates a nicely formatted device information string. * * @param device - Access device. * @param name - Optional name for the device. Defaults to the device type (e.g. `UA G2 Pro Black`). * @param deviceInfo - Optionally specify whether or not to include the IP address and MAC address in the returned string. Defaults to `false`. * * @returns Returns the Access device name in the following format: <code>*Access device name* [*Access device type*] (address: *IP address* mac: *MAC address*)</code>. * * @remarks The example above assumed the `deviceInfo` parameter is set to `true`. */ // Utility to generate a nicely formatted device string. getDeviceName(device, name = device?.alias?.length ? device.alias : device?.name, deviceInfo = false) { // Validate our inputs. if (!device) { return ""; } // Include the host address information, if we have it. const host = (("ip" in device) && device.ip) ? "address: " + device.ip + " " : ""; const type = (("display_model" in device) && device.display_model) ? device.display_model : device.device_type; // A completely enumerated device will appear as: // Device Name [Device Type] (address: IP address, mac: MAC address). return (name ?? type) + " [" + type + "]" + (deviceInfo ? " (" + host + "mac: " + device.mac.replace(/:/g, "").toUpperCase() + ")" : ""); } /** * Utility method that generates a combined, nicely formatted device and controller string. * * @param device - Access device. * * @returns Returns the Access device name in the following format: * <code>*Access controller name* [*Access controller type*] *Access device name* [*Access device type*]</code>. */ // Utility to generate a nicely formatted controller and device string. getFullName(device) { const deviceName = this.getDeviceName(device); // Returns: Controller [Controller Type] Device Name [Device Type] return this.name + (deviceName.length ? " " + deviceName : ""); } /** * Terminate any open connection to the UniFi Access API. */ // Utility to disconnect from the Access controller and reset the connection. reset() { this._bootstrap = null; this._floors = null; this._doors = null; this._devices = null; if (this.eventsTimer) { clearTimeout(this.eventsTimer); } this.eventsTimer = null; this.events?.terminate(); this.events = null; } /** * Clear the login credentials and terminate any open connection to the UniFi Access API. */ // Utility to clear out old login credentials or attempts. logout() { // Close any connection to the Access API. this.reset(); // Reset our parameters. this._isAdminUser = false; // Save our CSRF token, if we have one. const csrfToken = this.headers?.get("X-CSRF-Token"); // Initialize the headers we need. this.headers = new Headers(); this.headers.set("Content-Type", "application/json"); if (this.apikey) { this.headers.set("X-API-KEY", this.apikey); } // Restore the CSRF token if we have one. if (csrfToken) { this.headers.set("X-CSRF-Token", csrfToken); } } /** * Execute an HTTP fetch request to the Access controller. * * @param url - Requested endpoint type. Valid types are `livestream` and `talkback`. * @param options - Parameters to pass on for the endpoint request. * @param logErrors - Log errors that aren't already accounted for and handled, rather than failing silently. Defaults to `true`. * * @returns Returns a promise that will resolve to a Response object successful, and `null` otherwise. * * @remarks This method should be used when direct access to the Access controller is needed, or when this library doesn't have a needed method to access * controller capabilities. */ // Communicate HTTP requests with a Access controller. async retrieve(url, options = { method: "GET" }, logErrors = true) { return this._retrieve(url, options, logErrors); } // Internal interface to communicating HTTP requests with an Access controller, with error handling. async _retrieve(url, options = { method: "GET" }, logErrors = true, decodeResponse = true, isRetry = false) { // Catch Access controller server-side issues: // // 400: Bad request. // 404: Not found. // 429: Too many requests. // 500: Internal server error. // 502: Bad gateway. // 503: Service temporarily unavailable. const isServerSideIssue = (code) => [400, 404, 429, 500, 502, 503].some(x => x === code); let response; // Create a signal handler to deliver the abort operation. const signal = timeoutSignal(ACCESS_API_TIMEOUT * 1000); options.headers = this.headers; options.signal = signal; try { const now = Date.now(); // Throttle this after ACCESS_API_ERROR_LIMIT attempts. if (this.apiErrorCount >= ACCESS_API_ERROR_LIMIT) { // Let the user know we've got an API problem. if (this.apiErrorCount === ACCESS_API_ERROR_LIMIT) { this.log.error("Throttling API calls due to errors with the %s previous attempts. Pausing communication with the Access controller for %s minutes.", this.apiErrorCount, ACCESS_API_RETRY_INTERVAL / 60); this.apiErrorCount++; this.apiLastSuccess = now; return null; } // Check to see if we are still throttling our API calls. if ((this.apiLastSuccess + (ACCESS_API_RETRY_INTERVAL * 1000)) > now) { return null; } // Inform the user that we're out of the penalty box and try again. this.log.error("Resuming connectivity to the UniFi Access API after pausing for %s minutes.", ACCESS_API_RETRY_INTERVAL / 60); this.apiErrorCount = 0; this.reset(); if (!(await this.loginController())) { return null; } } response = await this.fetch(url, options); // The caller will sort through responses instead of us. if (!decodeResponse) { return response; } // Preemptively increase the error count. this.apiErrorCount++; // Bad username and password. if (response.status === 401) { this.logout(); this.log.error("Invalid login credentials given. Please check your login and password."); return null; } // Insufficient privileges. if (response.status === 403) { this.log.error("Insufficient privileges for this user. Please check the roles assigned to this user and ensure it has sufficient privileges."); return null; } if (!response.ok && isServerSideIssue(response.status)) { this.log.error("Unable to connect to the Access controller. This is usually temporary and will occur during Access controller reboots and firmware updates."); return null; } // Some other unknown error occurred. if (!response.ok) { this.log.error("%s - %s", response.status, response.statusText); return null; } this.apiLastSuccess = Date.now(); this.apiErrorCount = 0; return response; } catch (error) { this.apiErrorCount++; if (error instanceof AbortError) { this.log.error("Access controller is taking too long to respond to a request. This error can usually be safely ignored."); this.log.debug("Original request was: %s", url); return null; } if (error instanceof FetchError) { switch (error.code) { case "ECONNREFUSED": case "ERR_HTTP2_STREAM_CANCEL": this.log.error("Connection refused."); break; case "ECONNRESET": // Retry on connection reset, but no more than once. if (!isRetry) { return this._retrieve(url, options, logErrors, decodeResponse, true); } this.log.error("Network connection to Access controller has been reset."); break; case "ENOTFOUND": this.log.error("Hostname or IP address not found: %s. Please ensure the address you configured for this UniFi Access controller is correct.", this.address); break; default: // If we're logging when we have an error, do so. if (logErrors) { this.log.error("Error: %s %s.", error.code, error.message); } break; } } return null; } finally { // Clear out our response timeout if needed. signal.clear(); } } // Utility function for logging connection retries. logRetry(logMessage, isRetry) { // If we're over the API limit, no need to continue indicating errors since we already inform users we're throttling API calls. if (this.apiErrorCount >= ACCESS_API_ERROR_LIMIT) { return; } // If we're retrying, only log when debugging. if (isRetry) { this.log.debug("%s Retrying.", logMessage); } else { this.log.error(logMessage); } } /** * Return an API endpoint for the requested endpoint type. * * @param endpoint - Requested endpoint type. * * @returns Returns a URL to the requested endpoint if successful, and an empty string otherwise. * * @remarks Valid API endpoints are `bootstrap`, `device`, `login`, `self`, and `websocket`. */ // Return the appropriate URL to access various Access API endpoints. getApiEndpoint(endpoint) { let endpointSuffix; let endpointPrefix = "/proxy/access/api/v2/"; switch (endpoint) { case "bootstrap": endpointSuffix = "devices/topology4"; break; case "controller": endpointSuffix = "access/info"; break; case "device": endpointSuffix = "device"; break; case "location": endpointSuffix = "location"; break; case "login": endpointPrefix = "/api/"; endpointSuffix = "auth/login"; break; case "self": endpointPrefix = "/api/"; endpointSuffix = "users/self"; break; case "settings": endpointSuffix = "settings"; break; case "websocket": endpointSuffix = "ws"; break; default: break; } if (!endpointSuffix) { return ""; } return "https://" + this.address + endpointPrefix + endpointSuffix; } /** * Access the Access controller information JSON. * * @returns Returns the controller information JSON if the Access controller has been bootstrapped, `null` otherwise. */ // Get the controller JSON. get controller() { return this._controller; } /** * Access the Access controller bootstrap JSON. * * @returns Returns the bootstrap JSON if the Access controller has been bootstrapped, `null` otherwise. */ // Get the bootstrap JSON. get bootstrap() { return this._bootstrap; } /** * Access the Access controller list of devices. * * @returns Returns an array of all the devices from all the UniFi Access hubs associated with this controller, `null` otherwise. */ // Get the list of devices. get devices() { return this._devices; } /** * Access the Access controller list of doors. * * @returns Returns an array of all the doors from all the UniFi Access hubs associated with this controller, `null` otherwise. */ // Get the list of doors. get doors() { return this._doors; } /** * Access the Access controller list of floors. * * @returns Returns an array of all the floors from all the UniFi Access hubs associated with this controller, `null` otherwise. */ // Get the list of floors. get floors() { return this._floors; } /** * Utility method that returns whether the credentials that were used to login to the Access controller have administrative privileges or not. * * @returns Returns `true` if the logged in user has administrative privileges, `false` otherwise. */ // Return whether the logged in credentials are an admin user or not. get isAdminUser() { return this._isAdminUser; } /** * Utility method that returns a nicely formatted version of the Access controller name. * * @returns Returns the Access controller name in the following format: * <code>*Access controller name* [*Access controller type*]</code>. */ // Utility to generate a nicely formatted controller string. get name() { // Our controller string, if it exists, appears as `Controller [Controller Type]`. Otherwise, we appear as `address`. // Our controller string, if it exists, appears as `Controller`. Otherwise, we appear as `address`. if (this._bootstrap?.alias?.length || this._bootstrap?.name?.length) { return this._bootstrap.alias?.length ? this._bootstrap.alias : this._bootstrap.name; } else { return this.address; } } } //# sourceMappingURL=access-api.js.map