@camstack/addon-provider-reolink 1.1.18 → 1.1.19

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.
Files changed (3) hide show
  1. package/dist/addon.js +645 -10
  2. package/dist/addon.mjs +645 -10
  3. package/package.json +1 -1
package/dist/addon.mjs CHANGED
@@ -4650,7 +4650,7 @@ function _instanceof(cls, params = {}) {
4650
4650
  return inst;
4651
4651
  }
4652
4652
  //#endregion
4653
- //#region ../types/dist/sleep-BiDFW0E7.mjs
4653
+ //#region ../types/dist/sleep-CZDdRBua.mjs
4654
4654
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4655
4655
  EventCategory["SystemBoot"] = "system.boot";
4656
4656
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -7448,7 +7448,16 @@ var DecoderStatsSchema = object({
7448
7448
  inputFps: number(),
7449
7449
  outputFps: number(),
7450
7450
  avgDecodeTimeMs: number(),
7451
- droppedFrames: number()
7451
+ droppedFrames: number(),
7452
+ /**
7453
+ * Pull-mode adaptive-fps telemetry (optional — only pull sessions run the
7454
+ * lag-driven controller; push sessions omit these). `lagMs` is the EWMA of
7455
+ * the decoder's real-time drift (rising = falling behind live); `adaptiveFps`
7456
+ * is the current lag-throttled emit rate (≤ `effectiveFps` ceiling).
7457
+ */
7458
+ lagMs: number().optional(),
7459
+ effectiveFps: number().optional(),
7460
+ adaptiveFps: number().optional()
7452
7461
  });
