@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.
- package/assets/icon.svg +1 -0
- package/dist/addon.d.mts +72 -0
- package/dist/addon.d.ts +72 -0
- package/dist/addon.js +7115 -0
- package/dist/addon.js.map +1 -0
- package/dist/addon.mjs +7 -0
- package/dist/addon.mjs.map +1 -0
- package/dist/chunk-QQBDPNVO.mjs +7146 -0
- package/dist/chunk-QQBDPNVO.mjs.map +1 -0
- package/dist/index.d.mts +924 -0
- package/dist/index.d.ts +924 -0
- package/dist/index.js +7121 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +11 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +84 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
export { ReolinkProviderAddon } from './addon.mjs';
|
|
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 };
|