@camstack/addon-provider-reolink 1.2.142 → 1.2.144
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/dist/addon.js +361 -73
- package/dist/addon.mjs +361 -73
- package/package.json +2 -2
package/dist/addon.mjs
CHANGED
|
@@ -5381,7 +5381,7 @@ var ZodIssueCode = {
|
|
|
5381
5381
|
var ZodFirstPartyTypeKind;
|
|
5382
5382
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5383
5383
|
//#endregion
|
|
5384
|
-
//#region ../types/dist/sleep-
|
|
5384
|
+
//#region ../types/dist/sleep-CiR4z7r9.mjs
|
|
5385
5385
|
/**
|
|
5386
5386
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5387
5387
|
* window to float samples (D455).
|
|
@@ -6523,6 +6523,17 @@ var CamProfileSchema = _enum([
|
|
|
6523
6523
|
"mid",
|
|
6524
6524
|
"low"
|
|
6525
6525
|
]);
|
|
6526
|
+
/**
|
|
6527
|
+
* Which rung a per-camera consumer listens on: an explicit profile, or `auto`.
|
|
6528
|
+
*
|
|
6529
|
+
* `auto` is a REAL value, not an absent one — it means "resolve it by the rule
|
|
6530
|
+
* that consumer owns". It lives beside `CamProfile` because a rung is the only
|
|
6531
|
+
* operator-facing way to name a stream: a cam-stream id is broker-internal and
|
|
6532
|
+
* moves when a slot is reassigned, so a stored id silently stops meaning what
|
|
6533
|
+
* the operator picked. Today's one user is the audio analyser, whose rule is
|
|
6534
|
+
* "the lowest AVAILABLE native rung" (D537).
|
|
6535
|
+
*/
|
|
6536
|
+
var AudioStreamPickSchema = union([CamProfileSchema, literal("auto")]);
|
|
6526
6537
|
var CamStreamKindSchema = _enum([
|
|
6527
6538
|
"pull-rtsp",
|
|
6528
6539
|
"pull-rtmp",
|
|
@@ -7789,6 +7800,7 @@ var CameraSwitchIdSchema = _enum([
|
|
|
7789
7800
|
"device-audio",
|
|
7790
7801
|
"broker-audio",
|
|
7791
7802
|
"audio-analysis",
|
|
7803
|
+
"event-saving",
|
|
7792
7804
|
"recording",
|
|
7793
7805
|
"notifications"
|
|
7794
7806
|
]);
|
|
@@ -8263,11 +8275,31 @@ var RecordingBandTriggersSchema = RecordingTriggersSchema;
|
|
|
8263
8275
|
* band engine's `applies` rule). `start`/`end` are `HH:MM`; an `end <= start`
|
|
8264
8276
|
* span wraps past midnight (handled by the band engine).
|
|
8265
8277
|
*/
|
|
8278
|
+
/**
|
|
8279
|
+
* Whether a band's footage carries sound.
|
|
8280
|
+
*
|
|
8281
|
+
* The recorder has always attached with `audio: 'aac'`, so every band of every
|
|
8282
|
+
* camera recorded sound whether anyone wanted it or not. The broker already
|
|
8283
|
+
* serves the other answer — `audio: 'none'` takes the video-only `/muted`
|
|
8284
|
+
* restream — so this was a choice the product could always have offered.
|
|
8285
|
+
*
|
|
8286
|
+
* It belongs on the BAND rather than the camera because one camera can want
|
|
8287
|
+
* both: silent across the shop floor during opening hours, sound overnight.
|
|
8288
|
+
* And it applies to BOTH modes — an events band firing on motion has the same
|
|
8289
|
+
* question to answer as a continuous one.
|
|
8290
|
+
*/
|
|
8291
|
+
var RecordingBandAudioSchema = _enum(["on", "muted"]);
|
|
8266
8292
|
var RecordingBandSchema = object({
|
|
8267
8293
|
days: array(RecordingWeekdaySchema),
|
|
8268
8294
|
start: string().regex(HHMM),
|
|
8269
8295
|
end: string().regex(HHMM),
|
|
8270
8296
|
mode: RecordingBandModeSchema,
|
|
8297
|
+
/**
|
|
8298
|
+
* Sound, or silence. OPTIONAL and absent-means-`on`: every recording
|
|
8299
|
+
* written before this existed carried sound, and a band whose key is
|
|
8300
|
+
* missing must keep meaning what it meant. See {@link RecordingBandAudioSchema}.
|
|
8301
|
+
*/
|
|
8302
|
+
audio: RecordingBandAudioSchema.optional(),
|
|
8271
8303
|
triggers: RecordingBandTriggersSchema.optional(),
|
|
8272
8304
|
/**
|
|
8273
8305
|
* RETIRED (D381). A band no longer carries a pre-buffer of its own.
|
|
@@ -20889,6 +20921,7 @@ var MediaFileKindEnum = _enum([
|
|
|
20889
20921
|
"keyFrameSmall",
|
|
20890
20922
|
"thumbnailSmall",
|
|
20891
20923
|
"confirmationCrop",
|
|
20924
|
+
"debugCrop",
|
|
20892
20925
|
"sceneFrame"
|
|
20893
20926
|
]);
|
|
20894
20927
|
/**
|
|
@@ -22553,6 +22586,18 @@ var RunnerCameraConfigSchema = object({
|
|
|
22553
22586
|
motionStreamId: string(),
|
|
22554
22587
|
detectionStreamId: string(),
|
|
22555
22588
|
/**
|
|
22589
|
+
* Which rung this camera's audio analysis listens on — `auto` (the lowest
|
|
22590
|
+
* AVAILABLE native rung) or an explicit one.
|
|
22591
|
+
*
|
|
22592
|
+
* Not consumed by the runner: carried here so it travels with the camera's
|
|
22593
|
+
* config the way `audioMotionWindowMs` does, and so the orchestrator's
|
|
22594
|
+
* watchdog resolves the same stream the audio subscription did. OPTIONAL
|
|
22595
|
+
* with no default — absent means `auto`, which is what every camera written
|
|
22596
|
+
* before this field had, and an absent optional adds nothing to the
|
|
22597
|
+
* generated cap-input defaults (D537).
|
|
22598
|
+
*/
|
|
22599
|
+
audioStreamPick: AudioStreamPickSchema.optional(),
|
|
22600
|
+
/**
|
|
22556
22601
|
* Motion sources that can activate the pipeline. At least one must
|
|
22557
22602
|
* be present. Multiple sources OR-ed: any one firing arms the
|
|
22558
22603
|
* cooldown timer and transitions watching → active. The same
|
|
@@ -25148,6 +25193,104 @@ var WebrtcStreamChoiceSchema = object({
|
|
|
25148
25193
|
/** Bitrate in kbps (null for adaptive). */
|
|
25149
25194
|
bitrateKbps: number().nullable()
|
|
25150
25195
|
});
|
|
25196
|
+
/**
|
|
25197
|
+
* The last move the adaptive ladder made on this session, and the numbers it
|
|
25198
|
+
* rested on. Mirrors `LadderDecision` in the stream-broker's
|
|
25199
|
+
* `adaptive-controller.ts` — the log line's evidence, made readable.
|
|
25200
|
+
*
|
|
25201
|
+
* Nullability is load-bearing: `lossRatio: null` means the loss tracker had
|
|
25202
|
+
* no window to report, which is NOT a link with zero loss.
|
|
25203
|
+
*/
|
|
25204
|
+
var WebrtcLadderDecisionSchema = object({
|
|
25205
|
+
/** `loss` — RTCP / client-reported packet loss; `stall` — the picture
|
|
25206
|
+
* stopped moving; `healthy` — a promotion. */
|
|
25207
|
+
cause: _enum([
|
|
25208
|
+
"loss",
|
|
25209
|
+
"stall",
|
|
25210
|
+
"healthy"
|
|
25211
|
+
]),
|
|
25212
|
+
/** The rung the ladder moved TO. Null for a move that changed no rung
|
|
25213
|
+
* (an MTU reduction, a forced transcode-down at the bottom). */
|
|
25214
|
+
tier: CamProfileSchema.nullable(),
|
|
25215
|
+
/** Whether that move forced the egress transcode-down rung. */
|
|
25216
|
+
forcedTranscodeDown: boolean(),
|
|
25217
|
+
/** Loss ratio over the tracker's window at the moment of the decision. */
|
|
25218
|
+
lossRatio: number().nullable(),
|
|
25219
|
+
/** The client's reported downlink at that moment, in kbps. */
|
|
25220
|
+
clientKbps: number().nullable(),
|
|
25221
|
+
/** How long the stall detector had been seeing nothing arrive. */
|
|
25222
|
+
stalledForMs: number().nullable(),
|
|
25223
|
+
/** Whether the broker WAS sending while that was measured — the half that
|
|
25224
|
+
* separates "the camera stopped" from "the link stopped". */
|
|
25225
|
+
serverSending: boolean().nullable(),
|
|
25226
|
+
/** Epoch ms. The overlay prints its age; a decision from 40 minutes ago
|
|
25227
|
+
* must not read like one from this second. */
|
|
25228
|
+
at: number()
|
|
25229
|
+
});
|
|
25230
|
+
/** Per-tier count of MEASURED downgrades this viewer session has suffered. */
|
|
25231
|
+
var WebrtcTierFailureSchema = object({
|
|
25232
|
+
tier: CamProfileSchema,
|
|
25233
|
+
failures: number().int().nonnegative()
|
|
25234
|
+
});
|
|
25235
|
+
/**
|
|
25236
|
+
* What the SERVER decided for one WebRTC session — the half the client cannot
|
|
25237
|
+
* measure. See `getSessionDebug`.
|
|
25238
|
+
*/
|
|
25239
|
+
var WebrtcSessionDebugSchema = object({
|
|
25240
|
+
sessionId: string(),
|
|
25241
|
+
/** Whether the session still exists here. Everything below is null when
|
|
25242
|
+
* it does not — a dead session has no measurements, not zero ones. */
|
|
25243
|
+
alive: boolean(),
|
|
25244
|
+
/** Epoch ms this snapshot was taken, so the overlay can show its age. */
|
|
25245
|
+
observedAt: number(),
|
|
25246
|
+
/** The broker actually feeding this session (`<deviceId>/<camStreamId>`). */
|
|
25247
|
+
brokerId: string().nullable(),
|
|
25248
|
+
camStreamId: string().nullable(),
|
|
25249
|
+
/** `derived` = this hub BUILT the rung being served (a transcode/scale of
|
|
25250
|
+
* another stream); `native` = the camera's own encode. */
|
|
25251
|
+
sourceKind: _enum([
|
|
25252
|
+
"native",
|
|
25253
|
+
"derived",
|
|
25254
|
+
"unknown"
|
|
25255
|
+
]),
|
|
25256
|
+
/** Whether the ladder is running at all (an `adaptive` target). */
|
|
25257
|
+
adaptive: boolean(),
|
|
25258
|
+
/** The rung `auto` ACTUALLY resolved to right now. Null when the broker
|
|
25259
|
+
* is bound to no profile slot (a manually-activated stream). */
|
|
25260
|
+
servedTier: CamProfileSchema.nullable(),
|
|
25261
|
+
/** Which node ingests the source. Null when unresolved. */
|
|
25262
|
+
ingestNodeId: string().nullable(),
|
|
25263
|
+
/** Whether that node is a remote one (the media crosses a node boundary
|
|
25264
|
+
* before it reaches this session). Null when unresolved. */
|
|
25265
|
+
ingestIsRemote: boolean().nullable(),
|
|
25266
|
+
/** Codec off the source, and the one negotiated with this client. */
|
|
25267
|
+
sourceCodec: string().nullable(),
|
|
25268
|
+
negotiatedCodec: string().nullable(),
|
|
25269
|
+
/** Whether the EGRESS leg re-encodes for this client or forwards the
|
|
25270
|
+
* source bytes untouched. */
|
|
25271
|
+
egressLeg: _enum([
|
|
25272
|
+
"copy",
|
|
25273
|
+
"transcode",
|
|
25274
|
+
"unknown"
|
|
25275
|
+
]),
|
|
25276
|
+
/** What the broker itself reports about the rung it is serving. */
|
|
25277
|
+
brokerStatus: string().nullable(),
|
|
25278
|
+
brokerCodec: string().nullable(),
|
|
25279
|
+
brokerBitrateKbps: number().nullable(),
|
|
25280
|
+
brokerInputFps: number().nullable(),
|
|
25281
|
+
brokerUptimeMs: number().nullable(),
|
|
25282
|
+
/** RTP packets this session has sent — the server's own "am I sending?". */
|
|
25283
|
+
rtpPacketsSent: number().nullable(),
|
|
25284
|
+
/** D42: the track may be carrying the recorded feeder, not live. The ladder
|
|
25285
|
+
* is suspended there, so a frozen picture in `recorded` mode means
|
|
25286
|
+
* something entirely different. */
|
|
25287
|
+
trackMode: _enum(["live", "recorded"]).nullable(),
|
|
25288
|
+
lastDecision: WebrtcLadderDecisionSchema.nullable(),
|
|
25289
|
+
tierFailures: array(WebrtcTierFailureSchema),
|
|
25290
|
+
/** Successful promotions in this viewer session (drives the progressive
|
|
25291
|
+
* healthy timer). Null when no ladder is running. */
|
|
25292
|
+
upgradeCount: number().nullable()
|
|
25293
|
+
});
|
|
25151
25294
|
DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), array(WebrtcStreamChoiceSchema)), method(object({
|
|
25152
25295
|
deviceId: number().int().nonnegative(),
|
|
25153
25296
|
target: WebrtcStreamTargetSchema,
|
|
@@ -25312,7 +25455,10 @@ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), ar
|
|
|
25312
25455
|
* the same answer as "it ended": either way nothing is holding it up.
|
|
25313
25456
|
*/
|
|
25314
25457
|
alive: boolean()
|
|
25315
|
-
}))
|
|
25458
|
+
})), method(object({
|
|
25459
|
+
deviceId: number().int().nonnegative(),
|
|
25460
|
+
sessionId: string()
|
|
25461
|
+
}), WebrtcSessionDebugSchema, { kind: "query" });
|
|
25316
25462
|
object({
|
|
25317
25463
|
/** All accessory children of the parent. */
|
|
25318
25464
|
childDeviceIds: array(number()).readonly(),
|
|
@@ -29647,11 +29793,15 @@ var motionTriggerCapability = {
|
|
|
29647
29793
|
*
|
|
29648
29794
|
* Adding a fifth name to that list would have been the wrong fix twice over:
|
|
29649
29795
|
* that page is per-camera DETECTION tuning, and a grid's geometry belongs
|
|
29650
|
-
* beside PTZ and motion zones on the camera itself
|
|
29651
|
-
*
|
|
29652
|
-
*
|
|
29653
|
-
*
|
|
29654
|
-
*
|
|
29796
|
+
* beside PTZ and motion zones on the camera itself. The device page is
|
|
29797
|
+
* BINDING-driven (D12), so the way in is a capability bound to the device —
|
|
29798
|
+
* and this cap carries its section the way `recording` does, by RETURNING it
|
|
29799
|
+
* from `getDeviceSettingsContribution`.
|
|
29800
|
+
*
|
|
29801
|
+
* Seven other widgets are still declared the other way, through a
|
|
29802
|
+
* `deviceConfig.ui` block the framework derives a section from. That route
|
|
29803
|
+
* gives the addon no say in where its own panel lands and no way to decline
|
|
29804
|
+
* for a device the panel does not suit, which is why this one does not use it.
|
|
29655
29805
|
*
|
|
29656
29806
|
* ## Why one addon may implement it
|
|
29657
29807
|
*
|
|
@@ -29707,16 +29857,37 @@ var GridProfileOfferSchema = object({
|
|
|
29707
29857
|
]),
|
|
29708
29858
|
offered: boolean(),
|
|
29709
29859
|
/** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
|
|
29710
|
-
missingSources: array(number().int().positive())
|
|
29860
|
+
missingSources: array(number().int().positive()),
|
|
29861
|
+
/**
|
|
29862
|
+
* The canvas this profile composes onto, `WxH`, or empty when it is not
|
|
29863
|
+
* offered. DERIVED from the cells and the sources' own size at this profile —
|
|
29864
|
+
* it is reported because nothing else in the system would ever say what the
|
|
29865
|
+
* grid came out as, and because it is the number an operator would otherwise
|
|
29866
|
+
* expect to type.
|
|
29867
|
+
*/
|
|
29868
|
+
canvas: string(),
|
|
29869
|
+
/**
|
|
29870
|
+
* Whether this profile is PUBLISHED, of the ones the grid could serve.
|
|
29871
|
+
*
|
|
29872
|
+
* A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
|
|
29873
|
+
* a 4K canvas built from 4K decodes — something to opt into, not something a
|
|
29874
|
+
* viewer's adaptive should be handed by climbing to the top rung it can see.
|
|
29875
|
+
* Default is `mid` + `low`.
|
|
29876
|
+
*/
|
|
29877
|
+
published: boolean()
|
|
29711
29878
|
});
|
|
29712
29879
|
var GridLayoutViewSchema = object({
|
|
29713
29880
|
/** The persisted grid row this camera was declared from. */
|
|
29714
29881
|
instanceId: string(),
|
|
29715
29882
|
deviceId: number().int().nonnegative(),
|
|
29716
29883
|
name: string(),
|
|
29717
|
-
/**
|
|
29718
|
-
|
|
29719
|
-
|
|
29884
|
+
/**
|
|
29885
|
+
* NO canvas size. A grid's resolution is not authored: each profile derives
|
|
29886
|
+
* its own from the cells and its sources' dimensions. The two numbers that
|
|
29887
|
+
* used to be here were a text field that silently decided both how much the
|
|
29888
|
+
* composite cost and how sharp it was — see `profiles[].canvas` for what it
|
|
29889
|
+
* came out as.
|
|
29890
|
+
*/
|
|
29720
29891
|
fps: number().int(),
|
|
29721
29892
|
cells: array(GridLayoutCellSchema),
|
|
29722
29893
|
/** What the catalog will publish, and what it refuses to. Read-only. */
|
|
@@ -29725,9 +29896,13 @@ var GridLayoutViewSchema = object({
|
|
|
29725
29896
|
var GridLayoutPatchSchema = object({
|
|
29726
29897
|
deviceId: number().int().nonnegative(),
|
|
29727
29898
|
name: string().min(1).max(160).optional(),
|
|
29728
|
-
width: number().int().min(160).max(7680).optional(),
|
|
29729
|
-
height: number().int().min(120).max(4320).optional(),
|
|
29730
29899
|
fps: number().int().min(1).max(60).optional(),
|
|
29900
|
+
/** Which profiles to publish. See `GridProfileOffer.published`. */
|
|
29901
|
+
publishedProfiles: array(_enum([
|
|
29902
|
+
"high",
|
|
29903
|
+
"mid",
|
|
29904
|
+
"low"
|
|
29905
|
+
])).max(3).optional(),
|
|
29731
29906
|
/**
|
|
29732
29907
|
* The whole cell list at once. A per-cell patch would need an ordering the
|
|
29733
29908
|
* editor does not have, and a half-applied layout is a picture nobody asked
|
|
@@ -32142,6 +32317,25 @@ var ptzAutotrackCapability = {
|
|
|
32142
32317
|
scope: "device",
|
|
32143
32318
|
deviceNative: true,
|
|
32144
32319
|
mode: "singleton",
|
|
32320
|
+
/**
|
|
32321
|
+
* Two kinds of provider behind one name, the same shape `snapshot` and
|
|
32322
|
+
* `doorbell` use:
|
|
32323
|
+
*
|
|
32324
|
+
* - **Native**: the camera's own firmware autotrack, registered per device by
|
|
32325
|
+
* the driver when the probe says it exists (Reolink: yes; Hikvision:
|
|
32326
|
+
* model-dependent).
|
|
32327
|
+
* - **Wrapper**: the emulated implementation (`@camstack/system`'s `autotrack` builtin),
|
|
32328
|
+
* which follows a subject with `ptz.move` pulses driven by the detection
|
|
32329
|
+
* pipeline. `defaultActive: false` — the operator binds it per camera in
|
|
32330
|
+
* the bindings UI, exactly as `virtual-doorbell` is bound.
|
|
32331
|
+
*
|
|
32332
|
+
* The binding chooses WHICH implementation answers; `setEnabled` stays the
|
|
32333
|
+
* one switch for the function either way. A camera never has both driving its
|
|
32334
|
+
* head, which is the whole reason this is not a second cap with a second
|
|
32335
|
+
* toggle (D62, D520).
|
|
32336
|
+
*/
|
|
32337
|
+
kind: "wrapper",
|
|
32338
|
+
defaultActive: false,
|
|
32145
32339
|
deviceTypes: [DeviceType.Camera],
|
|
32146
32340
|
deviceConfig: { ui: {
|
|
32147
32341
|
kind: "widget",
|
|
@@ -43513,6 +43707,12 @@ Object.freeze({
|
|
|
43513
43707
|
addonId: null,
|
|
43514
43708
|
access: "view"
|
|
43515
43709
|
},
|
|
43710
|
+
"webrtcSession.getSessionDebug": {
|
|
43711
|
+
capName: "webrtc-session",
|
|
43712
|
+
capScope: "device",
|
|
43713
|
+
addonId: null,
|
|
43714
|
+
access: "view"
|
|
43715
|
+
},
|
|
43516
43716
|
"webrtcSession.getSessionState": {
|
|
43517
43717
|
capName: "webrtc-session",
|
|
43518
43718
|
capScope: "device",
|
|
@@ -45496,6 +45696,11 @@ Object.freeze({
|
|
|
45496
45696
|
form: "single",
|
|
45497
45697
|
optional: false
|
|
45498
45698
|
}],
|
|
45699
|
+
"webrtcSession.getSessionDebug": [{
|
|
45700
|
+
name: "deviceId",
|
|
45701
|
+
form: "single",
|
|
45702
|
+
optional: false
|
|
45703
|
+
}],
|
|
45499
45704
|
"webrtcSession.getSessionState": [{
|
|
45500
45705
|
name: "deviceId",
|
|
45501
45706
|
form: "single",
|
|
@@ -75181,7 +75386,7 @@ var require_lz4 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
75181
75386
|
};
|
|
75182
75387
|
}));
|
|
75183
75388
|
//#endregion
|
|
75184
|
-
//#region node_modules/@apocaliss92/nodelink-js/dist/chunk-
|
|
75389
|
+
//#region node_modules/@apocaliss92/nodelink-js/dist/chunk-7JMYBUEB.js
|
|
75185
75390
|
var import_fxp = require_fxp();
|
|
75186
75391
|
var import_lz4 = /* @__PURE__ */ __toESM(require_lz4(), 1);
|
|
75187
75392
|
function encodeHeader(h) {
|
|
@@ -83641,10 +83846,14 @@ function applyOsdDatetimePatch(rawXml, patch) {
|
|
|
83641
83846
|
return xml;
|
|
83642
83847
|
}
|
|
83643
83848
|
var OSD_POSITION_MAX = 65536;
|
|
83849
|
+
var OSD_POSITION_START_WRITE = 1;
|
|
83850
|
+
var OSD_POSITION_CENTER = 65537;
|
|
83644
83851
|
var OSD_CORNERS = [
|
|
83645
83852
|
"top-left",
|
|
83853
|
+
"top-center",
|
|
83646
83854
|
"top-right",
|
|
83647
83855
|
"bottom-left",
|
|
83856
|
+
"bottom-center",
|
|
83648
83857
|
"bottom-right"
|
|
83649
83858
|
];
|
|
83650
83859
|
function isStartEdge(value) {
|
|
@@ -83656,7 +83865,7 @@ function isEndEdge(value) {
|
|
|
83656
83865
|
function readOsdPosition(x, y) {
|
|
83657
83866
|
if (typeof x !== "number" || typeof y !== "number") return { kind: "unknown" };
|
|
83658
83867
|
if (!Number.isFinite(x) || !Number.isFinite(y)) return { kind: "unknown" };
|
|
83659
|
-
const horizontal = isStartEdge(x) ? "left" : isEndEdge(x) ? "right" : null;
|
|
83868
|
+
const horizontal = x === OSD_POSITION_CENTER ? "center" : isStartEdge(x) ? "left" : isEndEdge(x) ? "right" : null;
|
|
83660
83869
|
const vertical = isStartEdge(y) ? "top" : isEndEdge(y) ? "bottom" : null;
|
|
83661
83870
|
if (horizontal === null || vertical === null) return {
|
|
83662
83871
|
kind: "custom",
|
|
@@ -83670,8 +83879,8 @@ function readOsdPosition(x, y) {
|
|
|
83670
83879
|
}
|
|
83671
83880
|
function coordsForOsdCorner(corner) {
|
|
83672
83881
|
return {
|
|
83673
|
-
x: corner.endsWith("-right") ? OSD_POSITION_MAX :
|
|
83674
|
-
y: corner.startsWith("bottom-") ? OSD_POSITION_MAX :
|
|
83882
|
+
x: corner.endsWith("-center") ? OSD_POSITION_CENTER : corner.endsWith("-right") ? OSD_POSITION_MAX : OSD_POSITION_START_WRITE,
|
|
83883
|
+
y: corner.startsWith("bottom-") ? OSD_POSITION_MAX : OSD_POSITION_START_WRITE
|
|
83675
83884
|
};
|
|
83676
83885
|
}
|
|
83677
83886
|
function isOsdCorner(value) {
|
|
@@ -83682,8 +83891,10 @@ function formatOsdCoords(x, y) {
|
|
|
83682
83891
|
}
|
|
83683
83892
|
var OSD_CORNER_LABELS = {
|
|
83684
83893
|
"top-left": "Top left",
|
|
83894
|
+
"top-center": "Top centre",
|
|
83685
83895
|
"top-right": "Top right",
|
|
83686
83896
|
"bottom-left": "Bottom left",
|
|
83897
|
+
"bottom-center": "Bottom centre",
|
|
83687
83898
|
"bottom-right": "Bottom right"
|
|
83688
83899
|
};
|
|
83689
83900
|
function wireFloat(value) {
|
|
@@ -88210,8 +88421,12 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
88210
88421
|
/** Generic Baichuan cmd_id call, returns XML (if any). */
|
|
88211
88422
|
async sendXml(params, retry = 3) {
|
|
88212
88423
|
if (!this.client.loggedIn) await this.client.login();
|
|
88213
|
-
const
|
|
88214
|
-
|
|
88424
|
+
const addressed = params.channel != null && params.extensionXml == null ? {
|
|
88425
|
+
...params,
|
|
88426
|
+
extensionXml: buildChannelExtensionXml(params.channel)
|
|
88427
|
+
} : params;
|
|
88428
|
+
const frame = await this.client.sendFrame(addressed);
|
|
88429
|
+
if (frame.header.responseCode === 400) return await this.handleSendXml400(addressed, frame, retry);
|
|
88215
88430
|
if (frame.body.length === 0) return "";
|
|
88216
88431
|
return this.client.tryDecryptXml(frame.body, frame.header.channelId, this.client.enc);
|
|
88217
88432
|
}
|
|
@@ -94512,6 +94727,7 @@ ${xml}`);
|
|
|
94512
94727
|
await this.sendXml({
|
|
94513
94728
|
cmdId: 45,
|
|
94514
94729
|
channel: ch,
|
|
94730
|
+
extensionXml: buildChannelExtensionXml(ch),
|
|
94515
94731
|
payloadXml: ensureXmlHeader(xml),
|
|
94516
94732
|
...timeoutOpts
|
|
94517
94733
|
});
|
|
@@ -145546,6 +145762,7 @@ var RAW_READ_CATALOG = {
|
|
|
145546
145762
|
kind: "deviceCache",
|
|
145547
145763
|
snapshotKey: "autoFocusSnapshot"
|
|
145548
145764
|
},
|
|
145765
|
+
needs: "ptz",
|
|
145549
145766
|
invoke: (api, channel) => api.getAutoFocus(channel, { timeoutMs: 1500 })
|
|
145550
145767
|
},
|
|
145551
145768
|
netPort: {
|
|
@@ -145742,6 +145959,7 @@ var RawReadResultSchema = discriminatedUnion("ok", [object({
|
|
|
145742
145959
|
slice: RawReadSliceSchema,
|
|
145743
145960
|
reason: _enum([
|
|
145744
145961
|
"sleeping",
|
|
145962
|
+
"unsupported",
|
|
145745
145963
|
"login-failed",
|
|
145746
145964
|
"read-failed"
|
|
145747
145965
|
]),
|
|
@@ -145785,6 +146003,20 @@ function toPlainJson(value) {
|
|
|
145785
146003
|
async function performRawRead(slice, deps) {
|
|
145786
146004
|
const entry = RAW_READ_CATALOG[slice];
|
|
145787
146005
|
const believed = believedState(deps, entry);
|
|
146006
|
+
if (entry.needs === "ptz" && !deps.hasPtz) {
|
|
146007
|
+
deps.logger.info("reolink raw read refused — the camera has no PTZ head", {
|
|
146008
|
+
tags: { deviceId: deps.deviceId },
|
|
146009
|
+
meta: { slice }
|
|
146010
|
+
});
|
|
146011
|
+
return {
|
|
146012
|
+
ok: false,
|
|
146013
|
+
deviceId: deps.deviceId,
|
|
146014
|
+
slice,
|
|
146015
|
+
reason: "unsupported",
|
|
146016
|
+
message: `This camera has no pan/tilt/zoom head, so it has no focus motor and no "${slice}" to read — the Reolink app does not ask it either. Nothing is wrong with the camera.`,
|
|
146017
|
+
believed
|
|
146018
|
+
};
|
|
146019
|
+
}
|
|
145788
146020
|
const servedWithoutTouchingCamera = deps.hubChild && entry.servedByParentHub === true;
|
|
145789
146021
|
if (deps.sleeping && !servedWithoutTouchingCamera) {
|
|
145790
146022
|
deps.logger.info("reolink raw read refused — battery cam is sleeping", {
|
|
@@ -149448,7 +149680,6 @@ var reolinkCameraSchema = object({
|
|
|
149448
149680
|
* read via 52). Toggles the `<Shelter><enable>` flag. Zone
|
|
149449
149681
|
* editing is a separate flow not exposed by this driver.
|
|
149450
149682
|
*/
|
|
149451
|
-
privacyMaskEnabled: boolean().optional(),
|
|
149452
149683
|
/**
|
|
149453
149684
|
* Audio noise reduction level — pushed via `setAudioNoise`
|
|
149454
149685
|
* (cmd_id 440, read via 439). 0 disables; 1..3 maps to
|
|
@@ -150282,7 +150513,6 @@ function slicesForPatch(patch) {
|
|
|
150282
150513
|
if (has("motionEnabled", "motionSensitivity")) out.add("motion");
|
|
150283
150514
|
if (Object.keys(patch).some((k) => /^ai(Person|Vehicle|Animal|Face|Package)/.test(k))) out.add("ai");
|
|
150284
150515
|
if (has("gop")) out.add("enc");
|
|
150285
|
-
if (has("privacyMaskEnabled")) out.add("mask");
|
|
150286
150516
|
if (has("audioNoiseLevel")) out.add("audioNoise");
|
|
150287
150517
|
if (has("autoFocusEnabled")) out.add("autofocus");
|
|
150288
150518
|
if (has("netHttpEnable", "netHttpPort", "netHttpsEnable", "netHttpsPort", "netRtspEnable", "netRtspPort", "netRtmpEnable", "netRtmpPort", "netOnvifEnable", "netOnvifPort")) out.add("netPort");
|
|
@@ -150294,6 +150524,20 @@ function slicesForPatch(patch) {
|
|
|
150294
150524
|
if (has("timeZoneId")) out.add("systemGeneral");
|
|
150295
150525
|
return out;
|
|
150296
150526
|
}
|
|
150527
|
+
/**
|
|
150528
|
+
* What the operator's "Refresh from camera" actually managed.
|
|
150529
|
+
*
|
|
150530
|
+
* Returned rather than logged, because the button is the one place a person
|
|
150531
|
+
* asks "is what I am looking at true right now" — and an answer of `void`
|
|
150532
|
+
* cannot distinguish a full re-read from a camera that never woke.
|
|
150533
|
+
*/
|
|
150534
|
+
var RefreshSettingsResultSchema = object({
|
|
150535
|
+
/** False when a battery camera did not come up — everything below is then stale. */
|
|
150536
|
+
awake: boolean(),
|
|
150537
|
+
snapshotRefreshed: boolean(),
|
|
150538
|
+
capsWarmed: number(),
|
|
150539
|
+
capsTotal: number()
|
|
150540
|
+
});
|
|
150297
150541
|
var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
150298
150542
|
type = DeviceType.Camera;
|
|
150299
150543
|
/**
|
|
@@ -150763,9 +151007,12 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
150763
151007
|
* `setVideoInput`); motion is writable via `setMotionAlarm` so its
|
|
150764
151008
|
* snapshot also drives the dispatch's known-good baseline.
|
|
150765
151009
|
*/
|
|
150766
|
-
async refreshParentSettingsSnapshot(slices) {
|
|
150767
|
-
if (this.isBattery && this.sleeping) {
|
|
150768
|
-
this.ctx.logger.info("refreshParentSettingsSnapshot skipped — battery cam is sleeping", { tags: {
|
|
151010
|
+
async refreshParentSettingsSnapshot(slices, reason = "background") {
|
|
151011
|
+
if (this.isBattery && this.sleeping && reason !== "post-write") {
|
|
151012
|
+
this.ctx.logger.info("refreshParentSettingsSnapshot skipped — battery cam is sleeping", { tags: {
|
|
151013
|
+
deviceId: this.id,
|
|
151014
|
+
reason
|
|
151015
|
+
} });
|
|
150769
151016
|
return;
|
|
150770
151017
|
}
|
|
150771
151018
|
let api;
|
|
@@ -151010,6 +151257,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
151010
151257
|
channel: this.getChannel(),
|
|
151011
151258
|
sleeping: this.isBattery && this.sleeping,
|
|
151012
151259
|
hubChild: this.isHubChild(),
|
|
151260
|
+
hasPtz: this.getProbeFlags().hasPtz === true,
|
|
151013
151261
|
getApi: () => this.ensureApi(),
|
|
151014
151262
|
cache: this.config.get("deviceCache"),
|
|
151015
151263
|
batteryState: this.runtimeState.getCapState("battery"),
|
|
@@ -151103,6 +151351,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
151103
151351
|
});
|
|
151104
151352
|
await sleep$2(1500);
|
|
151105
151353
|
})(), new Promise((_, rej) => setTimeout(() => rej(/* @__PURE__ */ new Error("wakeIfSleeping timeout")), timeoutMs))]);
|
|
151354
|
+
if (this.commitSleepState(false, "confirmed-wake")) {
|
|
151355
|
+
this.ctx.logger.info("proactive action: camera is up — committing the mirror", { tags: { deviceId: this.id } });
|
|
151356
|
+
this.onWakeTransition("confirmed-wake").catch(() => {});
|
|
151357
|
+
}
|
|
151106
151358
|
return true;
|
|
151107
151359
|
} catch (err) {
|
|
151108
151360
|
this.ctx.logger.warn("proactive action: wake did not complete in time", {
|
|
@@ -151274,8 +151526,26 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
151274
151526
|
this.registerDeviceAction("auto-configure-email-push", deviceCustomAction(unknown(), _void(), { kind: "mutation" }), async () => {
|
|
151275
151527
|
await this.autoConfigureEmailPush();
|
|
151276
151528
|
});
|
|
151277
|
-
this.registerDeviceAction("refresh-settings", deviceCustomAction(unknown(),
|
|
151278
|
-
|
|
151529
|
+
this.registerDeviceAction("refresh-settings", deviceCustomAction(unknown(), RefreshSettingsResultSchema, { kind: "mutation" }), async () => {
|
|
151530
|
+
const awake = await this.wakeIfSleeping();
|
|
151531
|
+
const [snapshot, warm] = await Promise.allSettled([this.refreshParentSettingsSnapshot(void 0, "operator"), this.warmCapSlicesOnWake({ force: true })]);
|
|
151532
|
+
const warmed = warm.status === "fulfilled" ? warm.value.warmed : 0;
|
|
151533
|
+
const capsTotal = this.capWarmers.size;
|
|
151534
|
+
this.ctx.logger.info("refresh from camera: operator rehydrate", {
|
|
151535
|
+
tags: { deviceId: this.id },
|
|
151536
|
+
meta: {
|
|
151537
|
+
awake,
|
|
151538
|
+
snapshot: snapshot.status,
|
|
151539
|
+
warmed,
|
|
151540
|
+
capsTotal
|
|
151541
|
+
}
|
|
151542
|
+
});
|
|
151543
|
+
return {
|
|
151544
|
+
awake,
|
|
151545
|
+
snapshotRefreshed: snapshot.status === "fulfilled",
|
|
151546
|
+
capsWarmed: warmed,
|
|
151547
|
+
capsTotal
|
|
151548
|
+
};
|
|
151279
151549
|
});
|
|
151280
151550
|
this.registerDeviceAction("refresh-sessions", deviceCustomAction(unknown(), _void(), { kind: "mutation" }), async () => {
|
|
151281
151551
|
await this.refreshSessionsSnapshot();
|
|
@@ -151315,7 +151585,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
151315
151585
|
* registration so the general "Refresh from camera" button can warm
|
|
151316
151586
|
* the `stream-params` runtime-state slice (the editable Stream
|
|
151317
151587
|
* section's current values read from it). */
|
|
151318
|
-
refreshStreamParamsFromCamera = null;
|
|
151319
151588
|
/** Single-flight guard for the motion-zones cap refresh. */
|
|
151320
151589
|
motionZonesRefreshInFlight = null;
|
|
151321
151590
|
/** Single-flight guard for the privacy-mask cap refresh. */
|
|
@@ -151449,6 +151718,16 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
151449
151718
|
};
|
|
151450
151719
|
}
|
|
151451
151720
|
if (confirmed && !observedAwake && parent !== null) this.watchHubChildAwake(parent).catch(() => {});
|
|
151721
|
+
if (this.commitSleepState(false, "confirmed-wake")) {
|
|
151722
|
+
this.ctx.logger.info("battery wakeForStream: answering awake — committing the mirror", {
|
|
151723
|
+
tags: { deviceId: this.id },
|
|
151724
|
+
meta: {
|
|
151725
|
+
confirmedMs: Date.now() - startedAt,
|
|
151726
|
+
observedAwake
|
|
151727
|
+
}
|
|
151728
|
+
});
|
|
151729
|
+
this.onWakeTransition("confirmed-wake").catch(() => {});
|
|
151730
|
+
}
|
|
151452
151731
|
return {
|
|
151453
151732
|
awoke: true,
|
|
151454
151733
|
durationMs: Date.now() - startedAt
|
|
@@ -151574,7 +151853,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
151574
151853
|
commitSleepState(next, source) {
|
|
151575
151854
|
if (!this.isBattery) return false;
|
|
151576
151855
|
if (this.sleeping === next) return false;
|
|
151577
|
-
if (source
|
|
151856
|
+
if (source === "confirmed-wake" && next === false) this.sleepStateChangedAt = Date.now();
|
|
151857
|
+
else if (source !== "hub-summary" && !this.acceptSleepingTransition(next)) return false;
|
|
151578
151858
|
if (source === "hub-summary") {
|
|
151579
151859
|
const sinceCommitMs = Date.now() - this.sleepStateChangedAt;
|
|
151580
151860
|
if (next === true && this.sleepStateChangedAt > 0 && sinceCommitMs < ReolinkCamera.HUB_SUMMARY_SLEEP_GRACE_MS) {
|
|
@@ -152710,14 +152990,14 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
152710
152990
|
* swallowed: the window is short, partial progress accumulates across
|
|
152711
152991
|
* wakes because the slices persist.
|
|
152712
152992
|
*/
|
|
152713
|
-
async warmCapSlicesOnWake() {
|
|
152993
|
+
async warmCapSlicesOnWake(opts) {
|
|
152714
152994
|
const now = Date.now();
|
|
152715
152995
|
let warmed = 0;
|
|
152716
152996
|
let skipped = 0;
|
|
152717
152997
|
for (const [capName, warm] of this.capWarmers) {
|
|
152718
152998
|
const slice = this.runtimeState.getCapState(capName);
|
|
152719
152999
|
const fetchedAt = typeof slice?.lastFetchedAt === "number" ? slice.lastFetchedAt : 0;
|
|
152720
|
-
if (fetchedAt > 0 && now - fetchedAt < ReolinkCamera.WAKE_WARM_MAX_AGE_MS) {
|
|
153000
|
+
if (opts?.force !== true && fetchedAt > 0 && now - fetchedAt < ReolinkCamera.WAKE_WARM_MAX_AGE_MS) {
|
|
152721
153001
|
skipped += 1;
|
|
152722
153002
|
continue;
|
|
152723
153003
|
}
|
|
@@ -152742,6 +153022,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
152742
153022
|
total: this.capWarmers.size
|
|
152743
153023
|
}
|
|
152744
153024
|
});
|
|
153025
|
+
return {
|
|
153026
|
+
warmed,
|
|
153027
|
+
skipped
|
|
153028
|
+
};
|
|
152745
153029
|
}
|
|
152746
153030
|
/**
|
|
152747
153031
|
* Ask the snapshot wrapper for a frame while the camera is up, so its
|
|
@@ -152807,7 +153091,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
152807
153091
|
registerStreamParamsCap() {
|
|
152808
153092
|
const channel = this.getChannel();
|
|
152809
153093
|
const CAP_NAME = "stream-params";
|
|
152810
|
-
const STALE_MS = 1e4;
|
|
152811
153094
|
/** Round-trip `getEnc`, map into the cap status schema, write slice. */
|
|
152812
153095
|
const refreshFromCamera = async () => {
|
|
152813
153096
|
if (this.streamParamsRefreshInFlight) return this.streamParamsRefreshInFlight;
|
|
@@ -152840,13 +153123,13 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
152840
153123
|
this.streamParamsRefreshInFlight = null;
|
|
152841
153124
|
}
|
|
152842
153125
|
};
|
|
152843
|
-
this.refreshStreamParamsFromCamera = refreshFromCamera;
|
|
152844
153126
|
const bridge = createRuntimeStateBridge({
|
|
152845
153127
|
runtimeState: this.runtimeState,
|
|
152846
153128
|
cap: streamParamsCapability,
|
|
152847
153129
|
ownDeviceId: this.id,
|
|
152848
153130
|
refresh: this.sleepGatedRefresh(CAP_NAME, refreshFromCamera),
|
|
152849
|
-
staleMs:
|
|
153131
|
+
staleMs: OPERATOR_WRITTEN_STALE_MS,
|
|
153132
|
+
staleRead: "serve-and-revalidate",
|
|
152850
153133
|
empty: () => ({ lastFetchedAt: 0 })
|
|
152851
153134
|
});
|
|
152852
153135
|
const resolveOptions = async () => this.resolveCapOptions({
|
|
@@ -152931,7 +153214,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
152931
153214
|
registerMotionZonesCap() {
|
|
152932
153215
|
const channel = this.getChannel();
|
|
152933
153216
|
const CAP_NAME = "motion-zones";
|
|
152934
|
-
const STALE_MS = 1e4;
|
|
152935
153217
|
/** Round-trip `getMotionAlarm`, map into the cap status schema, write slice. */
|
|
152936
153218
|
const refreshFromCamera = async () => {
|
|
152937
153219
|
if (this.motionZonesRefreshInFlight) return this.motionZonesRefreshInFlight;
|
|
@@ -152981,7 +153263,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
152981
153263
|
cap: motionZonesCapability,
|
|
152982
153264
|
ownDeviceId: this.id,
|
|
152983
153265
|
refresh: this.sleepGatedRefresh(CAP_NAME, refreshFromCamera),
|
|
152984
|
-
staleMs:
|
|
153266
|
+
staleMs: OPERATOR_WRITTEN_STALE_MS,
|
|
153267
|
+
staleRead: "serve-and-revalidate",
|
|
152985
153268
|
empty: () => ({
|
|
152986
153269
|
enabled: false,
|
|
152987
153270
|
sensitivity: 1,
|
|
@@ -153312,7 +153595,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
153312
153595
|
registerDayNightCap() {
|
|
153313
153596
|
const channel = this.getChannel();
|
|
153314
153597
|
const CAP_NAME = "day-night";
|
|
153315
|
-
const STALE_MS = 1e4;
|
|
153316
153598
|
/** Round-trip `getVideoInput` + `getDayNightThreshold`, map into the
|
|
153317
153599
|
* cap status schema, write slice. */
|
|
153318
153600
|
const refreshFromCamera = async () => {
|
|
@@ -153355,7 +153637,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
153355
153637
|
cap: dayNightCapability,
|
|
153356
153638
|
ownDeviceId: this.id,
|
|
153357
153639
|
refresh: this.sleepGatedRefresh(CAP_NAME, refreshFromCamera),
|
|
153358
|
-
staleMs:
|
|
153640
|
+
staleMs: OPERATOR_WRITTEN_STALE_MS,
|
|
153641
|
+
staleRead: "serve-and-revalidate",
|
|
153359
153642
|
empty: () => ({
|
|
153360
153643
|
mode: "auto",
|
|
153361
153644
|
lastFetchedAt: 0
|
|
@@ -153448,7 +153731,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
153448
153731
|
registerImageSettingsCap() {
|
|
153449
153732
|
const channel = this.getChannel();
|
|
153450
153733
|
const CAP_NAME = "image-settings";
|
|
153451
|
-
const STALE_MS = 1e4;
|
|
153452
153734
|
const SLIDER_MIN = 0;
|
|
153453
153735
|
const SLIDER_MAX = 255;
|
|
153454
153736
|
const NORMALIZED_RANGE = {
|
|
@@ -153492,7 +153774,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
153492
153774
|
cap: imageSettingsCapability,
|
|
153493
153775
|
ownDeviceId: this.id,
|
|
153494
153776
|
refresh: this.sleepGatedRefresh(CAP_NAME, refreshFromCamera),
|
|
153495
|
-
staleMs:
|
|
153777
|
+
staleMs: OPERATOR_WRITTEN_STALE_MS,
|
|
153778
|
+
staleRead: "serve-and-revalidate",
|
|
153496
153779
|
empty: () => ({ lastFetchedAt: 0 })
|
|
153497
153780
|
}).getStatus,
|
|
153498
153781
|
getOptions: async ({ deviceId }) => {
|
|
@@ -153809,7 +154092,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
153809
154092
|
/** Stale-after window for served-from-cache reads. ~10s is short
|
|
153810
154093
|
* enough that the UI's 5s poll always sees a fresh round-trip,
|
|
153811
154094
|
* long enough that two consumers in the same tick fold into one. */
|
|
153812
|
-
const STALE_MS = 1e4;
|
|
153813
154095
|
const SUPPORTED_TARGET_TYPES = [
|
|
153814
154096
|
{
|
|
153815
154097
|
value: "people",
|
|
@@ -153875,7 +154157,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
153875
154157
|
cap: ptzAutotrackCapability,
|
|
153876
154158
|
ownDeviceId: this.id,
|
|
153877
154159
|
refresh: this.sleepGatedRefresh(CAP_NAME, refreshFromCamera),
|
|
153878
|
-
staleMs:
|
|
154160
|
+
staleMs: OPERATOR_WRITTEN_STALE_MS,
|
|
154161
|
+
staleRead: "serve-and-revalidate",
|
|
153879
154162
|
empty: () => ({
|
|
153880
154163
|
enabled: false,
|
|
153881
154164
|
lastChangedAt: 0,
|
|
@@ -155636,7 +155919,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
155636
155919
|
id: "driver-refresh",
|
|
155637
155920
|
tab: "general",
|
|
155638
155921
|
title: "Camera data",
|
|
155639
|
-
description: "
|
|
155922
|
+
description: "Go and ask the camera now. Every other read here is deliberately cheap about a battery camera — it serves what it already knows and refreshes quietly behind you, and never wakes a sleeping camera just because you opened this tab. This button is the exception: it wakes the camera, re-reads everything, and tells you what it got. Use it after changing settings from the Reolink app, or whenever you want to be sure the form is showing the camera and not a memory of it.",
|
|
155640
155923
|
columns: 1,
|
|
155641
155924
|
fields: [{
|
|
155642
155925
|
type: "device-action-button",
|
|
@@ -155644,7 +155927,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
155644
155927
|
label: "Refresh from camera",
|
|
155645
155928
|
buttonLabel: "Refresh now",
|
|
155646
155929
|
action: "refresh-settings",
|
|
155647
|
-
variant: "default"
|
|
155930
|
+
variant: "default",
|
|
155931
|
+
successMessage: "Camera awake: {awake} · settings re-read: {snapshotRefreshed} · slices refreshed: {capsWarmed}/{capsTotal}"
|
|
155648
155932
|
}]
|
|
155649
155933
|
},
|
|
155650
155934
|
{
|
|
@@ -156131,20 +156415,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
156131
156415
|
sleeping: this.isBattery && this.sleeping,
|
|
156132
156416
|
now: Date.now()
|
|
156133
156417
|
}),
|
|
156134
|
-
{
|
|
156135
|
-
id: "privacy-mask",
|
|
156136
|
-
tab: "image",
|
|
156137
|
-
title: "Privacy mask",
|
|
156138
|
-
description: "Master enable for the privacy zones configured on the camera (set up via the Reolink mobile app + camera UI). Pushed via `SetMask` (cmd_id 53). Disabling shows the full frame; enabling re-applies the persisted zones.",
|
|
156139
|
-
columns: 1,
|
|
156140
|
-
fields: [{
|
|
156141
|
-
type: "boolean",
|
|
156142
|
-
key: "privacyMaskEnabled",
|
|
156143
|
-
label: "Privacy mask enabled",
|
|
156144
|
-
default: cache?.maskSnapshot?.enabled ?? false,
|
|
156145
|
-
style: "switch"
|
|
156146
|
-
}]
|
|
156147
|
-
},
|
|
156148
156418
|
...cache?.autoFocusSnapshot?.supported === true ? [{
|
|
156149
156419
|
id: "auto-focus",
|
|
156150
156420
|
tab: "image",
|
|
@@ -156499,7 +156769,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
156499
156769
|
audioVolume: this.config.get("audioVolume") ?? 50,
|
|
156500
156770
|
audioTalkAndReplyVolume: this.config.get("audioTalkAndReplyVolume") ?? 50,
|
|
156501
156771
|
audioVisitorVolume: this.config.get("audioVisitorVolume") ?? 50,
|
|
156502
|
-
privacyMaskEnabled: this.config.get("privacyMaskEnabled") ?? cache?.maskSnapshot?.enabled ?? false,
|
|
156503
156772
|
audioNoiseLevel: this.config.get("audioNoiseLevel") ?? (cache?.audioNoiseSnapshot?.enabled ? cache.audioNoiseSnapshot?.level ?? 1 : 0),
|
|
156504
156773
|
autoFocusEnabled: this.config.get("autoFocusEnabled") ?? cache?.autoFocusSnapshot?.enabled ?? true,
|
|
156505
156774
|
pirReduceFalseAlarm: this.config.get("pirReduceFalseAlarm") ?? cache?.pirSnapshot?.reduceFalseAlarm ?? false,
|
|
@@ -156515,6 +156784,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
156515
156784
|
}
|
|
156516
156785
|
async applySettingsPatch(patch) {
|
|
156517
156786
|
const { emailSmtpServer, emailSmtpPort, emailUserName, emailPassword, emailSsl, emailRecipient1, emailRecipient2, emailRecipient3, emailSendNickname, emailScheduleEnabled, osdChannelEnabled, osdChannelName, osdChannelPosition, osdTimeEnabled, osdTimePosition, osdWatermark, ...rest } = patch;
|
|
156787
|
+
if ((osdChannelEnabled !== void 0 || osdChannelName !== void 0 || osdChannelPosition !== void 0 || osdTimeEnabled !== void 0 || osdTimePosition !== void 0 || osdWatermark !== void 0 || emailSmtpServer !== void 0 || emailRecipient1 !== void 0) && !await this.wakeIfSleeping()) this.ctx.logger.warn("settings write: battery cam did not wake — writing anyway", { tags: { deviceId: this.id } });
|
|
156518
156788
|
const emailFields = {
|
|
156519
156789
|
emailSmtpServer,
|
|
156520
156790
|
emailSmtpPort,
|
|
@@ -156825,13 +157095,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
156825
157095
|
this.ctx.logger.warn("reolink setNtp push failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
156826
157096
|
}
|
|
156827
157097
|
}
|
|
156828
|
-
if ("privacyMaskEnabled" in patch) try {
|
|
156829
|
-
const api = await this.ensureApi();
|
|
156830
|
-
const v = typedPatch.privacyMaskEnabled;
|
|
156831
|
-
if (typeof v === "boolean") await api.setMask(this.getChannel(), { enable: v });
|
|
156832
|
-
} catch (err) {
|
|
156833
|
-
this.ctx.logger.warn("mask push failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
156834
|
-
}
|
|
156835
157098
|
if ("audioNoiseLevel" in patch) try {
|
|
156836
157099
|
const api = await this.ensureApi();
|
|
156837
157100
|
const v = typedPatch.audioNoiseLevel;
|
|
@@ -156889,15 +157152,40 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
156889
157152
|
}
|
|
156890
157153
|
}
|
|
156891
157154
|
const changedSlices = slicesForPatch(patch);
|
|
156892
|
-
if (changedSlices.size > 0)
|
|
156893
|
-
this.
|
|
156894
|
-
|
|
156895
|
-
|
|
156896
|
-
|
|
156897
|
-
|
|
156898
|
-
|
|
157155
|
+
if (changedSlices.size > 0) {
|
|
157156
|
+
await this.refreshParentSettingsSnapshot(changedSlices, "post-write").catch((err) => {
|
|
157157
|
+
this.ctx.logger.info("reolink targeted settings refresh failed — snapshots left stale", {
|
|
157158
|
+
tags: { deviceId: this.id },
|
|
157159
|
+
meta: {
|
|
157160
|
+
slices: [...changedSlices],
|
|
157161
|
+
error: err instanceof Error ? err.message : String(err)
|
|
157162
|
+
}
|
|
157163
|
+
});
|
|
156899
157164
|
});
|
|
156900
|
-
|
|
157165
|
+
this.assertOsdWriteLanded(patch);
|
|
157166
|
+
}
|
|
157167
|
+
}
|
|
157168
|
+
/**
|
|
157169
|
+
* Compare what the operator asked for with what the camera now reports, and
|
|
157170
|
+
* REFUSE to call a lost write a success.
|
|
157171
|
+
*
|
|
157172
|
+
* Nothing used to do this, so an applied write and a write the camera never
|
|
157173
|
+
* took were indistinguishable — to the code and to the operator, who was
|
|
157174
|
+
* told `{success: true}` for a change the camera discarded (3825,
|
|
157175
|
+
* 2026-09-19). Throwing is the right shape: the admin UI's in-flight
|
|
157176
|
+
* overlay hands the operator their own edit back, marked unsaved, instead
|
|
157177
|
+
* of quietly reverting the field to the old value.
|
|
157178
|
+
*
|
|
157179
|
+
* Silence is the pass. A slice we could not re-read at all is already
|
|
157180
|
+
* reported by the caller as "snapshots left stale" — absence of a reading
|
|
157181
|
+
* is not evidence of a lost write, and must never be treated as one (D393).
|
|
157182
|
+
*/
|
|
157183
|
+
assertOsdWriteLanded(patch) {
|
|
157184
|
+
const asked = patch.osdTimePosition;
|
|
157185
|
+
if (typeof asked !== "string") return;
|
|
157186
|
+
const got = resolveOsdValues(this.config.get("deviceCache")?.osdSnapshot).osdTimePosition;
|
|
157187
|
+
if (typeof got !== "string" || got === asked) return;
|
|
157188
|
+
throw new Error(`the camera did not take the timestamp position: asked "${asked}", it reports "${got}"`);
|
|
156901
157189
|
}
|
|
156902
157190
|
/**
|
|
156903
157191
|
* Optionally inject an already-logged-in Baichuan api — used by
|