@camstack/addon-provider-hikvision 1.1.15 → 1.1.17

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.mjs CHANGED
@@ -4636,7 +4636,7 @@ function _instanceof(cls, params = {}) {
4636
4636
  return inst;
4637
4637
  }
4638
4638
  //#endregion
4639
- //#region ../types/dist/sleep-BiDFW0E7.mjs
4639
+ //#region ../types/dist/sleep-CZDdRBua.mjs
4640
4640
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4641
4641
  EventCategory["SystemBoot"] = "system.boot";
4642
4642
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -7410,7 +7410,16 @@ var DecoderStatsSchema = object({
7410
7410
  inputFps: number(),
7411
7411
  outputFps: number(),
7412
7412
  avgDecodeTimeMs: number(),
7413
- droppedFrames: number()
7413
+ droppedFrames: number(),
7414
+ /**
7415
+ * Pull-mode adaptive-fps telemetry (optional — only pull sessions run the
7416
+ * lag-driven controller; push sessions omit these). `lagMs` is the EWMA of
7417
+ * the decoder's real-time drift (rising = falling behind live); `adaptiveFps`
7418
+ * is the current lag-throttled emit rate (≤ `effectiveFps` ceiling).
7419
+ */
7420
+ lagMs: number().optional(),
7421
+ effectiveFps: number().optional(),
7422
+ adaptiveFps: number().optional()
7414
7423
  });
