@camstack/addon-provider-hikvision 1.2.118 → 1.2.120
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 +464 -24
- package/dist/addon.mjs +464 -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-vl6nBE8d.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,66 @@ 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
|
+
}),
|
|
31595
|
+
/**
|
|
31596
|
+
* Make WHERE THE HEAD IS POINTING RIGHT NOW this camera's home.
|
|
31597
|
+
*
|
|
31598
|
+
* `setHomePreset` takes a preset id, and a preset id is the wrong shape for
|
|
31599
|
+
* the most natural thing an operator does: aim the camera, then say "here".
|
|
31600
|
+
* It is also the only shape a camera whose home is NOT a preset can accept
|
|
31601
|
+
* -- Reolink's home is a guard point, a stored position with no id at all,
|
|
31602
|
+
* and hiding the control for that case (as the UI briefly did) left the one
|
|
31603
|
+
* vendor with a native home unable to set it.
|
|
31604
|
+
*
|
|
31605
|
+
* Each provider does it its own way:
|
|
31606
|
+
* reolink `setGrd` with `needSetPos`, pinning the guard point here
|
|
31607
|
+
* hikvision / amcrest save a preset, then pin it as home
|
|
31608
|
+
* onvif REFUSES -- its home is the PTZ origin and cannot be moved
|
|
31609
|
+
*
|
|
31610
|
+
* A provider that cannot honour it throws rather than silently doing
|
|
31611
|
+
* nothing: an operator who pressed "set home here" and saw no error would
|
|
31612
|
+
* believe it took.
|
|
31613
|
+
*/
|
|
31614
|
+
captureHomeHere: method(object({ deviceId: number() }), _void(), {
|
|
31615
|
+
kind: "mutation",
|
|
31616
|
+
auth: "admin"
|
|
31617
|
+
}),
|
|
31618
|
+
/**
|
|
31619
|
+
* How long the camera waits, idle, before returning home on its own --
|
|
31620
|
+
* `null` when it has no such behaviour.
|
|
31621
|
+
*
|
|
31622
|
+
* Reolink's guard point carries one (`timeout`, freely settable in
|
|
31623
|
+
* seconds). It is a real function of the camera that we did not expose at
|
|
31624
|
+
* all, and it belongs next to the home it governs rather than in a vendor
|
|
31625
|
+
* settings page nobody associates with Home.
|
|
31626
|
+
*/
|
|
31627
|
+
getHomeReturnSeconds: method(object({ deviceId: number() }), number().int().nullable()),
|
|
31628
|
+
/** Set the idle-return delay. Refused by a camera that has no such timer. */
|
|
31629
|
+
setHomeReturnSeconds: method(object({
|
|
31630
|
+
deviceId: number(),
|
|
31631
|
+
seconds: number().int().min(0).max(3600)
|
|
31632
|
+
}), _void(), {
|
|
31633
|
+
kind: "mutation",
|
|
31634
|
+
auth: "admin"
|
|
31635
|
+
}),
|
|
31445
31636
|
/**
|
|
31446
31637
|
* Pull the current PTZ position. Redundant with the auto-injected
|
|
31447
31638
|
* `getStatus` method (see `status` below); kept for callers that
|
|
@@ -41155,6 +41346,12 @@ Object.freeze({
|
|
|
41155
41346
|
addonId: null,
|
|
41156
41347
|
access: "create"
|
|
41157
41348
|
},
|
|
41349
|
+
"ptz.captureHomeHere": {
|
|
41350
|
+
capName: "ptz",
|
|
41351
|
+
capScope: "device",
|
|
41352
|
+
addonId: null,
|
|
41353
|
+
access: "create"
|
|
41354
|
+
},
|
|
41158
41355
|
"ptz.continuousMove": {
|
|
41159
41356
|
capName: "ptz",
|
|
41160
41357
|
capScope: "device",
|
|
@@ -41167,6 +41364,18 @@ Object.freeze({
|
|
|
41167
41364
|
addonId: null,
|
|
41168
41365
|
access: "delete"
|
|
41169
41366
|
},
|
|
41367
|
+
"ptz.getHomePreset": {
|
|
41368
|
+
capName: "ptz",
|
|
41369
|
+
capScope: "device",
|
|
41370
|
+
addonId: null,
|
|
41371
|
+
access: "view"
|
|
41372
|
+
},
|
|
41373
|
+
"ptz.getHomeReturnSeconds": {
|
|
41374
|
+
capName: "ptz",
|
|
41375
|
+
capScope: "device",
|
|
41376
|
+
addonId: null,
|
|
41377
|
+
access: "view"
|
|
41378
|
+
},
|
|
41170
41379
|
"ptz.getOptions": {
|
|
41171
41380
|
capName: "ptz",
|
|
41172
41381
|
capScope: "device",
|
|
@@ -41215,6 +41424,18 @@ Object.freeze({
|
|
|
41215
41424
|
addonId: null,
|
|
41216
41425
|
access: "create"
|
|
41217
41426
|
},
|
|
41427
|
+
"ptz.setHomePreset": {
|
|
41428
|
+
capName: "ptz",
|
|
41429
|
+
capScope: "device",
|
|
41430
|
+
addonId: null,
|
|
41431
|
+
access: "create"
|
|
41432
|
+
},
|
|
41433
|
+
"ptz.setHomeReturnSeconds": {
|
|
41434
|
+
capName: "ptz",
|
|
41435
|
+
capScope: "device",
|
|
41436
|
+
addonId: null,
|
|
41437
|
+
access: "create"
|
|
41438
|
+
},
|
|
41218
41439
|
"ptz.stop": {
|
|
41219
41440
|
capName: "ptz",
|
|
41220
41441
|
capScope: "device",
|
|
@@ -44378,6 +44599,11 @@ Object.freeze({
|
|
|
44378
44599
|
form: "single",
|
|
44379
44600
|
optional: false
|
|
44380
44601
|
}],
|
|
44602
|
+
"ptz.captureHomeHere": [{
|
|
44603
|
+
name: "deviceId",
|
|
44604
|
+
form: "single",
|
|
44605
|
+
optional: false
|
|
44606
|
+
}],
|
|
44381
44607
|
"ptz.continuousMove": [{
|
|
44382
44608
|
name: "deviceId",
|
|
44383
44609
|
form: "single",
|
|
@@ -44388,6 +44614,16 @@ Object.freeze({
|
|
|
44388
44614
|
form: "single",
|
|
44389
44615
|
optional: false
|
|
44390
44616
|
}],
|
|
44617
|
+
"ptz.getHomePreset": [{
|
|
44618
|
+
name: "deviceId",
|
|
44619
|
+
form: "single",
|
|
44620
|
+
optional: false
|
|
44621
|
+
}],
|
|
44622
|
+
"ptz.getHomeReturnSeconds": [{
|
|
44623
|
+
name: "deviceId",
|
|
44624
|
+
form: "single",
|
|
44625
|
+
optional: false
|
|
44626
|
+
}],
|
|
44391
44627
|
"ptz.getOptions": [{
|
|
44392
44628
|
name: "deviceId",
|
|
44393
44629
|
form: "single",
|
|
@@ -44428,6 +44664,16 @@ Object.freeze({
|
|
|
44428
44664
|
form: "single",
|
|
44429
44665
|
optional: false
|
|
44430
44666
|
}],
|
|
44667
|
+
"ptz.setHomePreset": [{
|
|
44668
|
+
name: "deviceId",
|
|
44669
|
+
form: "single",
|
|
44670
|
+
optional: false
|
|
44671
|
+
}],
|
|
44672
|
+
"ptz.setHomeReturnSeconds": [{
|
|
44673
|
+
name: "deviceId",
|
|
44674
|
+
form: "single",
|
|
44675
|
+
optional: false
|
|
44676
|
+
}],
|
|
44431
44677
|
"ptz.stop": [{
|
|
44432
44678
|
name: "deviceId",
|
|
44433
44679
|
form: "single",
|
|
@@ -49736,6 +49982,110 @@ function buildRawState(reader) {
|
|
|
49736
49982
|
};
|
|
49737
49983
|
}
|
|
49738
49984
|
//#endregion
|
|
49985
|
+
//#region src/isapi-ptz-speed.ts
|
|
49986
|
+
/**
|
|
49987
|
+
* Turn the cap's normalized direction + magnitude into an ISAPI continuous
|
|
49988
|
+
* speed.
|
|
49989
|
+
*
|
|
49990
|
+
* The `ptz` cap splits the two: `pan`/`tilt`/`zoom` are a DIRECTION in -1..1,
|
|
49991
|
+
* and `speed` is the magnitude in 0..1. reolink maps them to Baichuan 1..63,
|
|
49992
|
+
* amcrest to Dahua 1..8 — and hikvision used to map them to nothing at all. It
|
|
49993
|
+
* dropped `speed` before it left the provider and handed the raw direction to
|
|
49994
|
+
* `/ISAPI/PTZCtrl/channels/{cam}/continuous`, whose `<pan>` is a speed
|
|
49995
|
+
* PERCENTAGE in -100..100.
|
|
49996
|
+
*
|
|
49997
|
+
* So `usePTZ` sending its d-pad default (`pan: 1, speed: 0.5`) reached the
|
|
49998
|
+
* dispensa camera as `<pan>1</pan>`: one percent of the camera's speed. A
|
|
49999
|
+
* request at less than half magnitude rounded to `0` and produced no motion at
|
|
50000
|
+
* all.
|
|
50001
|
+
*
|
|
50002
|
+
* An absent `speed` is mid-scale, never zero — reolink defaults to 32 of 63 for
|
|
50003
|
+
* the same reason. A caller who did not state a magnitude asked to move, not to
|
|
50004
|
+
* stand still.
|
|
50005
|
+
*/
|
|
50006
|
+
var MID_SCALE = .5;
|
|
50007
|
+
function isapiSpeedFor(direction, speed) {
|
|
50008
|
+
const d = direction ?? 0;
|
|
50009
|
+
if (!Number.isFinite(d) || d === 0) return 0;
|
|
50010
|
+
const magnitude = speed === void 0 || !Number.isFinite(speed) ? MID_SCALE : Math.abs(speed);
|
|
50011
|
+
const percent = Math.round(Math.sign(d) * Math.min(1, Math.abs(d)) * Math.min(1, magnitude) * 100);
|
|
50012
|
+
return Math.max(-100, Math.min(100, percent));
|
|
50013
|
+
}
|
|
50014
|
+
//#endregion
|
|
50015
|
+
//#region src/ptz-preset-functions.ts
|
|
50016
|
+
var RESERVED = new Map([
|
|
50017
|
+
["34", "home"],
|
|
50018
|
+
["39", "day-mode"],
|
|
50019
|
+
["40", "night-mode"],
|
|
50020
|
+
["46", "day-night-auto"],
|
|
50021
|
+
["92", "set-limits"],
|
|
50022
|
+
["93", "save-limits"],
|
|
50023
|
+
["94", "reboot"],
|
|
50024
|
+
["95", "osd-menu"]
|
|
50025
|
+
]);
|
|
50026
|
+
/** The firmware function this id performs, or `null` for a stored position. */
|
|
50027
|
+
function presetFunctionFor(presetId) {
|
|
50028
|
+
return RESERVED.get(presetId) ?? null;
|
|
50029
|
+
}
|
|
50030
|
+
/**
|
|
50031
|
+
* Whether `savePreset` / `deletePreset` may target this id.
|
|
50032
|
+
*
|
|
50033
|
+
* Anything outside the known band stays writable: an undocumented firmware
|
|
50034
|
+
* must be allowed to refuse for itself rather than be guessed at here.
|
|
50035
|
+
*/
|
|
50036
|
+
function isPresetWritable(presetId) {
|
|
50037
|
+
return !RESERVED.has(presetId);
|
|
50038
|
+
}
|
|
50039
|
+
/**
|
|
50040
|
+
* Refuse a write to a reserved id, naming the function that owns it.
|
|
50041
|
+
*
|
|
50042
|
+
* Extracted so the REFUSAL itself is assertable. With the check inline in
|
|
50043
|
+
* `savePreset` / `deletePreset`, deleting it left every test green -- the table
|
|
50044
|
+
* spec proves what the band IS, not that anything consults it.
|
|
50045
|
+
*/
|
|
50046
|
+
function assertPresetWritable(presetId, action) {
|
|
50047
|
+
const fn = presetFunctionFor(presetId);
|
|
50048
|
+
if (fn === null) return;
|
|
50049
|
+
throw new Error(`Preset ${presetId} is a firmware function on this camera (${fn}) and cannot be ${action}` + (action === "overwritten" ? " — pick an unreserved id" : ""));
|
|
50050
|
+
}
|
|
50051
|
+
//#endregion
|
|
50052
|
+
//#region src/free-preset-slot.ts
|
|
50053
|
+
/**
|
|
50054
|
+
* The lowest preset id this camera will actually accept a write on.
|
|
50055
|
+
*
|
|
50056
|
+
* "Set home here" has to land somewhere, and on a camera whose home IS a preset
|
|
50057
|
+
* that means claiming a slot. Two things make the choice non-obvious:
|
|
50058
|
+
*
|
|
50059
|
+
* - the reserved FUNCTION band shares the preset namespace (34 "Back to
|
|
50060
|
+
* origin", 94 "Remote reboot", 95 "Call OSD menu" and friends), and writing
|
|
50061
|
+
* one answers HTTP 500;
|
|
50062
|
+
* - the operator's own presets must not be overwritten, ever. Losing
|
|
50063
|
+
* "Credenza" to make room for a home is a worse trade than refusing.
|
|
50064
|
+
*
|
|
50065
|
+
* So: the first id that is neither taken nor reserved, within the camera's
|
|
50066
|
+
* advertised budget. `null` when there is no room, and the caller must then
|
|
50067
|
+
* REFUSE rather than pick a victim.
|
|
50068
|
+
*/
|
|
50069
|
+
function findFreePresetSlot(taken, maxPresets) {
|
|
50070
|
+
const used = new Set(taken.map((p) => p.id));
|
|
50071
|
+
const ceiling = maxPresets === void 0 ? 32 : Math.min(maxPresets, 300);
|
|
50072
|
+
for (let id = 1; id <= ceiling; id += 1) {
|
|
50073
|
+
const candidate = String(id);
|
|
50074
|
+
if (used.has(candidate)) continue;
|
|
50075
|
+
if (!isPresetWritable(candidate)) continue;
|
|
50076
|
+
return candidate;
|
|
50077
|
+
}
|
|
50078
|
+
return null;
|
|
50079
|
+
}
|
|
50080
|
+
/**
|
|
50081
|
+
* The name written on a preset claimed by "set home here".
|
|
50082
|
+
*
|
|
50083
|
+
* Named rather than left a literal so the slot is recognisable in the camera's
|
|
50084
|
+
* own web UI — an operator who finds an unexplained preset has no way to know
|
|
50085
|
+
* it is ours.
|
|
50086
|
+
*/
|
|
50087
|
+
var HOME_PRESET_NAME = "CamStack Home";
|
|
50088
|
+
//#endregion
|
|
49739
50089
|
//#region src/schema.ts
|
|
49740
50090
|
/**
|
|
49741
50091
|
* Single source of truth for the addon id used in event-source tags +
|
|
@@ -50054,6 +50404,17 @@ var hikvisionCameraSchema = object({
|
|
|
50054
50404
|
*/
|
|
50055
50405
|
snapshotChannelId: string().default("").describe("ISAPI channel used for snapshots (empty = main stream)"),
|
|
50056
50406
|
/**
|
|
50407
|
+
* Which preset `ptz.goHome` targets on THIS camera.
|
|
50408
|
+
*
|
|
50409
|
+
* Empty = no operator choice. The provider then falls back to the preset the
|
|
50410
|
+
* FIRMWARE names as home (Hikvision returns id 34 "Back to origin" in its
|
|
50411
|
+
* reserved band), and only if that is absent too does `goHome` refuse.
|
|
50412
|
+
*
|
|
50413
|
+
* It has to be stored because the vendor "convention" is wrong here: the code
|
|
50414
|
+
* went to preset '1', and on the dispensa camera preset 1 is "Credenza".
|
|
50415
|
+
*/
|
|
50416
|
+
homePresetId: string().default("").describe("Preset id ptz.goHome targets (empty = use the firmware home)"),
|
|
50417
|
+
/**
|
|
50057
50418
|
* Motion detection master switch + sensitivity (0..100). Maps to
|
|
50058
50419
|
* ISAPI `/ISAPI/System/Video/inputs/channels/{cam}/motionDetection`.
|
|
50059
50420
|
* Disabling this stops the camera from emitting `<eventType>VMD</eventType>`
|
|
@@ -51091,6 +51452,41 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51091
51452
|
...slice
|
|
51092
51453
|
} });
|
|
51093
51454
|
}
|
|
51455
|
+
/**
|
|
51456
|
+
* Which preset `goHome` targets, and who decided it.
|
|
51457
|
+
*
|
|
51458
|
+
* The operator's pick wins. With none, the FIRMWARE's own home is adopted --
|
|
51459
|
+
* Hikvision names id 34 "Back to origin" in its reserved band -- so a camera
|
|
51460
|
+
* that already knows where home is needs nothing configured. Only when both
|
|
51461
|
+
* are absent is the answer `none`, and `goHome` then refuses rather than
|
|
51462
|
+
* moving the head to an arbitrary slot.
|
|
51463
|
+
*
|
|
51464
|
+
* A failed preset read yields `none`, never a guess: an unreadable camera is
|
|
51465
|
+
* not evidence that preset 34 exists (D393).
|
|
51466
|
+
*/
|
|
51467
|
+
async resolveHomePreset() {
|
|
51468
|
+
const chosen = this.config.get("homePresetId") ?? "";
|
|
51469
|
+
if (chosen !== "") return {
|
|
51470
|
+
presetId: chosen,
|
|
51471
|
+
source: "operator"
|
|
51472
|
+
};
|
|
51473
|
+
try {
|
|
51474
|
+
const firmwareHome = (await this.ensureClient().listPtzPresets(1)).find((preset) => presetFunctionFor(preset.id) === "home");
|
|
51475
|
+
if (firmwareHome !== void 0) return {
|
|
51476
|
+
presetId: firmwareHome.id,
|
|
51477
|
+
source: "firmware"
|
|
51478
|
+
};
|
|
51479
|
+
} catch (err) {
|
|
51480
|
+
this.ctx.logger.warn("hikvision home preset lookup failed — goHome will refuse", {
|
|
51481
|
+
tags: { deviceId: this.id },
|
|
51482
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
51483
|
+
});
|
|
51484
|
+
}
|
|
51485
|
+
return {
|
|
51486
|
+
presetId: null,
|
|
51487
|
+
source: "none"
|
|
51488
|
+
};
|
|
51489
|
+
}
|
|
51094
51490
|
/** Re-read the image-tuning snapshot from the camera and persist it.
|
|
51095
51491
|
* Best-effort: a refresh failure is logged and swallowed so a save
|
|
51096
51492
|
* never throws. Called after an Image-section push in
|
|
@@ -51367,12 +51763,12 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51367
51763
|
registerPtzCap(cameraNumber) {
|
|
51368
51764
|
const isapi = this.ensureClient();
|
|
51369
51765
|
this.ctx.registerNativeCap(ptzCapability, {
|
|
51370
|
-
move: async ({ deviceId, pan, tilt, zoom }) => {
|
|
51766
|
+
move: async ({ deviceId, pan, tilt, zoom, speed }) => {
|
|
51371
51767
|
if (deviceId !== this.id) return;
|
|
51372
51768
|
await isapi.ptzContinuous(cameraNumber, {
|
|
51373
|
-
pan,
|
|
51374
|
-
tilt,
|
|
51375
|
-
zoom
|
|
51769
|
+
pan: isapiSpeedFor(pan, speed),
|
|
51770
|
+
tilt: isapiSpeedFor(tilt, speed),
|
|
51771
|
+
zoom: isapiSpeedFor(zoom, speed)
|
|
51376
51772
|
});
|
|
51377
51773
|
await new Promise((r) => setTimeout(r, 500));
|
|
51378
51774
|
await isapi.ptzContinuous(cameraNumber, {
|
|
@@ -51381,12 +51777,12 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51381
51777
|
zoom: 0
|
|
51382
51778
|
});
|
|
51383
51779
|
},
|
|
51384
|
-
continuousMove: async ({ deviceId, pan, tilt, zoom }) => {
|
|
51780
|
+
continuousMove: async ({ deviceId, pan, tilt, zoom, speed }) => {
|
|
51385
51781
|
if (deviceId !== this.id) return;
|
|
51386
51782
|
await isapi.ptzContinuous(cameraNumber, {
|
|
51387
|
-
pan,
|
|
51388
|
-
tilt,
|
|
51389
|
-
zoom
|
|
51783
|
+
pan: isapiSpeedFor(pan, speed),
|
|
51784
|
+
tilt: isapiSpeedFor(tilt, speed),
|
|
51785
|
+
zoom: isapiSpeedFor(zoom, speed)
|
|
51390
51786
|
});
|
|
51391
51787
|
},
|
|
51392
51788
|
stop: async ({ deviceId }) => {
|
|
@@ -51401,7 +51797,9 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51401
51797
|
if (deviceId !== this.id) return [];
|
|
51402
51798
|
return (await isapi.listPtzPresets(cameraNumber)).map((p) => ({
|
|
51403
51799
|
id: p.id,
|
|
51404
|
-
name: p.name ?? `Preset ${p.id}
|
|
51800
|
+
name: p.name ?? `Preset ${p.id}`,
|
|
51801
|
+
fn: presetFunctionFor(p.id),
|
|
51802
|
+
writable: isPresetWritable(p.id)
|
|
51405
51803
|
}));
|
|
51406
51804
|
},
|
|
51407
51805
|
goToPreset: async ({ deviceId, presetId }) => {
|
|
@@ -51410,10 +51808,12 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51410
51808
|
},
|
|
51411
51809
|
savePreset: async ({ deviceId, presetId, name }) => {
|
|
51412
51810
|
if (deviceId !== this.id) return;
|
|
51811
|
+
assertPresetWritable(presetId, "overwritten");
|
|
51413
51812
|
await isapi.savePtzPreset(cameraNumber, presetId, name);
|
|
51414
51813
|
},
|
|
51415
51814
|
deletePreset: async ({ deviceId, presetId }) => {
|
|
51416
51815
|
if (deviceId !== this.id) return;
|
|
51816
|
+
assertPresetWritable(presetId, "deleted");
|
|
51417
51817
|
await isapi.deletePtzPreset(cameraNumber, presetId);
|
|
51418
51818
|
},
|
|
51419
51819
|
getOptions: async ({ deviceId }) => {
|
|
@@ -51422,7 +51822,9 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51422
51822
|
hasTilt: false,
|
|
51423
51823
|
hasZoom: false,
|
|
51424
51824
|
supportsPresets: false,
|
|
51425
|
-
hasAutofocus: false
|
|
51825
|
+
hasAutofocus: false,
|
|
51826
|
+
speedSteps: 100,
|
|
51827
|
+
moveImpulseMs: 500
|
|
51426
51828
|
};
|
|
51427
51829
|
const caps = await isapi.getPtzCapabilities(cameraNumber);
|
|
51428
51830
|
if (!caps) return {
|
|
@@ -51430,7 +51832,9 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51430
51832
|
hasTilt: false,
|
|
51431
51833
|
hasZoom: false,
|
|
51432
51834
|
supportsPresets: false,
|
|
51433
|
-
hasAutofocus: false
|
|
51835
|
+
hasAutofocus: false,
|
|
51836
|
+
speedSteps: 100,
|
|
51837
|
+
moveImpulseMs: 500
|
|
51434
51838
|
};
|
|
51435
51839
|
const maxPresets = caps.maxPresetCount ?? void 0;
|
|
51436
51840
|
return {
|
|
@@ -51439,25 +51843,61 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51439
51843
|
hasZoom: caps.hasZoom,
|
|
51440
51844
|
supportsPresets: maxPresets === void 0 ? true : maxPresets > 0,
|
|
51441
51845
|
hasAutofocus: false,
|
|
51846
|
+
speedSteps: 100,
|
|
51847
|
+
moveImpulseMs: 500,
|
|
51442
51848
|
...maxPresets !== void 0 ? { maxPresets } : {}
|
|
51443
51849
|
};
|
|
51444
51850
|
},
|
|
51445
51851
|
goHome: async ({ deviceId }) => {
|
|
51446
51852
|
if (deviceId !== this.id) return;
|
|
51447
|
-
await
|
|
51853
|
+
const home = await this.resolveHomePreset();
|
|
51854
|
+
if (home.source === "none" || home.presetId === null) throw new Error("No home preset configured for this camera — set one in the PTZ settings");
|
|
51855
|
+
await isapi.ptzGoToPreset(cameraNumber, home.presetId);
|
|
51856
|
+
},
|
|
51857
|
+
getHomePreset: async ({ deviceId }) => {
|
|
51858
|
+
if (deviceId !== this.id) return {
|
|
51859
|
+
presetId: null,
|
|
51860
|
+
source: "none"
|
|
51861
|
+
};
|
|
51862
|
+
return this.resolveHomePreset();
|
|
51863
|
+
},
|
|
51864
|
+
captureHomeHere: async ({ deviceId }) => {
|
|
51865
|
+
if (deviceId !== this.id) return;
|
|
51866
|
+
const [presets, caps] = await Promise.all([isapi.listPtzPresets(cameraNumber), isapi.getPtzCapabilities(cameraNumber)]);
|
|
51867
|
+
const slot = findFreePresetSlot(presets.map((p) => ({
|
|
51868
|
+
id: p.id,
|
|
51869
|
+
writable: isPresetWritable(p.id)
|
|
51870
|
+
})), caps?.maxPresetCount ?? void 0);
|
|
51871
|
+
if (slot === null) throw new Error("No free preset slot on this camera to store a home position — free one first");
|
|
51872
|
+
await isapi.savePtzPreset(cameraNumber, slot, HOME_PRESET_NAME);
|
|
51873
|
+
await this.config.setAll({ homePresetId: slot });
|
|
51874
|
+
this.ctx.logger.info("hikvision: home pinned to the current position", {
|
|
51875
|
+
tags: { deviceId: this.id },
|
|
51876
|
+
meta: { presetId: slot }
|
|
51877
|
+
});
|
|
51878
|
+
},
|
|
51879
|
+
getHomeReturnSeconds: async () => {
|
|
51880
|
+
return null;
|
|
51881
|
+
},
|
|
51882
|
+
setHomeReturnSeconds: async () => {
|
|
51883
|
+
throw new Error("Hikvision: the idle-return timer is not wired on this driver");
|
|
51884
|
+
},
|
|
51885
|
+
setHomePreset: async ({ deviceId, presetId }) => {
|
|
51886
|
+
if (deviceId !== this.id) return;
|
|
51887
|
+
await this.config.setAll({ homePresetId: presetId ?? "" });
|
|
51448
51888
|
},
|
|
51449
51889
|
getPosition: async () => {
|
|
51450
51890
|
return {
|
|
51451
|
-
pan:
|
|
51452
|
-
tilt:
|
|
51453
|
-
zoom:
|
|
51891
|
+
pan: null,
|
|
51892
|
+
tilt: null,
|
|
51893
|
+
zoom: null
|
|
51454
51894
|
};
|
|
51455
51895
|
},
|
|
51456
51896
|
getStatus: async () => {
|
|
51457
51897
|
return {
|
|
51458
|
-
pan:
|
|
51459
|
-
tilt:
|
|
51460
|
-
zoom:
|
|
51898
|
+
pan: null,
|
|
51899
|
+
tilt: null,
|
|
51900
|
+
zoom: null,
|
|
51461
51901
|
autofocus: false
|
|
51462
51902
|
};
|
|
51463
51903
|
},
|
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-vl6nBE8d.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,66 @@ 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
|
+
}),
|
|
31596
|
+
/**
|
|
31597
|
+
* Make WHERE THE HEAD IS POINTING RIGHT NOW this camera's home.
|
|
31598
|
+
*
|
|
31599
|
+
* `setHomePreset` takes a preset id, and a preset id is the wrong shape for
|
|
31600
|
+
* the most natural thing an operator does: aim the camera, then say "here".
|
|
31601
|
+
* It is also the only shape a camera whose home is NOT a preset can accept
|
|
31602
|
+
* -- Reolink's home is a guard point, a stored position with no id at all,
|
|
31603
|
+
* and hiding the control for that case (as the UI briefly did) left the one
|
|
31604
|
+
* vendor with a native home unable to set it.
|
|
31605
|
+
*
|
|
31606
|
+
* Each provider does it its own way:
|
|
31607
|
+
* reolink `setGrd` with `needSetPos`, pinning the guard point here
|
|
31608
|
+
* hikvision / amcrest save a preset, then pin it as home
|
|
31609
|
+
* onvif REFUSES -- its home is the PTZ origin and cannot be moved
|
|
31610
|
+
*
|
|
31611
|
+
* A provider that cannot honour it throws rather than silently doing
|
|
31612
|
+
* nothing: an operator who pressed "set home here" and saw no error would
|
|
31613
|
+
* believe it took.
|
|
31614
|
+
*/
|
|
31615
|
+
captureHomeHere: method(object({ deviceId: number() }), _void(), {
|
|
31616
|
+
kind: "mutation",
|
|
31617
|
+
auth: "admin"
|
|
31618
|
+
}),
|
|
31619
|
+
/**
|
|
31620
|
+
* How long the camera waits, idle, before returning home on its own --
|
|
31621
|
+
* `null` when it has no such behaviour.
|
|
31622
|
+
*
|
|
31623
|
+
* Reolink's guard point carries one (`timeout`, freely settable in
|
|
31624
|
+
* seconds). It is a real function of the camera that we did not expose at
|
|
31625
|
+
* all, and it belongs next to the home it governs rather than in a vendor
|
|
31626
|
+
* settings page nobody associates with Home.
|
|
31627
|
+
*/
|
|
31628
|
+
getHomeReturnSeconds: method(object({ deviceId: number() }), number().int().nullable()),
|
|
31629
|
+
/** Set the idle-return delay. Refused by a camera that has no such timer. */
|
|
31630
|
+
setHomeReturnSeconds: method(object({
|
|
31631
|
+
deviceId: number(),
|
|
31632
|
+
seconds: number().int().min(0).max(3600)
|
|
31633
|
+
}), _void(), {
|
|
31634
|
+
kind: "mutation",
|
|
31635
|
+
auth: "admin"
|
|
31636
|
+
}),
|
|
31446
31637
|
/**
|
|
31447
31638
|
* Pull the current PTZ position. Redundant with the auto-injected
|
|
31448
31639
|
* `getStatus` method (see `status` below); kept for callers that
|
|
@@ -41156,6 +41347,12 @@ Object.freeze({
|
|
|
41156
41347
|
addonId: null,
|
|
41157
41348
|
access: "create"
|
|
41158
41349
|
},
|
|
41350
|
+
"ptz.captureHomeHere": {
|
|
41351
|
+
capName: "ptz",
|
|
41352
|
+
capScope: "device",
|
|
41353
|
+
addonId: null,
|
|
41354
|
+
access: "create"
|
|
41355
|
+
},
|
|
41159
41356
|
"ptz.continuousMove": {
|
|
41160
41357
|
capName: "ptz",
|
|
41161
41358
|
capScope: "device",
|
|
@@ -41168,6 +41365,18 @@ Object.freeze({
|
|
|
41168
41365
|
addonId: null,
|
|
41169
41366
|
access: "delete"
|
|
41170
41367
|
},
|
|
41368
|
+
"ptz.getHomePreset": {
|
|
41369
|
+
capName: "ptz",
|
|
41370
|
+
capScope: "device",
|
|
41371
|
+
addonId: null,
|
|
41372
|
+
access: "view"
|
|
41373
|
+
},
|
|
41374
|
+
"ptz.getHomeReturnSeconds": {
|
|
41375
|
+
capName: "ptz",
|
|
41376
|
+
capScope: "device",
|
|
41377
|
+
addonId: null,
|
|
41378
|
+
access: "view"
|
|
41379
|
+
},
|
|
41171
41380
|
"ptz.getOptions": {
|
|
41172
41381
|
capName: "ptz",
|
|
41173
41382
|
capScope: "device",
|
|
@@ -41216,6 +41425,18 @@ Object.freeze({
|
|
|
41216
41425
|
addonId: null,
|
|
41217
41426
|
access: "create"
|
|
41218
41427
|
},
|
|
41428
|
+
"ptz.setHomePreset": {
|
|
41429
|
+
capName: "ptz",
|
|
41430
|
+
capScope: "device",
|
|
41431
|
+
addonId: null,
|
|
41432
|
+
access: "create"
|
|
41433
|
+
},
|
|
41434
|
+
"ptz.setHomeReturnSeconds": {
|
|
41435
|
+
capName: "ptz",
|
|
41436
|
+
capScope: "device",
|
|
41437
|
+
addonId: null,
|
|
41438
|
+
access: "create"
|
|
41439
|
+
},
|
|
41219
41440
|
"ptz.stop": {
|
|
41220
41441
|
capName: "ptz",
|
|
41221
41442
|
capScope: "device",
|
|
@@ -44379,6 +44600,11 @@ Object.freeze({
|
|
|
44379
44600
|
form: "single",
|
|
44380
44601
|
optional: false
|
|
44381
44602
|
}],
|
|
44603
|
+
"ptz.captureHomeHere": [{
|
|
44604
|
+
name: "deviceId",
|
|
44605
|
+
form: "single",
|
|
44606
|
+
optional: false
|
|
44607
|
+
}],
|
|
44382
44608
|
"ptz.continuousMove": [{
|
|
44383
44609
|
name: "deviceId",
|
|
44384
44610
|
form: "single",
|
|
@@ -44389,6 +44615,16 @@ Object.freeze({
|
|
|
44389
44615
|
form: "single",
|
|
44390
44616
|
optional: false
|
|
44391
44617
|
}],
|
|
44618
|
+
"ptz.getHomePreset": [{
|
|
44619
|
+
name: "deviceId",
|
|
44620
|
+
form: "single",
|
|
44621
|
+
optional: false
|
|
44622
|
+
}],
|
|
44623
|
+
"ptz.getHomeReturnSeconds": [{
|
|
44624
|
+
name: "deviceId",
|
|
44625
|
+
form: "single",
|
|
44626
|
+
optional: false
|
|
44627
|
+
}],
|
|
44392
44628
|
"ptz.getOptions": [{
|
|
44393
44629
|
name: "deviceId",
|
|
44394
44630
|
form: "single",
|
|
@@ -44429,6 +44665,16 @@ Object.freeze({
|
|
|
44429
44665
|
form: "single",
|
|
44430
44666
|
optional: false
|
|
44431
44667
|
}],
|
|
44668
|
+
"ptz.setHomePreset": [{
|
|
44669
|
+
name: "deviceId",
|
|
44670
|
+
form: "single",
|
|
44671
|
+
optional: false
|
|
44672
|
+
}],
|
|
44673
|
+
"ptz.setHomeReturnSeconds": [{
|
|
44674
|
+
name: "deviceId",
|
|
44675
|
+
form: "single",
|
|
44676
|
+
optional: false
|
|
44677
|
+
}],
|
|
44432
44678
|
"ptz.stop": [{
|
|
44433
44679
|
name: "deviceId",
|
|
44434
44680
|
form: "single",
|
|
@@ -49737,6 +49983,110 @@ function buildRawState(reader) {
|
|
|
49737
49983
|
};
|
|
49738
49984
|
}
|
|
49739
49985
|
//#endregion
|
|
49986
|
+
//#region src/isapi-ptz-speed.ts
|
|
49987
|
+
/**
|
|
49988
|
+
* Turn the cap's normalized direction + magnitude into an ISAPI continuous
|
|
49989
|
+
* speed.
|
|
49990
|
+
*
|
|
49991
|
+
* The `ptz` cap splits the two: `pan`/`tilt`/`zoom` are a DIRECTION in -1..1,
|
|
49992
|
+
* and `speed` is the magnitude in 0..1. reolink maps them to Baichuan 1..63,
|
|
49993
|
+
* amcrest to Dahua 1..8 — and hikvision used to map them to nothing at all. It
|
|
49994
|
+
* dropped `speed` before it left the provider and handed the raw direction to
|
|
49995
|
+
* `/ISAPI/PTZCtrl/channels/{cam}/continuous`, whose `<pan>` is a speed
|
|
49996
|
+
* PERCENTAGE in -100..100.
|
|
49997
|
+
*
|
|
49998
|
+
* So `usePTZ` sending its d-pad default (`pan: 1, speed: 0.5`) reached the
|
|
49999
|
+
* dispensa camera as `<pan>1</pan>`: one percent of the camera's speed. A
|
|
50000
|
+
* request at less than half magnitude rounded to `0` and produced no motion at
|
|
50001
|
+
* all.
|
|
50002
|
+
*
|
|
50003
|
+
* An absent `speed` is mid-scale, never zero — reolink defaults to 32 of 63 for
|
|
50004
|
+
* the same reason. A caller who did not state a magnitude asked to move, not to
|
|
50005
|
+
* stand still.
|
|
50006
|
+
*/
|
|
50007
|
+
var MID_SCALE = .5;
|
|
50008
|
+
function isapiSpeedFor(direction, speed) {
|
|
50009
|
+
const d = direction ?? 0;
|
|
50010
|
+
if (!Number.isFinite(d) || d === 0) return 0;
|
|
50011
|
+
const magnitude = speed === void 0 || !Number.isFinite(speed) ? MID_SCALE : Math.abs(speed);
|
|
50012
|
+
const percent = Math.round(Math.sign(d) * Math.min(1, Math.abs(d)) * Math.min(1, magnitude) * 100);
|
|
50013
|
+
return Math.max(-100, Math.min(100, percent));
|
|
50014
|
+
}
|
|
50015
|
+
//#endregion
|
|
50016
|
+
//#region src/ptz-preset-functions.ts
|
|
50017
|
+
var RESERVED = new Map([
|
|
50018
|
+
["34", "home"],
|
|
50019
|
+
["39", "day-mode"],
|
|
50020
|
+
["40", "night-mode"],
|
|
50021
|
+
["46", "day-night-auto"],
|
|
50022
|
+
["92", "set-limits"],
|
|
50023
|
+
["93", "save-limits"],
|
|
50024
|
+
["94", "reboot"],
|
|
50025
|
+
["95", "osd-menu"]
|
|
50026
|
+
]);
|
|
50027
|
+
/** The firmware function this id performs, or `null` for a stored position. */
|
|
50028
|
+
function presetFunctionFor(presetId) {
|
|
50029
|
+
return RESERVED.get(presetId) ?? null;
|
|
50030
|
+
}
|
|
50031
|
+
/**
|
|
50032
|
+
* Whether `savePreset` / `deletePreset` may target this id.
|
|
50033
|
+
*
|
|
50034
|
+
* Anything outside the known band stays writable: an undocumented firmware
|
|
50035
|
+
* must be allowed to refuse for itself rather than be guessed at here.
|
|
50036
|
+
*/
|
|
50037
|
+
function isPresetWritable(presetId) {
|
|
50038
|
+
return !RESERVED.has(presetId);
|
|
50039
|
+
}
|
|
50040
|
+
/**
|
|
50041
|
+
* Refuse a write to a reserved id, naming the function that owns it.
|
|
50042
|
+
*
|
|
50043
|
+
* Extracted so the REFUSAL itself is assertable. With the check inline in
|
|
50044
|
+
* `savePreset` / `deletePreset`, deleting it left every test green -- the table
|
|
50045
|
+
* spec proves what the band IS, not that anything consults it.
|
|
50046
|
+
*/
|
|
50047
|
+
function assertPresetWritable(presetId, action) {
|
|
50048
|
+
const fn = presetFunctionFor(presetId);
|
|
50049
|
+
if (fn === null) return;
|
|
50050
|
+
throw new Error(`Preset ${presetId} is a firmware function on this camera (${fn}) and cannot be ${action}` + (action === "overwritten" ? " — pick an unreserved id" : ""));
|
|
50051
|
+
}
|
|
50052
|
+
//#endregion
|
|
50053
|
+
//#region src/free-preset-slot.ts
|
|
50054
|
+
/**
|
|
50055
|
+
* The lowest preset id this camera will actually accept a write on.
|
|
50056
|
+
*
|
|
50057
|
+
* "Set home here" has to land somewhere, and on a camera whose home IS a preset
|
|
50058
|
+
* that means claiming a slot. Two things make the choice non-obvious:
|
|
50059
|
+
*
|
|
50060
|
+
* - the reserved FUNCTION band shares the preset namespace (34 "Back to
|
|
50061
|
+
* origin", 94 "Remote reboot", 95 "Call OSD menu" and friends), and writing
|
|
50062
|
+
* one answers HTTP 500;
|
|
50063
|
+
* - the operator's own presets must not be overwritten, ever. Losing
|
|
50064
|
+
* "Credenza" to make room for a home is a worse trade than refusing.
|
|
50065
|
+
*
|
|
50066
|
+
* So: the first id that is neither taken nor reserved, within the camera's
|
|
50067
|
+
* advertised budget. `null` when there is no room, and the caller must then
|
|
50068
|
+
* REFUSE rather than pick a victim.
|
|
50069
|
+
*/
|
|
50070
|
+
function findFreePresetSlot(taken, maxPresets) {
|
|
50071
|
+
const used = new Set(taken.map((p) => p.id));
|
|
50072
|
+
const ceiling = maxPresets === void 0 ? 32 : Math.min(maxPresets, 300);
|
|
50073
|
+
for (let id = 1; id <= ceiling; id += 1) {
|
|
50074
|
+
const candidate = String(id);
|
|
50075
|
+
if (used.has(candidate)) continue;
|
|
50076
|
+
if (!isPresetWritable(candidate)) continue;
|
|
50077
|
+
return candidate;
|
|
50078
|
+
}
|
|
50079
|
+
return null;
|
|
50080
|
+
}
|
|
50081
|
+
/**
|
|
50082
|
+
* The name written on a preset claimed by "set home here".
|
|
50083
|
+
*
|
|
50084
|
+
* Named rather than left a literal so the slot is recognisable in the camera's
|
|
50085
|
+
* own web UI — an operator who finds an unexplained preset has no way to know
|
|
50086
|
+
* it is ours.
|
|
50087
|
+
*/
|
|
50088
|
+
var HOME_PRESET_NAME = "CamStack Home";
|
|
50089
|
+
//#endregion
|
|
49740
50090
|
//#region src/schema.ts
|
|
49741
50091
|
/**
|
|
49742
50092
|
* Single source of truth for the addon id used in event-source tags +
|
|
@@ -50055,6 +50405,17 @@ var hikvisionCameraSchema = object({
|
|
|
50055
50405
|
*/
|
|
50056
50406
|
snapshotChannelId: string().default("").describe("ISAPI channel used for snapshots (empty = main stream)"),
|
|
50057
50407
|
/**
|
|
50408
|
+
* Which preset `ptz.goHome` targets on THIS camera.
|
|
50409
|
+
*
|
|
50410
|
+
* Empty = no operator choice. The provider then falls back to the preset the
|
|
50411
|
+
* FIRMWARE names as home (Hikvision returns id 34 "Back to origin" in its
|
|
50412
|
+
* reserved band), and only if that is absent too does `goHome` refuse.
|
|
50413
|
+
*
|
|
50414
|
+
* It has to be stored because the vendor "convention" is wrong here: the code
|
|
50415
|
+
* went to preset '1', and on the dispensa camera preset 1 is "Credenza".
|
|
50416
|
+
*/
|
|
50417
|
+
homePresetId: string().default("").describe("Preset id ptz.goHome targets (empty = use the firmware home)"),
|
|
50418
|
+
/**
|
|
50058
50419
|
* Motion detection master switch + sensitivity (0..100). Maps to
|
|
50059
50420
|
* ISAPI `/ISAPI/System/Video/inputs/channels/{cam}/motionDetection`.
|
|
50060
50421
|
* Disabling this stops the camera from emitting `<eventType>VMD</eventType>`
|
|
@@ -51092,6 +51453,41 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51092
51453
|
...slice
|
|
51093
51454
|
} });
|
|
51094
51455
|
}
|
|
51456
|
+
/**
|
|
51457
|
+
* Which preset `goHome` targets, and who decided it.
|
|
51458
|
+
*
|
|
51459
|
+
* The operator's pick wins. With none, the FIRMWARE's own home is adopted --
|
|
51460
|
+
* Hikvision names id 34 "Back to origin" in its reserved band -- so a camera
|
|
51461
|
+
* that already knows where home is needs nothing configured. Only when both
|
|
51462
|
+
* are absent is the answer `none`, and `goHome` then refuses rather than
|
|
51463
|
+
* moving the head to an arbitrary slot.
|
|
51464
|
+
*
|
|
51465
|
+
* A failed preset read yields `none`, never a guess: an unreadable camera is
|
|
51466
|
+
* not evidence that preset 34 exists (D393).
|
|
51467
|
+
*/
|
|
51468
|
+
async resolveHomePreset() {
|
|
51469
|
+
const chosen = this.config.get("homePresetId") ?? "";
|
|
51470
|
+
if (chosen !== "") return {
|
|
51471
|
+
presetId: chosen,
|
|
51472
|
+
source: "operator"
|
|
51473
|
+
};
|
|
51474
|
+
try {
|
|
51475
|
+
const firmwareHome = (await this.ensureClient().listPtzPresets(1)).find((preset) => presetFunctionFor(preset.id) === "home");
|
|
51476
|
+
if (firmwareHome !== void 0) return {
|
|
51477
|
+
presetId: firmwareHome.id,
|
|
51478
|
+
source: "firmware"
|
|
51479
|
+
};
|
|
51480
|
+
} catch (err) {
|
|
51481
|
+
this.ctx.logger.warn("hikvision home preset lookup failed — goHome will refuse", {
|
|
51482
|
+
tags: { deviceId: this.id },
|
|
51483
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
51484
|
+
});
|
|
51485
|
+
}
|
|
51486
|
+
return {
|
|
51487
|
+
presetId: null,
|
|
51488
|
+
source: "none"
|
|
51489
|
+
};
|
|
51490
|
+
}
|
|
51095
51491
|
/** Re-read the image-tuning snapshot from the camera and persist it.
|
|
51096
51492
|
* Best-effort: a refresh failure is logged and swallowed so a save
|
|
51097
51493
|
* never throws. Called after an Image-section push in
|
|
@@ -51368,12 +51764,12 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51368
51764
|
registerPtzCap(cameraNumber) {
|
|
51369
51765
|
const isapi = this.ensureClient();
|
|
51370
51766
|
this.ctx.registerNativeCap(ptzCapability, {
|
|
51371
|
-
move: async ({ deviceId, pan, tilt, zoom }) => {
|
|
51767
|
+
move: async ({ deviceId, pan, tilt, zoom, speed }) => {
|
|
51372
51768
|
if (deviceId !== this.id) return;
|
|
51373
51769
|
await isapi.ptzContinuous(cameraNumber, {
|
|
51374
|
-
pan,
|
|
51375
|
-
tilt,
|
|
51376
|
-
zoom
|
|
51770
|
+
pan: isapiSpeedFor(pan, speed),
|
|
51771
|
+
tilt: isapiSpeedFor(tilt, speed),
|
|
51772
|
+
zoom: isapiSpeedFor(zoom, speed)
|
|
51377
51773
|
});
|
|
51378
51774
|
await new Promise((r) => setTimeout(r, 500));
|
|
51379
51775
|
await isapi.ptzContinuous(cameraNumber, {
|
|
@@ -51382,12 +51778,12 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51382
51778
|
zoom: 0
|
|
51383
51779
|
});
|
|
51384
51780
|
},
|
|
51385
|
-
continuousMove: async ({ deviceId, pan, tilt, zoom }) => {
|
|
51781
|
+
continuousMove: async ({ deviceId, pan, tilt, zoom, speed }) => {
|
|
51386
51782
|
if (deviceId !== this.id) return;
|
|
51387
51783
|
await isapi.ptzContinuous(cameraNumber, {
|
|
51388
|
-
pan,
|
|
51389
|
-
tilt,
|
|
51390
|
-
zoom
|
|
51784
|
+
pan: isapiSpeedFor(pan, speed),
|
|
51785
|
+
tilt: isapiSpeedFor(tilt, speed),
|
|
51786
|
+
zoom: isapiSpeedFor(zoom, speed)
|
|
51391
51787
|
});
|
|
51392
51788
|
},
|
|
51393
51789
|
stop: async ({ deviceId }) => {
|
|
@@ -51402,7 +51798,9 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51402
51798
|
if (deviceId !== this.id) return [];
|
|
51403
51799
|
return (await isapi.listPtzPresets(cameraNumber)).map((p) => ({
|
|
51404
51800
|
id: p.id,
|
|
51405
|
-
name: p.name ?? `Preset ${p.id}
|
|
51801
|
+
name: p.name ?? `Preset ${p.id}`,
|
|
51802
|
+
fn: presetFunctionFor(p.id),
|
|
51803
|
+
writable: isPresetWritable(p.id)
|
|
51406
51804
|
}));
|
|
51407
51805
|
},
|
|
51408
51806
|
goToPreset: async ({ deviceId, presetId }) => {
|
|
@@ -51411,10 +51809,12 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51411
51809
|
},
|
|
51412
51810
|
savePreset: async ({ deviceId, presetId, name }) => {
|
|
51413
51811
|
if (deviceId !== this.id) return;
|
|
51812
|
+
assertPresetWritable(presetId, "overwritten");
|
|
51414
51813
|
await isapi.savePtzPreset(cameraNumber, presetId, name);
|
|
51415
51814
|
},
|
|
51416
51815
|
deletePreset: async ({ deviceId, presetId }) => {
|
|
51417
51816
|
if (deviceId !== this.id) return;
|
|
51817
|
+
assertPresetWritable(presetId, "deleted");
|
|
51418
51818
|
await isapi.deletePtzPreset(cameraNumber, presetId);
|
|
51419
51819
|
},
|
|
51420
51820
|
getOptions: async ({ deviceId }) => {
|
|
@@ -51423,7 +51823,9 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51423
51823
|
hasTilt: false,
|
|
51424
51824
|
hasZoom: false,
|
|
51425
51825
|
supportsPresets: false,
|
|
51426
|
-
hasAutofocus: false
|
|
51826
|
+
hasAutofocus: false,
|
|
51827
|
+
speedSteps: 100,
|
|
51828
|
+
moveImpulseMs: 500
|
|
51427
51829
|
};
|
|
51428
51830
|
const caps = await isapi.getPtzCapabilities(cameraNumber);
|
|
51429
51831
|
if (!caps) return {
|
|
@@ -51431,7 +51833,9 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51431
51833
|
hasTilt: false,
|
|
51432
51834
|
hasZoom: false,
|
|
51433
51835
|
supportsPresets: false,
|
|
51434
|
-
hasAutofocus: false
|
|
51836
|
+
hasAutofocus: false,
|
|
51837
|
+
speedSteps: 100,
|
|
51838
|
+
moveImpulseMs: 500
|
|
51435
51839
|
};
|
|
51436
51840
|
const maxPresets = caps.maxPresetCount ?? void 0;
|
|
51437
51841
|
return {
|
|
@@ -51440,25 +51844,61 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
51440
51844
|
hasZoom: caps.hasZoom,
|
|
51441
51845
|
supportsPresets: maxPresets === void 0 ? true : maxPresets > 0,
|
|
51442
51846
|
hasAutofocus: false,
|
|
51847
|
+
speedSteps: 100,
|
|
51848
|
+
moveImpulseMs: 500,
|
|
51443
51849
|
...maxPresets !== void 0 ? { maxPresets } : {}
|
|
51444
51850
|
};
|
|
51445
51851
|
},
|
|
51446
51852
|
goHome: async ({ deviceId }) => {
|
|
51447
51853
|
if (deviceId !== this.id) return;
|
|
51448
|
-
await
|
|
51854
|
+
const home = await this.resolveHomePreset();
|
|
51855
|
+
if (home.source === "none" || home.presetId === null) throw new Error("No home preset configured for this camera — set one in the PTZ settings");
|
|
51856
|
+
await isapi.ptzGoToPreset(cameraNumber, home.presetId);
|
|
51857
|
+
},
|
|
51858
|
+
getHomePreset: async ({ deviceId }) => {
|
|
51859
|
+
if (deviceId !== this.id) return {
|
|
51860
|
+
presetId: null,
|
|
51861
|
+
source: "none"
|
|
51862
|
+
};
|
|
51863
|
+
return this.resolveHomePreset();
|
|
51864
|
+
},
|
|
51865
|
+
captureHomeHere: async ({ deviceId }) => {
|
|
51866
|
+
if (deviceId !== this.id) return;
|
|
51867
|
+
const [presets, caps] = await Promise.all([isapi.listPtzPresets(cameraNumber), isapi.getPtzCapabilities(cameraNumber)]);
|
|
51868
|
+
const slot = findFreePresetSlot(presets.map((p) => ({
|
|
51869
|
+
id: p.id,
|
|
51870
|
+
writable: isPresetWritable(p.id)
|
|
51871
|
+
})), caps?.maxPresetCount ?? void 0);
|
|
51872
|
+
if (slot === null) throw new Error("No free preset slot on this camera to store a home position — free one first");
|
|
51873
|
+
await isapi.savePtzPreset(cameraNumber, slot, HOME_PRESET_NAME);
|
|
51874
|
+
await this.config.setAll({ homePresetId: slot });
|
|
51875
|
+
this.ctx.logger.info("hikvision: home pinned to the current position", {
|
|
51876
|
+
tags: { deviceId: this.id },
|
|
51877
|
+
meta: { presetId: slot }
|
|
51878
|
+
});
|
|
51879
|
+
},
|
|
51880
|
+
getHomeReturnSeconds: async () => {
|
|
51881
|
+
return null;
|
|
51882
|
+
},
|
|
51883
|
+
setHomeReturnSeconds: async () => {
|
|
51884
|
+
throw new Error("Hikvision: the idle-return timer is not wired on this driver");
|
|
51885
|
+
},
|
|
51886
|
+
setHomePreset: async ({ deviceId, presetId }) => {
|
|
51887
|
+
if (deviceId !== this.id) return;
|
|
51888
|
+
await this.config.setAll({ homePresetId: presetId ?? "" });
|
|
51449
51889
|
},
|
|
51450
51890
|
getPosition: async () => {
|
|
51451
51891
|
return {
|
|
51452
|
-
pan:
|
|
51453
|
-
tilt:
|
|
51454
|
-
zoom:
|
|
51892
|
+
pan: null,
|
|
51893
|
+
tilt: null,
|
|
51894
|
+
zoom: null
|
|
51455
51895
|
};
|
|
51456
51896
|
},
|
|
51457
51897
|
getStatus: async () => {
|
|
51458
51898
|
return {
|
|
51459
|
-
pan:
|
|
51460
|
-
tilt:
|
|
51461
|
-
zoom:
|
|
51899
|
+
pan: null,
|
|
51900
|
+
tilt: null,
|
|
51901
|
+
zoom: null,
|
|
51462
51902
|
autofocus: false
|
|
51463
51903
|
};
|
|
51464
51904
|
},
|
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.120",
|
|
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",
|