@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.js
CHANGED
|
@@ -5386,7 +5386,7 @@ var ZodIssueCode = {
|
|
|
5386
5386
|
var ZodFirstPartyTypeKind;
|
|
5387
5387
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5388
5388
|
//#endregion
|
|
5389
|
-
//#region ../types/dist/sleep-
|
|
5389
|
+
//#region ../types/dist/sleep-CiR4z7r9.mjs
|
|
5390
5390
|
/**
|
|
5391
5391
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5392
5392
|
* window to float samples (D455).
|
|
@@ -6528,6 +6528,17 @@ var CamProfileSchema = _enum([
|
|
|
6528
6528
|
"mid",
|
|
6529
6529
|
"low"
|
|
6530
6530
|
]);
|
|
6531
|
+
/**
|
|
6532
|
+
* Which rung a per-camera consumer listens on: an explicit profile, or `auto`.
|
|
6533
|
+
*
|
|
6534
|
+
* `auto` is a REAL value, not an absent one — it means "resolve it by the rule
|
|
6535
|
+
* that consumer owns". It lives beside `CamProfile` because a rung is the only
|
|
6536
|
+
* operator-facing way to name a stream: a cam-stream id is broker-internal and
|
|
6537
|
+
* moves when a slot is reassigned, so a stored id silently stops meaning what
|
|
6538
|
+
* the operator picked. Today's one user is the audio analyser, whose rule is
|
|
6539
|
+
* "the lowest AVAILABLE native rung" (D537).
|
|
6540
|
+
*/
|
|
6541
|
+
var AudioStreamPickSchema = union([CamProfileSchema, literal("auto")]);
|
|
6531
6542
|
var CamStreamKindSchema = _enum([
|
|
6532
6543
|
"pull-rtsp",
|
|
6533
6544
|
"pull-rtmp",
|
|
@@ -7794,6 +7805,7 @@ var CameraSwitchIdSchema = _enum([
|
|
|
7794
7805
|
"device-audio",
|
|
7795
7806
|
"broker-audio",
|
|
7796
7807
|
"audio-analysis",
|
|
7808
|
+
"event-saving",
|
|
7797
7809
|
"recording",
|
|
7798
7810
|
"notifications"
|
|
7799
7811
|
]);
|
|
@@ -8268,11 +8280,31 @@ var RecordingBandTriggersSchema = RecordingTriggersSchema;
|
|
|
8268
8280
|
* band engine's `applies` rule). `start`/`end` are `HH:MM`; an `end <= start`
|
|
8269
8281
|
* span wraps past midnight (handled by the band engine).
|
|
8270
8282
|
*/
|
|
8283
|
+
/**
|
|
8284
|
+
* Whether a band's footage carries sound.
|
|
8285
|
+
*
|
|
8286
|
+
* The recorder has always attached with `audio: 'aac'`, so every band of every
|
|
8287
|
+
* camera recorded sound whether anyone wanted it or not. The broker already
|
|
8288
|
+
* serves the other answer — `audio: 'none'` takes the video-only `/muted`
|
|
8289
|
+
* restream — so this was a choice the product could always have offered.
|
|
8290
|
+
*
|
|
8291
|
+
* It belongs on the BAND rather than the camera because one camera can want
|
|
8292
|
+
* both: silent across the shop floor during opening hours, sound overnight.
|
|
8293
|
+
* And it applies to BOTH modes — an events band firing on motion has the same
|
|
8294
|
+
* question to answer as a continuous one.
|
|
8295
|
+
*/
|
|
8296
|
+
var RecordingBandAudioSchema = _enum(["on", "muted"]);
|
|
8271
8297
|
var RecordingBandSchema = object({
|
|
8272
8298
|
days: array(RecordingWeekdaySchema),
|
|
8273
8299
|
start: string().regex(HHMM),
|
|
8274
8300
|
end: string().regex(HHMM),
|
|
8275
8301
|
mode: RecordingBandModeSchema,
|
|
8302
|
+
/**
|
|
8303
|
+
* Sound, or silence. OPTIONAL and absent-means-`on`: every recording
|
|
8304
|
+
* written before this existed carried sound, and a band whose key is
|
|
8305
|
+
* missing must keep meaning what it meant. See {@link RecordingBandAudioSchema}.
|
|
8306
|
+
*/
|
|
8307
|
+
audio: RecordingBandAudioSchema.optional(),
|
|
8276
8308
|
triggers: RecordingBandTriggersSchema.optional(),
|
|
8277
8309
|
/**
|
|
8278
8310
|
* RETIRED (D381). A band no longer carries a pre-buffer of its own.
|
|
@@ -20894,6 +20926,7 @@ var MediaFileKindEnum = _enum([
|
|
|
20894
20926
|
"keyFrameSmall",
|
|
20895
20927
|
"thumbnailSmall",
|
|
20896
20928
|
"confirmationCrop",
|
|
20929
|
+
"debugCrop",
|
|
20897
20930
|
"sceneFrame"
|
|
20898
20931
|
]);
|
|
20899
20932
|
/**
|
|
@@ -22558,6 +22591,18 @@ var RunnerCameraConfigSchema = object({
|
|
|
22558
22591
|
motionStreamId: string(),
|
|
22559
22592
|
detectionStreamId: string(),
|
|
22560
22593
|
/**
|
|
22594
|
+
* Which rung this camera's audio analysis listens on — `auto` (the lowest
|
|
22595
|
+
* AVAILABLE native rung) or an explicit one.
|
|
22596
|
+
*
|
|
22597
|
+
* Not consumed by the runner: carried here so it travels with the camera's
|
|
22598
|
+
* config the way `audioMotionWindowMs` does, and so the orchestrator's
|
|
22599
|
+
* watchdog resolves the same stream the audio subscription did. OPTIONAL
|
|
22600
|
+
* with no default — absent means `auto`, which is what every camera written
|
|
22601
|
+
* before this field had, and an absent optional adds nothing to the
|
|
22602
|
+
* generated cap-input defaults (D537).
|
|
22603
|
+
*/
|
|
22604
|
+
audioStreamPick: AudioStreamPickSchema.optional(),
|
|
22605
|
+
/**
|
|
22561
22606
|
* Motion sources that can activate the pipeline. At least one must
|
|
22562
22607
|
* be present. Multiple sources OR-ed: any one firing arms the
|
|
22563
22608
|
* cooldown timer and transitions watching → active. The same
|
|
@@ -25153,6 +25198,104 @@ var WebrtcStreamChoiceSchema = object({
|
|
|
25153
25198
|
/** Bitrate in kbps (null for adaptive). */
|
|
25154
25199
|
bitrateKbps: number().nullable()
|
|
25155
25200
|
});
|
|
25201
|
+
/**
|
|
25202
|
+
* The last move the adaptive ladder made on this session, and the numbers it
|
|
25203
|
+
* rested on. Mirrors `LadderDecision` in the stream-broker's
|
|
25204
|
+
* `adaptive-controller.ts` — the log line's evidence, made readable.
|
|
25205
|
+
*
|
|
25206
|
+
* Nullability is load-bearing: `lossRatio: null` means the loss tracker had
|
|
25207
|
+
* no window to report, which is NOT a link with zero loss.
|
|
25208
|
+
*/
|
|
25209
|
+
var WebrtcLadderDecisionSchema = object({
|
|
25210
|
+
/** `loss` — RTCP / client-reported packet loss; `stall` — the picture
|
|
25211
|
+
* stopped moving; `healthy` — a promotion. */
|
|
25212
|
+
cause: _enum([
|
|
25213
|
+
"loss",
|
|
25214
|
+
"stall",
|
|
25215
|
+
"healthy"
|
|
25216
|
+
]),
|
|
25217
|
+
/** The rung the ladder moved TO. Null for a move that changed no rung
|
|
25218
|
+
* (an MTU reduction, a forced transcode-down at the bottom). */
|
|
25219
|
+
tier: CamProfileSchema.nullable(),
|
|
25220
|
+
/** Whether that move forced the egress transcode-down rung. */
|
|
25221
|
+
forcedTranscodeDown: boolean(),
|
|
25222
|
+
/** Loss ratio over the tracker's window at the moment of the decision. */
|
|
25223
|
+
lossRatio: number().nullable(),
|
|
25224
|
+
/** The client's reported downlink at that moment, in kbps. */
|
|
25225
|
+
clientKbps: number().nullable(),
|
|
25226
|
+
/** How long the stall detector had been seeing nothing arrive. */
|
|
25227
|
+
stalledForMs: number().nullable(),
|
|
25228
|
+
/** Whether the broker WAS sending while that was measured — the half that
|
|
25229
|
+
* separates "the camera stopped" from "the link stopped". */
|
|
25230
|
+
serverSending: boolean().nullable(),
|
|
25231
|
+
/** Epoch ms. The overlay prints its age; a decision from 40 minutes ago
|
|
25232
|
+
* must not read like one from this second. */
|
|
25233
|
+
at: number()
|
|
25234
|
+
});
|
|
25235
|
+
/** Per-tier count of MEASURED downgrades this viewer session has suffered. */
|
|
25236
|
+
var WebrtcTierFailureSchema = object({
|
|
25237
|
+
tier: CamProfileSchema,
|
|
25238
|
+
failures: number().int().nonnegative()
|
|
25239
|
+
});
|
|
25240
|
+
/**
|
|
25241
|
+
* What the SERVER decided for one WebRTC session — the half the client cannot
|
|
25242
|
+
* measure. See `getSessionDebug`.
|
|
25243
|
+
*/
|
|
25244
|
+
var WebrtcSessionDebugSchema = object({
|
|
25245
|
+
sessionId: string(),
|
|
25246
|
+
/** Whether the session still exists here. Everything below is null when
|
|
25247
|
+
* it does not — a dead session has no measurements, not zero ones. */
|
|
25248
|
+
alive: boolean(),
|
|
25249
|
+
/** Epoch ms this snapshot was taken, so the overlay can show its age. */
|
|
25250
|
+
observedAt: number(),
|
|
25251
|
+
/** The broker actually feeding this session (`<deviceId>/<camStreamId>`). */
|
|
25252
|
+
brokerId: string().nullable(),
|
|
25253
|
+
camStreamId: string().nullable(),
|
|
25254
|
+
/** `derived` = this hub BUILT the rung being served (a transcode/scale of
|
|
25255
|
+
* another stream); `native` = the camera's own encode. */
|
|
25256
|
+
sourceKind: _enum([
|
|
25257
|
+
"native",
|
|
25258
|
+
"derived",
|
|
25259
|
+
"unknown"
|
|
25260
|
+
]),
|
|
25261
|
+
/** Whether the ladder is running at all (an `adaptive` target). */
|
|
25262
|
+
adaptive: boolean(),
|
|
25263
|
+
/** The rung `auto` ACTUALLY resolved to right now. Null when the broker
|
|
25264
|
+
* is bound to no profile slot (a manually-activated stream). */
|
|
25265
|
+
servedTier: CamProfileSchema.nullable(),
|
|
25266
|
+
/** Which node ingests the source. Null when unresolved. */
|
|
25267
|
+
ingestNodeId: string().nullable(),
|
|
25268
|
+
/** Whether that node is a remote one (the media crosses a node boundary
|
|
25269
|
+
* before it reaches this session). Null when unresolved. */
|
|
25270
|
+
ingestIsRemote: boolean().nullable(),
|
|
25271
|
+
/** Codec off the source, and the one negotiated with this client. */
|
|
25272
|
+
sourceCodec: string().nullable(),
|
|
25273
|
+
negotiatedCodec: string().nullable(),
|
|
25274
|
+
/** Whether the EGRESS leg re-encodes for this client or forwards the
|
|
25275
|
+
* source bytes untouched. */
|
|
25276
|
+
egressLeg: _enum([
|
|
25277
|
+
"copy",
|
|
25278
|
+
"transcode",
|
|
25279
|
+
"unknown"
|
|
25280
|
+
]),
|
|
25281
|
+
/** What the broker itself reports about the rung it is serving. */
|
|
25282
|
+
brokerStatus: string().nullable(),
|
|
25283
|
+
brokerCodec: string().nullable(),
|
|
25284
|
+
brokerBitrateKbps: number().nullable(),
|
|
25285
|
+
brokerInputFps: number().nullable(),
|
|
25286
|
+
brokerUptimeMs: number().nullable(),
|
|
25287
|
+
/** RTP packets this session has sent — the server's own "am I sending?". */
|
|
25288
|
+
rtpPacketsSent: number().nullable(),
|
|
25289
|
+
/** D42: the track may be carrying the recorded feeder, not live. The ladder
|
|
25290
|
+
* is suspended there, so a frozen picture in `recorded` mode means
|
|
25291
|
+
* something entirely different. */
|
|
25292
|
+
trackMode: _enum(["live", "recorded"]).nullable(),
|
|
25293
|
+
lastDecision: WebrtcLadderDecisionSchema.nullable(),
|
|
25294
|
+
tierFailures: array(WebrtcTierFailureSchema),
|
|
25295
|
+
/** Successful promotions in this viewer session (drives the progressive
|
|
25296
|
+
* healthy timer). Null when no ladder is running. */
|
|
25297
|
+
upgradeCount: number().nullable()
|
|
25298
|
+
});
|
|
25156
25299
|
DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), array(WebrtcStreamChoiceSchema)), method(object({
|
|
25157
25300
|
deviceId: number().int().nonnegative(),
|
|
25158
25301
|
target: WebrtcStreamTargetSchema,
|
|
@@ -25317,7 +25460,10 @@ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), ar
|
|
|
25317
25460
|
* the same answer as "it ended": either way nothing is holding it up.
|
|
25318
25461
|
*/
|
|
25319
25462
|
alive: boolean()
|
|
25320
|
-
}))
|
|
25463
|
+
})), method(object({
|
|
25464
|
+
deviceId: number().int().nonnegative(),
|
|
25465
|
+
sessionId: string()
|
|
25466
|
+
}), WebrtcSessionDebugSchema, { kind: "query" });
|
|
25321
25467
|
object({
|
|
25322
25468
|
/** All accessory children of the parent. */
|
|
25323
25469
|
childDeviceIds: array(number()).readonly(),
|
|
@@ -29652,11 +29798,15 @@ var motionTriggerCapability = {
|
|
|
29652
29798
|
*
|
|
29653
29799
|
* Adding a fifth name to that list would have been the wrong fix twice over:
|
|
29654
29800
|
* that page is per-camera DETECTION tuning, and a grid's geometry belongs
|
|
29655
|
-
* beside PTZ and motion zones on the camera itself
|
|
29656
|
-
*
|
|
29657
|
-
*
|
|
29658
|
-
*
|
|
29659
|
-
*
|
|
29801
|
+
* beside PTZ and motion zones on the camera itself. The device page is
|
|
29802
|
+
* BINDING-driven (D12), so the way in is a capability bound to the device —
|
|
29803
|
+
* and this cap carries its section the way `recording` does, by RETURNING it
|
|
29804
|
+
* from `getDeviceSettingsContribution`.
|
|
29805
|
+
*
|
|
29806
|
+
* Seven other widgets are still declared the other way, through a
|
|
29807
|
+
* `deviceConfig.ui` block the framework derives a section from. That route
|
|
29808
|
+
* gives the addon no say in where its own panel lands and no way to decline
|
|
29809
|
+
* for a device the panel does not suit, which is why this one does not use it.
|
|
29660
29810
|
*
|
|
29661
29811
|
* ## Why one addon may implement it
|
|
29662
29812
|
*
|
|
@@ -29712,16 +29862,37 @@ var GridProfileOfferSchema = object({
|
|
|
29712
29862
|
]),
|
|
29713
29863
|
offered: boolean(),
|
|
29714
29864
|
/** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
|
|
29715
|
-
missingSources: array(number().int().positive())
|
|
29865
|
+
missingSources: array(number().int().positive()),
|
|
29866
|
+
/**
|
|
29867
|
+
* The canvas this profile composes onto, `WxH`, or empty when it is not
|
|
29868
|
+
* offered. DERIVED from the cells and the sources' own size at this profile —
|
|
29869
|
+
* it is reported because nothing else in the system would ever say what the
|
|
29870
|
+
* grid came out as, and because it is the number an operator would otherwise
|
|
29871
|
+
* expect to type.
|
|
29872
|
+
*/
|
|
29873
|
+
canvas: string(),
|
|
29874
|
+
/**
|
|
29875
|
+
* Whether this profile is PUBLISHED, of the ones the grid could serve.
|
|
29876
|
+
*
|
|
29877
|
+
* A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
|
|
29878
|
+
* a 4K canvas built from 4K decodes — something to opt into, not something a
|
|
29879
|
+
* viewer's adaptive should be handed by climbing to the top rung it can see.
|
|
29880
|
+
* Default is `mid` + `low`.
|
|
29881
|
+
*/
|
|
29882
|
+
published: boolean()
|
|
29716
29883
|
});
|
|
29717
29884
|
var GridLayoutViewSchema = object({
|
|
29718
29885
|
/** The persisted grid row this camera was declared from. */
|
|
29719
29886
|
instanceId: string(),
|
|
29720
29887
|
deviceId: number().int().nonnegative(),
|
|
29721
29888
|
name: string(),
|
|
29722
|
-
/**
|
|
29723
|
-
|
|
29724
|
-
|
|
29889
|
+
/**
|
|
29890
|
+
* NO canvas size. A grid's resolution is not authored: each profile derives
|
|
29891
|
+
* its own from the cells and its sources' dimensions. The two numbers that
|
|
29892
|
+
* used to be here were a text field that silently decided both how much the
|
|
29893
|
+
* composite cost and how sharp it was — see `profiles[].canvas` for what it
|
|
29894
|
+
* came out as.
|
|
29895
|
+
*/
|
|
29725
29896
|
fps: number().int(),
|
|
29726
29897
|
cells: array(GridLayoutCellSchema),
|
|
29727
29898
|
/** What the catalog will publish, and what it refuses to. Read-only. */
|
|
@@ -29730,9 +29901,13 @@ var GridLayoutViewSchema = object({
|
|
|
29730
29901
|
var GridLayoutPatchSchema = object({
|
|
29731
29902
|
deviceId: number().int().nonnegative(),
|
|
29732
29903
|
name: string().min(1).max(160).optional(),
|
|
29733
|
-
width: number().int().min(160).max(7680).optional(),
|
|
29734
|
-
height: number().int().min(120).max(4320).optional(),
|
|
29735
29904
|
fps: number().int().min(1).max(60).optional(),
|
|
29905
|
+
/** Which profiles to publish. See `GridProfileOffer.published`. */
|
|
29906
|
+
publishedProfiles: array(_enum([
|
|
29907
|
+
"high",
|
|
29908
|
+
"mid",
|
|
29909
|
+
"low"
|
|
29910
|
+
])).max(3).optional(),
|
|
29736
29911
|
/**
|
|
29737
29912
|
* The whole cell list at once. A per-cell patch would need an ordering the
|
|
29738
29913
|
* editor does not have, and a half-applied layout is a picture nobody asked
|
|
@@ -32147,6 +32322,25 @@ var ptzAutotrackCapability = {
|
|
|
32147
32322
|
scope: "device",
|
|
32148
32323
|
deviceNative: true,
|
|
32149
32324
|
mode: "singleton",
|
|
32325
|
+
/**
|
|
32326
|
+
* Two kinds of provider behind one name, the same shape `snapshot` and
|
|
32327
|
+
* `doorbell` use:
|
|
32328
|
+
*
|
|
32329
|
+
* - **Native**: the camera's own firmware autotrack, registered per device by
|
|
32330
|
+
* the driver when the probe says it exists (Reolink: yes; Hikvision:
|
|
32331
|
+
* model-dependent).
|
|
32332
|
+
* - **Wrapper**: the emulated implementation (`@camstack/system`'s `autotrack` builtin),
|
|
32333
|
+
* which follows a subject with `ptz.move` pulses driven by the detection
|
|
32334
|
+
* pipeline. `defaultActive: false` — the operator binds it per camera in
|
|
32335
|
+
* the bindings UI, exactly as `virtual-doorbell` is bound.
|
|
32336
|
+
*
|
|
32337
|
+
* The binding chooses WHICH implementation answers; `setEnabled` stays the
|
|
32338
|
+
* one switch for the function either way. A camera never has both driving its
|
|
32339
|
+
* head, which is the whole reason this is not a second cap with a second
|
|
32340
|
+
* toggle (D62, D520).
|
|
32341
|
+
*/
|
|
32342
|
+
kind: "wrapper",
|
|
32343
|
+
defaultActive: false,
|
|
32150
32344
|
deviceTypes: [DeviceType.Camera],
|
|
32151
32345
|
deviceConfig: { ui: {
|
|
32152
32346
|
kind: "widget",
|
|
@@ -43518,6 +43712,12 @@ Object.freeze({
|
|
|
43518
43712
|
addonId: null,
|
|
43519
43713
|
access: "view"
|
|
43520
43714
|
},
|
|
43715
|
+
"webrtcSession.getSessionDebug": {
|
|
43716
|
+
capName: "webrtc-session",
|
|
43717
|
+
capScope: "device",
|
|
43718
|
+
addonId: null,
|
|
43719
|
+
access: "view"
|
|
43720
|
+
},
|
|
43521
43721
|
"webrtcSession.getSessionState": {
|
|
43522
43722
|
capName: "webrtc-session",
|
|
43523
43723
|
capScope: "device",
|
|
@@ -45501,6 +45701,11 @@ Object.freeze({
|
|
|
45501
45701
|
form: "single",
|
|
45502
45702
|
optional: false
|
|
45503
45703
|
}],
|
|
45704
|
+
"webrtcSession.getSessionDebug": [{
|
|
45705
|
+
name: "deviceId",
|
|
45706
|
+
form: "single",
|
|
45707
|
+
optional: false
|
|
45708
|
+
}],
|
|
45504
45709
|
"webrtcSession.getSessionState": [{
|
|
45505
45710
|
name: "deviceId",
|
|
45506
45711
|
form: "single",
|
|
@@ -75186,7 +75391,7 @@ var require_lz4 = /* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
|
|
|
75186
75391
|
};
|
|
75187
75392
|
}));
|
|
75188
75393
|
//#endregion
|
|
75189
|
-
//#region node_modules/@apocaliss92/nodelink-js/dist/chunk-
|
|
75394
|
+
//#region node_modules/@apocaliss92/nodelink-js/dist/chunk-7JMYBUEB.js
|
|
75190
75395
|
var import_fxp = require_fxp();
|
|
75191
75396
|
var import_lz4 = /* @__PURE__ */ require_chunk.__toESM(require_lz4(), 1);
|
|
75192
75397
|
function encodeHeader(h) {
|
|
@@ -83646,10 +83851,14 @@ function applyOsdDatetimePatch(rawXml, patch) {
|
|
|
83646
83851
|
return xml;
|
|
83647
83852
|
}
|
|
83648
83853
|
var OSD_POSITION_MAX = 65536;
|
|
83854
|
+
var OSD_POSITION_START_WRITE = 1;
|
|
83855
|
+
var OSD_POSITION_CENTER = 65537;
|
|
83649
83856
|
var OSD_CORNERS = [
|
|
83650
83857
|
"top-left",
|
|
83858
|
+
"top-center",
|
|
83651
83859
|
"top-right",
|
|
83652
83860
|
"bottom-left",
|
|
83861
|
+
"bottom-center",
|
|
83653
83862
|
"bottom-right"
|
|
83654
83863
|
];
|
|
83655
83864
|
function isStartEdge(value) {
|
|
@@ -83661,7 +83870,7 @@ function isEndEdge(value) {
|
|
|
83661
83870
|
function readOsdPosition(x, y) {
|
|
83662
83871
|
if (typeof x !== "number" || typeof y !== "number") return { kind: "unknown" };
|
|
83663
83872
|
if (!Number.isFinite(x) || !Number.isFinite(y)) return { kind: "unknown" };
|
|
83664
|
-
const horizontal = isStartEdge(x) ? "left" : isEndEdge(x) ? "right" : null;
|
|
83873
|
+
const horizontal = x === OSD_POSITION_CENTER ? "center" : isStartEdge(x) ? "left" : isEndEdge(x) ? "right" : null;
|
|
83665
83874
|
const vertical = isStartEdge(y) ? "top" : isEndEdge(y) ? "bottom" : null;
|
|
83666
83875
|
if (horizontal === null || vertical === null) return {
|
|
83667
83876
|
kind: "custom",
|
|
@@ -83675,8 +83884,8 @@ function readOsdPosition(x, y) {
|
|
|
83675
83884
|
}
|
|
83676
83885
|
function coordsForOsdCorner(corner) {
|
|
83677
83886
|
return {
|
|
83678
|
-
x: corner.endsWith("-right") ? OSD_POSITION_MAX :
|
|
83679
|
-
y: corner.startsWith("bottom-") ? OSD_POSITION_MAX :
|
|
83887
|
+
x: corner.endsWith("-center") ? OSD_POSITION_CENTER : corner.endsWith("-right") ? OSD_POSITION_MAX : OSD_POSITION_START_WRITE,
|
|
83888
|
+
y: corner.startsWith("bottom-") ? OSD_POSITION_MAX : OSD_POSITION_START_WRITE
|
|
83680
83889
|
};
|
|
83681
83890
|
}
|
|
83682
83891
|
function isOsdCorner(value) {
|
|
@@ -83687,8 +83896,10 @@ function formatOsdCoords(x, y) {
|
|
|
83687
83896
|
}
|
|
83688
83897
|
var OSD_CORNER_LABELS = {
|
|
83689
83898
|
"top-left": "Top left",
|
|
83899
|
+
"top-center": "Top centre",
|
|
83690
83900
|
"top-right": "Top right",
|
|
83691
83901
|
"bottom-left": "Bottom left",
|
|
83902
|
+
"bottom-center": "Bottom centre",
|
|
83692
83903
|
"bottom-right": "Bottom right"
|
|
83693
83904
|
};
|
|
83694
83905
|
function wireFloat(value) {
|
|
@@ -88215,8 +88426,12 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
88215
88426
|
/** Generic Baichuan cmd_id call, returns XML (if any). */
|
|
88216
88427
|
async sendXml(params, retry = 3) {
|
|
88217
88428
|
if (!this.client.loggedIn) await this.client.login();
|
|
88218
|
-
const
|
|
88219
|
-
|
|
88429
|
+
const addressed = params.channel != null && params.extensionXml == null ? {
|
|
88430
|
+
...params,
|
|
88431
|
+
extensionXml: buildChannelExtensionXml(params.channel)
|
|
88432
|
+
} : params;
|
|
88433
|
+
const frame = await this.client.sendFrame(addressed);
|
|
88434
|
+
if (frame.header.responseCode === 400) return await this.handleSendXml400(addressed, frame, retry);
|
|
88220
88435
|
if (frame.body.length === 0) return "";
|
|
88221
88436
|
return this.client.tryDecryptXml(frame.body, frame.header.channelId, this.client.enc);
|
|
88222
88437
|
}
|
|
@@ -94517,6 +94732,7 @@ ${xml}`);
|
|
|
94517
94732
|
await this.sendXml({
|
|
94518
94733
|
cmdId: 45,
|
|
94519
94734
|
channel: ch,
|
|
94735
|
+
extensionXml: buildChannelExtensionXml(ch),
|
|
94520
94736
|
payloadXml: ensureXmlHeader(xml),
|
|
94521
94737
|
...timeoutOpts
|
|
94522
94738
|
});
|
|
@@ -145551,6 +145767,7 @@ var RAW_READ_CATALOG = {
|
|
|
145551
145767
|
kind: "deviceCache",
|
|
145552
145768
|
snapshotKey: "autoFocusSnapshot"
|
|
145553
145769
|
},
|
|
145770
|
+
needs: "ptz",
|
|
145554
145771
|
invoke: (api, channel) => api.getAutoFocus(channel, { timeoutMs: 1500 })
|
|
145555
145772
|
},
|
|
145556
145773
|
netPort: {
|
|
@@ -145747,6 +145964,7 @@ var RawReadResultSchema = discriminatedUnion("ok", [object({
|
|
|
145747
145964
|
slice: RawReadSliceSchema,
|
|
145748
145965
|
reason: _enum([
|
|
145749
145966
|
"sleeping",
|
|
145967
|
+
"unsupported",
|
|
145750
145968
|
"login-failed",
|
|
145751
145969
|
"read-failed"
|
|
145752
145970
|
]),
|
|
@@ -145790,6 +146008,20 @@ function toPlainJson(value) {
|
|
|
145790
146008
|
async function performRawRead(slice, deps) {
|
|
145791
146009
|
const entry = RAW_READ_CATALOG[slice];
|
|
145792
146010
|
const believed = believedState(deps, entry);
|
|
146011
|
+
if (entry.needs === "ptz" && !deps.hasPtz) {
|
|
146012
|
+
deps.logger.info("reolink raw read refused — the camera has no PTZ head", {
|
|
146013
|
+
tags: { deviceId: deps.deviceId },
|
|
146014
|
+
meta: { slice }
|
|
146015
|
+
});
|
|
146016
|
+
return {
|
|
146017
|
+
ok: false,
|
|
146018
|
+
deviceId: deps.deviceId,
|
|
146019
|
+
slice,
|
|
146020
|
+
reason: "unsupported",
|
|
146021
|
+
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.`,
|
|
146022
|
+
believed
|
|
146023
|
+
};
|
|
146024
|
+
}
|
|
145793
146025
|
const servedWithoutTouchingCamera = deps.hubChild && entry.servedByParentHub === true;
|
|
145794
146026
|
if (deps.sleeping && !servedWithoutTouchingCamera) {
|
|
145795
146027
|
deps.logger.info("reolink raw read refused — battery cam is sleeping", {
|
|
@@ -149453,7 +149685,6 @@ var reolinkCameraSchema = object({
|
|
|
149453
149685
|
* read via 52). Toggles the `<Shelter><enable>` flag. Zone
|
|
149454
149686
|
* editing is a separate flow not exposed by this driver.
|
|
149455
149687
|
*/
|
|
149456
|
-
privacyMaskEnabled: boolean().optional(),
|
|
149457
149688
|
/**
|
|
149458
149689
|
* Audio noise reduction level — pushed via `setAudioNoise`
|
|
149459
149690
|
* (cmd_id 440, read via 439). 0 disables; 1..3 maps to
|
|
@@ -150287,7 +150518,6 @@ function slicesForPatch(patch) {
|
|
|
150287
150518
|
if (has("motionEnabled", "motionSensitivity")) out.add("motion");
|
|
150288
150519
|
if (Object.keys(patch).some((k) => /^ai(Person|Vehicle|Animal|Face|Package)/.test(k))) out.add("ai");
|
|
150289
150520
|
if (has("gop")) out.add("enc");
|
|
150290
|
-
if (has("privacyMaskEnabled")) out.add("mask");
|
|
150291
150521
|
if (has("audioNoiseLevel")) out.add("audioNoise");
|
|
150292
150522
|
if (has("autoFocusEnabled")) out.add("autofocus");
|
|
150293
150523
|
if (has("netHttpEnable", "netHttpPort", "netHttpsEnable", "netHttpsPort", "netRtspEnable", "netRtspPort", "netRtmpEnable", "netRtmpPort", "netOnvifEnable", "netOnvifPort")) out.add("netPort");
|
|
@@ -150299,6 +150529,20 @@ function slicesForPatch(patch) {
|
|
|
150299
150529
|
if (has("timeZoneId")) out.add("systemGeneral");
|
|
150300
150530
|
return out;
|
|
150301
150531
|
}
|
|
150532
|
+
/**
|
|
150533
|
+
* What the operator's "Refresh from camera" actually managed.
|
|
150534
|
+
*
|
|
150535
|
+
* Returned rather than logged, because the button is the one place a person
|
|
150536
|
+
* asks "is what I am looking at true right now" — and an answer of `void`
|
|
150537
|
+
* cannot distinguish a full re-read from a camera that never woke.
|
|
150538
|
+
*/
|
|
150539
|
+
var RefreshSettingsResultSchema = object({
|
|
150540
|
+
/** False when a battery camera did not come up — everything below is then stale. */
|
|
150541
|
+
awake: boolean(),
|
|
150542
|
+
snapshotRefreshed: boolean(),
|
|
150543
|
+
capsWarmed: number(),
|
|
150544
|
+
capsTotal: number()
|
|
150545
|
+
});
|
|
150302
150546
|
var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
150303
150547
|
type = DeviceType.Camera;
|
|
150304
150548
|
/**
|
|
@@ -150768,9 +151012,12 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
150768
151012
|
* `setVideoInput`); motion is writable via `setMotionAlarm` so its
|
|
150769
151013
|
* snapshot also drives the dispatch's known-good baseline.
|
|
150770
151014
|
*/
|
|
150771
|
-
async refreshParentSettingsSnapshot(slices) {
|
|
150772
|
-
if (this.isBattery && this.sleeping) {
|
|
150773
|
-
this.ctx.logger.info("refreshParentSettingsSnapshot skipped — battery cam is sleeping", { tags: {
|
|
151015
|
+
async refreshParentSettingsSnapshot(slices, reason = "background") {
|
|
151016
|
+
if (this.isBattery && this.sleeping && reason !== "post-write") {
|
|
151017
|
+
this.ctx.logger.info("refreshParentSettingsSnapshot skipped — battery cam is sleeping", { tags: {
|
|
151018
|
+
deviceId: this.id,
|
|
151019
|
+
reason
|
|
151020
|
+
} });
|
|
150774
151021
|
return;
|
|
150775
151022
|
}
|
|
150776
151023
|
let api;
|
|
@@ -151015,6 +151262,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
151015
151262
|
channel: this.getChannel(),
|
|
151016
151263
|
sleeping: this.isBattery && this.sleeping,
|
|
151017
151264
|
hubChild: this.isHubChild(),
|
|
151265
|
+
hasPtz: this.getProbeFlags().hasPtz === true,
|
|
151018
151266
|
getApi: () => this.ensureApi(),
|
|
151019
151267
|
cache: this.config.get("deviceCache"),
|
|
151020
151268
|
batteryState: this.runtimeState.getCapState("battery"),
|
|
@@ -151108,6 +151356,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
151108
151356
|
});
|
|
151109
151357
|
await sleep$2(1500);
|
|
151110
151358
|
})(), new Promise((_, rej) => setTimeout(() => rej(/* @__PURE__ */ new Error("wakeIfSleeping timeout")), timeoutMs))]);
|
|
151359
|
+
if (this.commitSleepState(false, "confirmed-wake")) {
|
|
151360
|
+
this.ctx.logger.info("proactive action: camera is up — committing the mirror", { tags: { deviceId: this.id } });
|
|
151361
|
+
this.onWakeTransition("confirmed-wake").catch(() => {});
|
|
151362
|
+
}
|
|
151111
151363
|
return true;
|
|
151112
151364
|
} catch (err) {
|
|
151113
151365
|
this.ctx.logger.warn("proactive action: wake did not complete in time", {
|
|
@@ -151279,8 +151531,26 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
151279
151531
|
this.registerDeviceAction("auto-configure-email-push", deviceCustomAction(unknown(), _void(), { kind: "mutation" }), async () => {
|
|
151280
151532
|
await this.autoConfigureEmailPush();
|
|
151281
151533
|
});
|
|
151282
|
-
this.registerDeviceAction("refresh-settings", deviceCustomAction(unknown(),
|
|
151283
|
-
|
|
151534
|
+
this.registerDeviceAction("refresh-settings", deviceCustomAction(unknown(), RefreshSettingsResultSchema, { kind: "mutation" }), async () => {
|
|
151535
|
+
const awake = await this.wakeIfSleeping();
|
|
151536
|
+
const [snapshot, warm] = await Promise.allSettled([this.refreshParentSettingsSnapshot(void 0, "operator"), this.warmCapSlicesOnWake({ force: true })]);
|
|
151537
|
+
const warmed = warm.status === "fulfilled" ? warm.value.warmed : 0;
|
|
151538
|
+
const capsTotal = this.capWarmers.size;
|
|
151539
|
+
this.ctx.logger.info("refresh from camera: operator rehydrate", {
|
|
151540
|
+
tags: { deviceId: this.id },
|
|
151541
|
+
meta: {
|
|
151542
|
+
awake,
|
|
151543
|
+
snapshot: snapshot.status,
|
|
151544
|
+
warmed,
|
|
151545
|
+
capsTotal
|
|
151546
|
+
}
|
|
151547
|
+
});
|
|
151548
|
+
return {
|
|
151549
|
+
awake,
|
|
151550
|
+
snapshotRefreshed: snapshot.status === "fulfilled",
|
|
151551
|
+
capsWarmed: warmed,
|
|
151552
|
+
capsTotal
|
|
151553
|
+
};
|
|
151284
151554
|
});
|
|
151285
151555
|
this.registerDeviceAction("refresh-sessions", deviceCustomAction(unknown(), _void(), { kind: "mutation" }), async () => {
|
|
151286
151556
|
await this.refreshSessionsSnapshot();
|
|
@@ -151320,7 +151590,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
151320
151590
|
* registration so the general "Refresh from camera" button can warm
|
|
151321
151591
|
* the `stream-params` runtime-state slice (the editable Stream
|
|
151322
151592
|
* section's current values read from it). */
|
|
151323
|
-
refreshStreamParamsFromCamera = null;
|
|
151324
151593
|
/** Single-flight guard for the motion-zones cap refresh. */
|
|
151325
151594
|
motionZonesRefreshInFlight = null;
|
|
151326
151595
|
/** Single-flight guard for the privacy-mask cap refresh. */
|
|
@@ -151454,6 +151723,16 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
151454
151723
|
};
|
|
151455
151724
|
}
|
|
151456
151725
|
if (confirmed && !observedAwake && parent !== null) this.watchHubChildAwake(parent).catch(() => {});
|
|
151726
|
+
if (this.commitSleepState(false, "confirmed-wake")) {
|
|
151727
|
+
this.ctx.logger.info("battery wakeForStream: answering awake — committing the mirror", {
|
|
151728
|
+
tags: { deviceId: this.id },
|
|
151729
|
+
meta: {
|
|
151730
|
+
confirmedMs: Date.now() - startedAt,
|
|
151731
|
+
observedAwake
|
|
151732
|
+
}
|
|
151733
|
+
});
|
|
151734
|
+
this.onWakeTransition("confirmed-wake").catch(() => {});
|
|
151735
|
+
}
|
|
151457
151736
|
return {
|
|
151458
151737
|
awoke: true,
|
|
151459
151738
|
durationMs: Date.now() - startedAt
|
|
@@ -151579,7 +151858,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
151579
151858
|
commitSleepState(next, source) {
|
|
151580
151859
|
if (!this.isBattery) return false;
|
|
151581
151860
|
if (this.sleeping === next) return false;
|
|
151582
|
-
if (source
|
|
151861
|
+
if (source === "confirmed-wake" && next === false) this.sleepStateChangedAt = Date.now();
|
|
151862
|
+
else if (source !== "hub-summary" && !this.acceptSleepingTransition(next)) return false;
|
|
151583
151863
|
if (source === "hub-summary") {
|
|
151584
151864
|
const sinceCommitMs = Date.now() - this.sleepStateChangedAt;
|
|
151585
151865
|
if (next === true && this.sleepStateChangedAt > 0 && sinceCommitMs < ReolinkCamera.HUB_SUMMARY_SLEEP_GRACE_MS) {
|
|
@@ -152715,14 +152995,14 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
152715
152995
|
* swallowed: the window is short, partial progress accumulates across
|
|
152716
152996
|
* wakes because the slices persist.
|
|
152717
152997
|
*/
|
|
152718
|
-
async warmCapSlicesOnWake() {
|
|
152998
|
+
async warmCapSlicesOnWake(opts) {
|
|
152719
152999
|
const now = Date.now();
|
|
152720
153000
|
let warmed = 0;
|
|
152721
153001
|
let skipped = 0;
|
|
152722
153002
|
for (const [capName, warm] of this.capWarmers) {
|
|
152723
153003
|
const slice = this.runtimeState.getCapState(capName);
|
|
152724
153004
|
const fetchedAt = typeof slice?.lastFetchedAt === "number" ? slice.lastFetchedAt : 0;
|
|
152725
|
-
if (fetchedAt > 0 && now - fetchedAt < ReolinkCamera.WAKE_WARM_MAX_AGE_MS) {
|
|
153005
|
+
if (opts?.force !== true && fetchedAt > 0 && now - fetchedAt < ReolinkCamera.WAKE_WARM_MAX_AGE_MS) {
|
|
152726
153006
|
skipped += 1;
|
|
152727
153007
|
continue;
|
|
152728
153008
|
}
|
|
@@ -152747,6 +153027,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
152747
153027
|
total: this.capWarmers.size
|
|
152748
153028
|
}
|
|
152749
153029
|
});
|
|
153030
|
+
return {
|
|
153031
|
+
warmed,
|
|
153032
|
+
skipped
|
|
153033
|
+
};
|
|
152750
153034
|
}
|
|
152751
153035
|
/**
|
|
152752
153036
|
* Ask the snapshot wrapper for a frame while the camera is up, so its
|
|
@@ -152812,7 +153096,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
152812
153096
|
registerStreamParamsCap() {
|
|
152813
153097
|
const channel = this.getChannel();
|
|
152814
153098
|
const CAP_NAME = "stream-params";
|
|
152815
|
-
const STALE_MS = 1e4;
|
|
152816
153099
|
/** Round-trip `getEnc`, map into the cap status schema, write slice. */
|
|
152817
153100
|
const refreshFromCamera = async () => {
|
|
152818
153101
|
if (this.streamParamsRefreshInFlight) return this.streamParamsRefreshInFlight;
|
|
@@ -152845,13 +153128,13 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
152845
153128
|
this.streamParamsRefreshInFlight = null;
|
|
152846
153129
|
}
|
|
152847
153130
|
};
|
|
152848
|
-
this.refreshStreamParamsFromCamera = refreshFromCamera;
|
|
152849
153131
|
const bridge = createRuntimeStateBridge({
|
|
152850
153132
|
runtimeState: this.runtimeState,
|
|
152851
153133
|
cap: streamParamsCapability,
|
|
152852
153134
|
ownDeviceId: this.id,
|
|
152853
153135
|
refresh: this.sleepGatedRefresh(CAP_NAME, refreshFromCamera),
|
|
152854
|
-
staleMs:
|
|
153136
|
+
staleMs: OPERATOR_WRITTEN_STALE_MS,
|
|
153137
|
+
staleRead: "serve-and-revalidate",
|
|
152855
153138
|
empty: () => ({ lastFetchedAt: 0 })
|
|
152856
153139
|
});
|
|
152857
153140
|
const resolveOptions = async () => this.resolveCapOptions({
|
|
@@ -152936,7 +153219,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
152936
153219
|
registerMotionZonesCap() {
|
|
152937
153220
|
const channel = this.getChannel();
|
|
152938
153221
|
const CAP_NAME = "motion-zones";
|
|
152939
|
-
const STALE_MS = 1e4;
|
|
152940
153222
|
/** Round-trip `getMotionAlarm`, map into the cap status schema, write slice. */
|
|
152941
153223
|
const refreshFromCamera = async () => {
|
|
152942
153224
|
if (this.motionZonesRefreshInFlight) return this.motionZonesRefreshInFlight;
|
|
@@ -152986,7 +153268,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
152986
153268
|
cap: motionZonesCapability,
|
|
152987
153269
|
ownDeviceId: this.id,
|
|
152988
153270
|
refresh: this.sleepGatedRefresh(CAP_NAME, refreshFromCamera),
|
|
152989
|
-
staleMs:
|
|
153271
|
+
staleMs: OPERATOR_WRITTEN_STALE_MS,
|
|
153272
|
+
staleRead: "serve-and-revalidate",
|
|
152990
153273
|
empty: () => ({
|
|
152991
153274
|
enabled: false,
|
|
152992
153275
|
sensitivity: 1,
|
|
@@ -153317,7 +153600,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
153317
153600
|
registerDayNightCap() {
|
|
153318
153601
|
const channel = this.getChannel();
|
|
153319
153602
|
const CAP_NAME = "day-night";
|
|
153320
|
-
const STALE_MS = 1e4;
|
|
153321
153603
|
/** Round-trip `getVideoInput` + `getDayNightThreshold`, map into the
|
|
153322
153604
|
* cap status schema, write slice. */
|
|
153323
153605
|
const refreshFromCamera = async () => {
|
|
@@ -153360,7 +153642,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
153360
153642
|
cap: dayNightCapability,
|
|
153361
153643
|
ownDeviceId: this.id,
|
|
153362
153644
|
refresh: this.sleepGatedRefresh(CAP_NAME, refreshFromCamera),
|
|
153363
|
-
staleMs:
|
|
153645
|
+
staleMs: OPERATOR_WRITTEN_STALE_MS,
|
|
153646
|
+
staleRead: "serve-and-revalidate",
|
|
153364
153647
|
empty: () => ({
|
|
153365
153648
|
mode: "auto",
|
|
153366
153649
|
lastFetchedAt: 0
|
|
@@ -153453,7 +153736,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
153453
153736
|
registerImageSettingsCap() {
|
|
153454
153737
|
const channel = this.getChannel();
|
|
153455
153738
|
const CAP_NAME = "image-settings";
|
|
153456
|
-
const STALE_MS = 1e4;
|
|
153457
153739
|
const SLIDER_MIN = 0;
|
|
153458
153740
|
const SLIDER_MAX = 255;
|
|
153459
153741
|
const NORMALIZED_RANGE = {
|
|
@@ -153497,7 +153779,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
153497
153779
|
cap: imageSettingsCapability,
|
|
153498
153780
|
ownDeviceId: this.id,
|
|
153499
153781
|
refresh: this.sleepGatedRefresh(CAP_NAME, refreshFromCamera),
|
|
153500
|
-
staleMs:
|
|
153782
|
+
staleMs: OPERATOR_WRITTEN_STALE_MS,
|
|
153783
|
+
staleRead: "serve-and-revalidate",
|
|
153501
153784
|
empty: () => ({ lastFetchedAt: 0 })
|
|
153502
153785
|
}).getStatus,
|
|
153503
153786
|
getOptions: async ({ deviceId }) => {
|
|
@@ -153814,7 +154097,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
153814
154097
|
/** Stale-after window for served-from-cache reads. ~10s is short
|
|
153815
154098
|
* enough that the UI's 5s poll always sees a fresh round-trip,
|
|
153816
154099
|
* long enough that two consumers in the same tick fold into one. */
|
|
153817
|
-
const STALE_MS = 1e4;
|
|
153818
154100
|
const SUPPORTED_TARGET_TYPES = [
|
|
153819
154101
|
{
|
|
153820
154102
|
value: "people",
|
|
@@ -153880,7 +154162,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
153880
154162
|
cap: ptzAutotrackCapability,
|
|
153881
154163
|
ownDeviceId: this.id,
|
|
153882
154164
|
refresh: this.sleepGatedRefresh(CAP_NAME, refreshFromCamera),
|
|
153883
|
-
staleMs:
|
|
154165
|
+
staleMs: OPERATOR_WRITTEN_STALE_MS,
|
|
154166
|
+
staleRead: "serve-and-revalidate",
|
|
153884
154167
|
empty: () => ({
|
|
153885
154168
|
enabled: false,
|
|
153886
154169
|
lastChangedAt: 0,
|
|
@@ -155641,7 +155924,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
155641
155924
|
id: "driver-refresh",
|
|
155642
155925
|
tab: "general",
|
|
155643
155926
|
title: "Camera data",
|
|
155644
|
-
description: "
|
|
155927
|
+
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.",
|
|
155645
155928
|
columns: 1,
|
|
155646
155929
|
fields: [{
|
|
155647
155930
|
type: "device-action-button",
|
|
@@ -155649,7 +155932,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
155649
155932
|
label: "Refresh from camera",
|
|
155650
155933
|
buttonLabel: "Refresh now",
|
|
155651
155934
|
action: "refresh-settings",
|
|
155652
|
-
variant: "default"
|
|
155935
|
+
variant: "default",
|
|
155936
|
+
successMessage: "Camera awake: {awake} · settings re-read: {snapshotRefreshed} · slices refreshed: {capsWarmed}/{capsTotal}"
|
|
155653
155937
|
}]
|
|
155654
155938
|
},
|
|
155655
155939
|
{
|
|
@@ -156136,20 +156420,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
156136
156420
|
sleeping: this.isBattery && this.sleeping,
|
|
156137
156421
|
now: Date.now()
|
|
156138
156422
|
}),
|
|
156139
|
-
{
|
|
156140
|
-
id: "privacy-mask",
|
|
156141
|
-
tab: "image",
|
|
156142
|
-
title: "Privacy mask",
|
|
156143
|
-
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.",
|
|
156144
|
-
columns: 1,
|
|
156145
|
-
fields: [{
|
|
156146
|
-
type: "boolean",
|
|
156147
|
-
key: "privacyMaskEnabled",
|
|
156148
|
-
label: "Privacy mask enabled",
|
|
156149
|
-
default: cache?.maskSnapshot?.enabled ?? false,
|
|
156150
|
-
style: "switch"
|
|
156151
|
-
}]
|
|
156152
|
-
},
|
|
156153
156423
|
...cache?.autoFocusSnapshot?.supported === true ? [{
|
|
156154
156424
|
id: "auto-focus",
|
|
156155
156425
|
tab: "image",
|
|
@@ -156504,7 +156774,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
156504
156774
|
audioVolume: this.config.get("audioVolume") ?? 50,
|
|
156505
156775
|
audioTalkAndReplyVolume: this.config.get("audioTalkAndReplyVolume") ?? 50,
|
|
156506
156776
|
audioVisitorVolume: this.config.get("audioVisitorVolume") ?? 50,
|
|
156507
|
-
privacyMaskEnabled: this.config.get("privacyMaskEnabled") ?? cache?.maskSnapshot?.enabled ?? false,
|
|
156508
156777
|
audioNoiseLevel: this.config.get("audioNoiseLevel") ?? (cache?.audioNoiseSnapshot?.enabled ? cache.audioNoiseSnapshot?.level ?? 1 : 0),
|
|
156509
156778
|
autoFocusEnabled: this.config.get("autoFocusEnabled") ?? cache?.autoFocusSnapshot?.enabled ?? true,
|
|
156510
156779
|
pirReduceFalseAlarm: this.config.get("pirReduceFalseAlarm") ?? cache?.pirSnapshot?.reduceFalseAlarm ?? false,
|
|
@@ -156520,6 +156789,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
156520
156789
|
}
|
|
156521
156790
|
async applySettingsPatch(patch) {
|
|
156522
156791
|
const { emailSmtpServer, emailSmtpPort, emailUserName, emailPassword, emailSsl, emailRecipient1, emailRecipient2, emailRecipient3, emailSendNickname, emailScheduleEnabled, osdChannelEnabled, osdChannelName, osdChannelPosition, osdTimeEnabled, osdTimePosition, osdWatermark, ...rest } = patch;
|
|
156792
|
+
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 } });
|
|
156523
156793
|
const emailFields = {
|
|
156524
156794
|
emailSmtpServer,
|
|
156525
156795
|
emailSmtpPort,
|
|
@@ -156830,13 +157100,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
156830
157100
|
this.ctx.logger.warn("reolink setNtp push failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
156831
157101
|
}
|
|
156832
157102
|
}
|
|
156833
|
-
if ("privacyMaskEnabled" in patch) try {
|
|
156834
|
-
const api = await this.ensureApi();
|
|
156835
|
-
const v = typedPatch.privacyMaskEnabled;
|
|
156836
|
-
if (typeof v === "boolean") await api.setMask(this.getChannel(), { enable: v });
|
|
156837
|
-
} catch (err) {
|
|
156838
|
-
this.ctx.logger.warn("mask push failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
156839
|
-
}
|
|
156840
157103
|
if ("audioNoiseLevel" in patch) try {
|
|
156841
157104
|
const api = await this.ensureApi();
|
|
156842
157105
|
const v = typedPatch.audioNoiseLevel;
|
|
@@ -156894,15 +157157,40 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
156894
157157
|
}
|
|
156895
157158
|
}
|
|
156896
157159
|
const changedSlices = slicesForPatch(patch);
|
|
156897
|
-
if (changedSlices.size > 0)
|
|
156898
|
-
this.
|
|
156899
|
-
|
|
156900
|
-
|
|
156901
|
-
|
|
156902
|
-
|
|
156903
|
-
|
|
157160
|
+
if (changedSlices.size > 0) {
|
|
157161
|
+
await this.refreshParentSettingsSnapshot(changedSlices, "post-write").catch((err) => {
|
|
157162
|
+
this.ctx.logger.info("reolink targeted settings refresh failed — snapshots left stale", {
|
|
157163
|
+
tags: { deviceId: this.id },
|
|
157164
|
+
meta: {
|
|
157165
|
+
slices: [...changedSlices],
|
|
157166
|
+
error: err instanceof Error ? err.message : String(err)
|
|
157167
|
+
}
|
|
157168
|
+
});
|
|
156904
157169
|
});
|
|
156905
|
-
|
|
157170
|
+
this.assertOsdWriteLanded(patch);
|
|
157171
|
+
}
|
|
157172
|
+
}
|
|
157173
|
+
/**
|
|
157174
|
+
* Compare what the operator asked for with what the camera now reports, and
|
|
157175
|
+
* REFUSE to call a lost write a success.
|
|
157176
|
+
*
|
|
157177
|
+
* Nothing used to do this, so an applied write and a write the camera never
|
|
157178
|
+
* took were indistinguishable — to the code and to the operator, who was
|
|
157179
|
+
* told `{success: true}` for a change the camera discarded (3825,
|
|
157180
|
+
* 2026-09-19). Throwing is the right shape: the admin UI's in-flight
|
|
157181
|
+
* overlay hands the operator their own edit back, marked unsaved, instead
|
|
157182
|
+
* of quietly reverting the field to the old value.
|
|
157183
|
+
*
|
|
157184
|
+
* Silence is the pass. A slice we could not re-read at all is already
|
|
157185
|
+
* reported by the caller as "snapshots left stale" — absence of a reading
|
|
157186
|
+
* is not evidence of a lost write, and must never be treated as one (D393).
|
|
157187
|
+
*/
|
|
157188
|
+
assertOsdWriteLanded(patch) {
|
|
157189
|
+
const asked = patch.osdTimePosition;
|
|
157190
|
+
if (typeof asked !== "string") return;
|
|
157191
|
+
const got = resolveOsdValues(this.config.get("deviceCache")?.osdSnapshot).osdTimePosition;
|
|
157192
|
+
if (typeof got !== "string" || got === asked) return;
|
|
157193
|
+
throw new Error(`the camera did not take the timestamp position: asked "${asked}", it reports "${got}"`);
|
|
156906
157194
|
}
|
|
156907
157195
|
/**
|
|
156908
157196
|
* Optionally inject an already-logged-in Baichuan api — used by
|