@camstack/addon-provider-reolink 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,924 @@
1
+ export { ReolinkProviderAddon } from './addon.js';
2
+ import * as _apocaliss92_nodelink_js from '@apocaliss92/nodelink-js';
3
+ import { Rfc4571TcpServer, ReolinkBaichuanApi } from '@apocaliss92/nodelink-js';
4
+ import * as _camstack_types from '@camstack/types';
5
+ import { BaseDevice, ICameraDevice, DeviceType, DeviceFeature, DeviceContext, StreamSourceEntry, ConfigUISchemaWithValues } from '@camstack/types';
6
+ import { z } from 'zod';
7
+
8
+ /**
9
+ * Reolink accessory base layer — kind enum, parent-host contract,
10
+ * shared `BaseDevice` glue every kind-specific subclass extends.
11
+ *
12
+ * Each accessory kind (Siren, Floodlight, PIR, Autotrack) lives in its
13
+ * own file (`./siren.ts`, `./floodlight.ts`, …) with its own Zod
14
+ * schema, settings UI, and cap-registration logic — they're
15
+ * independent device classes that share only the parent reference and
16
+ * the `channel` field.
17
+ *
18
+ * Why split per file:
19
+ * - Each kind owns a different subset of the lib's Baichuan
20
+ * surface (`setSiren`, `setWhiteLedState`, `setPirInfo`,
21
+ * `setAutotracking{,Settings}`) — keeping the dispatch in one
22
+ * monolithic switch obscures the per-kind contract.
23
+ * - Per-accessory settings UIs are different schemas; co-locating
24
+ * each schema with its consumer makes the kind self-contained
25
+ * (the file you read to understand "what does the siren expose"
26
+ * is `siren.ts`, full stop).
27
+ * - Adding a new kind (e.g. a future SpotlightAccessory) is one
28
+ * new file + one factory case, no edits to existing kinds.
29
+ */
30
+
31
+ /**
32
+ * Anything an accessory subclass needs from the parent camera. The
33
+ * subclass receives this rather than a direct `ReolinkCamera`
34
+ * reference so:
35
+ * - The accessory file doesn't import the camera class (avoids
36
+ * circular import the camera class doesn't need).
37
+ * - Tests can stub the host with a tiny fake instead of standing
38
+ * up a full camera.
39
+ * - The contract is explicit: subclasses use the parent's
40
+ * Baichuan socket via `ensureApi()` and read its display name
41
+ * for naming.
42
+ */
43
+ /**
44
+ * Aux-device reference exposed by each accessory to its parent.
45
+ * Mirrors scrypted-reolink-native's `alignAuxDevicesState` pattern:
46
+ * the parent camera owns the periodic align scheduling (cadence,
47
+ * sleep-gate, cooldown), the accessory owns the slice projection
48
+ * + cooldown timestamp. Single 30s background poll thus replaces N
49
+ * per-accessory timers AND can be sleep-gated centrally.
50
+ */
51
+ interface ReolinkAccessoryRef {
52
+ readonly kind: string;
53
+ readonly id: number;
54
+ /** Read latest state from firmware and write into the slice. Best-
55
+ * effort: per-accessory failures are logged + swallowed by the
56
+ * caller (parent's alignAuxDevicesState). */
57
+ refreshFromAlign(): Promise<void>;
58
+ /** True when a recent operator setState/setMotionTrigger call
59
+ * marked the cooldown — caller should skip align this round to
60
+ * avoid clobbering the just-applied state with a stale firmware
61
+ * read (camera takes 10s+ to reflect changes in some endpoints). */
62
+ isInCooldown(): boolean;
63
+ }
64
+
65
+ declare const reolinkCameraSchema: z.ZodObject<{
66
+ host: z.ZodString;
67
+ port: z.ZodDefault<z.ZodNumber>;
68
+ username: z.ZodDefault<z.ZodString>;
69
+ password: z.ZodString;
70
+ transport: z.ZodDefault<z.ZodEnum<{
71
+ tcp: "tcp";
72
+ udp: "udp";
73
+ }>>;
74
+ uid: z.ZodOptional<z.ZodString>;
75
+ udpDiscoveryMethod: z.ZodOptional<z.ZodEnum<{
76
+ "local-broadcast": "local-broadcast";
77
+ "local-direct": "local-direct";
78
+ remote: "remote";
79
+ map: "map";
80
+ relay: "relay";
81
+ }>>;
82
+ channel: z.ZodOptional<z.ZodNumber>;
83
+ deviceCache: z.ZodOptional<z.ZodObject<{
84
+ deviceType: z.ZodOptional<z.ZodEnum<{
85
+ camera: "camera";
86
+ "udp-camera": "udp-camera";
87
+ "battery-cam": "battery-cam";
88
+ nvr: "nvr";
89
+ multifocal: "multifocal";
90
+ }>>;
91
+ channelCount: z.ZodOptional<z.ZodNumber>;
92
+ model: z.ZodOptional<z.ZodString>;
93
+ serialNumber: z.ZodOptional<z.ZodString>;
94
+ mac: z.ZodOptional<z.ZodString>;
95
+ hardwareVersion: z.ZodOptional<z.ZodString>;
96
+ firmwareVersion: z.ZodOptional<z.ZodString>;
97
+ motionSnapshot: z.ZodOptional<z.ZodObject<{
98
+ enabled: z.ZodOptional<z.ZodBoolean>;
99
+ sensitivity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
100
+ }, z.core.$strip>>;
101
+ aiSensitivitySnapshot: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
102
+ sensitivity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
103
+ stayTime: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
104
+ }, z.core.$strip>>>;
105
+ aiDetectTypes: z.ZodOptional<z.ZodArray<z.ZodString>>;
106
+ imageSnapshot: z.ZodOptional<z.ZodObject<{
107
+ bright: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
108
+ contrast: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
109
+ saturation: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
110
+ hue: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
111
+ irCutSwap: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
112
+ dayNight: z.ZodOptional<z.ZodString>;
113
+ }, z.core.$strip>>;
114
+ encSnapshot: z.ZodOptional<z.ZodObject<{
115
+ audio: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
116
+ mainStream: z.ZodOptional<z.ZodObject<{
117
+ bitRate: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
118
+ frameRate: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
119
+ videoEncType: z.ZodOptional<z.ZodNullable<z.ZodString>>;
120
+ width: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
121
+ height: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
122
+ }, z.core.$strip>>;
123
+ subStream: z.ZodOptional<z.ZodObject<{
124
+ bitRate: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
125
+ frameRate: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
126
+ videoEncType: z.ZodOptional<z.ZodNullable<z.ZodString>>;
127
+ width: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
128
+ height: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
129
+ }, z.core.$strip>>;
130
+ }, z.core.$strip>>;
131
+ maskSnapshot: z.ZodOptional<z.ZodObject<{
132
+ enabled: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
133
+ }, z.core.$strip>>;
134
+ audioNoiseSnapshot: z.ZodOptional<z.ZodObject<{
135
+ enabled: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
136
+ level: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
137
+ }, z.core.$strip>>;
138
+ autoFocusSnapshot: z.ZodOptional<z.ZodObject<{
139
+ enabled: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
140
+ supported: z.ZodOptional<z.ZodBoolean>;
141
+ }, z.core.$strip>>;
142
+ }, z.core.$loose>>;
143
+ debugGeneral: z.ZodDefault<z.ZodBoolean>;
144
+ debugSocketLogs: z.ZodDefault<z.ZodArray<z.ZodEnum<{
145
+ debugRtsp: "debugRtsp";
146
+ traceNativeStream: "traceNativeStream";
147
+ traceRecordings: "traceRecordings";
148
+ traceTalk: "traceTalk";
149
+ traceEvents: "traceEvents";
150
+ }>>>;
151
+ motionEnabled: z.ZodOptional<z.ZodBoolean>;
152
+ motionSensitivity: z.ZodOptional<z.ZodNumber>;
153
+ aiPersonSensitivity: z.ZodOptional<z.ZodNumber>;
154
+ aiVehicleSensitivity: z.ZodOptional<z.ZodNumber>;
155
+ aiAnimalSensitivity: z.ZodOptional<z.ZodNumber>;
156
+ aiFaceSensitivity: z.ZodOptional<z.ZodNumber>;
157
+ aiPackageSensitivity: z.ZodOptional<z.ZodNumber>;
158
+ imgBright: z.ZodOptional<z.ZodNumber>;
159
+ imgContrast: z.ZodOptional<z.ZodNumber>;
160
+ imgSaturation: z.ZodOptional<z.ZodNumber>;
161
+ imgHue: z.ZodOptional<z.ZodNumber>;
162
+ imgSharpen: z.ZodOptional<z.ZodNumber>;
163
+ ispDayNight: z.ZodOptional<z.ZodString>;
164
+ ispExposure: z.ZodOptional<z.ZodString>;
165
+ ispHdr: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<1>]>>;
166
+ ispBinningMode: z.ZodOptional<z.ZodNumber>;
167
+ ispDayNightThreshold: z.ZodOptional<z.ZodNumber>;
168
+ irLightsState: z.ZodOptional<z.ZodEnum<{
169
+ On: "On";
170
+ Off: "Off";
171
+ Auto: "Auto";
172
+ }>>;
173
+ irLightsBrightness: z.ZodOptional<z.ZodNumber>;
174
+ audioVolume: z.ZodOptional<z.ZodNumber>;
175
+ audioTalkAndReplyVolume: z.ZodOptional<z.ZodNumber>;
176
+ audioVisitorVolume: z.ZodOptional<z.ZodNumber>;
177
+ streamMainBitRate: z.ZodOptional<z.ZodNumber>;
178
+ streamMainFrameRate: z.ZodOptional<z.ZodNumber>;
179
+ streamSubBitRate: z.ZodOptional<z.ZodNumber>;
180
+ streamSubFrameRate: z.ZodOptional<z.ZodNumber>;
181
+ streamAudioEnabled: z.ZodOptional<z.ZodBoolean>;
182
+ privacyMaskEnabled: z.ZodOptional<z.ZodBoolean>;
183
+ audioNoiseLevel: z.ZodOptional<z.ZodNumber>;
184
+ autoFocusEnabled: z.ZodOptional<z.ZodBoolean>;
185
+ intercomBlocksPerPayload: z.ZodOptional<z.ZodNumber>;
186
+ intercomMaxBacklogMs: z.ZodOptional<z.ZodNumber>;
187
+ intercomGain: z.ZodOptional<z.ZodNumber>;
188
+ }, z.core.$strip>;
189
+
190
+ /**
191
+ * Reolink camera device — connects via Baichuan protocol and pushes
192
+ * Annex-B H.264/H.265 directly to the stream broker.
193
+ *
194
+ * Lifecycle (Slice 1):
195
+ * 1. `publishToBroker()` registers `main` + `sub` cam streams as
196
+ * `kind: 'push-annexb'` so the broker emits demand events.
197
+ * 2. The owning addon listens for `stream-broker.onCamStreamDemand`
198
+ * events and forwards them to `onCamStreamDemand()` here.
199
+ * 3. We login (lazy), open a `BaichuanVideoStream` for the requested
200
+ * stream, fetch the broker handle, and forward every
201
+ * `videoAccessUnit` event as `pushEncodedPacket`.
202
+ * 4. On `onCamStreamIdle()` we stop the stream. The Baichuan socket
203
+ * stays alive while at least one stream is active — battery-aware
204
+ * sleep handling lands in Slice 3.
205
+ */
206
+ declare class ReolinkCamera extends BaseDevice<typeof reolinkCameraSchema> implements ICameraDevice {
207
+ readonly type: DeviceType.Camera;
208
+ /**
209
+ * Features derived from the post-probe `feature-probe` runtime-state
210
+ * slice. Surfaced via `device-manager.getDevice` so any service in
211
+ * the cluster (stream-broker, snapshot orchestrator, pipeline-runner)
212
+ * can derive policy from a single source.
213
+ *
214
+ * Returns a fresh array on each read so consumers can't mutate the
215
+ * underlying state. The set is small (≤6 entries) so allocation cost
216
+ * is negligible vs the staleness of caching.
217
+ */
218
+ get features(): readonly DeviceFeature[];
219
+ /** Lazy-connected Baichuan API. Spans the lifetime of every active stream. */
220
+ private api;
221
+ /**
222
+ * Stable bound handler for `api.onSimpleEvent` — created ONCE in
223
+ * the class so every `onSimpleEvent` / `offSimpleEvent` call uses
224
+ * the SAME function reference. The lib's `simpleEventListeners`
225
+ * is a `Set` and dedupes by reference, so passing inline arrows
226
+ * `(e) => this.handleSimpleEvent(e)` on each subscribe call (as
227
+ * the previous code did across login / adoptApi / watchdog
228
+ * paths) silently accumulated multiple listeners — same event
229
+ * dispatched N times on the camstack side, and `offSimpleEvent`
230
+ * couldn't remove the old one without the original ref. Net
231
+ * effect was that subscriptions drifted out of sync with the
232
+ * camera's TCP push channel: device 15 (Daniel) went silent for
233
+ * 2+ hours while device 8 (battery doorbell) kept pushing
234
+ * normally because its short connection lifecycle naturally
235
+ * reset the listener set.
236
+ */
237
+ private readonly handleSimpleEventBound;
238
+ /**
239
+ * Plugin-level event-health interval — fallback layer over the
240
+ * lib's internal 5-minute watchdog. Mirrors Scrypted's
241
+ * `startEventCheck` (`baichuan-base.ts:719-783`): every 60 s,
242
+ * if `eventSubscriptionActive` is false on a live socket OR no
243
+ * event has arrived in 10 min, force a full unsub/resub cycle.
244
+ */
245
+ private eventHealthCheckTimer;
246
+ /**
247
+ * Counter of consecutive event-health re-subscribes that did NOT
248
+ * recover an event. Drives the log-level degradation (first one is
249
+ * `info` so operators see something; the long tail drops to
250
+ * `debug`) and the polling backoff (60s → 5min → 15min so a
251
+ * chronically-silent camera doesn't spam the logs every minute).
252
+ * Reset to 0 the moment a simpleEvent arrives.
253
+ */
254
+ private consecutiveStaleHealthChecks;
255
+ /** Wall-clock ms when the next health check is allowed to fire.
256
+ * Used by `runEventHealthCheckTick` to short-circuit between
257
+ * backoff windows without changing the underlying interval. */
258
+ private nextEventHealthCheckAt;
259
+ /** Active video streams keyed by camStreamId. */
260
+ private readonly active;
261
+ /** Login lock — concurrent demand events must not race the login flow. */
262
+ private loginPromise;
263
+ /** True once we've discovered (via getBatteryInfo) that this is a battery cam. */
264
+ private isBattery;
265
+ /** Reconnect attempt counter — drives exponential backoff. */
266
+ private reconnectAttempts;
267
+ /** Pending reconnect timer; cleared on successful reconnect or removeDevice. */
268
+ private reconnectTimer;
269
+ /**
270
+ * Read-through to the `battery.sleeping` runtime-state slice with
271
+ * a `false` default for non-battery cams (no slice). Internal
272
+ * shorthand — driver code uses `this.state.battery.sleeping = b` to write
273
+ * (proxy on `BaseDevice`); reads via this getter to avoid sprinkling
274
+ * `?? false` everywhere conditional logic checks the flag.
275
+ */
276
+ private get sleeping();
277
+ /**
278
+ * Wall-clock ms when `this.sleeping` last flipped. Drives a
279
+ * hysteresis window that filters out the lib's UDP sleep-inference
280
+ * flapping. The lib runs an internal `getSleepStatus` every 2s on
281
+ * UDP/battery cameras and emits awake/sleeping events from socket
282
+ * I/O patterns alone — those events do NOT reflect real firmware
283
+ * state, especially on battery cams with idle-disconnect where
284
+ * socket activity drops periodically. Without this window every
285
+ * inferred flip would close active streams + emit a cap event.
286
+ */
287
+ private sleepStateChangedAt;
288
+ /**
289
+ * Min time between observed sleep transitions before we honour
290
+ * another flip. Picked >= the lib's full UDP inference cycle
291
+ * (~12s on battery cams with idle-disconnect) so a single
292
+ * inference flap doesn't pass through. 30s is also longer than
293
+ * `getIdleDisconnectTimeoutMs` (default 30s in the lib), which
294
+ * means a transient socket close → reopen pattern can't drive
295
+ * us through a full state cycle either.
296
+ */
297
+ private static readonly SLEEP_HYSTERESIS_MS;
298
+ /** Background timer that runs the passive sleep poll (battery cams only). */
299
+ private sleepPollTimer;
300
+ /** Periodic timer driving `alignAuxDevicesState()` on wired cams.
301
+ * Battery cams use the wake transition path instead. */
302
+ private alignAuxTimer;
303
+ /** Aux accessory references registered by spawned children
304
+ * (siren / floodlight / pir). The parent's centralised align
305
+ * method iterates these to refresh their slices on a single
306
+ * cadence — replaces the old per-accessory setInterval. Mirrors
307
+ * scrypted-reolink-native's `motionSiren` / `floodlight` / `pir`
308
+ * field refs + their `alignAuxDevicesState` method. */
309
+ private readonly auxAccessoryRefs;
310
+ /**
311
+ * Reliability watchdogs (Slice 10). Both timers are armed after a
312
+ * successful login (`ensureApi` / `adoptApi`) and torn down on
313
+ * `disconnectAll` / `removeDevice`.
314
+ *
315
+ * - `pingWatchdogTimer` — every 30s `api.ping()`s the live socket;
316
+ * 3 consecutive failures tear down the api so the next demand
317
+ * triggers a fresh login (recovers from D2C_DISC / ECONNRESET
318
+ * storms that would otherwise leave a half-open socket).
319
+ * - `eventWatchdogTimer` — every 60s checks how long since the last
320
+ * simple event; 10 minutes silent → unsubscribe + resubscribe
321
+ * (plugin-level fallback to the library's own watchdog).
322
+ */
323
+ private pingWatchdogTimer;
324
+ private consecutivePingFailures;
325
+ private eventWatchdogTimer;
326
+ /** Wall-clock ms when the most-recent simple event arrived; 0 = none yet. */
327
+ private lastEventAt;
328
+ /** Wall-clock ms when the watchdogs first armed (used as the no-event baseline). */
329
+ private watchdogStartedAt;
330
+ /** True once we've registered the battery capability provider. */
331
+ private batteryRegistered;
332
+ /**
333
+ * Cam-stream ids we've successfully published to the broker. Used by
334
+ * `publishToBroker` to retract entries that fall out of the camera's
335
+ * offer between probes, and by `removeDevice` to clean up exhaustively
336
+ * (instead of guessing from a synthetic id list).
337
+ */
338
+ private readonly publishedStreamIds;
339
+ /**
340
+ * Transient diagnostics for the device's "Sessions" settings tab.
341
+ * `null` until the first refresh completes. Re-populated ONLY by
342
+ * `refreshSessionsSnapshot()` triggered by the operator clicking
343
+ * the tab's Refresh button (`_refreshSessions` sentinel patch in
344
+ * `applySettingsPatch`). The earlier auto-fetch on stale was
345
+ * removed because `getOnlineUserList` (cmd 120) wakes a sleeping
346
+ * battery cam — and the Settings panel polls every ~2.5s, which
347
+ * was keeping the doorbell awake just by leaving the tab open.
348
+ */
349
+ private sessionsSnapshot;
350
+ /** Single-flight guard so concurrent reads share one round-trip. */
351
+ private sessionsRefreshInFlight;
352
+ constructor(ctx: DeviceContext);
353
+ /**
354
+ * Resolved Baichuan channel for per-channel cmd_ids. Children
355
+ * under a Hub carry their channel in the persisted config (set
356
+ * during `adoptDevice`). Standalone cameras default to 0 so the
357
+ * pre-Hub behavior is preserved verbatim.
358
+ */
359
+ private getChannel;
360
+ /**
361
+ * `true` iff this camera is a child of a Hub. Drives the
362
+ * connection delegation in `ensureApi` and gates the standalone
363
+ * lifecycle (own subscription, watchdog, reconnect loop). The
364
+ * persisted `channel` field is the discriminator — adoption sets
365
+ * it; standalone autodetect never touches it.
366
+ */
367
+ private isHubChild;
368
+ /**
369
+ * Resolve the parent Hub via the addon's device manager. Cached
370
+ * lazily — the lookup is async but cheap, and the parent device
371
+ * doesn't change over the camera's lifetime. Returns `null` for
372
+ * standalone cameras.
373
+ */
374
+ private getParentHub;
375
+ private cachedParentHub;
376
+ /**
377
+ * Phase 3 (kernel-driven) — populate the `feature-probe` runtime
378
+ * state slice. Runs ONCE after `register` and BEFORE
379
+ * `getAccessoryChildren()`, so siren / floodlight / PIR accessory
380
+ * spawn sees the post-probe firmware truth. Failures are swallowed
381
+ * (transient login race / battery cam asleep); next `reprobe()` call
382
+ * retries.
383
+ */
384
+ onProbe(): Promise<void>;
385
+ /**
386
+ * Phase 5 (kernel-driven) — fired after `onProbe()` + accessory
387
+ * reconciliation. Publishes streams to the broker. Best-effort; the
388
+ * provider's `system.ready-state` listener re-publishes if the broker
389
+ * isn't ready yet. Skipped when the device is soft-disabled.
390
+ */
391
+ onActivate(): Promise<void>;
392
+ /** Debounce timestamp for the on-demand snapshot retry kicked from
393
+ * `getSettingsUISchema`. Prevents the form open from spamming
394
+ * refresh calls on incomplete caches (battery cam still sleeping,
395
+ * legacy firmware that doesn't support some endpoints). */
396
+ private lastSettingsSnapshotRetryAt;
397
+ private static readonly SETTINGS_SNAPSHOT_RETRY_MIN_MS;
398
+ /** True when any settings-snapshot field that drives a UI section
399
+ * is missing from the persisted cache. Drives the on-demand retry
400
+ * in `getSettingsUISchema`. */
401
+ private hasIncompleteSettingsCache;
402
+ /**
403
+ * Probe `getVideoInput` + `getMotionAlarm` and persist into the
404
+ * `deviceCache` snapshots. Fires once on `onCreated`; future
405
+ * settings opens read straight from the persisted snapshot. Image
406
+ * is readonly in the UI (lib lacks `setVideoInput`); motion is
407
+ * writable via `setMotionAlarm` so its snapshot also drives the
408
+ * dispatch's known-good baseline.
409
+ */
410
+ private refreshParentSettingsSnapshot;
411
+ /**
412
+ * Declare on-camera accessory child devices the kernel should
413
+ * auto-spawn after `onCreated`. Each entry maps directly to a
414
+ * concrete accessory class via the existing `createAccessoryDevice`
415
+ * factory; the closure captures `this` for the child's
416
+ * `(ctx, parent)` constructor — accessories share the parent's
417
+ * Baichuan socket via `parent.ensureApi()`.
418
+ *
419
+ * Restoring an existing accessory is automatic: the kernel detects
420
+ * the persisted row by the deterministic stableId and skips the
421
+ * pre-persist step. Adding a new accessory between boots (operator
422
+ * enabled `hasFloodlight` via re-detection) appears on the next
423
+ * parent register.
424
+ */
425
+ getAccessoryChildren(): readonly _camstack_types.AccessoryChildSpec[];
426
+ /**
427
+ * Register per-device native capability providers (Slice 5+).
428
+ * Currently exposes `snapshot` via the Baichuan getSnapshot command;
429
+ * PTZ + intercom land in Slices 7 + 9 once we know the camera's
430
+ * abilities (see `config.features`).
431
+ */
432
+ private registerNativeCapabilities;
433
+ /**
434
+ * Idempotent guard for the PTZ provider — registered once per device
435
+ * instance lifetime. `removeDevice` tears down the instance, so a
436
+ * fresh device wires PTZ from scratch. `disconnectAll` does NOT
437
+ * change `hasPtz`, so we don't have to unregister/reregister around
438
+ * the api lifecycle.
439
+ */
440
+ private ptzRegistered;
441
+ /** Idempotent guard for the `ptz-autotrack` cap. Registered once
442
+ * per device instance once the abilities probe flagged
443
+ * `hasAutotrack`. */
444
+ private ptzAutotrackRegistered;
445
+ /** Single-flight guard for the autotrack camera refresh — back-to-back
446
+ * `getStatus`/`getSettings` calls within the same tick fold into one
447
+ * Baichuan round-trip. */
448
+ private autotrackRefreshInFlight;
449
+ /**
450
+ * Single-flight guard for snapshot fetches (Slice 10). When two
451
+ * consumers hit `getSnapshot` concurrently we issue ONE Baichuan
452
+ * request and let both await its result — saves a doubled wake on
453
+ * battery cams and halves load for regular ones. Pattern mirrors
454
+ * `takePictureInFlight` in scrypted-reolink-native.
455
+ */
456
+ private snapshotInFlight;
457
+ /**
458
+ * Map a nodelink `BatteryInfo` push payload into the camstack
459
+ * `BatteryStatus` shape (Slice 13). `sleeping` honours the runtime
460
+ * `this.sleeping` flag — the BatteryInfo's own `sleeping` field is
461
+ * present on some firmwares but absent on others, so the runtime
462
+ * tracker is more reliable.
463
+ */
464
+ private mapBatteryInfo;
465
+ /**
466
+ * Register the battery cap provider on battery-flagged devices
467
+ * (Slice 13). The wrapper `snapshot.addon.ts` calls
468
+ * `batteryCapability.getStatus({deviceId})` to decide whether to
469
+ * serve cached frames instead of waking the camera; without this
470
+ * provider the wrapper assumes "awake" and would wake the cam.
471
+ */
472
+ private registerBatteryIfSupported;
473
+ private refreshBatteryFromApi;
474
+ /**
475
+ * Decide whether to honour a sleep-state transition. Returns
476
+ * `false` when the lib's UDP inference is still inside the
477
+ * hysteresis window (8s by default) so we don't flap. The first
478
+ * transition AFTER an api login / restart is always honoured —
479
+ * no prior-flip timestamp gating it.
480
+ *
481
+ * @param next - the desired next value of `this.sleeping`
482
+ * @returns `true` if the caller should commit the new state
483
+ */
484
+ private acceptSleepingTransition;
485
+ private updateBatteryCache;
486
+ /**
487
+ * Battery cams require an explicit wake before cmd_id 109 will
488
+ * answer reliably. The lib documents this pattern in
489
+ * `predownloadRecordingMp4` (`ensureAwake` → `wakeUp` →
490
+ * operation → retry with longer wait on failure). `getSnapshot`
491
+ * itself only re-`login()`s, which over BCUDP can complete before
492
+ * the camera firmware is fully up — the snapshot then times out
493
+ * because the video subsystem isn't ready yet.
494
+ *
495
+ * Probe live testing (scripts/probe-reolink-snapshot.mts) confirmed:
496
+ * awake state: getSnapshot in ~1s
497
+ * wakeUp + getSnapshot: ~2.7s total (wakeUp 1.7s + snapshot 1s)
498
+ * The added latency is the cost we pay to actually return a frame
499
+ * instead of timing out.
500
+ */
501
+ /**
502
+ * Refresh the diagnostic Sessions snapshot from the live Baichuan
503
+ * api: who's currently logged into the camera, how many sockets we
504
+ * hold open, and whether the camera is currently rate-limiting our
505
+ * reconnect attempts. Single-flight (concurrent calls reuse the same
506
+ * round-trip) and tolerant of partial failures — each lib call's
507
+ * error is recorded but does not poison the rest of the snapshot.
508
+ *
509
+ * Side-effect: writes `this.sessionsSnapshot`. Best-effort: when
510
+ * `ensureApi()` fails (camera offline, sleeping battery cam) the
511
+ * snapshot still updates with a populated `error` field plus a
512
+ * fallback `socketPool` block so the operator sees the failure
513
+ * surface instead of stale data.
514
+ */
515
+ private refreshSessionsSnapshot;
516
+ /**
517
+ * Build the contents of the "Sessions" tab from `this.sessionsSnapshot`.
518
+ * Always returns at least the Refresh button so the operator can force
519
+ * a fetch when the snapshot is empty (cold start) or stale beyond the
520
+ * displayed timestamp. Section + tab layout details mirror the rest
521
+ * of the device-settings UI (one card per logical block).
522
+ *
523
+ * NO auto-fetch. `getOnlineUserList` (cmd 120) wakes a sleeping
524
+ * battery cam, and the Settings panel polls every ~2.5s — auto-
525
+ * triggering on stale would keep the doorbell awake indefinitely
526
+ * just because the operator left the tab open. The operator's
527
+ * explicit "Refresh" click is the sole trigger; it lands as
528
+ * `_refreshSessions` action sentinel through `applySettingsPatch`.
529
+ */
530
+ private buildSessionsTabSections;
531
+ private fetchSnapshotWithSingleFlight;
532
+ /**
533
+ * Idempotency for the doorbell provider. The cap is registered once
534
+ * per device-instance lifetime — `removeDevice` rebuilds the device
535
+ * from scratch.
536
+ */
537
+ private doorbellRegistered;
538
+ /** Push counter mirrored into the doorbell cap status. */
539
+ private doorbellPressCountSinceStart;
540
+ private doorbellLastPressedAt;
541
+ private registerDoorbellIfSupported;
542
+ /** One-time registration guard for `intercom` cap. */
543
+ private intercomRegistered;
544
+ /**
545
+ * Active intercom orchestrator. The `intercom` cap is
546
+ * `singleton`-mode device-scoped — at most one talk session is
547
+ * open at a time per camera (mirrors how Reolink's own UI
548
+ * behaves). The orchestrator owns the WebRTC peer + audio-codec
549
+ * decode session + Reolink talk channel for that one active
550
+ * session; `null` when idle.
551
+ */
552
+ private intercomOrchestrator;
553
+ /**
554
+ * Register the `intercom` cap when the camera advertises two-way
555
+ * audio support. Server-WebRTC-shaped: `startSession` returns an
556
+ * SDP offer, `handleAnswer` applies the browser's response, the
557
+ * orchestrator pumps Opus RTP through the `audio-codec` cap (Opus
558
+ * → PCM s16le @ camera rate, with resampling) and feeds the PCM
559
+ * into `ReolinkIntercomSession` for IMA ADPCM encoding + transport
560
+ * onto the camera's dedicated talk channel.
561
+ *
562
+ * The `audio-codec` cap is REQUIRED — without it (no audio-codec
563
+ * addon installed) intercom can't function. We register the cap
564
+ * regardless and surface the missing dep as a clear error from
565
+ * `startSession`; this preserves the "cap visible in bindings/docs"
566
+ * UX without falsely claiming readiness when audio decode isn't
567
+ * available.
568
+ */
569
+ private registerIntercomIfSupported;
570
+ /**
571
+ * Resolve the `audio-codec` cap router off `ctx.api`. Throws a
572
+ * clear error when the cap isn't mounted (no audio-codec addon
573
+ * installed) so the operator gets actionable feedback at
574
+ * `startSession` time rather than a generic dispatch failure.
575
+ *
576
+ * The shape we surface is the narrow `IntercomAudioCodecApi` used
577
+ * by the orchestrator — we don't expose the full router because
578
+ * intercom only needs decode + push + pull + close.
579
+ */
580
+ private resolveAudioCodecApi;
581
+ /**
582
+ * Battery-cam pre-wake hook for the intercom orchestrator. Mirrors
583
+ * scrypted-reolink-native's `intercom.ts:90-106`: ask the lib for
584
+ * the current sleep status (passive — no traffic), and if the cam
585
+ * is asleep issue an explicit `wakeUp(channel, {waitAfterWakeMs:2000})`
586
+ * + a 1s settle delay before letting the talk-session handshake
587
+ * fire. Without this, the handshake against a sleeping UDP/battery
588
+ * cam silently times out and the operator gets a generic "talk
589
+ * session open failed" error.
590
+ */
591
+ private wakeForIntercom;
592
+ /**
593
+ * Translate a Reolink Baichuan `doorbell` simple-event into a
594
+ * camstack-side doorbell press: bump the counters, emit the cap
595
+ * event, and bridge to the typed `EventCategory.DoorbellOnPressed`
596
+ * so non-cap consumers (UI toast, notifier) can subscribe without
597
+ * holding a cap reference.
598
+ */
599
+ private emitDoorbellPressed;
600
+ /**
601
+ * Register the `ptz-autotrack` cap when the abilities probe flagged
602
+ * `hasAutotrack`. The cap surface (`getStatus` / `setEnabled` /
603
+ * `getSettings` / `setSettings`) maps directly onto the lib's
604
+ * `getAutotracking` / `setAutotracking` / `setAutotrackingSettings`
605
+ * Baichuan commands.
606
+ *
607
+ * Source of truth is `runtimeState['ptz-autotrack']` — the kernel
608
+ * persists this slice across restarts and validates it against the
609
+ * cap's runtimeState schema. The four cap methods become trampolines:
610
+ * read from the slice, refresh from the camera when stale, write
611
+ * back to the slice. No private cache fields, no config-blob dance.
612
+ */
613
+ private registerPtzAutotrackIfSupported;
614
+ private registerPtzIfSupported;
615
+ /**
616
+ * Translate normalized (-1..1) pan/tilt/zoom to one or more discrete
617
+ * Baichuan PTZ commands. Camstack ptz uses ONVIF-style continuous
618
+ * motion vectors; Reolink Baichuan needs discrete directional
619
+ * commands (Left/Right/Up/Down/ZoomIn/ZoomOut). Magnitudes map to
620
+ * speed (0–63 typical). Continuous=true issues 'start' (camera moves
621
+ * until next 'stop'); continuous=false issues 'start' followed by an
622
+ * autoStop after a short window so a single tap of an arrow key
623
+ * results in a tiny nudge.
624
+ */
625
+ private runPtz;
626
+ getStreamSources(): Promise<readonly StreamSourceEntry[]>;
627
+ private channelCount;
628
+ /**
629
+ * Publish Reolink streams to the stream-broker. Reolink cameras
630
+ * typically expose the same logical channel over THREE containers —
631
+ * native Baichuan push, RTSP, RTMP. We publish all of them so the
632
+ * operator can pick any in the assignment UI; native streams are
633
+ * `autoEligible: true` (the recommended path) and RTSP / RTMP mirrors
634
+ * are `autoEligible: false` (selectable, but never the default).
635
+ *
636
+ * Stream IDs are kind-prefixed (`native:main`, `rtsp:main`, …) so the
637
+ * broker treats them as distinct rows even though they share the
638
+ * same camera profile.
639
+ *
640
+ * The lib's `buildVideoStreamOptions()` is the single source of truth
641
+ * for the camera's offer — it returns nativeStreams + rtspStreams +
642
+ * rtmpStreams with full metadata (width/height/frameRate/bitRate/
643
+ * videoEncType) in a single call. We resolve it lazily on every
644
+ * publish instead of caching it in `deviceCache`: the cache layer
645
+ * was a constant source of bugs (transient probe failures wiped
646
+ * the persisted entry, leaving the device stuck with `desired.size === 0`
647
+ * until manual recovery). Calling the lib directly is cheap (it
648
+ * caches internally) and self-healing across firmware upgrades.
649
+ *
650
+ * Re-publish is idempotent — the broker keys entries by
651
+ * `(deviceId, camStreamId)`. Streams from a previous publish that no
652
+ * longer appear in the latest snapshot are explicitly retracted so
653
+ * the broker registry stays in sync.
654
+ *
655
+ * Native streams need a per-(channel, profile) RFC 4571 TCP server on
656
+ * loopback — handed off to `ensureRfc4571Server`, which uses the lib's
657
+ * shared-server pool (`sharedRfc4571Servers` keyed by `(deviceId,
658
+ * channel, profile, variant, …)`). Without `deviceId` the lib falls
659
+ * back to the unshared path: every call opens a fresh
660
+ * BaichuanVideoStream on the same control socket, and 3 concurrent
661
+ * stream probes overload the camera's session table → ECONNRESET.
662
+ * With it, main + sub + ext coexist freely.
663
+ */
664
+ publishToBroker(): Promise<void>;
665
+ private publishOne;
666
+ /**
667
+ * Materialize the upstream rfc4571 streaming session for one
668
+ * camStreamId on demand. Called from the
669
+ * `StreamBrokerOnRequestStreamSourceRefresh` listener when the broker
670
+ * is about to dial a `lazy:rfc4571:` placeholder URL (or when an
671
+ * established session went stale and the lib's loopback teardown ran).
672
+ *
673
+ * The flow:
674
+ * 1. Parse camStreamId → (channel, profile)
675
+ * 2. Open the rfc4571 server via `ensureRfc4571Server`. THIS is the
676
+ * single point where a new TCP session to the NVR is opened —
677
+ * `publishToBroker` no longer triggers it.
678
+ * 3. Re-publish the broker entry with the real `tcp://...` URL +
679
+ * the lib's accurate SDP (the lib has SPS/PPS now). The broker
680
+ * picks up the fresh URL on its next `sourceProvider()` call.
681
+ *
682
+ * Idempotent: subsequent calls reuse the cached server when still
683
+ * listening; if the lib idle-tore-down between calls we recreate.
684
+ */
685
+ materializeStreamSocket(camStreamId: string): Promise<void>;
686
+ removeDevice(): Promise<void>;
687
+ /**
688
+ * Spin up a loopback RFC 4571 TCP server for one (channel, profile)
689
+ * tuple if we don't already have one. Returns the running server with
690
+ * its `host`/`port`/`sdp`/`username`/`password`. Idempotent — repeat
691
+ * calls return the existing instance.
692
+ *
693
+ * The lib's server manages the underlying Baichuan socket lifecycle
694
+ * (open on first client connect, close on idle teardown), which keeps
695
+ * battery cams asleep until a real consumer pulls the stream.
696
+ */
697
+ ensureRfc4571Server(camStreamId: string, channel: number, profile: 'main' | 'sub' | 'ext'): Promise<Rfc4571TcpServer | null>;
698
+ /** Map a camStreamId back to its broker profile (for `ActiveStream.profile`). */
699
+ private profileForCamStream;
700
+ /**
701
+ * Adapt the camstack scoped logger to the `Console` shape the lib expects.
702
+ * Drops the structured `meta` object — the lib only formats the message,
703
+ * not our hierarchical log fields.
704
+ */
705
+ private libLoggerAdapter;
706
+ /**
707
+ * Close every running RFC 4571 server (Slice 9). Used when the camera
708
+ * reports sleep / device removal. Keeps the api alive so we still
709
+ * receive `awake` events; the servers will be re-spawned the next
710
+ * time `publishToBroker` runs (after restore / reconnect).
711
+ */
712
+ private closeActiveStreams;
713
+ /**
714
+ * Periodic passive sleep-status poll (Slice 9, battery cams only).
715
+ * Calls `api.getSleepStatus()` which inspects recent socket I/O —
716
+ * no network traffic emitted, no risk of waking the camera. Used to
717
+ * reconcile our `this.sleeping` flag when push events miss the
718
+ * `sleeping`/`awake` transition (firmware quirks, NAT timeouts).
719
+ */
720
+ private startSleepPoll;
721
+ private stopSleepPoll;
722
+ /** Implements `ReolinkAccessoryHost.registerAccessoryChild`. Called
723
+ * from each accessory's constructor. Idempotent — re-registering the
724
+ * same kind overwrites the prior ref (covers a replay-on-restart
725
+ * scenario where the parent ctor lands before the child ctor of an
726
+ * already-spawned accessory). */
727
+ registerAccessoryChild(ref: ReolinkAccessoryRef): void;
728
+ /** Pull-once refresh of every registered aux accessory's slice.
729
+ * Best-effort: per-accessory failures are logged + swallowed by the
730
+ * individual `refreshFromAlign` impls; this loop never throws. */
731
+ private alignAuxDevicesState;
732
+ /** Start periodic align for wired cams. No-op for battery cams (wake
733
+ * path handles it). Idempotent. */
734
+ private startAlignAuxPolling;
735
+ private stopAlignAuxPolling;
736
+ /**
737
+ * Shared wake-transition handler invoked by both the simpleEvent
738
+ * `awake` push (canonical fast path) and the sleep poll's
739
+ * `sleeping → awake` flip (backstop). Mirrors Scrypted's
740
+ * `updateSleepingState`'s wake branch (camera.ts:3543-3560) — fan
741
+ * out the refreshes that depend on the camera being responsive:
742
+ * - aux accessories (siren/floodlight/PIR) re-read their
743
+ * firmware state via `alignAuxDevicesState('wake')`
744
+ * - parent settings snapshot (image / motion / AI / enc / mask /
745
+ * audioNoise / autoFocus) re-reads via
746
+ * `refreshParentSettingsSnapshot()` so the Settings UI shows
747
+ * camera-current values instead of the snapshot taken before
748
+ * the cam went to sleep.
749
+ * Both calls are best-effort; per-call failures land in their own
750
+ * debug logs and never break the wake-transition flow.
751
+ */
752
+ private onWakeTransition;
753
+ private static readonly PING_INTERVAL_MS;
754
+ private static readonly PING_MAX_FAILURES;
755
+ private static readonly EVENT_CHECK_INTERVAL_MS;
756
+ private static readonly EVENT_STALE_THRESHOLD_MS;
757
+ /**
758
+ * Arm the connection-liveness ping watchdog and the event-watchdog.
759
+ * Battery cams skip the ping watchdog — they keep `setIdleDisconnect=true`
760
+ * so the api intentionally goes silent and a ping would just wake the
761
+ * camera and burn battery for no signal.
762
+ *
763
+ * Idempotent: re-arming clears prior timers first so we don't end up
764
+ * with parallel watchdogs after a reconnect cycle.
765
+ */
766
+ private startWatchdogs;
767
+ private stopWatchdogs;
768
+ private runPingWatchdog;
769
+ private runEventWatchdog;
770
+ /**
771
+ * Subscribe (or re-subscribe) `handleSimpleEventBound` against
772
+ * the Baichuan API. Single-source-of-truth for the listener
773
+ * lifecycle:
774
+ *
775
+ * 1. Always `offSimpleEvent(handleSimpleEventBound)` first —
776
+ * Set-dedup is a no-op on the first call (handler isn't
777
+ * registered yet) but on re-subscribes it cleanly removes
778
+ * the prior registration so the lib's listener Set never
779
+ * grows past one entry per device.
780
+ * 2. Verify the connection is ready (socket + login). The
781
+ * lib's own `ensureSimpleEventSubscribed` checks this too,
782
+ * but failing fast at the camstack layer keeps the
783
+ * re-subscribe attempts in lock-step with the connection
784
+ * state we observe — important for the `event-health`
785
+ * interval that fires every 60s.
786
+ * 3. Register via `onSimpleEvent(handleSimpleEventBound)`.
787
+ * Lib starts/refreshes its own subscribe + 5min watchdog.
788
+ *
789
+ * Mirrors `subscribeToEventsInternal` in Scrypted's
790
+ * `baichuan-base.ts:837-888` — same pattern, same rationale.
791
+ */
792
+ private resubscribeSimpleEvents;
793
+ /**
794
+ * Plugin-level event-health watchdog. Fires every 60s while the
795
+ * api is alive. Two recovery paths:
796
+ *
797
+ * - If we've never received an event AND the watchdog baseline
798
+ * is older than EVENT_STALE_THRESHOLD_MS, force a full
799
+ * re-subscribe. Catches "subscribe call succeeded but the
800
+ * camera silently rejected the registration" — the lib's
801
+ * own watchdog uses the same trigger but a re-arm at the
802
+ * plugin level recovers from cases where the lib's internal
803
+ * state has drifted.
804
+ * - If the connection went down between our `runEventWatchdog`
805
+ * ticks (every 30s by default), the next tick of THIS check
806
+ * re-subscribes the moment the socket comes back up.
807
+ */
808
+ private startEventHealthCheck;
809
+ private stopEventHealthCheck;
810
+ /** Stop every active stream and close the Baichuan API. */
811
+ disconnectAll(): Promise<void>;
812
+ getSettingsUISchema(): ConfigUISchemaWithValues;
813
+ applySettingsPatch(patch: Record<string, unknown>): Promise<void>;
814
+ /**
815
+ * Optionally inject an already-logged-in Baichuan api — used by
816
+ * `onCreateDevice` to reuse the api instance returned by
817
+ * `autoDetectDeviceType` (Slice 15 socket reuse). Skips the next
818
+ * connect cycle entirely.
819
+ */
820
+ adoptApi(api: ReolinkBaichuanApi): void;
821
+ /**
822
+ * Expose the lazy-login Baichuan API to accessory children. The
823
+ * children share the parent's socket and credentials — there's no
824
+ * separate auth flow per accessory. `ReolinkAccessory` calls this
825
+ * inside its `applyOn` / `fetchOn` to reach the camera firmware.
826
+ */
827
+ /** Implements `ReolinkAccessoryHost.isBatteryCam` so spawned
828
+ * accessory children skip background polling on a sleeping
829
+ * battery cam (Baichuan polls on a sleeping camera return 400 +
830
+ * drain the battery). Reads from the runtime feature-probe slice
831
+ * populated during `onProbe()`; falls back to false when the probe
832
+ * hasn't completed yet. */
833
+ isBatteryCam(): boolean;
834
+ /** Implements `ReolinkAccessoryHost.isSleeping`. Reads the canonical
835
+ * source of truth — the `battery.sleeping` runtime-state slice — so
836
+ * hub-children (whose simpleEvents are routed in by the Hub) and
837
+ * top-level battery cams (whose own subscription / sleep poll write
838
+ * the slice) share one signal. Always returns `false` for non-
839
+ * battery cams (`this.sleeping` getter falls back to `false` when
840
+ * the slice is absent). */
841
+ isSleeping(): boolean;
842
+ ensureApi(): Promise<ReolinkBaichuanApi>;
843
+ /**
844
+ * Bridge Reolink hardware events to the camstack event bus. Mirrors
845
+ * the dispatch shape of scrypted-reolink-native's `onSimpleEvent`
846
+ * (`camera.ts:1979-2033`): online and sleeping are TWO ORTHOGONAL
847
+ * axes, each driven exclusively by their own firmware push events.
848
+ *
849
+ * - 'motion' + AI types (people, vehicle, animal, face, package):
850
+ * emit `EventCategory.MotionOnMotionChanged` (`source: 'onboard'`)
851
+ * so the runner's phase machine reacts via `reportMotion`. The
852
+ * runner is the sole writer of the `motion` runtime-state slice
853
+ * (no direct slice writes here — keeps onboard symmetric with the
854
+ * analyzer path). AI subtype also lands on
855
+ * `EventCategory.DetectionCameraNative`.
856
+ * - 'awake' / 'sleeping': emit DeviceAwake / DeviceSleeping. Battery
857
+ * power-state only — does NOT touch online state.
858
+ * - 'online' / 'offline': emit DeviceOnline / DeviceOffline + flip
859
+ * `this.online`. The ONLY emit site for these — socket close,
860
+ * reconnect, RFC4571 idle teardown all leave online state alone.
861
+ * - 'battery': refresh cache, emit cap event.
862
+ * - 'doorbell', 'daynight', 'other': informational debug log only.
863
+ */
864
+ /** Reusable event-source identity for every emit on this device. */
865
+ private eventSource;
866
+ /**
867
+ * Build the `DebugOptions` blob forwarded to `ReolinkBaichuanApi`'s
868
+ * constructor. Mirrors Scrypted's `getBaichuanDebugOptions` at
869
+ * `camera.ts:1778-1786`. Returns `undefined` when no flags are set
870
+ * so the lib falls back to its own defaults instead of an empty
871
+ * object override.
872
+ */
873
+ private getBaichuanDebugOptions;
874
+ /**
875
+ * Public entry point for simpleEvent dispatch. Used both by the
876
+ * camera's own subscription (`handleSimpleEventBound`) when the
877
+ * camera holds its own Baichuan socket AND by `ReolinkHub.routeSimpleEvent`
878
+ * when the camera lives under a Hub and inherits the parent's
879
+ * single subscription.
880
+ */
881
+ handleSimpleEvent(event: _apocaliss92_nodelink_js.ReolinkSimpleEvent): void;
882
+ /**
883
+ * Probe device abilities + channel count and persist into the camera
884
+ * config. Best-effort — unsupported camera firmwares may return
885
+ * partial data; we record what we can and skip the rest.
886
+ *
887
+ * Writes the result into the `feature-probe` runtime-state slice
888
+ * (the canonical store for hasPtz/hasIntercom/etc) and persists
889
+ * channelCount + deviceType in the config blob (so the camera
890
+ * lifecycle has them before next-boot rehydrate completes).
891
+ *
892
+ * Called by the kernel via `onProbe()` once after register, and by
893
+ * `reprobe()` on demand.
894
+ */
895
+ private probeAndPersistFeatures;
896
+ /**
897
+ * Pull `getInfo` and patch the device-meta `metadata` blob with the
898
+ * resolved manufacturer / model / firmware / hardware / serial / uid.
899
+ * Idempotent — `setMetadata` shallow-merges and emits a change event
900
+ * only when at least one field flipped, so re-running the probe on
901
+ * every reconnect doesn't churn the bus.
902
+ */
903
+ private populateMetadataFromFirmware;
904
+ /**
905
+ * Tear down the current API and schedule a reconnect with exponential
906
+ * backoff. Active streams are cleared — when the broker re-issues
907
+ * demand events on next consumer activity, ensureApi() reconnects
908
+ * lazily and the demand path re-arms BaichuanVideoStream instances.
909
+ *
910
+ * **Battery-camera path**: when the camera is sleeping (or already
911
+ * believed to be sleeping), a `close`/`error` from the Baichuan
912
+ * client is the EXPECTED steady state — the lib disconnects the
913
+ * idle UDP socket on its own, the camera is asleep, and there is
914
+ * nothing to reconnect to. Aggressively reconnecting in that state
915
+ * is what was waking up the doorbell every ~60 s in production
916
+ * logs. Mirror Scrypted's reolink-native handler: drop the api
917
+ * reference, but DO NOT schedule a reconnect timer. The next
918
+ * `awake` push (or a snapshot demand from the operator) will
919
+ * lazily call `ensureApi()` and re-establish the client.
920
+ */
921
+ private scheduleReconnect;
922
+ }
923
+
924
+ export { ReolinkCamera, reolinkCameraSchema };