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.
112 lines • 7.28 kB
TypeScript
import type { ConnectionState } from "unifi-protect";
import type { Nullable } from "homebridge-plugin-utils";
import type { NvrPhase } from "./nvr.ts";
/**
* The startup-resilient connect retry policy, as a pure stateful predicate factory. Authentication faults get a small consecutive budget so a controller still sorting
* out its own auth state recovers, but genuinely-wrong credentials fail fast rather than looping forever. Any non-auth
* fault (network, transient) resets the budget, so a slow-to-appear controller is retried without bound. A pure factory so the budget logic is testable without standing
* up a client - the separation of the retry *decision* from the connect *effect*.
*
* @param limit - The consecutive-auth-failure ceiling. Defaults to {@link PROTECT_AUTH_FAILURE_LIMIT}.
*
* @returns A `shouldRetry` predicate suitable for the retry() options; it closes over the running consecutive-auth count.
*/
export declare function createConnectRetryPolicy(limit?: number): {
shouldRetry: (error: unknown) => boolean;
};
/**
* Whether a lifecycle phase change is legal. Two changes are refused: a same-phase change (the long-standing no-op), and any change OUT of "shuttingDown".
* "shuttingDown" is one-way because entering it aborts the terminal shutdown signal and tears down the whole observe/firehose tree - nothing may resurrect a
* torn-down controller's lifecycle, so a stale reboot timer, a late-resolving connect, or any other deferred wake that tries to move the phase onward is
* rejected here rather than at each call site. Entering "shuttingDown" from any other phase stays legal. An options object because both arguments share the
* NvrPhase type and a positional swap would silently invert the predicate - the same reason {@link shouldResumeFromInducedReboot} takes a named shape.
*/
export declare function canTransition(options: {
from: NvrPhase;
to: NvrPhase;
}): boolean;
/**
* Whether a livestream disruption is INDUCED - the controller is rebooting or shutting down because we asked it to - rather than ORGANIC. An induced disruption is
* expected and already narrated at the controller level; an organic one is a single camera unexpectedly in trouble. It deliberately excludes "connecting", which is an
* organic startup/reconnection window where a disruption should still surface to the user. The recovery policy's induced-disruption guard consults this directly. The
* per-camera disruption logs consult a SUPERSET - induced OR {@link isWithinRebootRecency} - because a controller reboot's post-return re-establishment blip fires
* after the plugin has already concluded the reboot and left the induced phase, so the phase alone no longer recognizes that blip as the reboot's tail.
*/
export declare function isInducedDisruption(phase: NvrPhase): boolean;
export declare function isWithinRebootRecency(options: {
lastRebootMs: Nullable<number>;
nowMs: number;
windowMs: number;
}): boolean;
export declare function shouldResumeFromInducedReboot(options: {
from: ConnectionState;
phase: NvrPhase;
to: ConnectionState;
}): boolean;
/**
* Create a per-episode livestream-recovery quiet-classification latch. A livestream recovery episode opens (interruption) and later closes (recovery) on two independent
* unifi-protect library events; the recovery edge cannot read the controller phase reliably (the controller has already returned), so it must consult what the
* interruption edge recorded. What it records is `quiet` - whether the episode should be logged quietly (the reboot-tail-or-induced case) rather than at warn (a
* genuine single-camera drop). Keyed by the library's livestream pool `key` (one slot per concurrent session, not per camera). The owner records at interruption and
* consumes at recovery; `forgetCamera` reclaims any entries a removed camera left behind (a started-without-recovery episode would otherwise never be consumed). The Map
* is owned here, never exposed - mirroring createConnectRetryPolicy's encapsulated state.
*/
export declare function createLivestreamEpisodeLatch(): {
consume: (key: string) => boolean;
forgetCamera: (cameraId: string) => void;
record: (key: string, cameraId: string, quiet: boolean) => void;
};
/**
* The HomeKit-membership delta between the controller's adopted-id set and the ids we have already configured, as a pure set diff. `toAdd` is the adopted ids we have
* not configured; `toRemove` is the configured ids no longer adopted. This single set diff drives the whole of device sync - one engine for both adoption and
* unadoption; isolating it makes the reconcile read as decision (this pure diff) then effect (add/remove). Order-preserving relative to the inputs.
*
* @param adoptedIds - The controller's current adopted ids for one device category.
* @param configuredIds - The ids we have already configured in that category.
*
* @returns The ids to add and the ids to remove.
*/
export declare function membershipDelta(adoptedIds: readonly string[], configuredIds: readonly string[]): {
toAdd: string[];
toRemove: string[];
};
/**
* Whether a completed HTTP request belongs to this controller, from the unifi-protect library's process-global `http:request:end` diagnostic payload. The channel
* carries every request from every client in the process, so each NVR filters by host to keep its health scoped to its own controller. The payload's `host` is the exact
* address the client was built against, and this NVR passes its configured `address` to `connect()`, so an exact string comparison against that same address
* attributes a request precisely - both strings descend from one configuration value. Exact identity keeps the port significant: a ported address ("1.2.3.4:8443")
* attributes to itself, two controllers on one host at different ports stay distinct, and a mixed-case address matches itself verbatim.
*
* @param options - The configured controller `address` and the payload's reported `host`.
*
* @returns `true` when the request's host is exactly this controller's configured address.
*/
export declare function isRequestForController(options: {
address: string;
host: string;
}): boolean;
/**
* Whether a completed HTTP request was successful, from the unifi-protect library's `http:request:end` diagnostic payload: no transport-level error, and a 2xx status.
* Everything else - a transport error, an absent status, or a non-2xx response - is a failure. Isolated so the request-outcome-to-health-symptom mapping the NVR feeds
* into NvrHealth is testable apart from the diagnostics-channel subscription that drives it.
*
* @param payload - The request-end payload (only `error` and `statusCode` are read).
*
* @returns `true` when the request succeeded.
*/
export declare function isSuccessfulRequest(payload: {
error?: string;
statusCode?: number;
}): boolean;
export declare function computeStableSince(options: {
hasStabilizedBefore: boolean;
nowMs: number;
uptimeMs: number;
windowMs: number;
}): number;
export declare function isStabilityWindowElapsed(options: {
nowMs: number;
stableSinceMs: Nullable<number>;
windowMs: number;
}): boolean;
//# sourceMappingURL=nvr-policy.d.ts.map