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