@camstack/addon-provider-reolink 1.1.18 → 1.1.20

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.js CHANGED
@@ -4655,7 +4655,7 @@ function _instanceof(cls, params = {}) {
4655
4655
  return inst;
4656
4656
  }
4657
4657
  //#endregion
4658
- //#region ../types/dist/sleep-BiDFW0E7.mjs
4658
+ //#region ../types/dist/sleep-CZDdRBua.mjs
4659
4659
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4660
4660
  EventCategory["SystemBoot"] = "system.boot";
4661
4661
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -7453,7 +7453,16 @@ var DecoderStatsSchema = object({
7453
7453
  inputFps: number(),
7454
7454
  outputFps: number(),
7455
7455
  avgDecodeTimeMs: number(),
7456
- droppedFrames: number()
7456
+ droppedFrames: number(),
7457
+ /**
7458
+ * Pull-mode adaptive-fps telemetry (optional — only pull sessions run the
7459
+ * lag-driven controller; push sessions omit these). `lagMs` is the EWMA of
7460
+ * the decoder's real-time drift (rising = falling behind live); `adaptiveFps`
7461
+ * is the current lag-throttled emit rate (≤ `effectiveFps` ceiling).
7462
+ */
7463
+ lagMs: number().optional(),
7464
+ effectiveFps: number().optional(),
7465
+ adaptiveFps: number().optional()
7457
7466
  });
