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.
150 lines • 9.16 kB
TypeScript
import { MqttClient } from "homebridge-plugin-utils";
import type { Camera, ProtectDeviceConfig, ProtectNvrConfig } from "unifi-protect";
import { ProtectClient } from "unifi-protect";
import type { HomebridgePluginLogging, Nullable } from "homebridge-plugin-utils";
import type { ProtectAccessory, ProtectDevices } from "../types.ts";
import { DoorbellCapability } from "../devices/cameras/doorbell.ts";
import { NvrHealth } from "./nvr-health.ts";
import { ProtectCamera } from "../devices/cameras/camera.ts";
import { ProtectCameraPackage } from "../devices/cameras/camera-package.ts";
import { ProtectEventDispatch } from "./event-dispatch.ts";
import type { ProtectNvrOptions } from "../options.ts";
import { ProtectNvrSystemInfo } from "./nvr-systeminfo.ts";
import type { ProtectPlatform } from "../platform.ts";
/**
* The NVR's lifecycle phase. The single source of truth for "what is this NVR doing right now?", consulted by every component that needs to distinguish induced
* disruptions (where the plugin is intentionally driving an outage and downstream noise should be suppressed) from organic disruptions (where something genuinely
* unexpected happened and the noise IS the signal).
*
* - `connecting`: initial setup, or post-disruption reconnection attempt. Real connection errors should still surface to the user (e.g., "invalid credentials"),
* but health-symptom observation is suspended because we have not yet established a baseline of "running" against which to weigh stress.
* - `running`: connected, normal steady-state operation. Errors are organic; health observation is active; consumers log unexpected events normally.
* - `rebooting`: the plugin initiated a controller reboot. Errors are induced (we know the controller is going away); health observation is suspended; consumers
* suppress noise about the disruption.
* - `shuttingDown`: terminal. The plugin or the host process is going away. Same suppression as `rebooting` but no recovery is expected.
*
* Transitions are owned by `ProtectNvr.transition()`. Components consume via `nvr.phase`. Derived predicates (currently `logApiErrors`) are getters that project
* from phase, so phase is the lone mutable axis.
*/
export type NvrPhase = "connecting" | "rebooting" | "running" | "shuttingDown";
export declare class ProtectNvr {
#private;
private api;
client: ProtectClient;
readonly config: ProtectNvrOptions;
readonly configuredDevices: Map<string, ProtectDevices>;
private connectionSubscriptions;
private controllerStableSince;
private deviceRemovalTimers;
readonly events: ProtectEventDispatch;
private featureLog;
private hap;
private hasStabilizedOnce;
readonly health: NvrHealth;
private lastRebootObservedAt;
private readonly livestreamEpisodes;
private liveviews;
readonly log: HomebridgePluginLogging;
mqtt: Nullable<MqttClient>;
private name;
private nvrRebootTimer;
private rebootConfirmTimer;
private _phase;
readonly platform: ProtectPlatform;
private stabilityReachedTimer;
systemInfo: Nullable<ProtectNvrSystemInfo>;
private unsupportedDevices;
private readonly clientLog;
private readonly deviceDescriptors;
constructor(platform: ProtectPlatform, nvrOptions: ProtectNvrOptions);
/**
* Wire the NVR-health and user-facing log feeds for livestream disruptions, sourced from the unifi-protect library's process-global livestream-recovery diagnostics
* channels. The library owns the recovery protocol and publishes each episode's lifecycle on these channels; the plugin translates them into its own health model and
* its own per-camera logs, scoped to this controller's cameras.
*
* We subscribe the two episode-boundary channels - `recovery:started` (a stream was disrupted; the episode begins) and `recovery:recovered` (it resumed). These are
* the stall/recovery health feed: one stress symptom per disruption episode, one recovery symptom per recovery, so a recovered episode
* nets zero and a failed one (started, never recovered) stays +1 - correct stress accounting with no double count. The other livestream channels are deliberately not
* consumed here: `stall:detected` is a subset of `recovery:started` (every detected stall begins an episode) and consuming both would double-count;
* `recovery:exhausted` is the consumer-side self-heal's concern; `session:closed`/`codec:changed` are neither health nor log signals.
*
* The channels are process-global - shared by every `ProtectNvr` in this process - so each handler filters to this controller's cameras via `getDeviceById(cameraId)`
* and drops the rest (another controller's camera, or one we do not configure). A package-camera stream carries its PARENT camera's device id, so the lookup resolves
* the parent `ProtectCamera` and the log names the parent - correct, since there is no separate package-camera device to name.
*/
private wireLivestreamHealth;
/**
* Current lifecycle phase. Components that need to distinguish induced disruption (rebooting, shutting down) from organic operation (running) consult this
* property. Pure read - mutation goes through {@link transition} only.
*/
get phase(): NvrPhase;
/**
* The terminal plugin-shutdown abort signal. Aborted exactly once, in `transition("shuttingDown")`. Every NVR-level observe loop and every per-accessory abort
* controller composes against it, so plugin shutdown tears the whole tree down as one cascade. Pure read - the controller is private and aborted only through the
* transition chokepoint.
*/
get signal(): AbortSignal;
/**
* Read-through NVR configuration. Replaces the held bootstrap snapshot with the live unifi-protect projection, so every `nvr.ufp.<field>` read across the plugin
* reflects the current reduced state with no merge and no reassignment. A read before the first successful connect() throws (the getter dereferences
* `this.client`, which is unset until connect() assigns it) - this is deliberate: a too-early read should fail loudly, not silently return a stale snapshot, which
* is the held-state footgun this read-through design avoids. No code path reaches that throw: the constructor and the only other pre-connect path (`login()`'s
* global enable gate) both avoid `ufp` - the gate consults feature options by global scope, not the controller mac, and `ProtectEventDispatch` construction is
* structural-only and does not read `hasFeature`/`ufp`.
*/
get ufp(): Readonly<ProtectNvrConfig>;
/**
* Whether API error logging is currently surfaced to the user. Derived from phase: errors are visible during `connecting` (so credential or address problems
* reach the user) and `running` (organic errors are real signal), but suppressed during `rebooting` and `shuttingDown` where the errors are induced by our own
* teardown. Read by the unifi-protect client logger callback in this NVR's constructor.
*/
get logApiErrors(): boolean;
private transition;
private publishLifecycle;
private connect;
private wireConnectionHealth;
private onControllerRebooted;
private onControllerRecovered;
private resumeFromInducedReboot;
private disconnect;
login(): Promise<void>;
private configureNvr;
private configureScheduledReboot;
private executeScheduledReboot;
private get deviceConfigs();
private deviceConfig;
private deviceConfigByMac;
private reconcileMembership;
private currentAdoptedMacs;
private sweepOrphans;
private startDeviceObservers;
private refreshRemovalStability;
private get removalStable();
private sweepRemovableDevices;
scheduleDeviceRemoval(options: {
accessory: ProtectAccessory;
reason?: string;
remove?: () => void;
stillGone: () => boolean;
}): void;
cancelDeviceRemovalFor(uuid: string): void;
private cancelAllDeviceRemovals;
private startConnectionObserver;
private spawnLoop;
createDoorbellCapability(camera: ProtectCamera, device: Camera, signal: AbortSignal): DoorbellCapability;
createPackageCamera(accessory: ProtectAccessory, device: Camera): ProtectCameraPackage;
removeStaleDoorbellServices(accessory: ProtectAccessory): void;
addHomeKitDevice(device: ProtectDeviceConfig): boolean;
removeHomeKitDevice(accessory: ProtectAccessory): void;
removeAccessoryFromHomeKit(accessory: ProtectAccessory): void;
private devices;
private deviceEndpoints;
private episodeCameraReachable;
getDeviceById(deviceId: string): Nullable<ProtectDevices>;
getFeatureFloat(option: string): Nullable<number | undefined>;
getFeatureNumber(option: string): Nullable<number | undefined>;
isNvrFeature(option: string, device?: ProtectDeviceConfig | ProtectNvrConfig): boolean;
hasFeature(option: string, device?: ProtectDeviceConfig | ProtectNvrConfig): boolean;
logFeature(option: string, message: string): void;
}
//# sourceMappingURL=nvr.d.ts.map