7415
7424
  var DecoderSessionConfigSchema = object({
7416
7425
  codec: string(),
@@ -7451,7 +7460,15 @@ var DecoderSessionConfigSchema = object({
7451
7460
  * other — `pullFrames` returns nothing for an `'shm'` session and
7452
7461
  * `pullHandles` returns nothing for a `'callback'` session.
7453
7462
  */
7454
- frameSink: _enum(["callback", "shm"]).default("callback")
7463
+ frameSink: _enum(["callback", "shm"]).default("callback"),
7464
+ /**
7465
+ * Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
7466
+ * a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
7467
+ * real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
7468
+ * stream-broker's `streamingDebug` gate — off by default so production logs
7469
+ * stay quiet and the emit path pays zero per-frame cost when disabled.
7470
+ */
7471
+ debug: boolean().optional()
7455
7472
  });
7456
7473
  var EncodeProfileSchema = object({
7457
7474
  video: object({
@@ -10515,6 +10532,100 @@ var coverCapability = {
10515
10532
  runtimeState: CoverStatusSchema
10516
10533
  };
10517
10534
  /**
10535
+ * Vendor-neutral day/night (IR-cut) control — the per-camera config cap
10536
+ * shared by reolink / hikvision / amcrest. Models the common firmware
10537
+ * surface: the IR-cut switching MODE plus the two knobs that gate it
10538
+ * (photocell `sensitivity` + `switchDelaySec`). Each vendor maps these
10539
+ * onto its own ISAPI / Baichuan / Dahua-CGI fields; the cap standardises
10540
+ * the shape so ONE derived-form renders every camera.
10541
+ *
10542
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
10543
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
10544
+ * injected from `status`) reports the live values, and a single
10545
+ * `setSettings` mutation applies a partial change. No hand-written
10546
+ * settings-contribution methods — the framework derives the UI + save
10547
+ * routing from this surface.
10548
+ */
10549
+ /** IR-cut switching mode. `schedule` = time-of-day table configured on the camera. */
10550
+ var DayNightModeSchema = _enum([
10551
+ "auto",
10552
+ "day",
10553
+ "night",
10554
+ "schedule"
10555
+ ]);
10556
+ /** Normalized numeric range descriptor — `{ min, max, step }` per the
10557
+ * getOptions availability convention. Normalized values are 0–100. */
10558
+ var NormalizedRangeSchema$1 = object({
10559
+ min: number(),
10560
+ max: number(),
10561
+ step: number()
10562
+ });
10563
+ /**
10564
+ * Current day/night state. Optional fields are absent when the camera
10565
+ * does not expose that knob (a photocell-less model reports no
10566
+ * `sensitivity`). `lastFetchedAt` feeds the runtime-state bridge.
10567
+ */
10568
+ var DayNightStatusSchema = object({
10569
+ mode: DayNightModeSchema,
10570
+ /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
10571
+ sensitivity: number().optional(),
10572
+ /** Delay before the IR-cut filter flips, in seconds. */
10573
+ switchDelaySec: number().optional(),
10574
+ lastFetchedAt: number()
10575
+ });
10576
+ /**
10577
+ * Per-camera availability descriptor — drives which controls the admin UI
10578
+ * renders. Booleans as `supportsX`; numeric ranges as `{ min, max, step }`
10579
+ * (normalized 0–100); the mode choice-set as an array. A provider returns
10580
+ * honest, camera-probed values — never hardcoded.
10581
+ */
10582
+ var DayNightOptionsSchema = object({
10583
+ /** Modes this camera accepts. Empty → the camera has no configurable day/night mode. */
10584
+ modes: array(DayNightModeSchema),
10585
+ supportsSensitivity: boolean(),
10586
+ /** Present when `supportsSensitivity` — the normalized 0–100 range. */
10587
+ sensitivity: NormalizedRangeSchema$1.optional(),
10588
+ supportsSwitchDelay: boolean(),
10589
+ /** Present when `supportsSwitchDelay` — the allowed delay range in seconds. */
10590
+ switchDelaySec: NormalizedRangeSchema$1.optional()
10591
+ });
10592
+ /**
10593
+ * Partial change to the day/night config — every field optional. A
10594
+ * provider ignores fields it does not support.
10595
+ */
10596
+ var DayNightSettingsPatchSchema = object({
10597
+ mode: DayNightModeSchema.optional(),
10598
+ sensitivity: number().optional(),
10599
+ switchDelaySec: number().optional()
10600
+ });
10601
+ var dayNightCapability = {
10602
+ name: "day-night",
10603
+ scope: "device",
10604
+ deviceNative: true,
10605
+ mode: "singleton",
10606
+ deviceTypes: [DeviceType.Camera],
10607
+ deviceConfig: { ui: {
10608
+ kind: "derived-form",
10609
+ builderId: "day-night",
10610
+ tab: "image"
10611
+ } },
10612
+ methods: {
10613
+ getOptions: method(object({ deviceId: number() }), DayNightOptionsSchema),
10614
+ setSettings: method(object({
10615
+ deviceId: number(),
10616
+ settings: DayNightSettingsPatchSchema
10617
+ }), _void(), {
10618
+ kind: "mutation",
10619
+ auth: "admin"
10620
+ })
10621
+ },
10622
+ status: {
10623
+ schema: DayNightStatusSchema,
10624
+ kind: "poll"
10625
+ },
10626
+ runtimeState: DayNightStatusSchema
10627
+ };
10628
+ /**
10518
10629
  * Identity envelope for a device's upstream-system metadata.
10519
10630
  *
10520
10631
  * Two jobs:
@@ -11173,6 +11284,155 @@ var imageCapability = {
11173
11284
  runtimeState: ImageStatusSchema
11174
11285
  };
11175
11286
  /**
11287
+ * Vendor-neutral image / picture-adjustment cap — the per-camera config
11288
+ * cap shared by reolink / hikvision / amcrest. Models the common ISP
11289
+ * surface: the four picture sliders (brightness / contrast / saturation /
11290
+ * sharpness), orientation (mirror / flip / rotate), white-balance,
11291
+ * exposure and backlight-compensation modes.
11292
+ *
11293
+ * NORMALIZATION: every slider is a normalized int 0–100. Vendors expose
11294
+ * these natively as 0–100 or 0–255 (or other ranges); each provider maps
11295
+ * its native range to/from this normalized 0–100 space so the cap surface
11296
+ * (and the derived form) is identical across cameras. `warmth` (manual
11297
+ * white-balance) is likewise normalized 0–100.
11298
+ *
11299
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
11300
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
11301
+ * injected from `status`) reports the live values, and a single
11302
+ * `setSettings` mutation applies a partial change. No hand-written
11303
+ * settings-contribution methods — the framework derives the UI + save
11304
+ * routing from this surface.
11305
+ */
11306
+ /** Sensor/image rotation, degrees clockwise. */
11307
+ var ImageRotateSchema = _enum([
11308
+ "0",
11309
+ "90",
11310
+ "180",
11311
+ "270"
11312
+ ]);
11313
+ /** White-balance mode. `manual` unlocks the normalized `warmth` knob. */
11314
+ var WhiteBalanceModeSchema = _enum(["auto", "manual"]);
11315
+ /** Exposure mode. */
11316
+ var ExposureModeSchema = _enum(["auto", "manual"]);
11317
+ /**
11318
+ * Backlight-compensation mode:
11319
+ * - `off` — disabled
11320
+ * - `blc` — backlight compensation
11321
+ * - `wdr` — wide dynamic range
11322
+ * - `hlc` — highlight compensation
11323
+ */
11324
+ var BacklightModeSchema = _enum([
11325
+ "off",
11326
+ "blc",
11327
+ "wdr",
11328
+ "hlc"
11329
+ ]);
11330
+ /** Normalized numeric range descriptor — `{ min, max, step }` per the
11331
+ * getOptions availability convention. Slider values are normalized 0–100. */
11332
+ var NormalizedRangeSchema = object({
11333
+ min: number(),
11334
+ max: number(),
11335
+ step: number()
11336
+ });
11337
+ /**
11338
+ * Current image-adjustment state. Every field optional — absent when the
11339
+ * camera does not expose that control. Slider values are NORMALIZED 0–100.
11340
+ * `lastFetchedAt` feeds the runtime-state bridge.
11341
+ */
11342
+ var ImageSettingsStatusSchema = object({
11343
+ /** Normalized 0–100. */
11344
+ brightness: number().optional(),
11345
+ /** Normalized 0–100. */
11346
+ contrast: number().optional(),
11347
+ /** Normalized 0–100. */
11348
+ saturation: number().optional(),
11349
+ /** Normalized 0–100. */
11350
+ sharpness: number().optional(),
11351
+ mirror: boolean().optional(),
11352
+ flip: boolean().optional(),
11353
+ rotate: ImageRotateSchema.optional(),
11354
+ whiteBalance: WhiteBalanceModeSchema.optional(),
11355
+ /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
11356
+ warmth: number().optional(),
11357
+ exposureMode: ExposureModeSchema.optional(),
11358
+ backlightMode: BacklightModeSchema.optional(),
11359
+ lastFetchedAt: number()
11360
+ });
11361
+ /**
11362
+ * Per-camera availability descriptor — drives which controls the admin UI
11363
+ * renders. Booleans as `supportsX`; numeric ranges as `{ min, max, step }`
11364
+ * (the normalized 0–100 range); enums as arrays of supported values (empty
11365
+ * array → control hidden). A provider returns honest, camera-probed values
11366
+ * — never hardcoded.
11367
+ */
11368
+ var ImageSettingsOptionsSchema = object({
11369
+ supportsBrightness: boolean(),
11370
+ brightness: NormalizedRangeSchema.optional(),
11371
+ supportsContrast: boolean(),
11372
+ contrast: NormalizedRangeSchema.optional(),
11373
+ supportsSaturation: boolean(),
11374
+ saturation: NormalizedRangeSchema.optional(),
11375
+ supportsSharpness: boolean(),
11376
+ sharpness: NormalizedRangeSchema.optional(),
11377
+ supportsMirror: boolean(),
11378
+ supportsFlip: boolean(),
11379
+ /** Supported rotation values. Empty → rotation not configurable. */
11380
+ rotateOptions: array(ImageRotateSchema),
11381
+ /** Supported white-balance modes. Empty → white-balance not configurable. */
11382
+ whiteBalanceModes: array(WhiteBalanceModeSchema),
11383
+ supportsWarmth: boolean(),
11384
+ /** Present when `supportsWarmth` — the normalized 0–100 range. */
11385
+ warmth: NormalizedRangeSchema.optional(),
11386
+ /** Supported exposure modes. Empty → exposure not configurable. */
11387
+ exposureModes: array(ExposureModeSchema),
11388
+ /** Supported backlight modes. Empty → backlight-compensation not configurable. */
11389
+ backlightModes: array(BacklightModeSchema)
11390
+ });
11391
+ /**
11392
+ * Partial change to the image config — every field optional. Slider values
11393
+ * are normalized 0–100. A provider ignores fields it does not support.
11394
+ */
11395
+ var ImageSettingsPatchSchema = object({
11396
+ brightness: number().optional(),
11397
+ contrast: number().optional(),
11398
+ saturation: number().optional(),
11399
+ sharpness: number().optional(),
11400
+ mirror: boolean().optional(),
11401
+ flip: boolean().optional(),
11402
+ rotate: ImageRotateSchema.optional(),
11403
+ whiteBalance: WhiteBalanceModeSchema.optional(),
11404
+ warmth: number().optional(),
11405
+ exposureMode: ExposureModeSchema.optional(),
11406
+ backlightMode: BacklightModeSchema.optional()
11407
+ });
11408
+ var imageSettingsCapability = {
11409
+ name: "image-settings",
11410
+ scope: "device",
11411
+ deviceNative: true,
11412
+ mode: "singleton",
11413
+ deviceTypes: [DeviceType.Camera],
11414
+ deviceConfig: { ui: {
11415
+ kind: "derived-form",
11416
+ builderId: "image-settings",
11417
+ tab: "image"
11418
+ } },
11419
+ methods: {
11420
+ getOptions: method(object({ deviceId: number() }), ImageSettingsOptionsSchema),
11421
+ setSettings: method(object({
11422
+ deviceId: number(),
11423
+ settings: ImageSettingsPatchSchema
11424
+ }), _void(), {
11425
+ kind: "mutation",
11426
+ auth: "admin"
11427
+ })
11428
+ },
11429
+ status: {
11430
+ schema: ImageSettingsStatusSchema,
11431
+ kind: "poll"
11432
+ },
11433
+ runtimeState: ImageSettingsStatusSchema
11434
+ };
11435
+ /**
11176
11436
  * Robotic lawn-mower cap. Models HA `lawn_mower.*` entities — anything
11177
11437
  * with a mowing lifecycle plus a dock action.
11178
11438
  *
@@ -12171,6 +12431,16 @@ var RunnerCameraConfigSchema = object({
12171
12431
  * this gate is bypassed.
12172
12432
  */
12173
12433
  onboardMotionDrivesAnalyzer: boolean().default(true),
12434
+ /**
12435
+ * Master toggle for the occupancy re-check. When `false` (DEFAULT) the runner
12436
+ * never arms the periodic recheck timer, regardless of `occupancyRecheckSec` —
12437
+ * this is off by default because the recheck re-subscribes a detection session
12438
+ * every N seconds while `watching`, a major source of pull-decoder re-dial
12439
+ * churn (each cycle creates+tears a session → RTSP re-dial → latency). The
12440
+ * `occupancyRecheckSec` / `occupancyRecheckFrames` sliders only take effect
12441
+ * (and only render) when this is enabled.
12442
+ */
12443
+ occupancyRecheckEnabled: boolean().default(false),
12174
12444
  occupancyRecheckSec: number().min(occupancyRecheckSecField.min).max(occupancyRecheckSecField.max).default(occupancyRecheckSecField.default),
12175
12445
  occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default),
12176
12446
  /**
@@ -14176,6 +14446,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
14176
14446
  contact: contactCapability,
14177
14447
  control: controlCapability,
14178
14448
  cover: coverCapability,
14449
+ dayNight: dayNightCapability,
14179
14450
  deviceDiscovery: deviceDiscoveryCapability,
14180
14451
  deviceStatus: deviceStatusCapability,
14181
14452
  doorbell: doorbellCapability,
@@ -14188,6 +14459,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
14188
14459
  humidifier: humidifierCapability,
14189
14460
  humiditySensor: humiditySensorCapability,
14190
14461
  image: imageCapability,
14462
+ imageSettings: imageSettingsCapability,
14191
14463
  lawnMowerControl: lawnMowerControlCapability,
14192
14464
  lockControl: lockControlCapability,
14193
14465
  mediaPlayer: mediaPlayerCapability,
@@ -16114,7 +16386,9 @@ method(object({ codec: string() }), boolean()), method(_void(), object({
16114
16386
  id: string(),
16115
16387
  name: string(),
16116
16388
  isPullMode: boolean().optional(),
16117
- priority: number().optional()
16389
+ priority: number().optional(),
16390
+ hwaccel: string().optional(),
16391
+ probedBestHwaccel: string().optional()
16118
16392
  })), method(DecoderSessionConfigSchema, object({
16119
16393
  sessionId: string(),
16120
16394
  nodeId: string()
@@ -18028,7 +18302,17 @@ var AgentAddonConfigSchema = object({
18028
18302
  });
18029
18303
  var AgentPipelineSettingsSchema = object({
18030
18304
  addonDefaults: record(string(), AgentAddonConfigSchema).readonly(),
18031
- maxCameras: number().int().nonnegative().nullable().default(null)
18305
+ maxCameras: number().int().nonnegative().nullable().default(null),
18306
+ /** Per-node detection weight (relative share for the quota balancer). */
18307
+ detectWeight: number().positive().optional(),
18308
+ /** Node is eligible to run the detection pipeline (decode + inference). */
18309
+ detect: boolean().optional(),
18310
+ /** Node is eligible to host decoder sessions. */
18311
+ decode: boolean().optional(),
18312
+ /** Node is eligible to run audio-analyzer sessions. */
18313
+ audio: boolean().optional(),
18314
+ /** Node is eligible to be the ingest / source-owner (serve the restream). */
18315
+ ingest: boolean().optional()
18032
18316
  });
18033
18317
  var CameraPipelineForAgentSchema = object({
18034
18318
  steps: array(PipelineStepInputSchema).readonly(),
@@ -18334,6 +18618,21 @@ method(object({
18334
18618
  }), object({ success: literal(true) }), {
18335
18619
  kind: "mutation",
18336
18620
  auth: "admin"
18621
+ }), method(object({
18622
+ agentNodeId: string(),
18623
+ detectWeight: number().positive().nullable()
18624
+ }), object({ success: literal(true) }), {
18625
+ kind: "mutation",
18626
+ auth: "admin"
18627
+ }), method(object({
18628
+ agentNodeId: string(),
18629
+ detect: boolean().nullable().optional(),
18630
+ decode: boolean().nullable().optional(),
18631
+ audio: boolean().nullable().optional(),
18632
+ ingest: boolean().nullable().optional()
18633
+ }), object({ success: literal(true) }), {
18634
+ kind: "mutation",
18635
+ auth: "admin"
18337
18636
  }), method(object({ deviceId: number() }), CameraPipelineSettingsSchema.nullable()), method(object({
18338
18637
  deviceId: number(),
18339
18638
  addonId: string(),
@@ -22499,6 +22798,18 @@ Object.freeze({
22499
22798
  addonId: null,
22500
22799
  access: "view"
22501
22800
  },
22801
+ "dayNight.getOptions": {
22802
+ capName: "day-night",
22803
+ capScope: "device",
22804
+ addonId: null,
22805
+ access: "view"
22806
+ },
22807
+ "dayNight.setSettings": {
22808
+ capName: "day-night",
22809
+ capScope: "device",
22810
+ addonId: null,
22811
+ access: "create"
22812
+ },
22502
22813
  "decoder.createSession": {
22503
22814
  capName: "decoder",
22504
22815
  capScope: "system",
@@ -23429,6 +23740,18 @@ Object.freeze({
23429
23740
  addonId: null,
23430
23741
  access: "create"
23431
23742
  },
23743
+ "imageSettings.getOptions": {
23744
+ capName: "image-settings",
23745
+ capScope: "device",
23746
+ addonId: null,
23747
+ access: "view"
23748
+ },
23749
+ "imageSettings.setSettings": {
23750
+ capName: "image-settings",
23751
+ capScope: "device",
23752
+ addonId: null,
23753
+ access: "create"
23754
+ },
23432
23755
  "integrations.create": {
23433
23756
  capName: "integrations",
23434
23757
  capScope: "system",
@@ -24611,6 +24934,18 @@ Object.freeze({
24611
24934
  addonId: null,
24612
24935
  access: "create"
24613
24936
  },
24937
+ "pipelineOrchestrator.setAgentCapabilities": {
24938
+ capName: "pipeline-orchestrator",
24939
+ capScope: "system",
24940
+ addonId: null,
24941
+ access: "create"
24942
+ },
24943
+ "pipelineOrchestrator.setAgentDetectWeight": {
24944
+ capName: "pipeline-orchestrator",
24945
+ capScope: "system",
24946
+ addonId: null,
24947
+ access: "create"
24948
+ },
24614
24949
  "pipelineOrchestrator.setAgentMaxCameras": {
24615
24950
  capName: "pipeline-orchestrator",
24616
24951
  capScope: "system",
@@ -29870,6 +30205,26 @@ async function withTimeout(promise, ms, label) {
29870
30205
  }
29871
30206
  }
29872
30207
  /**
30208
+ * Hikvision's `IrcutFilterType` enumerates the same four values as the
30209
+ * `day-night` cap's `DayNightMode` — no value mapping needed, only a
30210
+ * type-narrowing guard for the loosely-typed ISAPI response.
30211
+ */
30212
+ var HIKVISION_DAY_NIGHT_MODES = [
30213
+ "auto",
30214
+ "day",
30215
+ "night",
30216
+ "schedule"
30217
+ ];
30218
+ function isDayNightMode(mode) {
30219
+ return mode === "auto" || mode === "day" || mode === "night" || mode === "schedule";
30220
+ }
30221
+ /** Narrow the ISAPI `IrcutFilterType` string to `DayNightMode`, falling
30222
+ * back to `'auto'` for any value outside the known enum (defensive —
30223
+ * firmware is not expected to emit anything else). */
30224
+ function toDayNightMode(mode) {
30225
+ return isDayNightMode(mode) ? mode : "auto";
30226
+ }
30227
+ /**
29873
30228
  * Hikvision camera device — ISAPI control channel for snapshot, reboot,
29874
30229
  * and the alarm event stream; RTSP URLs handed to the broker for video
29875
30230
  * pull (the broker handles the actual TCP/UDP RTSP, codec negotiation,
@@ -29950,6 +30305,10 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
29950
30305
  motionZonesRefreshInFlight = null;
29951
30306
  /** Single-flight guard for the privacy-mask camera refresh. */
29952
30307
  privacyMaskRefreshInFlight = null;
30308
+ /** Single-flight guard for the day-night camera refresh. */
30309
+ dayNightRefreshInFlight = null;
30310
+ /** Single-flight guard for the image-settings camera refresh. */
30311
+ imageSettingsRefreshInFlight = null;
29953
30312
  /**
29954
30313
  * Single-flight guard for the v0.2 capability discovery probe. Reset
29955
30314
  * on settings patches that change credentials (`disconnectAll`) so
@@ -30037,6 +30396,8 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
30037
30396
  this.registerStreamParamsCap(1);
30038
30397
  this.registerMotionZonesCap(1);
30039
30398
  this.registerPrivacyMaskCap(1);
30399
+ this.registerDayNightCap(1);
30400
+ this.registerImageSettingsCap(1);
30040
30401
  this.registerStreamCatalogProvider();
30041
30402
  this.registerDeviceAction("syncTimeFromLocal", deviceCustomAction(object({}), object({ success: boolean() }), { kind: "mutation" }), async () => {
30042
30403
  try {
@@ -31508,6 +31869,222 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
31508
31869
  });
31509
31870
  }
31510
31871
  /**
31872
+ * Register the `day-night` native cap provider — the vendor-neutral
31873
+ * IR-cut mode control shared with reolink/amcrest. Hikvision exposes
31874
+ * IR-cut switching mode at `/ISAPI/Image/channels/{cam}/IrcutFilter`
31875
+ * (`IrcutFilterType`: auto/day/night/schedule) — the enum matches the
31876
+ * cap's `DayNightMode` one-for-one, so no value mapping is needed.
31877
+ *
31878
+ * TODO: photocell `sensitivity` and `switchDelaySec` have no wired
31879
+ * ISAPI equivalent in `HikvisionIsapiClient` yet — the existing
31880
+ * `IrcutFilter` parser only reads `IrcutFilterType`. Advertised as
31881
+ * unsupported (`false`) rather than guessing an undocumented XML tag;
31882
+ * revisit once a real-device capture confirms the sensitivity/delay
31883
+ * field names.
31884
+ */
31885
+ registerDayNightCap(cameraNumber) {
31886
+ const isapi = this.ensureClient();
31887
+ const CAP_NAME = "day-night";
31888
+ const STALE_MS = 1e4;
31889
+ /** Round-trip the IR-cut filter mode, map into the cap status, write slice. */
31890
+ const refreshFromCamera = async () => {
31891
+ if (this.dayNightRefreshInFlight) return this.dayNightRefreshInFlight;
31892
+ const promise = (async () => {
31893
+ try {
31894
+ const filter = await isapi.getIrcutFilter(cameraNumber);
31895
+ if (!filter) return;
31896
+ const next = {
31897
+ mode: toDayNightMode(filter.mode),
31898
+ lastFetchedAt: Date.now()
31899
+ };
31900
+ this.runtimeState.setCapState(CAP_NAME, next);
31901
+ } catch (err) {
31902
+ this.ctx.logger.debug("hikvision day-night refresh failed — keeping last slice", {
31903
+ tags: { deviceId: this.id },
31904
+ meta: { error: err instanceof Error ? err.message : String(err) }
31905
+ });
31906
+ }
31907
+ })();
31908
+ this.dayNightRefreshInFlight = promise;
31909
+ try {
31910
+ await promise;
31911
+ } finally {
31912
+ this.dayNightRefreshInFlight = null;
31913
+ }
31914
+ };
31915
+ const provider = {
31916
+ getStatus: createRuntimeStateBridge({
31917
+ runtimeState: this.runtimeState,
31918
+ cap: dayNightCapability,
31919
+ ownDeviceId: this.id,
31920
+ refresh: refreshFromCamera,
31921
+ staleMs: STALE_MS,
31922
+ empty: () => ({
31923
+ mode: "auto",
31924
+ lastFetchedAt: 0
31925
+ })
31926
+ }).getStatus,
31927
+ getOptions: async ({ deviceId }) => {
31928
+ const cold = {
31929
+ modes: [],
31930
+ supportsSensitivity: false,
31931
+ supportsSwitchDelay: false
31932
+ };
31933
+ if (deviceId !== this.id) return cold;
31934
+ if (!await isapi.getIrcutFilter(cameraNumber)) return cold;
31935
+ return {
31936
+ modes: [...HIKVISION_DAY_NIGHT_MODES],
31937
+ supportsSensitivity: false,
31938
+ supportsSwitchDelay: false
31939
+ };
31940
+ },
31941
+ setSettings: async ({ deviceId, settings }) => {
31942
+ if (deviceId !== this.id) return;
31943
+ if (settings.mode !== void 0) await isapi.setIrcutFilter(cameraNumber, { mode: settings.mode });
31944
+ await refreshFromCamera();
31945
+ }
31946
+ };
31947
+ this.ctx.registerNativeCap(dayNightCapability, provider);
31948
+ this.ctx.logger.info("hikvision: day-night cap registered", {
31949
+ tags: { deviceId: this.id },
31950
+ meta: { cameraNumber }
31951
+ });
31952
+ }
31953
+ /**
31954
+ * Register the `image-settings` native cap provider — vendor-neutral
31955
+ * picture-adjustment controls shared with reolink/amcrest. Hikvision's
31956
+ * `/ISAPI/Image/channels/{cam}` sub-resources map cleanly onto FOUR
31957
+ * of the cap's fields:
31958
+ * - brightness/contrast/saturation ← `/color` (`getImageColor`/`setImageColor`)
31959
+ * - sharpness ← `/sharpness` (separate endpoint)
31960
+ * - backlightMode `'off'`/`'wdr'` ← `/WDR` (`getImageWdr`/`setImageWdr`, open/close)
31961
+ * All four are ALREADY normalized 0..100 on the wire (the ISAPI
31962
+ * client's `setImageColor`/`setImageSharpness` clamp to that range),
31963
+ * so no unit conversion is needed for the cap's normalized-0-100
31964
+ * contract — the range descriptor is the identity `{0,100,1}`.
31965
+ *
31966
+ * TODO — no wired ISAPI equivalent in `HikvisionIsapiClient` yet, left
31967
+ * unsupported (honest `false`/`[]` in `getOptions`) rather than
31968
+ * guessing undocumented XML tags:
31969
+ * - mirror / flip / rotate
31970
+ * - white-balance mode + manual warmth
31971
+ * - exposure mode
31972
+ * - backlightMode `'blc'` / `'hlc'` (only the WDR on/off toggle is wired)
31973
+ */
31974
+ registerImageSettingsCap(cameraNumber) {
31975
+ const isapi = this.ensureClient();
31976
+ const CAP_NAME = "image-settings";
31977
+ const STALE_MS = 1e4;
31978
+ const NORMALIZED_RANGE = {
31979
+ min: 0,
31980
+ max: 100,
31981
+ step: 1
31982
+ };
31983
+ /** Round-trip color + sharpness + WDR, map into the cap status, write slice. */
31984
+ const refreshFromCamera = async () => {
31985
+ if (this.imageSettingsRefreshInFlight) return this.imageSettingsRefreshInFlight;
31986
+ const promise = (async () => {
31987
+ try {
31988
+ const [color, sharpness, wdr] = await Promise.all([
31989
+ isapi.getImageColor(cameraNumber),
31990
+ isapi.getImageSharpness(cameraNumber),
31991
+ isapi.getImageWdr(cameraNumber)
31992
+ ]);
31993
+ if (!color && sharpness === null && !wdr) return;
31994
+ const next = {
31995
+ brightness: color?.brightness ?? void 0,
31996
+ contrast: color?.contrast ?? void 0,
31997
+ saturation: color?.saturation ?? void 0,
31998
+ sharpness: sharpness ?? color?.sharpness ?? void 0,
31999
+ backlightMode: wdr ? wdr.enabled ? "wdr" : "off" : void 0,
32000
+ lastFetchedAt: Date.now()
32001
+ };
32002
+ this.runtimeState.setCapState(CAP_NAME, next);
32003
+ } catch (err) {
32004
+ this.ctx.logger.debug("hikvision image-settings refresh failed — keeping last slice", {
32005
+ tags: { deviceId: this.id },
32006
+ meta: { error: err instanceof Error ? err.message : String(err) }
32007
+ });
32008
+ }
32009
+ })();
32010
+ this.imageSettingsRefreshInFlight = promise;
32011
+ try {
32012
+ await promise;
32013
+ } finally {
32014
+ this.imageSettingsRefreshInFlight = null;
32015
+ }
32016
+ };
32017
+ const provider = {
32018
+ getStatus: createRuntimeStateBridge({
32019
+ runtimeState: this.runtimeState,
32020
+ cap: imageSettingsCapability,
32021
+ ownDeviceId: this.id,
32022
+ refresh: refreshFromCamera,
32023
+ staleMs: STALE_MS,
32024
+ empty: () => ({ lastFetchedAt: 0 })
32025
+ }).getStatus,
32026
+ getOptions: async ({ deviceId }) => {
32027
+ const cold = {
32028
+ supportsBrightness: false,
32029
+ supportsContrast: false,
32030
+ supportsSaturation: false,
32031
+ supportsSharpness: false,
32032
+ supportsMirror: false,
32033
+ supportsFlip: false,
32034
+ rotateOptions: [],
32035
+ whiteBalanceModes: [],
32036
+ supportsWarmth: false,
32037
+ exposureModes: [],
32038
+ backlightModes: []
32039
+ };
32040
+ if (deviceId !== this.id) return cold;
32041
+ const [color, sharpness, wdr] = await Promise.all([
32042
+ isapi.getImageColor(cameraNumber),
32043
+ isapi.getImageSharpness(cameraNumber),
32044
+ isapi.getImageWdr(cameraNumber)
32045
+ ]);
32046
+ const hasBrightness = color?.brightness !== null && color?.brightness !== void 0;
32047
+ const hasContrast = color?.contrast !== null && color?.contrast !== void 0;
32048
+ const hasSaturation = color?.saturation !== null && color?.saturation !== void 0;
32049
+ const hasSharpness = sharpness !== null;
32050
+ return {
32051
+ supportsBrightness: hasBrightness,
32052
+ brightness: hasBrightness ? NORMALIZED_RANGE : void 0,
32053
+ supportsContrast: hasContrast,
32054
+ contrast: hasContrast ? NORMALIZED_RANGE : void 0,
32055
+ supportsSaturation: hasSaturation,
32056
+ saturation: hasSaturation ? NORMALIZED_RANGE : void 0,
32057
+ supportsSharpness: hasSharpness,
32058
+ sharpness: hasSharpness ? NORMALIZED_RANGE : void 0,
32059
+ supportsMirror: false,
32060
+ supportsFlip: false,
32061
+ rotateOptions: [],
32062
+ whiteBalanceModes: [],
32063
+ supportsWarmth: false,
32064
+ exposureModes: [],
32065
+ backlightModes: wdr ? ["off", "wdr"] : []
32066
+ };
32067
+ },
32068
+ setSettings: async ({ deviceId, settings }) => {
32069
+ if (deviceId !== this.id) return;
32070
+ if (settings.brightness !== void 0 || settings.contrast !== void 0 || settings.saturation !== void 0) await isapi.setImageColor(cameraNumber, {
32071
+ brightness: settings.brightness,
32072
+ contrast: settings.contrast,
32073
+ saturation: settings.saturation
32074
+ });
32075
+ if (settings.sharpness !== void 0) await isapi.setImageSharpness(cameraNumber, settings.sharpness);
32076
+ if (settings.backlightMode === "wdr") await isapi.setImageWdr(cameraNumber, { enabled: true });
32077
+ else if (settings.backlightMode === "off") await isapi.setImageWdr(cameraNumber, { enabled: false });
32078
+ await refreshFromCamera();
32079
+ }
32080
+ };
32081
+ this.ctx.registerNativeCap(imageSettingsCapability, provider);
32082
+ this.ctx.logger.info("hikvision: image-settings cap registered", {
32083
+ tags: { deviceId: this.id },
32084
+ meta: { cameraNumber }
32085
+ });
32086
+ }
32087
+ /**
31511
32088
  * Translate a cap-layer `StreamProfilePatch` into the field set
31512
32089
  * `setStreamingChannel` accepts. `current` is the freshly-read
31513
32090
  * channel config — used to resolve which quality-mode field a