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.
123 lines • 6.82 kB
JavaScript
import { APIEvent, FeatureOptions, FfmpegCodecs, RtpPortAllocator, recordingProcessFactory, systemClock } from "homebridge-plugin-utils";
import { featureOptionCategories, featureOptions } from "./options.js";
import { PROTECT_MQTT_TOPIC } from "./settings.js";
import { ProtectNvr } from "./nvr/nvr.js";
import ffmpegPath from "ffmpeg-for-homebridge";
import { streamingDelegateFactory } from "./media/stream.js";
import util from "node:util";
export class ProtectPlatform {
accessories;
api;
clock;
_codecSupport = null;
config;
controllers;
featureOptions;
log;
pluginLog;
recordingProcessFactory;
rtpPorts;
streamingDelegateFactory;
verboseFfmpeg;
constructor(log, config, api) {
this.accessories = [];
this.api = api;
this.clock = systemClock;
this.controllers = [];
this.featureOptions = new FeatureOptions(featureOptionCategories, featureOptions, config?.options ?? []);
this.log = log;
// The plugin-side logging root. Debug routes through our gated debug utility; the other levels route to the Homebridge logger, which formats each line
// once at the sink. Per-device and per-controller loggers derive from this root through prefixedLog, so the sink wiring lives here and nowhere else;
// this.log stays the raw Homebridge logger for direct platform-level output.
this.pluginLog = {
debug: (message, ...parameters) => this.debug(message, ...parameters),
error: (message, ...parameters) => this.log.error(message, ...parameters),
info: (message, ...parameters) => this.log.info(message, ...parameters),
warn: (message, ...parameters) => this.log.warn(message, ...parameters)
};
this.recordingProcessFactory = recordingProcessFactory;
this.rtpPorts = new RtpPortAllocator();
this.streamingDelegateFactory = streamingDelegateFactory;
this.verboseFfmpeg = false;
// Plugin options into our config variables.
this.config = {
controllers: config?.controllers ?? [],
debugAll: config?.debug === true,
options: config?.options ?? [],
ringDelay: config?.ringDelay ?? 0,
verboseFfmpeg: config?.verboseFfmpeg === true,
videoProcessor: config?.videoProcessor ?? ffmpegPath ?? "ffmpeg"
};
// We need a UniFi Protect controller configured to do anything.
if (!this.config.controllers.length) {
this.log.info("No UniFi Protect controllers have been configured.");
return;
}
// Debugging - most people shouldn't enable this.
this.debug("Debug logging on. Expect a lot of data.");
// Debug FFmpeg.
if (this.config.verboseFfmpeg) {
this.verboseFfmpeg = true;
this.log.info("Verbose logging of video streaming sessions enabled. Expect a lot of data.");
}
// Loop through each configured NVR and instantiate it.
for (const controllerConfig of this.config.controllers) {
// We need an address, or there's nothing to do.
if (!controllerConfig.address) {
this.log.info("No host or IP address has been configured.");
continue;
}
// We need login credentials or we're skipping this one.
if (!controllerConfig.username || !controllerConfig.password) {
this.log.info("No UniFi Protect login credentials have been configured.");
continue;
}
// MQTT topic to use.
controllerConfig.mqttTopic ||= PROTECT_MQTT_TOPIC;
this.controllers.push(new ProtectNvr(this, controllerConfig));
}
// Avoid a prospective race condition by waiting to configure our controllers until Homebridge is done loading all the cached accessories it knows about, and calling
// configureAccessory() on each.
api.on(APIEvent.DID_FINISH_LAUNCHING, () => void this.launchControllers());
}
// The host's probed FFmpeg capabilities. Established exactly once, when launchControllers() completes a successful probe, and read-only thereafter. Every consumer
// (streaming, recording, snapshots) runs only after a controller has logged in - which happens after the probe - so the value is always present by the time it is read;
// the guard narrows the nullable backing and turns a would-be access-before-probe into a clear error rather than an undefined-property dereference.
get codecSupport() {
if (!this._codecSupport) {
throw new Error("The FFmpeg codec capabilities were accessed before they were probed.");
}
return this._codecSupport;
}
// This gets called when homebridge restores cached accessories at startup. We intentionally avoid doing anything significant here, and save it for device discovery.
configureAccessory(accessory) {
// Add this to the accessory array so we can track it.
this.accessories.push(accessory);
}
// Launch our configured controllers once all accessories have been loaded. Once we do, they will sustain themselves.
async launchControllers() {
// First things first - ensure we've got a working video processor before we do anything else. The probe runs the full FFmpeg capability detection and returns a
// populated, immutable FfmpegCodecs value object on success, or null when probing fails. We run it without a cancellation signal: the platform has no shutdown
// controller, and probing happens once at launch, before any controller is brought up.
const codecs = await FfmpegCodecs.probe({ ffmpegExec: this.config.videoProcessor, log: this.log, verbose: this.verboseFfmpeg });
if (!codecs) {
this.log.error("This plugin requires a working version of FFmpeg. " +
"If you need to specify a path to your FFmpeg, you can do so under 'Settings | Additional Settings' in the plugin configuration webUI.");
return;
}
this._codecSupport = codecs;
// Iterate through all our controllers and startup.
for (const controller of this.controllers) {
// Login to the Protect controller.
void controller.login();
}
}
// Utility for debug logging. We route this through log.warn rather than log.debug so that plugin-level debug output stays visible regardless of whether
// Homebridge itself is run with its own debug flag, which is what gates log.debug.
debug(message, ...parameters) {
if (this.config.debugAll) {
this.log.warn(util.format(message, ...parameters));
}
}
}
//# sourceMappingURL=platform.js.map