7458
7467
  var DecoderSessionConfigSchema = object({
7459
7468
  codec: string(),
@@ -7494,7 +7503,15 @@ var DecoderSessionConfigSchema = object({
7494
7503
  * other — `pullFrames` returns nothing for an `'shm'` session and
7495
7504
  * `pullHandles` returns nothing for a `'callback'` session.
7496
7505
  */
7497
- frameSink: _enum(["callback", "shm"]).default("callback")
7506
+ frameSink: _enum(["callback", "shm"]).default("callback"),
7507
+ /**
7508
+ * Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
7509
+ * a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
7510
+ * real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
7511
+ * stream-broker's `streamingDebug` gate — off by default so production logs
7512
+ * stay quiet and the emit path pays zero per-frame cost when disabled.
7513
+ */
7514
+ debug: boolean().optional()
7498
7515
  });
7499
7516
  var EncodeProfileSchema = object({
7500
7517
  video: object({
@@ -10558,6 +10575,100 @@ var coverCapability = {
10558
10575
  runtimeState: CoverStatusSchema
10559
10576
  };
10560
10577
  /**
10578
+ * Vendor-neutral day/night (IR-cut) control — the per-camera config cap
10579
+ * shared by reolink / hikvision / amcrest. Models the common firmware
10580
+ * surface: the IR-cut switching MODE plus the two knobs that gate it
10581
+ * (photocell `sensitivity` + `switchDelaySec`). Each vendor maps these
10582
+ * onto its own ISAPI / Baichuan / Dahua-CGI fields; the cap standardises
10583
+ * the shape so ONE derived-form renders every camera.
10584
+ *
10585
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
10586
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
10587
+ * injected from `status`) reports the live values, and a single
10588
+ * `setSettings` mutation applies a partial change. No hand-written
10589
+ * settings-contribution methods — the framework derives the UI + save
10590
+ * routing from this surface.
10591
+ */
10592
+ /** IR-cut switching mode. `schedule` = time-of-day table configured on the camera. */
10593
+ var DayNightModeSchema = _enum([
10594
+ "auto",
10595
+ "day",
10596
+ "night",
10597
+ "schedule"
10598
+ ]);
10599
+ /** Normalized numeric range descriptor — `{ min, max, step }` per the
10600
+ * getOptions availability convention. Normalized values are 0–100. */
10601
+ var NormalizedRangeSchema$1 = object({
10602
+ min: number(),
10603
+ max: number(),
10604
+ step: number()
10605
+ });
10606
+ /**
10607
+ * Current day/night state. Optional fields are absent when the camera
10608
+ * does not expose that knob (a photocell-less model reports no
10609
+ * `sensitivity`). `lastFetchedAt` feeds the runtime-state bridge.
10610
+ */
10611
+ var DayNightStatusSchema = object({
10612
+ mode: DayNightModeSchema,
10613
+ /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
10614
+ sensitivity: number().optional(),
10615
+ /** Delay before the IR-cut filter flips, in seconds. */
10616
+ switchDelaySec: number().optional(),
10617
+ lastFetchedAt: number()
10618
+ });
10619
+ /**
10620
+ * Per-camera availability descriptor — drives which controls the admin UI
10621
+ * renders. Booleans as `supportsX`; numeric ranges as `{ min, max, step }`
10622
+ * (normalized 0–100); the mode choice-set as an array. A provider returns
10623
+ * honest, camera-probed values — never hardcoded.
10624
+ */
10625
+ var DayNightOptionsSchema = object({
10626
+ /** Modes this camera accepts. Empty → the camera has no configurable day/night mode. */
10627
+ modes: array(DayNightModeSchema),
10628
+ supportsSensitivity: boolean(),
10629
+ /** Present when `supportsSensitivity` — the normalized 0–100 range. */
10630
+ sensitivity: NormalizedRangeSchema$1.optional(),
10631
+ supportsSwitchDelay: boolean(),
10632
+ /** Present when `supportsSwitchDelay` — the allowed delay range in seconds. */
10633
+ switchDelaySec: NormalizedRangeSchema$1.optional()
10634
+ });
10635
+ /**
10636
+ * Partial change to the day/night config — every field optional. A
10637
+ * provider ignores fields it does not support.
10638
+ */
10639
+ var DayNightSettingsPatchSchema = object({
10640
+ mode: DayNightModeSchema.optional(),
10641
+ sensitivity: number().optional(),
10642
+ switchDelaySec: number().optional()
10643
+ });
10644
+ var dayNightCapability = {
10645
+ name: "day-night",
10646
+ scope: "device",
10647
+ deviceNative: true,
10648
+ mode: "singleton",
10649
+ deviceTypes: [DeviceType.Camera],
10650
+ deviceConfig: { ui: {
10651
+ kind: "derived-form",
10652
+ builderId: "day-night",
10653
+ tab: "image"
10654
+ } },
10655
+ methods: {
10656
+ getOptions: method(object({ deviceId: number() }), DayNightOptionsSchema),
10657
+ setSettings: method(object({
10658
+ deviceId: number(),
10659
+ settings: DayNightSettingsPatchSchema
10660
+ }), _void(), {
10661
+ kind: "mutation",
10662
+ auth: "admin"
10663
+ })
10664
+ },
10665
+ status: {
10666
+ schema: DayNightStatusSchema,
10667
+ kind: "poll"
10668
+ },
10669
+ runtimeState: DayNightStatusSchema
10670
+ };
10671
+ /**
10561
10672
  * Identity envelope for a device's upstream-system metadata.
10562
10673
  *
10563
10674
  * Two jobs:
@@ -11216,6 +11327,155 @@ var imageCapability = {
11216
11327
  runtimeState: ImageStatusSchema
11217
11328
  };
11218
11329
  /**
11330
+ * Vendor-neutral image / picture-adjustment cap — the per-camera config
11331
+ * cap shared by reolink / hikvision / amcrest. Models the common ISP
11332
+ * surface: the four picture sliders (brightness / contrast / saturation /
11333
+ * sharpness), orientation (mirror / flip / rotate), white-balance,
11334
+ * exposure and backlight-compensation modes.
11335
+ *
11336
+ * NORMALIZATION: every slider is a normalized int 0–100. Vendors expose
11337
+ * these natively as 0–100 or 0–255 (or other ranges); each provider maps
11338
+ * its native range to/from this normalized 0–100 space so the cap surface
11339
+ * (and the derived form) is identical across cameras. `warmth` (manual
11340
+ * white-balance) is likewise normalized 0–100.
11341
+ *
11342
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
11343
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
11344
+ * injected from `status`) reports the live values, and a single
11345
+ * `setSettings` mutation applies a partial change. No hand-written
11346
+ * settings-contribution methods — the framework derives the UI + save
11347
+ * routing from this surface.
11348
+ */
11349
+ /** Sensor/image rotation, degrees clockwise. */
11350
+ var ImageRotateSchema = _enum([
11351
+ "0",
11352
+ "90",
11353
+ "180",
11354
+ "270"
11355
+ ]);
11356
+ /** White-balance mode. `manual` unlocks the normalized `warmth` knob. */
11357
+ var WhiteBalanceModeSchema = _enum(["auto", "manual"]);
11358
+ /** Exposure mode. */
11359
+ var ExposureModeSchema = _enum(["auto", "manual"]);
11360
+ /**
11361
+ * Backlight-compensation mode:
11362
+ * - `off` — disabled
11363
+ * - `blc` — backlight compensation
11364
+ * - `wdr` — wide dynamic range
11365
+ * - `hlc` — highlight compensation
11366
+ */
11367
+ var BacklightModeSchema = _enum([
11368
+ "off",
11369
+ "blc",
11370
+ "wdr",
11371
+ "hlc"
11372
+ ]);
11373
+ /** Normalized numeric range descriptor — `{ min, max, step }` per the
11374
+ * getOptions availability convention. Slider values are normalized 0–100. */
11375
+ var NormalizedRangeSchema = object({
11376
+ min: number(),
11377
+ max: number(),
11378
+ step: number()
11379
+ });
11380
+ /**
11381
+ * Current image-adjustment state. Every field optional — absent when the
11382
+ * camera does not expose that control. Slider values are NORMALIZED 0–100.
11383
+ * `lastFetchedAt` feeds the runtime-state bridge.
11384
+ */
11385
+ var ImageSettingsStatusSchema = object({
11386
+ /** Normalized 0–100. */
11387
+ brightness: number().optional(),
11388
+ /** Normalized 0–100. */
11389
+ contrast: number().optional(),
11390
+ /** Normalized 0–100. */
11391
+ saturation: number().optional(),
11392
+ /** Normalized 0–100. */
11393
+ sharpness: number().optional(),
11394
+ mirror: boolean().optional(),
11395
+ flip: boolean().optional(),
11396
+ rotate: ImageRotateSchema.optional(),
11397
+ whiteBalance: WhiteBalanceModeSchema.optional(),
11398
+ /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
11399
+ warmth: number().optional(),
11400
+ exposureMode: ExposureModeSchema.optional(),
11401
+ backlightMode: BacklightModeSchema.optional(),
11402
+ lastFetchedAt: number()
11403
+ });
11404
+ /**
11405
+ * Per-camera availability descriptor — drives which controls the admin UI
11406
+ * renders. Booleans as `supportsX`; numeric ranges as `{ min, max, step }`
11407
+ * (the normalized 0–100 range); enums as arrays of supported values (empty
11408
+ * array → control hidden). A provider returns honest, camera-probed values
11409
+ * — never hardcoded.
11410
+ */
11411
+ var ImageSettingsOptionsSchema = object({
11412
+ supportsBrightness: boolean(),
11413
+ brightness: NormalizedRangeSchema.optional(),
11414
+ supportsContrast: boolean(),
11415
+ contrast: NormalizedRangeSchema.optional(),
11416
+ supportsSaturation: boolean(),
11417
+ saturation: NormalizedRangeSchema.optional(),
11418
+ supportsSharpness: boolean(),
11419
+ sharpness: NormalizedRangeSchema.optional(),
11420
+ supportsMirror: boolean(),
11421
+ supportsFlip: boolean(),
11422
+ /** Supported rotation values. Empty → rotation not configurable. */
11423
+ rotateOptions: array(ImageRotateSchema),
11424
+ /** Supported white-balance modes. Empty → white-balance not configurable. */
11425
+ whiteBalanceModes: array(WhiteBalanceModeSchema),
11426
+ supportsWarmth: boolean(),
11427
+ /** Present when `supportsWarmth` — the normalized 0–100 range. */
11428
+ warmth: NormalizedRangeSchema.optional(),
11429
+ /** Supported exposure modes. Empty → exposure not configurable. */
11430
+ exposureModes: array(ExposureModeSchema),
11431
+ /** Supported backlight modes. Empty → backlight-compensation not configurable. */
11432
+ backlightModes: array(BacklightModeSchema)
11433
+ });
11434
+ /**
11435
+ * Partial change to the image config — every field optional. Slider values
11436
+ * are normalized 0–100. A provider ignores fields it does not support.
11437
+ */
11438
+ var ImageSettingsPatchSchema = object({
11439
+ brightness: number().optional(),
11440
+ contrast: number().optional(),
11441
+ saturation: number().optional(),
11442
+ sharpness: number().optional(),
11443
+ mirror: boolean().optional(),
11444
+ flip: boolean().optional(),
11445
+ rotate: ImageRotateSchema.optional(),
11446
+ whiteBalance: WhiteBalanceModeSchema.optional(),
11447
+ warmth: number().optional(),
11448
+ exposureMode: ExposureModeSchema.optional(),
11449
+ backlightMode: BacklightModeSchema.optional()
11450
+ });
11451
+ var imageSettingsCapability = {
11452
+ name: "image-settings",
11453
+ scope: "device",
11454
+ deviceNative: true,
11455
+ mode: "singleton",
11456
+ deviceTypes: [DeviceType.Camera],
11457
+ deviceConfig: { ui: {
11458
+ kind: "derived-form",
11459
+ builderId: "image-settings",
11460
+ tab: "image"
11461
+ } },
11462
+ methods: {
11463
+ getOptions: method(object({ deviceId: number() }), ImageSettingsOptionsSchema),
11464
+ setSettings: method(object({
11465
+ deviceId: number(),
11466
+ settings: ImageSettingsPatchSchema
11467
+ }), _void(), {
11468
+ kind: "mutation",
11469
+ auth: "admin"
11470
+ })
11471
+ },
11472
+ status: {
11473
+ schema: ImageSettingsStatusSchema,
11474
+ kind: "poll"
11475
+ },
11476
+ runtimeState: ImageSettingsStatusSchema
11477
+ };
11478
+ /**
11219
11479
  * Robotic lawn-mower cap. Models HA `lawn_mower.*` entities — anything
11220
11480
  * with a mowing lifecycle plus a dock action.
11221
11481
  *
@@ -12214,6 +12474,16 @@ var RunnerCameraConfigSchema = object({
12214
12474
  * this gate is bypassed.
12215
12475
  */
12216
12476
  onboardMotionDrivesAnalyzer: boolean().default(true),
12477
+ /**
12478
+ * Master toggle for the occupancy re-check. When `false` (DEFAULT) the runner
12479
+ * never arms the periodic recheck timer, regardless of `occupancyRecheckSec` —
12480
+ * this is off by default because the recheck re-subscribes a detection session
12481
+ * every N seconds while `watching`, a major source of pull-decoder re-dial
12482
+ * churn (each cycle creates+tears a session → RTSP re-dial → latency). The
12483
+ * `occupancyRecheckSec` / `occupancyRecheckFrames` sliders only take effect
12484
+ * (and only render) when this is enabled.
12485
+ */
12486
+ occupancyRecheckEnabled: boolean().default(false),
12217
12487
  occupancyRecheckSec: number().min(occupancyRecheckSecField.min).max(occupancyRecheckSecField.max).default(occupancyRecheckSecField.default),
12218
12488
  occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default),
12219
12489
  /**
@@ -14219,6 +14489,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
14219
14489
  contact: contactCapability,
14220
14490
  control: controlCapability,
14221
14491
  cover: coverCapability,
14492
+ dayNight: dayNightCapability,
14222
14493
  deviceDiscovery: deviceDiscoveryCapability,
14223
14494
  deviceStatus: deviceStatusCapability,
14224
14495
  doorbell: doorbellCapability,
@@ -14231,6 +14502,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
14231
14502
  humidifier: humidifierCapability,
14232
14503
  humiditySensor: humiditySensorCapability,
14233
14504
  image: imageCapability,
14505
+ imageSettings: imageSettingsCapability,
14234
14506
  lawnMowerControl: lawnMowerControlCapability,
14235
14507
  lockControl: lockControlCapability,
14236
14508
  mediaPlayer: mediaPlayerCapability,
@@ -16157,7 +16429,9 @@ method(object({ codec: string() }), boolean()), method(_void(), object({
16157
16429
  id: string(),
16158
16430
  name: string(),
16159
16431
  isPullMode: boolean().optional(),
16160
- priority: number().optional()
16432
+ priority: number().optional(),
16433
+ hwaccel: string().optional(),
16434
+ probedBestHwaccel: string().optional()
16161
16435
  })), method(DecoderSessionConfigSchema, object({
16162
16436
  sessionId: string(),
16163
16437
  nodeId: string()
@@ -22521,6 +22795,18 @@ Object.freeze({
22521
22795
  addonId: null,
22522
22796
  access: "view"
22523
22797
  },
22798
+ "dayNight.getOptions": {
22799
+ capName: "day-night",
22800
+ capScope: "device",
22801
+ addonId: null,
22802
+ access: "view"
22803
+ },
22804
+ "dayNight.setSettings": {
22805
+ capName: "day-night",
22806
+ capScope: "device",
22807
+ addonId: null,
22808
+ access: "create"
22809
+ },
22524
22810
  "decoder.createSession": {
22525
22811
  capName: "decoder",
22526
22812
  capScope: "system",
@@ -23451,6 +23737,18 @@ Object.freeze({
23451
23737
  addonId: null,
23452
23738
  access: "create"
23453
23739
  },
23740
+ "imageSettings.getOptions": {
23741
+ capName: "image-settings",
23742
+ capScope: "device",
23743
+ addonId: null,
23744
+ access: "view"
23745
+ },
23746
+ "imageSettings.setSettings": {
23747
+ capName: "image-settings",
23748
+ capScope: "device",
23749
+ addonId: null,
23750
+ access: "create"
23751
+ },
23454
23752
  "integrations.create": {
23455
23753
  capName: "integrations",
23456
23754
  capScope: "system",
@@ -214556,6 +214854,94 @@ function buildRawState(reader) {
214556
214854
  };
214557
214855
  }
214558
214856
  //#endregion
214857
+ //#region src/day-night-mapping.ts
214858
+ /**
214859
+ * Maps between the vendor-neutral `day-night` cap's `DayNightMode` and
214860
+ * Reolink's raw `VideoInput.dayNight` / `InputAdvanceCfg.DayNight.mode`
214861
+ * string (Baichuan cmdId 25/26 via `setIsp`/`getVideoInput`). Firmwares
214862
+ * report/accept camel-cased values such as `auto`, `color`,
214863
+ * `blackAndWhite` — case varies by model; the lib's `normalizeDayNightMode`
214864
+ * lower-cases the first letter before push, so the mapping here works off
214865
+ * a case-insensitive compare.
214866
+ *
214867
+ * There is no on-camera "schedule" mode in the Baichuan protocol — the
214868
+ * `day-night` cap's `getOptions.modes` never advertises `'schedule'` for
214869
+ * a Reolink device.
214870
+ */
214871
+ var NIGHT_TOKENS = new Set([
214872
+ "blackandwhite",
214873
+ "black&white",
214874
+ "bw",
214875
+ "night",
214876
+ "blackwhite"
214877
+ ]);
214878
+ var DAY_TOKENS = new Set([
214879
+ "color",
214880
+ "colour",
214881
+ "day"
214882
+ ]);
214883
+ /**
214884
+ * Map the camera's raw `dayNight` string to the cap's normalized mode.
214885
+ * Falls back to `'auto'` for an unrecognized or missing value — every
214886
+ * Reolink camera defaults to auto IR-cut switching out of the box.
214887
+ */
214888
+ function reolinkDayNightModeToCap(raw) {
214889
+ if (typeof raw !== "string" || raw.length === 0) return "auto";
214890
+ const normalized = raw.toLowerCase();
214891
+ if (normalized === "auto") return "auto";
214892
+ if (NIGHT_TOKENS.has(normalized)) return "night";
214893
+ if (DAY_TOKENS.has(normalized)) return "day";
214894
+ return "auto";
214895
+ }
214896
+ /**
214897
+ * Map the cap's normalized mode to the raw value Reolink's `setIsp`
214898
+ * expects. `'schedule'` has no Reolink equivalent — `getOptions.modes`
214899
+ * never advertises it, so a well-behaved caller never passes it here;
214900
+ * guard defensively in case one does anyway.
214901
+ */
214902
+ function capDayNightModeToReolink(mode) {
214903
+ switch (mode) {
214904
+ case "auto": return "auto";
214905
+ case "day": return "color";
214906
+ case "night": return "blackAndWhite";
214907
+ case "schedule": throw new Error("Reolink cameras do not support a scheduled day/night mode");
214908
+ }
214909
+ }
214910
+ //#endregion
214911
+ //#region src/image-settings-mapping.ts
214912
+ /**
214913
+ * Reolink's `InputAdvanceCfg.Exposure.mode` (Baichuan cmdId 25/26, via
214914
+ * `setIsp`/`getIsp`) uses the same two values as the `image-settings`
214915
+ * cap's `ExposureMode` (`'auto' | 'manual'`), modulo case — firmwares
214916
+ * report it in varying case. Returns `undefined` for any other/missing
214917
+ * value so the caller can omit the field rather than report a lie.
214918
+ */
214919
+ function reolinkExposureModeToCap(raw) {
214920
+ if (typeof raw !== "string") return void 0;
214921
+ const normalized = raw.toLowerCase();
214922
+ return normalized === "auto" || normalized === "manual" ? normalized : void 0;
214923
+ }
214924
+ //#endregion
214925
+ //#region src/image-value-normalization.ts
214926
+ /**
214927
+ * Generic native-range <-> normalized-0-100 conversion shared by the
214928
+ * `day-night` (sensitivity) and `image-settings` (brightness / contrast /
214929
+ * saturation / sharpness) native caps. Both caps' vendor-neutral contract
214930
+ * normalizes every slider to an integer 0-100 scale; each provider maps
214931
+ * its own native range (Reolink's image sliders are 0..255, the day/night
214932
+ * threshold range is camera-probed) to/from that scale here.
214933
+ */
214934
+ /** Map a native value inside `[min, max]` to the normalized 0-100 scale. */
214935
+ function normalizeToPercent(value, min, max) {
214936
+ if (max <= min) return 0;
214937
+ const percent = (value - min) / (max - min) * 100;
214938
+ return Math.max(0, Math.min(100, Math.round(percent)));
214939
+ }
214940
+ /** Map a normalized 0-100 value back to the native `[min, max]` range. */
214941
+ function denormalizeFromPercent(percent, min, max) {
214942
+ return Math.round(min + Math.max(0, Math.min(100, percent)) / 100 * (max - min));
214943
+ }
214944
+ //#endregion
214559
214945
  //#region src/native-sdp-overlay.ts
214560
214946
  /**
214561
214947
  * Return a new descriptor list in which every native `pull-rfc4571` entry with
@@ -218790,6 +219176,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
218790
219176
  });
218791
219177
  this.registerMotionZonesCap();
218792
219178
  this.registerPrivacyMaskCap();
219179
+ this.registerDayNightCap();
219180
+ this.registerImageSettingsCap();
218793
219181
  this.registerStreamCatalogProvider();
218794
219182
  this.registerNativeObjectDetectionCap();
218795
219183
  const cache = this.config.get("deviceCache");
@@ -218830,6 +219218,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
218830
219218
  * cached from `getMotionAlarm` so `setZone` can re-encode a patch's
218831
219219
  * `cells` without an extra GET round-trip. */
218832
219220
  motionZonesGrid = null;
219221
+ /** Single-flight guard for the `day-night` cap refresh. */
219222
+ dayNightRefreshInFlight = null;
219223
+ /** Single-flight guard for the `image-settings` cap refresh. */
219224
+ imageSettingsRefreshInFlight = null;
218833
219225
  /**
218834
219226
  * Single-flight guard for snapshot fetches (Slice 10). When two
218835
219227
  * consumers hit `getSnapshot` concurrently we issue ONE Baichuan
@@ -220091,6 +220483,244 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
220091
220483
  this.ctx.logger.info("Reolink privacy-mask cap registered (read + enable + zone write)", { tags: { deviceId: this.id } });
220092
220484
  }
220093
220485
  /**
220486
+ * Register the vendor-neutral `day-night` cap.
220487
+ *
220488
+ * Mirrors `registerMotionZonesCap`: the trampoline pattern over
220489
+ * `createRuntimeStateBridge`, with a single-flight `refresh` that
220490
+ * round-trips `getVideoInput` (mode) + `getDayNightThreshold`
220491
+ * (sensitivity). Both are Baichuan cmdId 25/26/297 reads already used
220492
+ * elsewhere in this file (`refreshParentSettingsSnapshot`,
220493
+ * `applySettingsPatch`'s ISP section) — this cap just wraps them
220494
+ * behind the shared `getOptions`/`getStatus`/`setSettings` surface
220495
+ * instead of the legacy settings-form blob.
220496
+ *
220497
+ * - `getStatus` — slice-driven via bridge (stale → refresh).
220498
+ * - `getOptions` — on-demand probe; `modes` is gated on the camera
220499
+ * actually reporting a `dayNight` value, `supportsSensitivity` on a
220500
+ * successful `getDayNightThreshold` probe. Reolink has no
220501
+ * switch-delay knob, so `supportsSwitchDelay` is always `false`.
220502
+ * - `setSettings` — read-modify-write via `setIsp`; `sensitivity` is
220503
+ * denormalized against the camera's OWN probed threshold range
220504
+ * (never a hardcoded 0..255 guess).
220505
+ */
220506
+ registerDayNightCap() {
220507
+ const channel = this.getChannel();
220508
+ const CAP_NAME = "day-night";
220509
+ const STALE_MS = 1e4;
220510
+ /** Round-trip `getVideoInput` + `getDayNightThreshold`, map into the
220511
+ * cap status schema, write slice. */
220512
+ const refreshFromCamera = async () => {
220513
+ if (this.dayNightRefreshInFlight) return this.dayNightRefreshInFlight;
220514
+ const promise = (async () => {
220515
+ try {
220516
+ const api = await this.ensureApi();
220517
+ const vi = (await api.getVideoInput(channel))?.body?.VideoInput;
220518
+ const next = {
220519
+ mode: reolinkDayNightModeToCap(vi?.dayNight),
220520
+ lastFetchedAt: Date.now()
220521
+ };
220522
+ try {
220523
+ const range = (await api.getDayNightThreshold(channel))?.body?.DayNightThreshold?.thresholdval;
220524
+ if (typeof range?.min === "number" && typeof range?.max === "number" && typeof range?.cur === "number") next.sensitivity = normalizeToPercent(range.cur, range.min, range.max);
220525
+ } catch (err) {
220526
+ this.ctx.logger.debug("day-night threshold probe failed", {
220527
+ tags: { deviceId: this.id },
220528
+ meta: { error: err instanceof Error ? err.message : String(err) }
220529
+ });
220530
+ }
220531
+ this.runtimeState.setCapState(CAP_NAME, next);
220532
+ } catch (err) {
220533
+ this.ctx.logger.debug("day-night refresh failed — keeping last slice", {
220534
+ tags: { deviceId: this.id },
220535
+ meta: { error: err instanceof Error ? err.message : String(err) }
220536
+ });
220537
+ }
220538
+ })();
220539
+ this.dayNightRefreshInFlight = promise;
220540
+ try {
220541
+ await promise;
220542
+ } finally {
220543
+ this.dayNightRefreshInFlight = null;
220544
+ }
220545
+ };
220546
+ const provider = {
220547
+ getStatus: createRuntimeStateBridge({
220548
+ runtimeState: this.runtimeState,
220549
+ cap: dayNightCapability,
220550
+ ownDeviceId: this.id,
220551
+ refresh: refreshFromCamera,
220552
+ staleMs: STALE_MS,
220553
+ empty: () => ({
220554
+ mode: "auto",
220555
+ lastFetchedAt: 0
220556
+ })
220557
+ }).getStatus,
220558
+ getOptions: async ({ deviceId }) => {
220559
+ if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
220560
+ const api = await this.ensureApi();
220561
+ const modes = ((await api.getVideoInput(channel))?.body?.VideoInput)?.dayNight !== void 0 ? [
220562
+ "auto",
220563
+ "day",
220564
+ "night"
220565
+ ] : [];
220566
+ let supportsSensitivity = false;
220567
+ let sensitivityRange;
220568
+ try {
220569
+ const range = (await api.getDayNightThreshold(channel))?.body?.DayNightThreshold?.thresholdval;
220570
+ if (typeof range?.min === "number" && typeof range?.max === "number") {
220571
+ supportsSensitivity = true;
220572
+ sensitivityRange = {
220573
+ min: 0,
220574
+ max: 100,
220575
+ step: 1
220576
+ };
220577
+ }
220578
+ } catch (err) {
220579
+ this.ctx.logger.debug("day-night threshold options probe failed", {
220580
+ tags: { deviceId: this.id },
220581
+ meta: { error: err instanceof Error ? err.message : String(err) }
220582
+ });
220583
+ }
220584
+ return {
220585
+ modes,
220586
+ supportsSensitivity,
220587
+ ...sensitivityRange !== void 0 ? { sensitivity: sensitivityRange } : {},
220588
+ supportsSwitchDelay: false
220589
+ };
220590
+ },
220591
+ setSettings: async ({ deviceId, settings }) => {
220592
+ if (deviceId !== this.id) return;
220593
+ const api = await this.ensureApi();
220594
+ const ispPatch = {};
220595
+ if (settings.mode !== void 0) ispPatch.dayNight = capDayNightModeToReolink(settings.mode);
220596
+ if (settings.sensitivity !== void 0) {
220597
+ const range = (await api.getDayNightThreshold(channel))?.body?.DayNightThreshold?.thresholdval;
220598
+ const min = typeof range?.min === "number" ? range.min : 0;
220599
+ const max = typeof range?.max === "number" ? range.max : 100;
220600
+ ispPatch.dayNightThreshold = denormalizeFromPercent(settings.sensitivity, min, max);
220601
+ }
220602
+ if (Object.keys(ispPatch).length > 0) await api.setIsp(channel, ispPatch);
220603
+ await refreshFromCamera();
220604
+ }
220605
+ };
220606
+ this.ctx.registerNativeCap(dayNightCapability, provider);
220607
+ this.ctx.logger.info("Reolink day-night cap registered", { tags: { deviceId: this.id } });
220608
+ }
220609
+ /**
220610
+ * Register the vendor-neutral `image-settings` cap.
220611
+ *
220612
+ * Mirrors `registerDayNightCap` / `registerMotionZonesCap`: the
220613
+ * trampoline pattern over `createRuntimeStateBridge`, with a
220614
+ * single-flight `refresh` that round-trips `getIsp` (cmdId 26 — the
220615
+ * merged `VideoInput` + `InputAdvanceCfg` blob covers brightness /
220616
+ * contrast / saturation / sharpness AND the exposure mode in one
220617
+ * call). Sliders are normalized against Reolink's native 0..255 image
220618
+ * range; `exposureMode` maps 1:1 onto the cap's `'auto' | 'manual'`.
220619
+ *
220620
+ * Reolink's Baichuan API (`@apocaliss92/nodelink-js` 0.6.7) has no
220621
+ * write path for mirror / flip / rotate / white-balance / warmth, and
220622
+ * `InputAdvanceCfg.BLC` (backlight) has no matching setter — those
220623
+ * fields are honestly reported as unsupported (`false` / `[]`) rather
220624
+ * than rendering dead controls. TODO: revisit if nodelink-js adds
220625
+ * setters for any of these.
220626
+ */
220627
+ registerImageSettingsCap() {
220628
+ const channel = this.getChannel();
220629
+ const CAP_NAME = "image-settings";
220630
+ const STALE_MS = 1e4;
220631
+ const SLIDER_MIN = 0;
220632
+ const SLIDER_MAX = 255;
220633
+ const NORMALIZED_RANGE = {
220634
+ min: 0,
220635
+ max: 100,
220636
+ step: 1
220637
+ };
220638
+ /** Round-trip `getIsp`, map into the cap status schema, write slice. */
220639
+ const refreshFromCamera = async () => {
220640
+ if (this.imageSettingsRefreshInFlight) return this.imageSettingsRefreshInFlight;
220641
+ const promise = (async () => {
220642
+ try {
220643
+ const isp = await (await this.ensureApi()).getIsp(channel);
220644
+ const vi = isp?.body?.VideoInput;
220645
+ const exposureRaw = isp?.body?.InputAdvanceCfg?.Exposure?.mode;
220646
+ const next = { lastFetchedAt: Date.now() };
220647
+ if (typeof vi?.bright === "number") next.brightness = normalizeToPercent(vi.bright, SLIDER_MIN, SLIDER_MAX);
220648
+ if (typeof vi?.contrast === "number") next.contrast = normalizeToPercent(vi.contrast, SLIDER_MIN, SLIDER_MAX);
220649
+ if (typeof vi?.saturation === "number") next.saturation = normalizeToPercent(vi.saturation, SLIDER_MIN, SLIDER_MAX);
220650
+ if (typeof vi?.sharpen === "number") next.sharpness = normalizeToPercent(vi.sharpen, SLIDER_MIN, SLIDER_MAX);
220651
+ const exposureMode = reolinkExposureModeToCap(typeof exposureRaw === "string" ? exposureRaw : void 0);
220652
+ if (exposureMode !== void 0) next.exposureMode = exposureMode;
220653
+ this.runtimeState.setCapState(CAP_NAME, next);
220654
+ } catch (err) {
220655
+ this.ctx.logger.debug("image-settings refresh failed — keeping last slice", {
220656
+ tags: { deviceId: this.id },
220657
+ meta: { error: err instanceof Error ? err.message : String(err) }
220658
+ });
220659
+ }
220660
+ })();
220661
+ this.imageSettingsRefreshInFlight = promise;
220662
+ try {
220663
+ await promise;
220664
+ } finally {
220665
+ this.imageSettingsRefreshInFlight = null;
220666
+ }
220667
+ };
220668
+ const provider = {
220669
+ getStatus: createRuntimeStateBridge({
220670
+ runtimeState: this.runtimeState,
220671
+ cap: imageSettingsCapability,
220672
+ ownDeviceId: this.id,
220673
+ refresh: refreshFromCamera,
220674
+ staleMs: STALE_MS,
220675
+ empty: () => ({ lastFetchedAt: 0 })
220676
+ }).getStatus,
220677
+ getOptions: async ({ deviceId }) => {
220678
+ if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
220679
+ const isp = await (await this.ensureApi()).getIsp(channel);
220680
+ const vi = isp?.body?.VideoInput;
220681
+ const exposureRaw = isp?.body?.InputAdvanceCfg?.Exposure?.mode;
220682
+ const supportsBrightness = typeof vi?.bright === "number";
220683
+ const supportsContrast = typeof vi?.contrast === "number";
220684
+ const supportsSaturation = typeof vi?.saturation === "number";
220685
+ const supportsSharpness = typeof vi?.sharpen === "number";
220686
+ const exposureModes = typeof exposureRaw === "string" ? ["auto", "manual"] : [];
220687
+ return {
220688
+ supportsBrightness,
220689
+ ...supportsBrightness ? { brightness: NORMALIZED_RANGE } : {},
220690
+ supportsContrast,
220691
+ ...supportsContrast ? { contrast: NORMALIZED_RANGE } : {},
220692
+ supportsSaturation,
220693
+ ...supportsSaturation ? { saturation: NORMALIZED_RANGE } : {},
220694
+ supportsSharpness,
220695
+ ...supportsSharpness ? { sharpness: NORMALIZED_RANGE } : {},
220696
+ supportsMirror: false,
220697
+ supportsFlip: false,
220698
+ rotateOptions: [],
220699
+ whiteBalanceModes: [],
220700
+ supportsWarmth: false,
220701
+ exposureModes,
220702
+ backlightModes: []
220703
+ };
220704
+ },
220705
+ setSettings: async ({ deviceId, settings }) => {
220706
+ if (deviceId !== this.id) return;
220707
+ const api = await this.ensureApi();
220708
+ const imagePatch = {};
220709
+ if (settings.brightness !== void 0) imagePatch.bright = denormalizeFromPercent(settings.brightness, SLIDER_MIN, SLIDER_MAX);
220710
+ if (settings.contrast !== void 0) imagePatch.contrast = denormalizeFromPercent(settings.contrast, SLIDER_MIN, SLIDER_MAX);
220711
+ if (settings.saturation !== void 0) imagePatch.saturation = denormalizeFromPercent(settings.saturation, SLIDER_MIN, SLIDER_MAX);
220712
+ if (settings.sharpness !== void 0) imagePatch.sharpen = denormalizeFromPercent(settings.sharpness, SLIDER_MIN, SLIDER_MAX);
220713
+ if (Object.keys(imagePatch).length > 0) await api.setImage(channel, imagePatch);
220714
+ const ispPatch = {};
220715
+ if (settings.exposureMode !== void 0) ispPatch.exposure = settings.exposureMode;
220716
+ if (Object.keys(ispPatch).length > 0) await api.setIsp(channel, ispPatch);
220717
+ await refreshFromCamera();
220718
+ }
220719
+ };
220720
+ this.ctx.registerNativeCap(imageSettingsCapability, provider);
220721
+ this.ctx.logger.info("Reolink image-settings cap registered", { tags: { deviceId: this.id } });
220722
+ }
220723
+ /**
220094
220724
  * Register the `native-object-detection` cap.
220095
220725
  *
220096
220726
  * The runtimeState holds three fields:
@@ -220121,6 +220751,12 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
220121
220751
  }
220122
220752
  return classes;
220123
220753
  };
220754
+ const buildEmptyState = () => ({
220755
+ enabled: false,
220756
+ lastByClass: {},
220757
+ supportedClasses: buildSupportedClasses(),
220758
+ lastFetchedAt: 0
220759
+ });
220124
220760
  const provider = {
220125
220761
  getStatus: createRuntimeStateBridge({
220126
220762
  runtimeState: this.runtimeState,
@@ -220128,12 +220764,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
220128
220764
  ownDeviceId: this.id,
220129
220765
  refresh: async () => {},
220130
220766
  staleMs: Infinity,
220131
- empty: () => ({
220132
- enabled: false,
220133
- lastByClass: {},
220134
- supportedClasses: buildSupportedClasses(),
220135
- lastFetchedAt: 0
220136
- })
220767
+ empty: buildEmptyState
220137
220768
  }).getStatus,
220138
220769
  setEnabled: async ({ deviceId, enabled }) => {
220139
220770
  if (deviceId !== this.id) return;
@@ -220147,6 +220778,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
220147
220778
  }
220148
220779
  };
220149
220780
  this.ctx.registerNativeCap(nativeObjectDetectionCapability, provider);
220781
+ if (this.runtimeState.getCapState(CAP_NAME) === void 0) this.runtimeState.setCapState(CAP_NAME, {
220782
+ ...buildEmptyState(),
220783
+ lastFetchedAt: Date.now()
220784
+ });
220150
220785
  this.ctx.logger.info("Reolink native-object-detection cap registered", { tags: { deviceId: this.id } });
220151
220786
  }
220152
220787
  /**