7453
7462
  var DecoderSessionConfigSchema = object({
7454
7463
  codec: string(),
@@ -7489,7 +7498,15 @@ var DecoderSessionConfigSchema = object({
7489
7498
  * other — `pullFrames` returns nothing for an `'shm'` session and
7490
7499
  * `pullHandles` returns nothing for a `'callback'` session.
7491
7500
  */
7492
- frameSink: _enum(["callback", "shm"]).default("callback")
7501
+ frameSink: _enum(["callback", "shm"]).default("callback"),
7502
+ /**
7503
+ * Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
7504
+ * a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
7505
+ * real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
7506
+ * stream-broker's `streamingDebug` gate — off by default so production logs
7507
+ * stay quiet and the emit path pays zero per-frame cost when disabled.
7508
+ */
7509
+ debug: boolean().optional()
7493
7510
  });
7494
7511
  var EncodeProfileSchema = object({
7495
7512
  video: object({
@@ -10553,6 +10570,100 @@ var coverCapability = {
10553
10570
  runtimeState: CoverStatusSchema
10554
10571
  };
10555
10572
  /**
10573
+ * Vendor-neutral day/night (IR-cut) control — the per-camera config cap
10574
+ * shared by reolink / hikvision / amcrest. Models the common firmware
10575
+ * surface: the IR-cut switching MODE plus the two knobs that gate it
10576
+ * (photocell `sensitivity` + `switchDelaySec`). Each vendor maps these
10577
+ * onto its own ISAPI / Baichuan / Dahua-CGI fields; the cap standardises
10578
+ * the shape so ONE derived-form renders every camera.
10579
+ *
10580
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
10581
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
10582
+ * injected from `status`) reports the live values, and a single
10583
+ * `setSettings` mutation applies a partial change. No hand-written
10584
+ * settings-contribution methods — the framework derives the UI + save
10585
+ * routing from this surface.
10586
+ */
10587
+ /** IR-cut switching mode. `schedule` = time-of-day table configured on the camera. */
10588
+ var DayNightModeSchema = _enum([
10589
+ "auto",
10590
+ "day",
10591
+ "night",
10592
+ "schedule"
10593
+ ]);
10594
+ /** Normalized numeric range descriptor — `{ min, max, step }` per the
10595
+ * getOptions availability convention. Normalized values are 0–100. */
10596
+ var NormalizedRangeSchema$1 = object({
10597
+ min: number(),
10598
+ max: number(),
10599
+ step: number()
10600
+ });
10601
+ /**
10602
+ * Current day/night state. Optional fields are absent when the camera
10603
+ * does not expose that knob (a photocell-less model reports no
10604
+ * `sensitivity`). `lastFetchedAt` feeds the runtime-state bridge.
10605
+ */
10606
+ var DayNightStatusSchema = object({
10607
+ mode: DayNightModeSchema,
10608
+ /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
10609
+ sensitivity: number().optional(),
10610
+ /** Delay before the IR-cut filter flips, in seconds. */
10611
+ switchDelaySec: number().optional(),
10612
+ lastFetchedAt: number()
10613
+ });
10614
+ /**
10615
+ * Per-camera availability descriptor — drives which controls the admin UI
10616
+ * renders. Booleans as `supportsX`; numeric ranges as `{ min, max, step }`
10617
+ * (normalized 0–100); the mode choice-set as an array. A provider returns
10618
+ * honest, camera-probed values — never hardcoded.
10619
+ */
10620
+ var DayNightOptionsSchema = object({
10621
+ /** Modes this camera accepts. Empty → the camera has no configurable day/night mode. */
10622
+ modes: array(DayNightModeSchema),
10623
+ supportsSensitivity: boolean(),
10624
+ /** Present when `supportsSensitivity` — the normalized 0–100 range. */
10625
+ sensitivity: NormalizedRangeSchema$1.optional(),
10626
+ supportsSwitchDelay: boolean(),
10627
+ /** Present when `supportsSwitchDelay` — the allowed delay range in seconds. */
10628
+ switchDelaySec: NormalizedRangeSchema$1.optional()
10629
+ });
10630
+ /**
10631
+ * Partial change to the day/night config — every field optional. A
10632
+ * provider ignores fields it does not support.
10633
+ */
10634
+ var DayNightSettingsPatchSchema = object({
10635
+ mode: DayNightModeSchema.optional(),
10636
+ sensitivity: number().optional(),
10637
+ switchDelaySec: number().optional()
10638
+ });
10639
+ var dayNightCapability = {
10640
+ name: "day-night",
10641
+ scope: "device",
10642
+ deviceNative: true,
10643
+ mode: "singleton",
10644
+ deviceTypes: [DeviceType.Camera],
10645
+ deviceConfig: { ui: {
10646
+ kind: "derived-form",
10647
+ builderId: "day-night",
10648
+ tab: "image"
10649
+ } },
10650
+ methods: {
10651
+ getOptions: method(object({ deviceId: number() }), DayNightOptionsSchema),
10652
+ setSettings: method(object({
10653
+ deviceId: number(),
10654
+ settings: DayNightSettingsPatchSchema
10655
+ }), _void(), {
10656
+ kind: "mutation",
10657
+ auth: "admin"
10658
+ })
10659
+ },
10660
+ status: {
10661
+ schema: DayNightStatusSchema,
10662
+ kind: "poll"
10663
+ },
10664
+ runtimeState: DayNightStatusSchema
10665
+ };
10666
+ /**
10556
10667
  * Identity envelope for a device's upstream-system metadata.
10557
10668
  *
10558
10669
  * Two jobs:
@@ -11211,6 +11322,155 @@ var imageCapability = {
11211
11322
  runtimeState: ImageStatusSchema
11212
11323
  };
11213
11324
  /**
11325
+ * Vendor-neutral image / picture-adjustment cap — the per-camera config
11326
+ * cap shared by reolink / hikvision / amcrest. Models the common ISP
11327
+ * surface: the four picture sliders (brightness / contrast / saturation /
11328
+ * sharpness), orientation (mirror / flip / rotate), white-balance,
11329
+ * exposure and backlight-compensation modes.
11330
+ *
11331
+ * NORMALIZATION: every slider is a normalized int 0–100. Vendors expose
11332
+ * these natively as 0–100 or 0–255 (or other ranges); each provider maps
11333
+ * its native range to/from this normalized 0–100 space so the cap surface
11334
+ * (and the derived form) is identical across cameras. `warmth` (manual
11335
+ * white-balance) is likewise normalized 0–100.
11336
+ *
11337
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
11338
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
11339
+ * injected from `status`) reports the live values, and a single
11340
+ * `setSettings` mutation applies a partial change. No hand-written
11341
+ * settings-contribution methods — the framework derives the UI + save
11342
+ * routing from this surface.
11343
+ */
11344
+ /** Sensor/image rotation, degrees clockwise. */
11345
+ var ImageRotateSchema = _enum([
11346
+ "0",
11347
+ "90",
11348
+ "180",
11349
+ "270"
11350
+ ]);
11351
+ /** White-balance mode. `manual` unlocks the normalized `warmth` knob. */
11352
+ var WhiteBalanceModeSchema = _enum(["auto", "manual"]);
11353
+ /** Exposure mode. */
11354
+ var ExposureModeSchema = _enum(["auto", "manual"]);
11355
+ /**
11356
+ * Backlight-compensation mode:
11357
+ * - `off` — disabled
11358
+ * - `blc` — backlight compensation
11359
+ * - `wdr` — wide dynamic range
11360
+ * - `hlc` — highlight compensation
11361
+ */
11362
+ var BacklightModeSchema = _enum([
11363
+ "off",
11364
+ "blc",
11365
+ "wdr",
11366
+ "hlc"
11367
+ ]);
11368
+ /** Normalized numeric range descriptor — `{ min, max, step }` per the
11369
+ * getOptions availability convention. Slider values are normalized 0–100. */
11370
+ var NormalizedRangeSchema = object({
11371
+ min: number(),
11372
+ max: number(),
11373
+ step: number()
11374
+ });
11375
+ /**
11376
+ * Current image-adjustment state. Every field optional — absent when the
11377
+ * camera does not expose that control. Slider values are NORMALIZED 0–100.
11378
+ * `lastFetchedAt` feeds the runtime-state bridge.
11379
+ */
11380
+ var ImageSettingsStatusSchema = object({
11381
+ /** Normalized 0–100. */
11382
+ brightness: number().optional(),
11383
+ /** Normalized 0–100. */
11384
+ contrast: number().optional(),
11385
+ /** Normalized 0–100. */
11386
+ saturation: number().optional(),
11387
+ /** Normalized 0–100. */
11388
+ sharpness: number().optional(),
11389
+ mirror: boolean().optional(),
11390
+ flip: boolean().optional(),
11391
+ rotate: ImageRotateSchema.optional(),
11392
+ whiteBalance: WhiteBalanceModeSchema.optional(),
11393
+ /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
11394
+ warmth: number().optional(),
11395
+ exposureMode: ExposureModeSchema.optional(),
11396
+ backlightMode: BacklightModeSchema.optional(),
11397
+ lastFetchedAt: number()
11398
+ });
11399
+ /**
11400
+ * Per-camera availability descriptor — drives which controls the admin UI
11401
+ * renders. Booleans as `supportsX`; numeric ranges as `{ min, max, step }`
11402
+ * (the normalized 0–100 range); enums as arrays of supported values (empty
11403
+ * array → control hidden). A provider returns honest, camera-probed values
11404
+ * — never hardcoded.
11405
+ */
11406
+ var ImageSettingsOptionsSchema = object({
11407
+ supportsBrightness: boolean(),
11408
+ brightness: NormalizedRangeSchema.optional(),
11409
+ supportsContrast: boolean(),
11410
+ contrast: NormalizedRangeSchema.optional(),
11411
+ supportsSaturation: boolean(),
11412
+ saturation: NormalizedRangeSchema.optional(),
11413
+ supportsSharpness: boolean(),
11414
+ sharpness: NormalizedRangeSchema.optional(),
11415
+ supportsMirror: boolean(),
11416
+ supportsFlip: boolean(),
11417
+ /** Supported rotation values. Empty → rotation not configurable. */
11418
+ rotateOptions: array(ImageRotateSchema),
11419
+ /** Supported white-balance modes. Empty → white-balance not configurable. */
11420
+ whiteBalanceModes: array(WhiteBalanceModeSchema),
11421
+ supportsWarmth: boolean(),
11422
+ /** Present when `supportsWarmth` — the normalized 0–100 range. */
11423
+ warmth: NormalizedRangeSchema.optional(),
11424
+ /** Supported exposure modes. Empty → exposure not configurable. */
11425
+ exposureModes: array(ExposureModeSchema),
11426
+ /** Supported backlight modes. Empty → backlight-compensation not configurable. */
11427
+ backlightModes: array(BacklightModeSchema)
11428
+ });
11429
+ /**
11430
+ * Partial change to the image config — every field optional. Slider values
11431
+ * are normalized 0–100. A provider ignores fields it does not support.
11432
+ */
11433
+ var ImageSettingsPatchSchema = object({
11434
+ brightness: number().optional(),
11435
+ contrast: number().optional(),
11436
+ saturation: number().optional(),
11437
+ sharpness: number().optional(),
11438
+ mirror: boolean().optional(),
11439
+ flip: boolean().optional(),
11440
+ rotate: ImageRotateSchema.optional(),
11441
+ whiteBalance: WhiteBalanceModeSchema.optional(),
11442
+ warmth: number().optional(),
11443
+ exposureMode: ExposureModeSchema.optional(),
11444
+ backlightMode: BacklightModeSchema.optional()
11445
+ });
11446
+ var imageSettingsCapability = {
11447
+ name: "image-settings",
11448
+ scope: "device",
11449
+ deviceNative: true,
11450
+ mode: "singleton",
11451
+ deviceTypes: [DeviceType.Camera],
11452
+ deviceConfig: { ui: {
11453
+ kind: "derived-form",
11454
+ builderId: "image-settings",
11455
+ tab: "image"
11456
+ } },
11457
+ methods: {
11458
+ getOptions: method(object({ deviceId: number() }), ImageSettingsOptionsSchema),
11459
+ setSettings: method(object({
11460
+ deviceId: number(),
11461
+ settings: ImageSettingsPatchSchema
11462
+ }), _void(), {
11463
+ kind: "mutation",
11464
+ auth: "admin"
11465
+ })
11466
+ },
11467
+ status: {
11468
+ schema: ImageSettingsStatusSchema,
11469
+ kind: "poll"
11470
+ },
11471
+ runtimeState: ImageSettingsStatusSchema
11472
+ };
11473
+ /**
11214
11474
  * Robotic lawn-mower cap. Models HA `lawn_mower.*` entities — anything
11215
11475
  * with a mowing lifecycle plus a dock action.
11216
11476
  *
@@ -12209,6 +12469,16 @@ var RunnerCameraConfigSchema = object({
12209
12469
  * this gate is bypassed.
12210
12470
  */
12211
12471
  onboardMotionDrivesAnalyzer: boolean().default(true),
12472
+ /**
12473
+ * Master toggle for the occupancy re-check. When `false` (DEFAULT) the runner
12474
+ * never arms the periodic recheck timer, regardless of `occupancyRecheckSec` —
12475
+ * this is off by default because the recheck re-subscribes a detection session
12476
+ * every N seconds while `watching`, a major source of pull-decoder re-dial
12477
+ * churn (each cycle creates+tears a session → RTSP re-dial → latency). The
12478
+ * `occupancyRecheckSec` / `occupancyRecheckFrames` sliders only take effect
12479
+ * (and only render) when this is enabled.
12480
+ */
12481
+ occupancyRecheckEnabled: boolean().default(false),
12212
12482
  occupancyRecheckSec: number().min(occupancyRecheckSecField.min).max(occupancyRecheckSecField.max).default(occupancyRecheckSecField.default),
12213
12483
  occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default),
12214
12484
  /**
@@ -14214,6 +14484,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
14214
14484
  contact: contactCapability,
14215
14485
  control: controlCapability,
14216
14486
  cover: coverCapability,
14487
+ dayNight: dayNightCapability,
14217
14488
  deviceDiscovery: deviceDiscoveryCapability,
14218
14489
  deviceStatus: deviceStatusCapability,
14219
14490
  doorbell: doorbellCapability,
@@ -14226,6 +14497,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
14226
14497
  humidifier: humidifierCapability,
14227
14498
  humiditySensor: humiditySensorCapability,
14228
14499
  image: imageCapability,
14500
+ imageSettings: imageSettingsCapability,
14229
14501
  lawnMowerControl: lawnMowerControlCapability,
14230
14502
  lockControl: lockControlCapability,
14231
14503
  mediaPlayer: mediaPlayerCapability,
@@ -16152,7 +16424,9 @@ method(object({ codec: string() }), boolean()), method(_void(), object({
16152
16424
  id: string(),
16153
16425
  name: string(),
16154
16426
  isPullMode: boolean().optional(),
16155
- priority: number().optional()
16427
+ priority: number().optional(),
16428
+ hwaccel: string().optional(),
16429
+ probedBestHwaccel: string().optional()
16156
16430
  })), method(DecoderSessionConfigSchema, object({
16157
16431
  sessionId: string(),
16158
16432
  nodeId: string()
@@ -22516,6 +22790,18 @@ Object.freeze({
22516
22790
  addonId: null,
22517
22791
  access: "view"
22518
22792
  },
22793
+ "dayNight.getOptions": {
22794
+ capName: "day-night",
22795
+ capScope: "device",
22796
+ addonId: null,
22797
+ access: "view"
22798
+ },
22799
+ "dayNight.setSettings": {
22800
+ capName: "day-night",
22801
+ capScope: "device",
22802
+ addonId: null,
22803
+ access: "create"
22804
+ },
22519
22805
  "decoder.createSession": {
22520
22806
  capName: "decoder",
22521
22807
  capScope: "system",
@@ -23446,6 +23732,18 @@ Object.freeze({
23446
23732
  addonId: null,
23447
23733
  access: "create"
23448
23734
  },
23735
+ "imageSettings.getOptions": {
23736
+ capName: "image-settings",
23737
+ capScope: "device",
23738
+ addonId: null,
23739
+ access: "view"
23740
+ },
23741
+ "imageSettings.setSettings": {
23742
+ capName: "image-settings",
23743
+ capScope: "device",
23744
+ addonId: null,
23745
+ access: "create"
23746
+ },
23449
23747
  "integrations.create": {
23450
23748
  capName: "integrations",
23451
23749
  capScope: "system",
@@ -214536,6 +214834,94 @@ function buildRawState(reader) {
214536
214834
  };
214537
214835
  }
214538
214836
  //#endregion
214837
+ //#region src/day-night-mapping.ts
214838
+ /**
214839
+ * Maps between the vendor-neutral `day-night` cap's `DayNightMode` and
214840
+ * Reolink's raw `VideoInput.dayNight` / `InputAdvanceCfg.DayNight.mode`
214841
+ * string (Baichuan cmdId 25/26 via `setIsp`/`getVideoInput`). Firmwares
214842
+ * report/accept camel-cased values such as `auto`, `color`,
214843
+ * `blackAndWhite` — case varies by model; the lib's `normalizeDayNightMode`
214844
+ * lower-cases the first letter before push, so the mapping here works off
214845
+ * a case-insensitive compare.
214846
+ *
214847
+ * There is no on-camera "schedule" mode in the Baichuan protocol — the
214848
+ * `day-night` cap's `getOptions.modes` never advertises `'schedule'` for
214849
+ * a Reolink device.
214850
+ */
214851
+ var NIGHT_TOKENS = new Set([
214852
+ "blackandwhite",
214853
+ "black&white",
214854
+ "bw",
214855
+ "night",
214856
+ "blackwhite"
214857
+ ]);
214858
+ var DAY_TOKENS = new Set([
214859
+ "color",
214860
+ "colour",
214861
+ "day"
214862
+ ]);
214863
+ /**
214864
+ * Map the camera's raw `dayNight` string to the cap's normalized mode.
214865
+ * Falls back to `'auto'` for an unrecognized or missing value — every
214866
+ * Reolink camera defaults to auto IR-cut switching out of the box.
214867
+ */
214868
+ function reolinkDayNightModeToCap(raw) {
214869
+ if (typeof raw !== "string" || raw.length === 0) return "auto";
214870
+ const normalized = raw.toLowerCase();
214871
+ if (normalized === "auto") return "auto";
214872
+ if (NIGHT_TOKENS.has(normalized)) return "night";
214873
+ if (DAY_TOKENS.has(normalized)) return "day";
214874
+ return "auto";
214875
+ }
214876
+ /**
214877
+ * Map the cap's normalized mode to the raw value Reolink's `setIsp`
214878
+ * expects. `'schedule'` has no Reolink equivalent — `getOptions.modes`
214879
+ * never advertises it, so a well-behaved caller never passes it here;
214880
+ * guard defensively in case one does anyway.
214881
+ */
214882
+ function capDayNightModeToReolink(mode) {
214883
+ switch (mode) {
214884
+ case "auto": return "auto";
214885
+ case "day": return "color";
214886
+ case "night": return "blackAndWhite";
214887
+ case "schedule": throw new Error("Reolink cameras do not support a scheduled day/night mode");
214888
+ }
214889
+ }
214890
+ //#endregion
214891
+ //#region src/image-settings-mapping.ts
214892
+ /**
214893
+ * Reolink's `InputAdvanceCfg.Exposure.mode` (Baichuan cmdId 25/26, via
214894
+ * `setIsp`/`getIsp`) uses the same two values as the `image-settings`
214895
+ * cap's `ExposureMode` (`'auto' | 'manual'`), modulo case — firmwares
214896
+ * report it in varying case. Returns `undefined` for any other/missing
214897
+ * value so the caller can omit the field rather than report a lie.
214898
+ */
214899
+ function reolinkExposureModeToCap(raw) {
214900
+ if (typeof raw !== "string") return void 0;
214901
+ const normalized = raw.toLowerCase();
214902
+ return normalized === "auto" || normalized === "manual" ? normalized : void 0;
214903
+ }
214904
+ //#endregion
214905
+ //#region src/image-value-normalization.ts
214906
+ /**
214907
+ * Generic native-range <-> normalized-0-100 conversion shared by the
214908
+ * `day-night` (sensitivity) and `image-settings` (brightness / contrast /
214909
+ * saturation / sharpness) native caps. Both caps' vendor-neutral contract
214910
+ * normalizes every slider to an integer 0-100 scale; each provider maps
214911
+ * its own native range (Reolink's image sliders are 0..255, the day/night
214912
+ * threshold range is camera-probed) to/from that scale here.
214913
+ */
214914
+ /** Map a native value inside `[min, max]` to the normalized 0-100 scale. */
214915
+ function normalizeToPercent(value, min, max) {
214916
+ if (max <= min) return 0;
214917
+ const percent = (value - min) / (max - min) * 100;
214918
+ return Math.max(0, Math.min(100, Math.round(percent)));
214919
+ }
214920
+ /** Map a normalized 0-100 value back to the native `[min, max]` range. */
214921
+ function denormalizeFromPercent(percent, min, max) {
214922
+ return Math.round(min + Math.max(0, Math.min(100, percent)) / 100 * (max - min));
214923
+ }
214924
+ //#endregion
214539
214925
  //#region src/native-sdp-overlay.ts
214540
214926
  /**
214541
214927
  * Return a new descriptor list in which every native `pull-rfc4571` entry with
@@ -218770,6 +219156,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
218770
219156
  });
218771
219157
  this.registerMotionZonesCap();
218772
219158
  this.registerPrivacyMaskCap();
219159
+ this.registerDayNightCap();
219160
+ this.registerImageSettingsCap();
218773
219161
  this.registerStreamCatalogProvider();
218774
219162
  this.registerNativeObjectDetectionCap();
218775
219163
  const cache = this.config.get("deviceCache");
@@ -218810,6 +219198,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
218810
219198
  * cached from `getMotionAlarm` so `setZone` can re-encode a patch's
218811
219199
  * `cells` without an extra GET round-trip. */
218812
219200
  motionZonesGrid = null;
219201
+ /** Single-flight guard for the `day-night` cap refresh. */
219202
+ dayNightRefreshInFlight = null;
219203
+ /** Single-flight guard for the `image-settings` cap refresh. */
219204
+ imageSettingsRefreshInFlight = null;
218813
219205
  /**
218814
219206
  * Single-flight guard for snapshot fetches (Slice 10). When two
218815
219207
  * consumers hit `getSnapshot` concurrently we issue ONE Baichuan
@@ -220071,6 +220463,244 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
220071
220463
  this.ctx.logger.info("Reolink privacy-mask cap registered (read + enable + zone write)", { tags: { deviceId: this.id } });
220072
220464
  }
220073
220465
  /**
220466
+ * Register the vendor-neutral `day-night` cap.
220467
+ *
220468
+ * Mirrors `registerMotionZonesCap`: the trampoline pattern over
220469
+ * `createRuntimeStateBridge`, with a single-flight `refresh` that
220470
+ * round-trips `getVideoInput` (mode) + `getDayNightThreshold`
220471
+ * (sensitivity). Both are Baichuan cmdId 25/26/297 reads already used
220472
+ * elsewhere in this file (`refreshParentSettingsSnapshot`,
220473
+ * `applySettingsPatch`'s ISP section) — this cap just wraps them
220474
+ * behind the shared `getOptions`/`getStatus`/`setSettings` surface
220475
+ * instead of the legacy settings-form blob.
220476
+ *
220477
+ * - `getStatus` — slice-driven via bridge (stale → refresh).
220478
+ * - `getOptions` — on-demand probe; `modes` is gated on the camera
220479
+ * actually reporting a `dayNight` value, `supportsSensitivity` on a
220480
+ * successful `getDayNightThreshold` probe. Reolink has no
220481
+ * switch-delay knob, so `supportsSwitchDelay` is always `false`.
220482
+ * - `setSettings` — read-modify-write via `setIsp`; `sensitivity` is
220483
+ * denormalized against the camera's OWN probed threshold range
220484
+ * (never a hardcoded 0..255 guess).
220485
+ */
220486
+ registerDayNightCap() {
220487
+ const channel = this.getChannel();
220488
+ const CAP_NAME = "day-night";
220489
+ const STALE_MS = 1e4;
220490
+ /** Round-trip `getVideoInput` + `getDayNightThreshold`, map into the
220491
+ * cap status schema, write slice. */
220492
+ const refreshFromCamera = async () => {
220493
+ if (this.dayNightRefreshInFlight) return this.dayNightRefreshInFlight;
220494
+ const promise = (async () => {
220495
+ try {
220496
+ const api = await this.ensureApi();
220497
+ const vi = (await api.getVideoInput(channel))?.body?.VideoInput;
220498
+ const next = {
220499
+ mode: reolinkDayNightModeToCap(vi?.dayNight),
220500
+ lastFetchedAt: Date.now()
220501
+ };
220502
+ try {
220503
+ const range = (await api.getDayNightThreshold(channel))?.body?.DayNightThreshold?.thresholdval;
220504
+ if (typeof range?.min === "number" && typeof range?.max === "number" && typeof range?.cur === "number") next.sensitivity = normalizeToPercent(range.cur, range.min, range.max);
220505
+ } catch (err) {
220506
+ this.ctx.logger.debug("day-night threshold probe failed", {
220507
+ tags: { deviceId: this.id },
220508
+ meta: { error: err instanceof Error ? err.message : String(err) }
220509
+ });
220510
+ }
220511
+ this.runtimeState.setCapState(CAP_NAME, next);
220512
+ } catch (err) {
220513
+ this.ctx.logger.debug("day-night refresh failed — keeping last slice", {
220514
+ tags: { deviceId: this.id },
220515
+ meta: { error: err instanceof Error ? err.message : String(err) }
220516
+ });
220517
+ }
220518
+ })();
220519
+ this.dayNightRefreshInFlight = promise;
220520
+ try {
220521
+ await promise;
220522
+ } finally {
220523
+ this.dayNightRefreshInFlight = null;
220524
+ }
220525
+ };
220526
+ const provider = {
220527
+ getStatus: createRuntimeStateBridge({
220528
+ runtimeState: this.runtimeState,
220529
+ cap: dayNightCapability,
220530
+ ownDeviceId: this.id,
220531
+ refresh: refreshFromCamera,
220532
+ staleMs: STALE_MS,
220533
+ empty: () => ({
220534
+ mode: "auto",
220535
+ lastFetchedAt: 0
220536
+ })
220537
+ }).getStatus,
220538
+ getOptions: async ({ deviceId }) => {
220539
+ if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
220540
+ const api = await this.ensureApi();
220541
+ const modes = ((await api.getVideoInput(channel))?.body?.VideoInput)?.dayNight !== void 0 ? [
220542
+ "auto",
220543
+ "day",
220544
+ "night"
220545
+ ] : [];
220546
+ let supportsSensitivity = false;
220547
+ let sensitivityRange;
220548
+ try {
220549
+ const range = (await api.getDayNightThreshold(channel))?.body?.DayNightThreshold?.thresholdval;
220550
+ if (typeof range?.min === "number" && typeof range?.max === "number") {
220551
+ supportsSensitivity = true;
220552
+ sensitivityRange = {
220553
+ min: 0,
220554
+ max: 100,
220555
+ step: 1
220556
+ };
220557
+ }
220558
+ } catch (err) {
220559
+ this.ctx.logger.debug("day-night threshold options probe failed", {
220560
+ tags: { deviceId: this.id },
220561
+ meta: { error: err instanceof Error ? err.message : String(err) }
220562
+ });
220563
+ }
220564
+ return {
220565
+ modes,
220566
+ supportsSensitivity,
220567
+ ...sensitivityRange !== void 0 ? { sensitivity: sensitivityRange } : {},
220568
+ supportsSwitchDelay: false
220569
+ };
220570
+ },
220571
+ setSettings: async ({ deviceId, settings }) => {
220572
+ if (deviceId !== this.id) return;
220573
+ const api = await this.ensureApi();
220574
+ const ispPatch = {};
220575
+ if (settings.mode !== void 0) ispPatch.dayNight = capDayNightModeToReolink(settings.mode);
220576
+ if (settings.sensitivity !== void 0) {
220577
+ const range = (await api.getDayNightThreshold(channel))?.body?.DayNightThreshold?.thresholdval;
220578
+ const min = typeof range?.min === "number" ? range.min : 0;
220579
+ const max = typeof range?.max === "number" ? range.max : 100;
220580
+ ispPatch.dayNightThreshold = denormalizeFromPercent(settings.sensitivity, min, max);
220581
+ }
220582
+ if (Object.keys(ispPatch).length > 0) await api.setIsp(channel, ispPatch);
220583
+ await refreshFromCamera();
220584
+ }
220585
+ };
220586
+ this.ctx.registerNativeCap(dayNightCapability, provider);
220587
+ this.ctx.logger.info("Reolink day-night cap registered", { tags: { deviceId: this.id } });
220588
+ }
220589
+ /**
220590
+ * Register the vendor-neutral `image-settings` cap.
220591
+ *
220592
+ * Mirrors `registerDayNightCap` / `registerMotionZonesCap`: the
220593
+ * trampoline pattern over `createRuntimeStateBridge`, with a
220594
+ * single-flight `refresh` that round-trips `getIsp` (cmdId 26 — the
220595
+ * merged `VideoInput` + `InputAdvanceCfg` blob covers brightness /
220596
+ * contrast / saturation / sharpness AND the exposure mode in one
220597
+ * call). Sliders are normalized against Reolink's native 0..255 image
220598
+ * range; `exposureMode` maps 1:1 onto the cap's `'auto' | 'manual'`.
220599
+ *
220600
+ * Reolink's Baichuan API (`@apocaliss92/nodelink-js` 0.6.7) has no
220601
+ * write path for mirror / flip / rotate / white-balance / warmth, and
220602
+ * `InputAdvanceCfg.BLC` (backlight) has no matching setter — those
220603
+ * fields are honestly reported as unsupported (`false` / `[]`) rather
220604
+ * than rendering dead controls. TODO: revisit if nodelink-js adds
220605
+ * setters for any of these.
220606
+ */
220607
+ registerImageSettingsCap() {
220608
+ const channel = this.getChannel();
220609
+ const CAP_NAME = "image-settings";
220610
+ const STALE_MS = 1e4;
220611
+ const SLIDER_MIN = 0;
220612
+ const SLIDER_MAX = 255;
220613
+ const NORMALIZED_RANGE = {
220614
+ min: 0,
220615
+ max: 100,
220616
+ step: 1
220617
+ };
220618
+ /** Round-trip `getIsp`, map into the cap status schema, write slice. */
220619
+ const refreshFromCamera = async () => {
220620
+ if (this.imageSettingsRefreshInFlight) return this.imageSettingsRefreshInFlight;
220621
+ const promise = (async () => {
220622
+ try {
220623
+ const isp = await (await this.ensureApi()).getIsp(channel);
220624
+ const vi = isp?.body?.VideoInput;
220625
+ const exposureRaw = isp?.body?.InputAdvanceCfg?.Exposure?.mode;
220626
+ const next = { lastFetchedAt: Date.now() };
220627
+ if (typeof vi?.bright === "number") next.brightness = normalizeToPercent(vi.bright, SLIDER_MIN, SLIDER_MAX);
220628
+ if (typeof vi?.contrast === "number") next.contrast = normalizeToPercent(vi.contrast, SLIDER_MIN, SLIDER_MAX);
220629
+ if (typeof vi?.saturation === "number") next.saturation = normalizeToPercent(vi.saturation, SLIDER_MIN, SLIDER_MAX);
220630
+ if (typeof vi?.sharpen === "number") next.sharpness = normalizeToPercent(vi.sharpen, SLIDER_MIN, SLIDER_MAX);
220631
+ const exposureMode = reolinkExposureModeToCap(typeof exposureRaw === "string" ? exposureRaw : void 0);
220632
+ if (exposureMode !== void 0) next.exposureMode = exposureMode;
220633
+ this.runtimeState.setCapState(CAP_NAME, next);
220634
+ } catch (err) {
220635
+ this.ctx.logger.debug("image-settings refresh failed — keeping last slice", {
220636
+ tags: { deviceId: this.id },
220637
+ meta: { error: err instanceof Error ? err.message : String(err) }
220638
+ });
220639
+ }
220640
+ })();
220641
+ this.imageSettingsRefreshInFlight = promise;
220642
+ try {
220643
+ await promise;
220644
+ } finally {
220645
+ this.imageSettingsRefreshInFlight = null;
220646
+ }
220647
+ };
220648
+ const provider = {
220649
+ getStatus: createRuntimeStateBridge({
220650
+ runtimeState: this.runtimeState,
220651
+ cap: imageSettingsCapability,
220652
+ ownDeviceId: this.id,
220653
+ refresh: refreshFromCamera,
220654
+ staleMs: STALE_MS,
220655
+ empty: () => ({ lastFetchedAt: 0 })
220656
+ }).getStatus,
220657
+ getOptions: async ({ deviceId }) => {
220658
+ if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
220659
+ const isp = await (await this.ensureApi()).getIsp(channel);
220660
+ const vi = isp?.body?.VideoInput;
220661
+ const exposureRaw = isp?.body?.InputAdvanceCfg?.Exposure?.mode;
220662
+ const supportsBrightness = typeof vi?.bright === "number";
220663
+ const supportsContrast = typeof vi?.contrast === "number";
220664
+ const supportsSaturation = typeof vi?.saturation === "number";
220665
+ const supportsSharpness = typeof vi?.sharpen === "number";
220666
+ const exposureModes = typeof exposureRaw === "string" ? ["auto", "manual"] : [];
220667
+ return {
220668
+ supportsBrightness,
220669
+ ...supportsBrightness ? { brightness: NORMALIZED_RANGE } : {},
220670
+ supportsContrast,
220671
+ ...supportsContrast ? { contrast: NORMALIZED_RANGE } : {},
220672
+ supportsSaturation,
220673
+ ...supportsSaturation ? { saturation: NORMALIZED_RANGE } : {},
220674
+ supportsSharpness,
220675
+ ...supportsSharpness ? { sharpness: NORMALIZED_RANGE } : {},
220676
+ supportsMirror: false,
220677
+ supportsFlip: false,
220678
+ rotateOptions: [],
220679
+ whiteBalanceModes: [],
220680
+ supportsWarmth: false,
220681
+ exposureModes,
220682
+ backlightModes: []
220683
+ };
220684
+ },
220685
+ setSettings: async ({ deviceId, settings }) => {
220686
+ if (deviceId !== this.id) return;
220687
+ const api = await this.ensureApi();
220688
+ const imagePatch = {};
220689
+ if (settings.brightness !== void 0) imagePatch.bright = denormalizeFromPercent(settings.brightness, SLIDER_MIN, SLIDER_MAX);
220690
+ if (settings.contrast !== void 0) imagePatch.contrast = denormalizeFromPercent(settings.contrast, SLIDER_MIN, SLIDER_MAX);
220691
+ if (settings.saturation !== void 0) imagePatch.saturation = denormalizeFromPercent(settings.saturation, SLIDER_MIN, SLIDER_MAX);
220692
+ if (settings.sharpness !== void 0) imagePatch.sharpen = denormalizeFromPercent(settings.sharpness, SLIDER_MIN, SLIDER_MAX);
220693
+ if (Object.keys(imagePatch).length > 0) await api.setImage(channel, imagePatch);
220694
+ const ispPatch = {};
220695
+ if (settings.exposureMode !== void 0) ispPatch.exposure = settings.exposureMode;
220696
+ if (Object.keys(ispPatch).length > 0) await api.setIsp(channel, ispPatch);
220697
+ await refreshFromCamera();
220698
+ }
220699
+ };
220700
+ this.ctx.registerNativeCap(imageSettingsCapability, provider);
220701
+ this.ctx.logger.info("Reolink image-settings cap registered", { tags: { deviceId: this.id } });
220702
+ }
220703
+ /**
220074
220704
  * Register the `native-object-detection` cap.
220075
220705
  *
220076
220706
  * The runtimeState holds three fields:
@@ -220101,6 +220731,12 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
220101
220731
  }
220102
220732
  return classes;
220103
220733
  };
220734
+ const buildEmptyState = () => ({
220735
+ enabled: false,
220736
+ lastByClass: {},
220737
+ supportedClasses: buildSupportedClasses(),
220738
+ lastFetchedAt: 0
220739
+ });
220104
220740
  const provider = {
220105
220741
  getStatus: createRuntimeStateBridge({
220106
220742
  runtimeState: this.runtimeState,
@@ -220108,12 +220744,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
220108
220744
  ownDeviceId: this.id,
220109
220745
  refresh: async () => {},
220110
220746
  staleMs: Infinity,
220111
- empty: () => ({
220112
- enabled: false,
220113
- lastByClass: {},
220114
- supportedClasses: buildSupportedClasses(),
220115
- lastFetchedAt: 0
220116
- })
220747
+ empty: buildEmptyState
220117
220748
  }).getStatus,
220118
220749
  setEnabled: async ({ deviceId, enabled }) => {
220119
220750
  if (deviceId !== this.id) return;
@@ -220127,6 +220758,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
220127
220758
  }
220128
220759
  };
220129
220760
  this.ctx.registerNativeCap(nativeObjectDetectionCapability, provider);
220761
+ if (this.runtimeState.getCapState(CAP_NAME) === void 0) this.runtimeState.setCapState(CAP_NAME, {
220762
+ ...buildEmptyState(),
220763
+ lastFetchedAt: Date.now()
220764
+ });
220130
220765
  this.ctx.logger.info("Reolink native-object-detection cap registered", { tags: { deviceId: this.id } });
220131
220766
  }
220132
220767
  /**