UNPKG

homebridge-unifi-protect

Version:

Homebridge UniFi Protect plugin providing complete HomeKit integration for the entire UniFi Protect ecosystem with full support for most features including HomeKit Secure Video, multiple controllers, blazing fast performance, and much more.

102 lines (74 loc) 5.38 kB
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved. * * server.js: homebridge-unifi-protect webUI server API. * * This module is heavily inspired by, and borrows from, the homebridge-config-ui-x source code. * Thank you oznu for your contributions to the HomeKit world. */ "use strict"; import { ProtectClient, isDeviceAdopted, noopLog } from "unifi-protect"; import { featureOptionCategories, featureOptions } from "../dist/options.js"; import { HomebridgePluginUiServer } from "@homebridge/plugin-ui-utils"; import util from "node:util"; class PluginUiServer extends HomebridgePluginUiServer { constructor() { super(); this.#registerGetDevices(); this.#registerGetOptions(); this.ready(); } #registerGetDevices() { // Retrieve the list of devices from a Protect controller to populate the feature options device picker. this.onRequest("/getDevices", async (controller) => { // The connection outcome is probe-local state that travels back with the device list: a failed probe fills this with a user-facing message and the response // returns it alongside an empty device array, so the webUI reads the failure from the response it belongs to. let errorInfo = ""; // A quiet logging shim for the short-lived client: debug/info/warn stay silent (noopLog), while error captures the message so the probe can return the most // recent failure reason to the user. The unifi-protect ProtectLogging signature is (message, ...parameters), so we spread the rest parameters into util.format. const log = { ...noopLog, error: (message, ...parameters) => { errorInfo = util.format(message, ...parameters); // eslint-disable-next-line no-console console.error(errorInfo); } }; try { // Connect to the controller. unifi-protect's connect() performs login and the initial bootstrap atomically, throwing a typed error on failure. This file ships // as raw JavaScript executed directly by the host's Node runtime with no transpile step, so an `await using` declaration (explicit-resource-management syntax, // which the Node 24 runtime provides) cannot appear here while the package's engines floor is below Node 24. The inner try/finally is the equivalent: it // guarantees the client and its realtime connection are released on every exit path, success or throw. We disable the periodic refresh failsafe since this is a // one-shot discovery probe. When the engines floor reaches Node 24, an `await using` declaration is the preferred form. const client = await ProtectClient.connect({ host: controller.address, log: log, password: controller.password, refreshIntervalMs: false, username: controller.username }); try { // Sort adopted devices by display name, falling back to the market name when a device has no user-assigned name. const byName = (a, b) => (a.name ?? a.marketName).localeCompare(b.name ?? b.marketName); // Project each live device collection to its raw config record, keep only the devices adopted by this controller, and sort. We read the config records rather // than the command-bearing projections because the webUI consumes plain controller fields and serializes them across the request boundary. const adopted = (devices) => devices.map(device => device.config).filter(isDeviceAdopted).sort(byName); // Return the controller first, then the adopted devices by category, paired with an empty error. The controller is element 0 - the controller-as-device // entry the feature options webUI selects on initial load. return { devices: [ client.nvr.config, ...adopted(client.cameras), ...adopted(client.chimes), ...adopted(client.fobs), ...adopted(client.lights), ...adopted(client.relays), ...adopted(client.sensors), ...adopted(client.viewers) ], error: "" }; } finally { // Release the client and its realtime connection on every exit path... the raw-JavaScript equivalent of an `await using` declaration's scope-bound disposal. await client[Symbol.asyncDispose](); } } catch(error) { // The library reports failures through the log shim above, which captured a friendly message into errorInfo and already logged it. The typed transport errors, // however, propagate WITHOUT logging - so when errorInfo is still empty we capture the thrown error's friendly .message and log that single clean sentence here. // We never log the raw error object, which would dump its stack and chained cause into the log; and the empty-errorInfo guard avoids logging the same failure // twice when the shim already reported it. if(!errorInfo) { errorInfo = (error instanceof Error) ? error.message : String(error); // eslint-disable-next-line no-console console.error(errorInfo); } return { devices: [], error: errorInfo }; } }); } #registerGetOptions() { // Return the full feature-option category and option catalog the webUI renders; per-device filtering happens client-side. this.onRequest("/getOptions", () => ({ categories: featureOptionCategories, options: featureOptions })); } } (() => new PluginUiServer())();