@camstack/addon-provider-hikvision 1.2.117 → 1.2.119
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 +332 -24
- package/dist/addon.mjs +332 -24
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -5367,7 +5367,7 @@ var ZodIssueCode = {
|
|
|
5367
5367
|
var ZodFirstPartyTypeKind;
|
|
5368
5368
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5369
5369
|
//#endregion
|
|
5370
|
-
//#region ../types/dist/sleep-
|
|
5370
|
+
//#region ../types/dist/sleep-B1y0Fo1U.mjs
|
|
5371
5371
|
/**
|
|
5372
5372
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5373
5373
|
* window to float samples (D455).
|
|
@@ -31371,14 +31371,81 @@ function summarisePrivacyAudio(profiles) {
|
|
|
31371
31371
|
if (profiles.length === 0) return null;
|
|
31372
31372
|
return profiles.some((p) => p.audioEnabled);
|
|
31373
31373
|
}
|
|
31374
|
+
/**
|
|
31375
|
+
* What a preset id DOES on this camera.
|
|
31376
|
+
*
|
|
31377
|
+
* On several PTZ firmwares the preset namespace is shared: low ids are stored
|
|
31378
|
+
* positions, a reserved band triggers camera FUNCTIONS. The dispensa camera
|
|
31379
|
+
* (Hikvision) returns its own reserved band in the preset list, named by the
|
|
31380
|
+
* firmware:
|
|
31381
|
+
*
|
|
31382
|
+
* 34 Back to origin · 39 Day mode · 40 Night mode · 46 Day/Night Auto Mode
|
|
31383
|
+
* 92 Set manual limits · 93 Save manual limits · 94 Remote reboot
|
|
31384
|
+
* 95 Call OSD menu
|
|
31385
|
+
*
|
|
31386
|
+
* We used to hand all of these to the UI as ordinary presets. Saving over one
|
|
31387
|
+
* is what produced `PUT /ISAPI/PTZCtrl/channels/1/presets/34 -> HTTP 500`: the
|
|
31388
|
+
* camera refusing to let "Back to origin" be overwritten, surfaced to the
|
|
31389
|
+
* operator as a transport failure.
|
|
31390
|
+
*
|
|
31391
|
+
* A function preset stays fully usable as a DESTINATION -- `goToPreset('34')`
|
|
31392
|
+
* is the single most useful thing that camera can do. Only `savePreset` and
|
|
31393
|
+
* `deletePreset` are gated.
|
|
31394
|
+
*/
|
|
31395
|
+
var PtzPresetFunctionSchema = _enum([
|
|
31396
|
+
"home",
|
|
31397
|
+
"day-mode",
|
|
31398
|
+
"night-mode",
|
|
31399
|
+
"day-night-auto",
|
|
31400
|
+
"set-limits",
|
|
31401
|
+
"save-limits",
|
|
31402
|
+
"reboot",
|
|
31403
|
+
"osd-menu"
|
|
31404
|
+
]);
|
|
31374
31405
|
var PtzPresetSchema = object({
|
|
31375
31406
|
id: string(),
|
|
31376
|
-
name: string()
|
|
31407
|
+
name: string(),
|
|
31408
|
+
/**
|
|
31409
|
+
* The firmware function this id performs, when the provider KNOWS the id is
|
|
31410
|
+
* reserved. `null` = an ordinary stored position, OR a reserved id this
|
|
31411
|
+
* provider does not recognise -- the two are told apart by `writable`.
|
|
31412
|
+
*/
|
|
31413
|
+
fn: PtzPresetFunctionSchema.nullable(),
|
|
31414
|
+
/**
|
|
31415
|
+
* Whether `savePreset` / `deletePreset` may target this id. A provider sets
|
|
31416
|
+
* it false only for a band it KNOWS is reserved; where the firmware is not
|
|
31417
|
+
* documented it stays true and a refusal comes from the camera, named.
|
|
31418
|
+
*/
|
|
31419
|
+
writable: boolean()
|
|
31377
31420
|
});
|
|
31421
|
+
/**
|
|
31422
|
+
* Where the head is pointing -- per axis, and `null` when the driver cannot
|
|
31423
|
+
* read it.
|
|
31424
|
+
*
|
|
31425
|
+
* It used to be three REQUIRED numbers, which left a provider that cannot read
|
|
31426
|
+
* position no way to say so. All four said `0`:
|
|
31427
|
+
*
|
|
31428
|
+
* reolink `return { pan: 0, tilt: 0, zoom: 0 }` "until a position-query
|
|
31429
|
+
* path lands upstream"
|
|
31430
|
+
* hikvision stub, "firmware-dependent and frequently absent"
|
|
31431
|
+
* amcrest stub, "no generic absolute-position read for this model family"
|
|
31432
|
+
* onvif throws
|
|
31433
|
+
*
|
|
31434
|
+
* So every consumer asking where a camera points was told "perfectly centred"
|
|
31435
|
+
* by four cameras that had never looked. That is D393 -- a measurement that
|
|
31436
|
+
* FAILED is `null`, never `0`, and the type says so all the way to the
|
|
31437
|
+
* decision -- the same shape as the unreadable `statfs` folded into "0 bytes of
|
|
31438
|
+
* headroom", which evacuated a healthy disk.
|
|
31439
|
+
*
|
|
31440
|
+
* D393 also says to check for a narrower structural TWIN of the result type.
|
|
31441
|
+
* There is one: `PtzStatusSchema` extends this, so the lie had already
|
|
31442
|
+
* propagated into `getStatus`, which is the surface `getPosition`'s own comment
|
|
31443
|
+
* tells callers to migrate to.
|
|
31444
|
+
*/
|
|
31378
31445
|
var PtzPositionSchema = object({
|
|
31379
|
-
pan: number(),
|
|
31380
|
-
tilt: number(),
|
|
31381
|
-
zoom: number()
|
|
31446
|
+
pan: number().nullable(),
|
|
31447
|
+
tilt: number().nullable(),
|
|
31448
|
+
zoom: number().nullable()
|
|
31382
31449
|
});
|
|
31383
31450
|
var PtzMoveCommandSchema = object({
|
|
31384
31451
|
pan: number().optional(),
|
|
@@ -31400,7 +31467,72 @@ var PtzOptionsSchema = object({
|
|
|
31400
31467
|
maxPresets: number().optional(),
|
|
31401
31468
|
/** Whether the camera exposes a controllable autofocus toggle
|
|
31402
31469
|
* (boolean `hasX` per the getOptions availability convention). */
|
|
31403
|
-
hasAutofocus: boolean()
|
|
31470
|
+
hasAutofocus: boolean(),
|
|
31471
|
+
/**
|
|
31472
|
+
* How many distinct speeds this camera's NATIVE scale offers.
|
|
31473
|
+
*
|
|
31474
|
+
* The cap's `speed` is normalized 0..1 and each provider maps it down --
|
|
31475
|
+
* Baichuan 1..63, Dahua 1..8, ISAPI's percentage -100..100. The normalized
|
|
31476
|
+
* value hides how coarse that really is: asking an amcrest for `0.37` is
|
|
31477
|
+
* meaningless, because it has eight steps and three of them round to the
|
|
31478
|
+
* same one. A loop that tunes its own gain has to know the granularity of
|
|
31479
|
+
* the knob it is turning.
|
|
31480
|
+
*
|
|
31481
|
+
* `null` = the driver does not know, or the scale is continuous (ONVIF takes
|
|
31482
|
+
* a float). Never a made-up number.
|
|
31483
|
+
*/
|
|
31484
|
+
speedSteps: number().int().positive().nullable(),
|
|
31485
|
+
/**
|
|
31486
|
+
* How long ONE `move` pulse runs on this driver, ms.
|
|
31487
|
+
*
|
|
31488
|
+
* `move` is a self-terminating burst everywhere, but every provider hardcoded
|
|
31489
|
+
* its own duration in private -- 200 reolink, 350 amcrest, 500 hikvision,
|
|
31490
|
+
* 1000 onvif -- so no caller could read it. It is the DENOMINATOR of "how far
|
|
31491
|
+
* did the head travel per pulse": without it a measured displacement has no
|
|
31492
|
+
* gain to be divided into.
|
|
31493
|
+
*
|
|
31494
|
+
* `null` = the driver cannot say.
|
|
31495
|
+
*/
|
|
31496
|
+
moveImpulseMs: number().int().positive().nullable()
|
|
31497
|
+
});
|
|
31498
|
+
/**
|
|
31499
|
+
* Which preset `goHome` goes to, and WHO decided.
|
|
31500
|
+
*
|
|
31501
|
+
* `goHome` used to be three vendor "conventions" written in three comments --
|
|
31502
|
+
* Reolink preset 0, Hikvision preset '1', Dahua preset 1 -- and on this fleet
|
|
31503
|
+
* not one of the three cameras honours its own:
|
|
31504
|
+
*
|
|
31505
|
+
* 592 Videocamera camera Daniel (reolink) preset 0 holds "stanza"
|
|
31506
|
+
* 1438 Videocamera dispensa (hikvision) preset 1 holds "Credenza"
|
|
31507
|
+
* 3836 Videocamera studio (amcrest) preset 1 holds "Preset1"
|
|
31508
|
+
*
|
|
31509
|
+
* So `goHome` meant "go wherever the operator happened to save in slot 0 or 1",
|
|
31510
|
+
* and two of the three swallowed the failure in a bare `catch`. For autotrack
|
|
31511
|
+
* this is the HOTTEST path -- it runs every time a subject is released -- so it
|
|
31512
|
+
* needs an answer that is true per camera and audible when it is missing.
|
|
31513
|
+
*
|
|
31514
|
+
* `source` is what makes it honest:
|
|
31515
|
+
* - `operator` -- picked in the UI, stored by the provider.
|
|
31516
|
+
* - `firmware` -- the camera itself named a preset `fn: 'home'` (Hikvision's
|
|
31517
|
+
* "Back to origin"), adopted as the default with nothing to configure.
|
|
31518
|
+
* - `native` -- the driver homes WITHOUT a preset at all. ONVIF does:
|
|
31519
|
+
* `goHome` is `ptzAbsoluteMove({x:0, y:0, zoom:0})`. `presetId` is null and
|
|
31520
|
+
* that is not a failure -- there is nothing to pick, and the UI must offer
|
|
31521
|
+
* no picker.
|
|
31522
|
+
* - `none` -- nothing is configured and the camera names nothing.
|
|
31523
|
+
* `goHome` REFUSES rather than moving the head somewhere arbitrary.
|
|
31524
|
+
*
|
|
31525
|
+
* `source === 'none'` is the refusal condition, NOT `presetId === null`: the
|
|
31526
|
+
* native case has no preset and homes perfectly well.
|
|
31527
|
+
*/
|
|
31528
|
+
var PtzHomePresetSchema = object({
|
|
31529
|
+
presetId: string().nullable(),
|
|
31530
|
+
source: _enum([
|
|
31531
|
+
"operator",
|
|
31532
|
+
"firmware",
|
|
31533
|
+
"native",
|
|
31534
|
+
"none"
|
|
31535
|
+
])
|
|
31404
31536
|
});
|
|
31405
31537
|
var ptzCapability = {
|
|
31406
31538
|
name: "ptz",
|
|
@@ -31441,7 +31573,25 @@ var ptzCapability = {
|
|
|
31441
31573
|
auth: "admin"
|
|
31442
31574
|
}),
|
|
31443
31575
|
getOptions: method(object({ deviceId: number() }), PtzOptionsSchema),
|
|
31576
|
+
/**
|
|
31577
|
+
* Move to the camera's configured home preset. THROWS when
|
|
31578
|
+
* `getHomePreset()` resolves to `none` -- a head that did not move must
|
|
31579
|
+
* never look like a head that went home.
|
|
31580
|
+
*/
|
|
31444
31581
|
goHome: method(object({ deviceId: number() }), _void(), { kind: "mutation" }),
|
|
31582
|
+
/** Which preset `goHome` targets, and who decided it. */
|
|
31583
|
+
getHomePreset: method(object({ deviceId: number() }), PtzHomePresetSchema),
|
|
31584
|
+
/**
|
|
31585
|
+
* Pin the home preset for this camera. `null` clears the operator's choice
|
|
31586
|
+
* and falls back to whatever the firmware names, or to `none`.
|
|
31587
|
+
*/
|
|
31588
|
+
setHomePreset: method(object({
|
|
31589
|
+
deviceId: number(),
|
|
31590
|
+
presetId: string().nullable()
|
|
31591
|
+
}), _void(), {
|
|
31592
|
+
kind: "mutation",
|
|
31593
|
+
auth: "admin"
|
|
31594
|
+
}),
|
|
31445
31595
|
/**
|
|
31446
31596
|
* Pull the current PTZ position. Redundant with the auto-injected
|
|
31447
31597
|
* `getStatus` method (see `status` below); kept for callers that
|
|
@@ -41167,6 +41317,12 @@ Object.freeze({
|
|
|
41167
41317
|
addonId: null,
|
|
41168
41318
|
access: "delete"
|
|
41169
41319
|
},
|
|
41320
|
+
"ptz.getHomePreset": {
|
|
41321
|
+
capName: "ptz",
|
|
41322
|
+
capScope: "device",
|
|
41323
|
+
addonId: null,
|
|
41324
|
+
access: "view"
|
|
41325
|
+
},
|
|
41170
41326
|
"ptz.getOptions": {
|
|
41171
41327
|
capName: "ptz",
|
|
41172
41328
|
capScope: "device",
|
|
@@ -41215,6 +41371,12 @@ Object.freeze({
|
|
|
41215
41371
|
addonId: null,
|
|
41216
41372
|
access: "create"
|
|
41217
41373
|
},
|
|
41374
|
+
"ptz.setHomePreset": {
|
|
41375
|
+
capName: "ptz",
|
|
41376
|
+
capScope: "device",
|
|
41377
|
+
addonId: null,
|
|
41378
|
+
access: "create"
|
|
41379
|
+
},
|
|
41218
41380
|
"ptz.stop": {
|
|
41219
41381
|
capName: "ptz",
|
|
41220
41382
|
capScope: "device",
|
|
@@ -44388,6 +44550,11 @@ Object.freeze({
|
|
|
44388
44550
|
form: "single",
|
|
44389
44551
|
optional: false
|
|
44390
44552
|
}],
|
|
44553
|
+
"ptz.getHomePreset": [{
|
|
44554
|
+
name: "deviceId",
|
|
44555
|
+
form: "single",
|
|
44556
|
+
optional: false
|
|
44557
|
+
}],
|
|
44391
44558
|
"ptz.getOptions": [{
|
|
44392
44559
|
name: "deviceId",
|
|
44393
44560
|
form: "single",
|
|
@@ -44428,6 +44595,11 @@ Object.freeze({
|
|
|
44428
44595
|
form: "single",
|
|
44429
44596
|
optional: false
|
|
44430
44597
|
}],
|
|
44598
|
+
"ptz.setHomePreset": [{
|
|
44599
|
+
name: "deviceId",
|
|
44600
|
+
form: "single",
|
|
44601
|
+
optional: false
|
|
44602
|
+
}],
|
|
44431
44603
|
"ptz.stop": [{
|
|
44432
44604
|
name: "deviceId",
|
|
44433
44605
|
form: "single",
|
|
@@ -49736,6 +49908,73 @@ function buildRawState(reader) {
|
|
|
49736
49908
|
};
|
|
49737
49909
|
}
|
|
49738
49910
|
//#endregion
|
|
49911
|
+
//#region src/isapi-ptz-speed.ts
|
|
49912
|
+
/**
|
|
49913
|
+
* Turn the cap's normalized direction + magnitude into an ISAPI continuous
|
|
49914
|
+
* speed.
|
|
49915
|
+
*
|
|
49916
|
+
* The `ptz` cap splits the two: `pan`/`tilt`/`zoom` are a DIRECTION in -1..1,
|
|
49917
|
+
* and `speed` is the magnitude in 0..1. reolink maps them to Baichuan 1..63,
|
|
49918
|
+
* amcrest to Dahua 1..8 — and hikvision used to map them to nothing at all. It
|
|
49919
|
+
* dropped `speed` before it left the provider and handed the raw direction to
|
|
49920
|
+
* `/ISAPI/PTZCtrl/channels/{cam}/continuous`, whose `<pan>` is a speed
|
|
49921
|
+
* PERCENTAGE in -100..100.
|
|
49922
|
+
*
|
|
49923
|
+
* So `usePTZ` sending its d-pad default (`pan: 1, speed: 0.5`) reached the
|
|
49924
|
+
* dispensa camera as `<pan>1</pan>`: one percent of the camera's speed. A
|
|
49925
|
+
* request at less than half magnitude rounded to `0` and produced no motion at
|
|
49926
|
+
* all.
|
|
49927
|
+
*
|
|
49928
|
+
* An absent `speed` is mid-scale, never zero — reolink defaults to 32 of 63 for
|
|
49929
|
+
* the same reason. A caller who did not state a magnitude asked to move, not to
|
|
49930
|
+
* stand still.
|
|
49931
|
+
*/
|
|
49932
|
+
var MID_SCALE = .5;
|
|
49933
|
+
function isapiSpeedFor(direction, speed) {
|
|
49934
|
+
const d = direction ?? 0;
|
|
49935
|
+
if (!Number.isFinite(d) || d === 0) return 0;
|
|
49936
|
+
const magnitude = speed === void 0 || !Number.isFinite(speed) ? MID_SCALE : Math.abs(speed);
|
|
49937
|
+
const percent = Math.round(Math.sign(d) * Math.min(1, Math.abs(d)) * Math.min(1, magnitude) * 100);
|
|
49938
|
+
return Math.max(-100, Math.min(100, percent));
|
|
49939
|
+
}
|
|
49940
|
+
//#endregion
|
|
49941
|
+
//#region src/ptz-preset-functions.ts
|
|
49942
|
+
var RESERVED = new Map([
|
|
49943
|
+
["34", "home"],
|
|
49944
|
+
["39", "day-mode"],
|
|
49945
|
+
["40", "night-mode"],
|
|
49946
|
+
["46", "day-night-auto"],
|
|
49947
|
+
["92", "set-limits"],
|
|
49948
|
+
["93", "save-limits"],
|
|
49949
|
+
["94", "reboot"],
|
|
49950
|
+
["95", "osd-menu"]
|
|
49951
|
+
]);
|
|
49952
|
+
/** The firmware function this id performs, or `null` for a stored position. */
|
|
49953
|
+
function presetFunctionFor(presetId) {
|
|
49954
|
+
return RESERVED.get(presetId) ?? null;
|
|
49955
|
+
}
|
|
49956
|
+
/**
|
|
49957
|
+
* Whether `savePreset` / `deletePreset` may target this id.
|
|
49958
|
+
*
|
|
49959
|
+
* Anything outside the known band stays writable: an undocumented firmware
|
|
49960
|
+
* must be allowed to refuse for itself rather than be guessed at here.
|
|
49961
|
+
*/
|
|
49962
|
+
function isPresetWritable(presetId) {
|
|
49963
|
+
return !RESERVED.has(presetId);
|
|
49964
|
+
}
|
|
49965
|
+
/**
|
|
49966
|
+
* Refuse a write to a reserved id, naming the function that owns it.
|
|
49967
|
+
*
|
|
49968
|
+
* Extracted so the REFUSAL itself is assertable. With the check inline in
|
|
49969
|
+
* `savePreset` / `deletePreset`, deleting it left every test green -- the table
|
|
49970
|
+
* spec proves what the band IS, not that anything consults it.
|
|
49971
|
+
*/
|
|
49972
|
+
function assertPresetWritable(presetId, action) {
|
|
49973
|
+
const fn = presetFunctionFor(presetId);
|
|
49974
|
+
if (fn === null) return;
|
|
49975
|
+
throw new Error(`Preset ${presetId} is a firmware function on this camera (${fn}) and cannot be ${action}` + (action === "overwritten" ? " — pick an unreserved id" : ""));
|
|
49976
|
+
}
|
|
49977
|
+
//#endregion
|
|
49739
49978
|
//#region src/schema.ts
|
|
49740
49979
|
/**
|
|
49741
49980
|
* Single source of truth for the addon id used in event-source tags +
|
|
@@ -50054,6 +50293,17 @@ var hikvisionCameraSchema = object({
|
|
|
50054
50293
|
*/
|
|
50055
50294
|
snapshotChannelId: string().default("").describe("ISAPI channel used for snapshots (empty = main stream)"),
|
|
50056
50295
|
/**
|
|
50296
|
+
* Which preset `ptz.goHome` targets on THIS camera.
|
|
50297
|
+
*
|
|
50298
|
+
* Empty = no operator choice. The provider then falls back to the preset the
|
|
50299
|
+
* FIRMWARE names as home (Hikvision returns id 34 "Back to origin" in its
|
|
50300
|
+
* reserved band), and only if that is absent too does `goHome` refuse.
|
|
50301
|
+
*
|
|
50302
|
+
* It has to be stored because the vendor "convention" is wrong here: the code
|
|
50303
|
+
* went to preset '1', and on the dispensa camera preset 1 is "Credenza".
|
|
50304
|
+
*/
|
|
50305
|
+
homePresetId: string().default("").describe("Preset id ptz.goHome targets (empty = use the firmware home)"),
|
|
50306
|
+
/**
|
|
50057
50307
|
* Motion detection master switch + sensitivity (0..100). Maps to
|
|
50058
50308
|
* ISAPI `/ISAPI/System/Video/inputs/channels/{cam}/motionDetection`.
|
|
50059
50309
|
* Disabling this stops the camera from emitting `<eventType>VMD</eventType>`
|
|
@@ -51091,6 +51341,41 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51091
51341
|
...slice
|
|
51092
51342
|
} });
|
|
51093
51343
|
}
|
|
51344
|
+
/**
|
|
51345
|
+
* Which preset `goHome` targets, and who decided it.
|
|
51346
|
+
*
|
|
51347
|
+
* The operator's pick wins. With none, the FIRMWARE's own home is adopted --
|
|
51348
|
+
* Hikvision names id 34 "Back to origin" in its reserved band -- so a camera
|
|
51349
|
+
* that already knows where home is needs nothing configured. Only when both
|
|
51350
|
+
* are absent is the answer `none`, and `goHome` then refuses rather than
|
|
51351
|
+
* moving the head to an arbitrary slot.
|
|
51352
|
+
*
|
|
51353
|
+
* A failed preset read yields `none`, never a guess: an unreadable camera is
|
|
51354
|
+
* not evidence that preset 34 exists (D393).
|
|
51355
|
+
*/
|
|
51356
|
+
async resolveHomePreset() {
|
|
51357
|
+
const chosen = this.config.get("homePresetId") ?? "";
|
|
51358
|
+
if (chosen !== "") return {
|
|
51359
|
+
presetId: chosen,
|
|
51360
|
+
source: "operator"
|
|
51361
|
+
};
|
|
51362
|
+
try {
|
|
51363
|
+
const firmwareHome = (await this.ensureClient().listPtzPresets(1)).find((preset) => presetFunctionFor(preset.id) === "home");
|
|
51364
|
+
if (firmwareHome !== void 0) return {
|
|
51365
|
+
presetId: firmwareHome.id,
|
|
51366
|
+
source: "firmware"
|
|
51367
|
+
};
|
|
51368
|
+
} catch (err) {
|
|
51369
|
+
this.ctx.logger.warn("hikvision home preset lookup failed — goHome will refuse", {
|
|
51370
|
+
tags: { deviceId: this.id },
|
|
51371
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
51372
|
+
});
|
|
51373
|
+
}
|
|
51374
|
+
return {
|
|
51375
|
+
presetId: null,
|
|
51376
|
+
source: "none"
|
|
51377
|
+
};
|
|
51378
|
+
}
|
|
51094
51379
|
/** Re-read the image-tuning snapshot from the camera and persist it.
|
|
51095
51380
|
* Best-effort: a refresh failure is logged and swallowed so a save
|
|
51096
51381
|
* never throws. Called after an Image-section push in
|
|
@@ -51367,12 +51652,12 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51367
51652
|
registerPtzCap(cameraNumber) {
|
|
51368
51653
|
const isapi = this.ensureClient();
|
|
51369
51654
|
this.ctx.registerNativeCap(ptzCapability, {
|
|
51370
|
-
move: async ({ deviceId, pan, tilt, zoom }) => {
|
|
51655
|
+
move: async ({ deviceId, pan, tilt, zoom, speed }) => {
|
|
51371
51656
|
if (deviceId !== this.id) return;
|
|
51372
51657
|
await isapi.ptzContinuous(cameraNumber, {
|
|
51373
|
-
pan,
|
|
51374
|
-
tilt,
|
|
51375
|
-
zoom
|
|
51658
|
+
pan: isapiSpeedFor(pan, speed),
|
|
51659
|
+
tilt: isapiSpeedFor(tilt, speed),
|
|
51660
|
+
zoom: isapiSpeedFor(zoom, speed)
|
|
51376
51661
|
});
|
|
51377
51662
|
await new Promise((r) => setTimeout(r, 500));
|
|
51378
51663
|
await isapi.ptzContinuous(cameraNumber, {
|
|
@@ -51381,12 +51666,12 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51381
51666
|
zoom: 0
|
|
51382
51667
|
});
|
|
51383
51668
|
},
|
|
51384
|
-
continuousMove: async ({ deviceId, pan, tilt, zoom }) => {
|
|
51669
|
+
continuousMove: async ({ deviceId, pan, tilt, zoom, speed }) => {
|
|
51385
51670
|
if (deviceId !== this.id) return;
|
|
51386
51671
|
await isapi.ptzContinuous(cameraNumber, {
|
|
51387
|
-
pan,
|
|
51388
|
-
tilt,
|
|
51389
|
-
zoom
|
|
51672
|
+
pan: isapiSpeedFor(pan, speed),
|
|
51673
|
+
tilt: isapiSpeedFor(tilt, speed),
|
|
51674
|
+
zoom: isapiSpeedFor(zoom, speed)
|
|
51390
51675
|
});
|
|
51391
51676
|
},
|
|
51392
51677
|
stop: async ({ deviceId }) => {
|
|
@@ -51401,7 +51686,9 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51401
51686
|
if (deviceId !== this.id) return [];
|
|
51402
51687
|
return (await isapi.listPtzPresets(cameraNumber)).map((p) => ({
|
|
51403
51688
|
id: p.id,
|
|
51404
|
-
name: p.name ?? `Preset ${p.id}
|
|
51689
|
+
name: p.name ?? `Preset ${p.id}`,
|
|
51690
|
+
fn: presetFunctionFor(p.id),
|
|
51691
|
+
writable: isPresetWritable(p.id)
|
|
51405
51692
|
}));
|
|
51406
51693
|
},
|
|
51407
51694
|
goToPreset: async ({ deviceId, presetId }) => {
|
|
@@ -51410,10 +51697,12 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51410
51697
|
},
|
|
51411
51698
|
savePreset: async ({ deviceId, presetId, name }) => {
|
|
51412
51699
|
if (deviceId !== this.id) return;
|
|
51700
|
+
assertPresetWritable(presetId, "overwritten");
|
|
51413
51701
|
await isapi.savePtzPreset(cameraNumber, presetId, name);
|
|
51414
51702
|
},
|
|
51415
51703
|
deletePreset: async ({ deviceId, presetId }) => {
|
|
51416
51704
|
if (deviceId !== this.id) return;
|
|
51705
|
+
assertPresetWritable(presetId, "deleted");
|
|
51417
51706
|
await isapi.deletePtzPreset(cameraNumber, presetId);
|
|
51418
51707
|
},
|
|
51419
51708
|
getOptions: async ({ deviceId }) => {
|
|
@@ -51422,7 +51711,9 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51422
51711
|
hasTilt: false,
|
|
51423
51712
|
hasZoom: false,
|
|
51424
51713
|
supportsPresets: false,
|
|
51425
|
-
hasAutofocus: false
|
|
51714
|
+
hasAutofocus: false,
|
|
51715
|
+
speedSteps: 100,
|
|
51716
|
+
moveImpulseMs: 500
|
|
51426
51717
|
};
|
|
51427
51718
|
const caps = await isapi.getPtzCapabilities(cameraNumber);
|
|
51428
51719
|
if (!caps) return {
|
|
@@ -51430,7 +51721,9 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51430
51721
|
hasTilt: false,
|
|
51431
51722
|
hasZoom: false,
|
|
51432
51723
|
supportsPresets: false,
|
|
51433
|
-
hasAutofocus: false
|
|
51724
|
+
hasAutofocus: false,
|
|
51725
|
+
speedSteps: 100,
|
|
51726
|
+
moveImpulseMs: 500
|
|
51434
51727
|
};
|
|
51435
51728
|
const maxPresets = caps.maxPresetCount ?? void 0;
|
|
51436
51729
|
return {
|
|
@@ -51439,25 +51732,40 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51439
51732
|
hasZoom: caps.hasZoom,
|
|
51440
51733
|
supportsPresets: maxPresets === void 0 ? true : maxPresets > 0,
|
|
51441
51734
|
hasAutofocus: false,
|
|
51735
|
+
speedSteps: 100,
|
|
51736
|
+
moveImpulseMs: 500,
|
|
51442
51737
|
...maxPresets !== void 0 ? { maxPresets } : {}
|
|
51443
51738
|
};
|
|
51444
51739
|
},
|
|
51445
51740
|
goHome: async ({ deviceId }) => {
|
|
51446
51741
|
if (deviceId !== this.id) return;
|
|
51447
|
-
await
|
|
51742
|
+
const home = await this.resolveHomePreset();
|
|
51743
|
+
if (home.source === "none" || home.presetId === null) throw new Error("No home preset configured for this camera — set one in the PTZ settings");
|
|
51744
|
+
await isapi.ptzGoToPreset(cameraNumber, home.presetId);
|
|
51745
|
+
},
|
|
51746
|
+
getHomePreset: async ({ deviceId }) => {
|
|
51747
|
+
if (deviceId !== this.id) return {
|
|
51748
|
+
presetId: null,
|
|
51749
|
+
source: "none"
|
|
51750
|
+
};
|
|
51751
|
+
return this.resolveHomePreset();
|
|
51752
|
+
},
|
|
51753
|
+
setHomePreset: async ({ deviceId, presetId }) => {
|
|
51754
|
+
if (deviceId !== this.id) return;
|
|
51755
|
+
await this.config.setAll({ homePresetId: presetId ?? "" });
|
|
51448
51756
|
},
|
|
51449
51757
|
getPosition: async () => {
|
|
51450
51758
|
return {
|
|
51451
|
-
pan:
|
|
51452
|
-
tilt:
|
|
51453
|
-
zoom:
|
|
51759
|
+
pan: null,
|
|
51760
|
+
tilt: null,
|
|
51761
|
+
zoom: null
|
|
51454
51762
|
};
|
|
51455
51763
|
},
|
|
51456
51764
|
getStatus: async () => {
|
|
51457
51765
|
return {
|
|
51458
|
-
pan:
|
|
51459
|
-
tilt:
|
|
51460
|
-
zoom:
|
|
51766
|
+
pan: null,
|
|
51767
|
+
tilt: null,
|
|
51768
|
+
zoom: null,
|
|
51461
51769
|
autofocus: false
|
|
51462
51770
|
};
|
|
51463
51771
|
},
|
package/dist/addon.mjs
CHANGED
|
@@ -5368,7 +5368,7 @@ var ZodIssueCode = {
|
|
|
5368
5368
|
var ZodFirstPartyTypeKind;
|
|
5369
5369
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5370
5370
|
//#endregion
|
|
5371
|
-
//#region ../types/dist/sleep-
|
|
5371
|
+
//#region ../types/dist/sleep-B1y0Fo1U.mjs
|
|
5372
5372
|
/**
|
|
5373
5373
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5374
5374
|
* window to float samples (D455).
|
|
@@ -31372,14 +31372,81 @@ function summarisePrivacyAudio(profiles) {
|
|
|
31372
31372
|
if (profiles.length === 0) return null;
|
|
31373
31373
|
return profiles.some((p) => p.audioEnabled);
|
|
31374
31374
|
}
|
|
31375
|
+
/**
|
|
31376
|
+
* What a preset id DOES on this camera.
|
|
31377
|
+
*
|
|
31378
|
+
* On several PTZ firmwares the preset namespace is shared: low ids are stored
|
|
31379
|
+
* positions, a reserved band triggers camera FUNCTIONS. The dispensa camera
|
|
31380
|
+
* (Hikvision) returns its own reserved band in the preset list, named by the
|
|
31381
|
+
* firmware:
|
|
31382
|
+
*
|
|
31383
|
+
* 34 Back to origin · 39 Day mode · 40 Night mode · 46 Day/Night Auto Mode
|
|
31384
|
+
* 92 Set manual limits · 93 Save manual limits · 94 Remote reboot
|
|
31385
|
+
* 95 Call OSD menu
|
|
31386
|
+
*
|
|
31387
|
+
* We used to hand all of these to the UI as ordinary presets. Saving over one
|
|
31388
|
+
* is what produced `PUT /ISAPI/PTZCtrl/channels/1/presets/34 -> HTTP 500`: the
|
|
31389
|
+
* camera refusing to let "Back to origin" be overwritten, surfaced to the
|
|
31390
|
+
* operator as a transport failure.
|
|
31391
|
+
*
|
|
31392
|
+
* A function preset stays fully usable as a DESTINATION -- `goToPreset('34')`
|
|
31393
|
+
* is the single most useful thing that camera can do. Only `savePreset` and
|
|
31394
|
+
* `deletePreset` are gated.
|
|
31395
|
+
*/
|
|
31396
|
+
var PtzPresetFunctionSchema = _enum([
|
|
31397
|
+
"home",
|
|
31398
|
+
"day-mode",
|
|
31399
|
+
"night-mode",
|
|
31400
|
+
"day-night-auto",
|
|
31401
|
+
"set-limits",
|
|
31402
|
+
"save-limits",
|
|
31403
|
+
"reboot",
|
|
31404
|
+
"osd-menu"
|
|
31405
|
+
]);
|
|
31375
31406
|
var PtzPresetSchema = object({
|
|
31376
31407
|
id: string(),
|
|
31377
|
-
name: string()
|
|
31408
|
+
name: string(),
|
|
31409
|
+
/**
|
|
31410
|
+
* The firmware function this id performs, when the provider KNOWS the id is
|
|
31411
|
+
* reserved. `null` = an ordinary stored position, OR a reserved id this
|
|
31412
|
+
* provider does not recognise -- the two are told apart by `writable`.
|
|
31413
|
+
*/
|
|
31414
|
+
fn: PtzPresetFunctionSchema.nullable(),
|
|
31415
|
+
/**
|
|
31416
|
+
* Whether `savePreset` / `deletePreset` may target this id. A provider sets
|
|
31417
|
+
* it false only for a band it KNOWS is reserved; where the firmware is not
|
|
31418
|
+
* documented it stays true and a refusal comes from the camera, named.
|
|
31419
|
+
*/
|
|
31420
|
+
writable: boolean()
|
|
31378
31421
|
});
|
|
31422
|
+
/**
|
|
31423
|
+
* Where the head is pointing -- per axis, and `null` when the driver cannot
|
|
31424
|
+
* read it.
|
|
31425
|
+
*
|
|
31426
|
+
* It used to be three REQUIRED numbers, which left a provider that cannot read
|
|
31427
|
+
* position no way to say so. All four said `0`:
|
|
31428
|
+
*
|
|
31429
|
+
* reolink `return { pan: 0, tilt: 0, zoom: 0 }` "until a position-query
|
|
31430
|
+
* path lands upstream"
|
|
31431
|
+
* hikvision stub, "firmware-dependent and frequently absent"
|
|
31432
|
+
* amcrest stub, "no generic absolute-position read for this model family"
|
|
31433
|
+
* onvif throws
|
|
31434
|
+
*
|
|
31435
|
+
* So every consumer asking where a camera points was told "perfectly centred"
|
|
31436
|
+
* by four cameras that had never looked. That is D393 -- a measurement that
|
|
31437
|
+
* FAILED is `null`, never `0`, and the type says so all the way to the
|
|
31438
|
+
* decision -- the same shape as the unreadable `statfs` folded into "0 bytes of
|
|
31439
|
+
* headroom", which evacuated a healthy disk.
|
|
31440
|
+
*
|
|
31441
|
+
* D393 also says to check for a narrower structural TWIN of the result type.
|
|
31442
|
+
* There is one: `PtzStatusSchema` extends this, so the lie had already
|
|
31443
|
+
* propagated into `getStatus`, which is the surface `getPosition`'s own comment
|
|
31444
|
+
* tells callers to migrate to.
|
|
31445
|
+
*/
|
|
31379
31446
|
var PtzPositionSchema = object({
|
|
31380
|
-
pan: number(),
|
|
31381
|
-
tilt: number(),
|
|
31382
|
-
zoom: number()
|
|
31447
|
+
pan: number().nullable(),
|
|
31448
|
+
tilt: number().nullable(),
|
|
31449
|
+
zoom: number().nullable()
|
|
31383
31450
|
});
|
|
31384
31451
|
var PtzMoveCommandSchema = object({
|
|
31385
31452
|
pan: number().optional(),
|
|
@@ -31401,7 +31468,72 @@ var PtzOptionsSchema = object({
|
|
|
31401
31468
|
maxPresets: number().optional(),
|
|
31402
31469
|
/** Whether the camera exposes a controllable autofocus toggle
|
|
31403
31470
|
* (boolean `hasX` per the getOptions availability convention). */
|
|
31404
|
-
hasAutofocus: boolean()
|
|
31471
|
+
hasAutofocus: boolean(),
|
|
31472
|
+
/**
|
|
31473
|
+
* How many distinct speeds this camera's NATIVE scale offers.
|
|
31474
|
+
*
|
|
31475
|
+
* The cap's `speed` is normalized 0..1 and each provider maps it down --
|
|
31476
|
+
* Baichuan 1..63, Dahua 1..8, ISAPI's percentage -100..100. The normalized
|
|
31477
|
+
* value hides how coarse that really is: asking an amcrest for `0.37` is
|
|
31478
|
+
* meaningless, because it has eight steps and three of them round to the
|
|
31479
|
+
* same one. A loop that tunes its own gain has to know the granularity of
|
|
31480
|
+
* the knob it is turning.
|
|
31481
|
+
*
|
|
31482
|
+
* `null` = the driver does not know, or the scale is continuous (ONVIF takes
|
|
31483
|
+
* a float). Never a made-up number.
|
|
31484
|
+
*/
|
|
31485
|
+
speedSteps: number().int().positive().nullable(),
|
|
31486
|
+
/**
|
|
31487
|
+
* How long ONE `move` pulse runs on this driver, ms.
|
|
31488
|
+
*
|
|
31489
|
+
* `move` is a self-terminating burst everywhere, but every provider hardcoded
|
|
31490
|
+
* its own duration in private -- 200 reolink, 350 amcrest, 500 hikvision,
|
|
31491
|
+
* 1000 onvif -- so no caller could read it. It is the DENOMINATOR of "how far
|
|
31492
|
+
* did the head travel per pulse": without it a measured displacement has no
|
|
31493
|
+
* gain to be divided into.
|
|
31494
|
+
*
|
|
31495
|
+
* `null` = the driver cannot say.
|
|
31496
|
+
*/
|
|
31497
|
+
moveImpulseMs: number().int().positive().nullable()
|
|
31498
|
+
});
|
|
31499
|
+
/**
|
|
31500
|
+
* Which preset `goHome` goes to, and WHO decided.
|
|
31501
|
+
*
|
|
31502
|
+
* `goHome` used to be three vendor "conventions" written in three comments --
|
|
31503
|
+
* Reolink preset 0, Hikvision preset '1', Dahua preset 1 -- and on this fleet
|
|
31504
|
+
* not one of the three cameras honours its own:
|
|
31505
|
+
*
|
|
31506
|
+
* 592 Videocamera camera Daniel (reolink) preset 0 holds "stanza"
|
|
31507
|
+
* 1438 Videocamera dispensa (hikvision) preset 1 holds "Credenza"
|
|
31508
|
+
* 3836 Videocamera studio (amcrest) preset 1 holds "Preset1"
|
|
31509
|
+
*
|
|
31510
|
+
* So `goHome` meant "go wherever the operator happened to save in slot 0 or 1",
|
|
31511
|
+
* and two of the three swallowed the failure in a bare `catch`. For autotrack
|
|
31512
|
+
* this is the HOTTEST path -- it runs every time a subject is released -- so it
|
|
31513
|
+
* needs an answer that is true per camera and audible when it is missing.
|
|
31514
|
+
*
|
|
31515
|
+
* `source` is what makes it honest:
|
|
31516
|
+
* - `operator` -- picked in the UI, stored by the provider.
|
|
31517
|
+
* - `firmware` -- the camera itself named a preset `fn: 'home'` (Hikvision's
|
|
31518
|
+
* "Back to origin"), adopted as the default with nothing to configure.
|
|
31519
|
+
* - `native` -- the driver homes WITHOUT a preset at all. ONVIF does:
|
|
31520
|
+
* `goHome` is `ptzAbsoluteMove({x:0, y:0, zoom:0})`. `presetId` is null and
|
|
31521
|
+
* that is not a failure -- there is nothing to pick, and the UI must offer
|
|
31522
|
+
* no picker.
|
|
31523
|
+
* - `none` -- nothing is configured and the camera names nothing.
|
|
31524
|
+
* `goHome` REFUSES rather than moving the head somewhere arbitrary.
|
|
31525
|
+
*
|
|
31526
|
+
* `source === 'none'` is the refusal condition, NOT `presetId === null`: the
|
|
31527
|
+
* native case has no preset and homes perfectly well.
|
|
31528
|
+
*/
|
|
31529
|
+
var PtzHomePresetSchema = object({
|
|
31530
|
+
presetId: string().nullable(),
|
|
31531
|
+
source: _enum([
|
|
31532
|
+
"operator",
|
|
31533
|
+
"firmware",
|
|
31534
|
+
"native",
|
|
31535
|
+
"none"
|
|
31536
|
+
])
|
|
31405
31537
|
});
|
|
31406
31538
|
var ptzCapability = {
|
|
31407
31539
|
name: "ptz",
|
|
@@ -31442,7 +31574,25 @@ var ptzCapability = {
|
|
|
31442
31574
|
auth: "admin"
|
|
31443
31575
|
}),
|
|
31444
31576
|
getOptions: method(object({ deviceId: number() }), PtzOptionsSchema),
|
|
31577
|
+
/**
|
|
31578
|
+
* Move to the camera's configured home preset. THROWS when
|
|
31579
|
+
* `getHomePreset()` resolves to `none` -- a head that did not move must
|
|
31580
|
+
* never look like a head that went home.
|
|
31581
|
+
*/
|
|
31445
31582
|
goHome: method(object({ deviceId: number() }), _void(), { kind: "mutation" }),
|
|
31583
|
+
/** Which preset `goHome` targets, and who decided it. */
|
|
31584
|
+
getHomePreset: method(object({ deviceId: number() }), PtzHomePresetSchema),
|
|
31585
|
+
/**
|
|
31586
|
+
* Pin the home preset for this camera. `null` clears the operator's choice
|
|
31587
|
+
* and falls back to whatever the firmware names, or to `none`.
|
|
31588
|
+
*/
|
|
31589
|
+
setHomePreset: method(object({
|
|
31590
|
+
deviceId: number(),
|
|
31591
|
+
presetId: string().nullable()
|
|
31592
|
+
}), _void(), {
|
|
31593
|
+
kind: "mutation",
|
|
31594
|
+
auth: "admin"
|
|
31595
|
+
}),
|
|
31446
31596
|
/**
|
|
31447
31597
|
* Pull the current PTZ position. Redundant with the auto-injected
|
|
31448
31598
|
* `getStatus` method (see `status` below); kept for callers that
|
|
@@ -41168,6 +41318,12 @@ Object.freeze({
|
|
|
41168
41318
|
addonId: null,
|
|
41169
41319
|
access: "delete"
|
|
41170
41320
|
},
|
|
41321
|
+
"ptz.getHomePreset": {
|
|
41322
|
+
capName: "ptz",
|
|
41323
|
+
capScope: "device",
|
|
41324
|
+
addonId: null,
|
|
41325
|
+
access: "view"
|
|
41326
|
+
},
|
|
41171
41327
|
"ptz.getOptions": {
|
|
41172
41328
|
capName: "ptz",
|
|
41173
41329
|
capScope: "device",
|
|
@@ -41216,6 +41372,12 @@ Object.freeze({
|
|
|
41216
41372
|
addonId: null,
|
|
41217
41373
|
access: "create"
|
|
41218
41374
|
},
|
|
41375
|
+
"ptz.setHomePreset": {
|
|
41376
|
+
capName: "ptz",
|
|
41377
|
+
capScope: "device",
|
|
41378
|
+
addonId: null,
|
|
41379
|
+
access: "create"
|
|
41380
|
+
},
|
|
41219
41381
|
"ptz.stop": {
|
|
41220
41382
|
capName: "ptz",
|
|
41221
41383
|
capScope: "device",
|
|
@@ -44389,6 +44551,11 @@ Object.freeze({
|
|
|
44389
44551
|
form: "single",
|
|
44390
44552
|
optional: false
|
|
44391
44553
|
}],
|
|
44554
|
+
"ptz.getHomePreset": [{
|
|
44555
|
+
name: "deviceId",
|
|
44556
|
+
form: "single",
|
|
44557
|
+
optional: false
|
|
44558
|
+
}],
|
|
44392
44559
|
"ptz.getOptions": [{
|
|
44393
44560
|
name: "deviceId",
|
|
44394
44561
|
form: "single",
|
|
@@ -44429,6 +44596,11 @@ Object.freeze({
|
|
|
44429
44596
|
form: "single",
|
|
44430
44597
|
optional: false
|
|
44431
44598
|
}],
|
|
44599
|
+
"ptz.setHomePreset": [{
|
|
44600
|
+
name: "deviceId",
|
|
44601
|
+
form: "single",
|
|
44602
|
+
optional: false
|
|
44603
|
+
}],
|
|
44432
44604
|
"ptz.stop": [{
|
|
44433
44605
|
name: "deviceId",
|
|
44434
44606
|
form: "single",
|
|
@@ -49737,6 +49909,73 @@ function buildRawState(reader) {
|
|
|
49737
49909
|
};
|
|
49738
49910
|
}
|
|
49739
49911
|
//#endregion
|
|
49912
|
+
//#region src/isapi-ptz-speed.ts
|
|
49913
|
+
/**
|
|
49914
|
+
* Turn the cap's normalized direction + magnitude into an ISAPI continuous
|
|
49915
|
+
* speed.
|
|
49916
|
+
*
|
|
49917
|
+
* The `ptz` cap splits the two: `pan`/`tilt`/`zoom` are a DIRECTION in -1..1,
|
|
49918
|
+
* and `speed` is the magnitude in 0..1. reolink maps them to Baichuan 1..63,
|
|
49919
|
+
* amcrest to Dahua 1..8 — and hikvision used to map them to nothing at all. It
|
|
49920
|
+
* dropped `speed` before it left the provider and handed the raw direction to
|
|
49921
|
+
* `/ISAPI/PTZCtrl/channels/{cam}/continuous`, whose `<pan>` is a speed
|
|
49922
|
+
* PERCENTAGE in -100..100.
|
|
49923
|
+
*
|
|
49924
|
+
* So `usePTZ` sending its d-pad default (`pan: 1, speed: 0.5`) reached the
|
|
49925
|
+
* dispensa camera as `<pan>1</pan>`: one percent of the camera's speed. A
|
|
49926
|
+
* request at less than half magnitude rounded to `0` and produced no motion at
|
|
49927
|
+
* all.
|
|
49928
|
+
*
|
|
49929
|
+
* An absent `speed` is mid-scale, never zero — reolink defaults to 32 of 63 for
|
|
49930
|
+
* the same reason. A caller who did not state a magnitude asked to move, not to
|
|
49931
|
+
* stand still.
|
|
49932
|
+
*/
|
|
49933
|
+
var MID_SCALE = .5;
|
|
49934
|
+
function isapiSpeedFor(direction, speed) {
|
|
49935
|
+
const d = direction ?? 0;
|
|
49936
|
+
if (!Number.isFinite(d) || d === 0) return 0;
|
|
49937
|
+
const magnitude = speed === void 0 || !Number.isFinite(speed) ? MID_SCALE : Math.abs(speed);
|
|
49938
|
+
const percent = Math.round(Math.sign(d) * Math.min(1, Math.abs(d)) * Math.min(1, magnitude) * 100);
|
|
49939
|
+
return Math.max(-100, Math.min(100, percent));
|
|
49940
|
+
}
|
|
49941
|
+
//#endregion
|
|
49942
|
+
//#region src/ptz-preset-functions.ts
|
|
49943
|
+
var RESERVED = new Map([
|
|
49944
|
+
["34", "home"],
|
|
49945
|
+
["39", "day-mode"],
|
|
49946
|
+
["40", "night-mode"],
|
|
49947
|
+
["46", "day-night-auto"],
|
|
49948
|
+
["92", "set-limits"],
|
|
49949
|
+
["93", "save-limits"],
|
|
49950
|
+
["94", "reboot"],
|
|
49951
|
+
["95", "osd-menu"]
|
|
49952
|
+
]);
|
|
49953
|
+
/** The firmware function this id performs, or `null` for a stored position. */
|
|
49954
|
+
function presetFunctionFor(presetId) {
|
|
49955
|
+
return RESERVED.get(presetId) ?? null;
|
|
49956
|
+
}
|
|
49957
|
+
/**
|
|
49958
|
+
* Whether `savePreset` / `deletePreset` may target this id.
|
|
49959
|
+
*
|
|
49960
|
+
* Anything outside the known band stays writable: an undocumented firmware
|
|
49961
|
+
* must be allowed to refuse for itself rather than be guessed at here.
|
|
49962
|
+
*/
|
|
49963
|
+
function isPresetWritable(presetId) {
|
|
49964
|
+
return !RESERVED.has(presetId);
|
|
49965
|
+
}
|
|
49966
|
+
/**
|
|
49967
|
+
* Refuse a write to a reserved id, naming the function that owns it.
|
|
49968
|
+
*
|
|
49969
|
+
* Extracted so the REFUSAL itself is assertable. With the check inline in
|
|
49970
|
+
* `savePreset` / `deletePreset`, deleting it left every test green -- the table
|
|
49971
|
+
* spec proves what the band IS, not that anything consults it.
|
|
49972
|
+
*/
|
|
49973
|
+
function assertPresetWritable(presetId, action) {
|
|
49974
|
+
const fn = presetFunctionFor(presetId);
|
|
49975
|
+
if (fn === null) return;
|
|
49976
|
+
throw new Error(`Preset ${presetId} is a firmware function on this camera (${fn}) and cannot be ${action}` + (action === "overwritten" ? " — pick an unreserved id" : ""));
|
|
49977
|
+
}
|
|
49978
|
+
//#endregion
|
|
49740
49979
|
//#region src/schema.ts
|
|
49741
49980
|
/**
|
|
49742
49981
|
* Single source of truth for the addon id used in event-source tags +
|
|
@@ -50055,6 +50294,17 @@ var hikvisionCameraSchema = object({
|
|
|
50055
50294
|
*/
|
|
50056
50295
|
snapshotChannelId: string().default("").describe("ISAPI channel used for snapshots (empty = main stream)"),
|
|
50057
50296
|
/**
|
|
50297
|
+
* Which preset `ptz.goHome` targets on THIS camera.
|
|
50298
|
+
*
|
|
50299
|
+
* Empty = no operator choice. The provider then falls back to the preset the
|
|
50300
|
+
* FIRMWARE names as home (Hikvision returns id 34 "Back to origin" in its
|
|
50301
|
+
* reserved band), and only if that is absent too does `goHome` refuse.
|
|
50302
|
+
*
|
|
50303
|
+
* It has to be stored because the vendor "convention" is wrong here: the code
|
|
50304
|
+
* went to preset '1', and on the dispensa camera preset 1 is "Credenza".
|
|
50305
|
+
*/
|
|
50306
|
+
homePresetId: string().default("").describe("Preset id ptz.goHome targets (empty = use the firmware home)"),
|
|
50307
|
+
/**
|
|
50058
50308
|
* Motion detection master switch + sensitivity (0..100). Maps to
|
|
50059
50309
|
* ISAPI `/ISAPI/System/Video/inputs/channels/{cam}/motionDetection`.
|
|
50060
50310
|
* Disabling this stops the camera from emitting `<eventType>VMD</eventType>`
|
|
@@ -51092,6 +51342,41 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51092
51342
|
...slice
|
|
51093
51343
|
} });
|
|
51094
51344
|
}
|
|
51345
|
+
/**
|
|
51346
|
+
* Which preset `goHome` targets, and who decided it.
|
|
51347
|
+
*
|
|
51348
|
+
* The operator's pick wins. With none, the FIRMWARE's own home is adopted --
|
|
51349
|
+
* Hikvision names id 34 "Back to origin" in its reserved band -- so a camera
|
|
51350
|
+
* that already knows where home is needs nothing configured. Only when both
|
|
51351
|
+
* are absent is the answer `none`, and `goHome` then refuses rather than
|
|
51352
|
+
* moving the head to an arbitrary slot.
|
|
51353
|
+
*
|
|
51354
|
+
* A failed preset read yields `none`, never a guess: an unreadable camera is
|
|
51355
|
+
* not evidence that preset 34 exists (D393).
|
|
51356
|
+
*/
|
|
51357
|
+
async resolveHomePreset() {
|
|
51358
|
+
const chosen = this.config.get("homePresetId") ?? "";
|
|
51359
|
+
if (chosen !== "") return {
|
|
51360
|
+
presetId: chosen,
|
|
51361
|
+
source: "operator"
|
|
51362
|
+
};
|
|
51363
|
+
try {
|
|
51364
|
+
const firmwareHome = (await this.ensureClient().listPtzPresets(1)).find((preset) => presetFunctionFor(preset.id) === "home");
|
|
51365
|
+
if (firmwareHome !== void 0) return {
|
|
51366
|
+
presetId: firmwareHome.id,
|
|
51367
|
+
source: "firmware"
|
|
51368
|
+
};
|
|
51369
|
+
} catch (err) {
|
|
51370
|
+
this.ctx.logger.warn("hikvision home preset lookup failed — goHome will refuse", {
|
|
51371
|
+
tags: { deviceId: this.id },
|
|
51372
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
51373
|
+
});
|
|
51374
|
+
}
|
|
51375
|
+
return {
|
|
51376
|
+
presetId: null,
|
|
51377
|
+
source: "none"
|
|
51378
|
+
};
|
|
51379
|
+
}
|
|
51095
51380
|
/** Re-read the image-tuning snapshot from the camera and persist it.
|
|
51096
51381
|
* Best-effort: a refresh failure is logged and swallowed so a save
|
|
51097
51382
|
* never throws. Called after an Image-section push in
|
|
@@ -51368,12 +51653,12 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51368
51653
|
registerPtzCap(cameraNumber) {
|
|
51369
51654
|
const isapi = this.ensureClient();
|
|
51370
51655
|
this.ctx.registerNativeCap(ptzCapability, {
|
|
51371
|
-
move: async ({ deviceId, pan, tilt, zoom }) => {
|
|
51656
|
+
move: async ({ deviceId, pan, tilt, zoom, speed }) => {
|
|
51372
51657
|
if (deviceId !== this.id) return;
|
|
51373
51658
|
await isapi.ptzContinuous(cameraNumber, {
|
|
51374
|
-
pan,
|
|
51375
|
-
tilt,
|
|
51376
|
-
zoom
|
|
51659
|
+
pan: isapiSpeedFor(pan, speed),
|
|
51660
|
+
tilt: isapiSpeedFor(tilt, speed),
|
|
51661
|
+
zoom: isapiSpeedFor(zoom, speed)
|
|
51377
51662
|
});
|
|
51378
51663
|
await new Promise((r) => setTimeout(r, 500));
|
|
51379
51664
|
await isapi.ptzContinuous(cameraNumber, {
|
|
@@ -51382,12 +51667,12 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51382
51667
|
zoom: 0
|
|
51383
51668
|
});
|
|
51384
51669
|
},
|
|
51385
|
-
continuousMove: async ({ deviceId, pan, tilt, zoom }) => {
|
|
51670
|
+
continuousMove: async ({ deviceId, pan, tilt, zoom, speed }) => {
|
|
51386
51671
|
if (deviceId !== this.id) return;
|
|
51387
51672
|
await isapi.ptzContinuous(cameraNumber, {
|
|
51388
|
-
pan,
|
|
51389
|
-
tilt,
|
|
51390
|
-
zoom
|
|
51673
|
+
pan: isapiSpeedFor(pan, speed),
|
|
51674
|
+
tilt: isapiSpeedFor(tilt, speed),
|
|
51675
|
+
zoom: isapiSpeedFor(zoom, speed)
|
|
51391
51676
|
});
|
|
51392
51677
|
},
|
|
51393
51678
|
stop: async ({ deviceId }) => {
|
|
@@ -51402,7 +51687,9 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51402
51687
|
if (deviceId !== this.id) return [];
|
|
51403
51688
|
return (await isapi.listPtzPresets(cameraNumber)).map((p) => ({
|
|
51404
51689
|
id: p.id,
|
|
51405
|
-
name: p.name ?? `Preset ${p.id}
|
|
51690
|
+
name: p.name ?? `Preset ${p.id}`,
|
|
51691
|
+
fn: presetFunctionFor(p.id),
|
|
51692
|
+
writable: isPresetWritable(p.id)
|
|
51406
51693
|
}));
|
|
51407
51694
|
},
|
|
51408
51695
|
goToPreset: async ({ deviceId, presetId }) => {
|
|
@@ -51411,10 +51698,12 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51411
51698
|
},
|
|
51412
51699
|
savePreset: async ({ deviceId, presetId, name }) => {
|
|
51413
51700
|
if (deviceId !== this.id) return;
|
|
51701
|
+
assertPresetWritable(presetId, "overwritten");
|
|
51414
51702
|
await isapi.savePtzPreset(cameraNumber, presetId, name);
|
|
51415
51703
|
},
|
|
51416
51704
|
deletePreset: async ({ deviceId, presetId }) => {
|
|
51417
51705
|
if (deviceId !== this.id) return;
|
|
51706
|
+
assertPresetWritable(presetId, "deleted");
|
|
51418
51707
|
await isapi.deletePtzPreset(cameraNumber, presetId);
|
|
51419
51708
|
},
|
|
51420
51709
|
getOptions: async ({ deviceId }) => {
|
|
@@ -51423,7 +51712,9 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51423
51712
|
hasTilt: false,
|
|
51424
51713
|
hasZoom: false,
|
|
51425
51714
|
supportsPresets: false,
|
|
51426
|
-
hasAutofocus: false
|
|
51715
|
+
hasAutofocus: false,
|
|
51716
|
+
speedSteps: 100,
|
|
51717
|
+
moveImpulseMs: 500
|
|
51427
51718
|
};
|
|
51428
51719
|
const caps = await isapi.getPtzCapabilities(cameraNumber);
|
|
51429
51720
|
if (!caps) return {
|
|
@@ -51431,7 +51722,9 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51431
51722
|
hasTilt: false,
|
|
51432
51723
|
hasZoom: false,
|
|
51433
51724
|
supportsPresets: false,
|
|
51434
|
-
hasAutofocus: false
|
|
51725
|
+
hasAutofocus: false,
|
|
51726
|
+
speedSteps: 100,
|
|
51727
|
+
moveImpulseMs: 500
|
|
51435
51728
|
};
|
|
51436
51729
|
const maxPresets = caps.maxPresetCount ?? void 0;
|
|
51437
51730
|
return {
|
|
@@ -51440,25 +51733,40 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51440
51733
|
hasZoom: caps.hasZoom,
|
|
51441
51734
|
supportsPresets: maxPresets === void 0 ? true : maxPresets > 0,
|
|
51442
51735
|
hasAutofocus: false,
|
|
51736
|
+
speedSteps: 100,
|
|
51737
|
+
moveImpulseMs: 500,
|
|
51443
51738
|
...maxPresets !== void 0 ? { maxPresets } : {}
|
|
51444
51739
|
};
|
|
51445
51740
|
},
|
|
51446
51741
|
goHome: async ({ deviceId }) => {
|
|
51447
51742
|
if (deviceId !== this.id) return;
|
|
51448
|
-
await
|
|
51743
|
+
const home = await this.resolveHomePreset();
|
|
51744
|
+
if (home.source === "none" || home.presetId === null) throw new Error("No home preset configured for this camera — set one in the PTZ settings");
|
|
51745
|
+
await isapi.ptzGoToPreset(cameraNumber, home.presetId);
|
|
51746
|
+
},
|
|
51747
|
+
getHomePreset: async ({ deviceId }) => {
|
|
51748
|
+
if (deviceId !== this.id) return {
|
|
51749
|
+
presetId: null,
|
|
51750
|
+
source: "none"
|
|
51751
|
+
};
|
|
51752
|
+
return this.resolveHomePreset();
|
|
51753
|
+
},
|
|
51754
|
+
setHomePreset: async ({ deviceId, presetId }) => {
|
|
51755
|
+
if (deviceId !== this.id) return;
|
|
51756
|
+
await this.config.setAll({ homePresetId: presetId ?? "" });
|
|
51449
51757
|
},
|
|
51450
51758
|
getPosition: async () => {
|
|
51451
51759
|
return {
|
|
51452
|
-
pan:
|
|
51453
|
-
tilt:
|
|
51454
|
-
zoom:
|
|
51760
|
+
pan: null,
|
|
51761
|
+
tilt: null,
|
|
51762
|
+
zoom: null
|
|
51455
51763
|
};
|
|
51456
51764
|
},
|
|
51457
51765
|
getStatus: async () => {
|
|
51458
51766
|
return {
|
|
51459
|
-
pan:
|
|
51460
|
-
tilt:
|
|
51461
|
-
zoom:
|
|
51767
|
+
pan: null,
|
|
51768
|
+
tilt: null,
|
|
51769
|
+
zoom: null,
|
|
51462
51770
|
autofocus: false
|
|
51463
51771
|
};
|
|
51464
51772
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-provider-hikvision",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.119",
|
|
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",
|