@camstack/addon-provider-amcrest 0.2.112 → 0.2.114
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 +347 -15
- package/dist/addon.mjs +347 -15
- 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).
|
|
@@ -31059,14 +31059,81 @@ function summarisePrivacyAudio(profiles) {
|
|
|
31059
31059
|
if (profiles.length === 0) return null;
|
|
31060
31060
|
return profiles.some((p) => p.audioEnabled);
|
|
31061
31061
|
}
|
|
31062
|
+
/**
|
|
31063
|
+
* What a preset id DOES on this camera.
|
|
31064
|
+
*
|
|
31065
|
+
* On several PTZ firmwares the preset namespace is shared: low ids are stored
|
|
31066
|
+
* positions, a reserved band triggers camera FUNCTIONS. The dispensa camera
|
|
31067
|
+
* (Hikvision) returns its own reserved band in the preset list, named by the
|
|
31068
|
+
* firmware:
|
|
31069
|
+
*
|
|
31070
|
+
* 34 Back to origin · 39 Day mode · 40 Night mode · 46 Day/Night Auto Mode
|
|
31071
|
+
* 92 Set manual limits · 93 Save manual limits · 94 Remote reboot
|
|
31072
|
+
* 95 Call OSD menu
|
|
31073
|
+
*
|
|
31074
|
+
* We used to hand all of these to the UI as ordinary presets. Saving over one
|
|
31075
|
+
* is what produced `PUT /ISAPI/PTZCtrl/channels/1/presets/34 -> HTTP 500`: the
|
|
31076
|
+
* camera refusing to let "Back to origin" be overwritten, surfaced to the
|
|
31077
|
+
* operator as a transport failure.
|
|
31078
|
+
*
|
|
31079
|
+
* A function preset stays fully usable as a DESTINATION -- `goToPreset('34')`
|
|
31080
|
+
* is the single most useful thing that camera can do. Only `savePreset` and
|
|
31081
|
+
* `deletePreset` are gated.
|
|
31082
|
+
*/
|
|
31083
|
+
var PtzPresetFunctionSchema = _enum([
|
|
31084
|
+
"home",
|
|
31085
|
+
"day-mode",
|
|
31086
|
+
"night-mode",
|
|
31087
|
+
"day-night-auto",
|
|
31088
|
+
"set-limits",
|
|
31089
|
+
"save-limits",
|
|
31090
|
+
"reboot",
|
|
31091
|
+
"osd-menu"
|
|
31092
|
+
]);
|
|
31062
31093
|
var PtzPresetSchema = object({
|
|
31063
31094
|
id: string(),
|
|
31064
|
-
name: string()
|
|
31095
|
+
name: string(),
|
|
31096
|
+
/**
|
|
31097
|
+
* The firmware function this id performs, when the provider KNOWS the id is
|
|
31098
|
+
* reserved. `null` = an ordinary stored position, OR a reserved id this
|
|
31099
|
+
* provider does not recognise -- the two are told apart by `writable`.
|
|
31100
|
+
*/
|
|
31101
|
+
fn: PtzPresetFunctionSchema.nullable(),
|
|
31102
|
+
/**
|
|
31103
|
+
* Whether `savePreset` / `deletePreset` may target this id. A provider sets
|
|
31104
|
+
* it false only for a band it KNOWS is reserved; where the firmware is not
|
|
31105
|
+
* documented it stays true and a refusal comes from the camera, named.
|
|
31106
|
+
*/
|
|
31107
|
+
writable: boolean()
|
|
31065
31108
|
});
|
|
31109
|
+
/**
|
|
31110
|
+
* Where the head is pointing -- per axis, and `null` when the driver cannot
|
|
31111
|
+
* read it.
|
|
31112
|
+
*
|
|
31113
|
+
* It used to be three REQUIRED numbers, which left a provider that cannot read
|
|
31114
|
+
* position no way to say so. All four said `0`:
|
|
31115
|
+
*
|
|
31116
|
+
* reolink `return { pan: 0, tilt: 0, zoom: 0 }` "until a position-query
|
|
31117
|
+
* path lands upstream"
|
|
31118
|
+
* hikvision stub, "firmware-dependent and frequently absent"
|
|
31119
|
+
* amcrest stub, "no generic absolute-position read for this model family"
|
|
31120
|
+
* onvif throws
|
|
31121
|
+
*
|
|
31122
|
+
* So every consumer asking where a camera points was told "perfectly centred"
|
|
31123
|
+
* by four cameras that had never looked. That is D393 -- a measurement that
|
|
31124
|
+
* FAILED is `null`, never `0`, and the type says so all the way to the
|
|
31125
|
+
* decision -- the same shape as the unreadable `statfs` folded into "0 bytes of
|
|
31126
|
+
* headroom", which evacuated a healthy disk.
|
|
31127
|
+
*
|
|
31128
|
+
* D393 also says to check for a narrower structural TWIN of the result type.
|
|
31129
|
+
* There is one: `PtzStatusSchema` extends this, so the lie had already
|
|
31130
|
+
* propagated into `getStatus`, which is the surface `getPosition`'s own comment
|
|
31131
|
+
* tells callers to migrate to.
|
|
31132
|
+
*/
|
|
31066
31133
|
var PtzPositionSchema = object({
|
|
31067
|
-
pan: number(),
|
|
31068
|
-
tilt: number(),
|
|
31069
|
-
zoom: number()
|
|
31134
|
+
pan: number().nullable(),
|
|
31135
|
+
tilt: number().nullable(),
|
|
31136
|
+
zoom: number().nullable()
|
|
31070
31137
|
});
|
|
31071
31138
|
var PtzMoveCommandSchema = object({
|
|
31072
31139
|
pan: number().optional(),
|
|
@@ -31088,7 +31155,72 @@ var PtzOptionsSchema = object({
|
|
|
31088
31155
|
maxPresets: number().optional(),
|
|
31089
31156
|
/** Whether the camera exposes a controllable autofocus toggle
|
|
31090
31157
|
* (boolean `hasX` per the getOptions availability convention). */
|
|
31091
|
-
hasAutofocus: boolean()
|
|
31158
|
+
hasAutofocus: boolean(),
|
|
31159
|
+
/**
|
|
31160
|
+
* How many distinct speeds this camera's NATIVE scale offers.
|
|
31161
|
+
*
|
|
31162
|
+
* The cap's `speed` is normalized 0..1 and each provider maps it down --
|
|
31163
|
+
* Baichuan 1..63, Dahua 1..8, ISAPI's percentage -100..100. The normalized
|
|
31164
|
+
* value hides how coarse that really is: asking an amcrest for `0.37` is
|
|
31165
|
+
* meaningless, because it has eight steps and three of them round to the
|
|
31166
|
+
* same one. A loop that tunes its own gain has to know the granularity of
|
|
31167
|
+
* the knob it is turning.
|
|
31168
|
+
*
|
|
31169
|
+
* `null` = the driver does not know, or the scale is continuous (ONVIF takes
|
|
31170
|
+
* a float). Never a made-up number.
|
|
31171
|
+
*/
|
|
31172
|
+
speedSteps: number().int().positive().nullable(),
|
|
31173
|
+
/**
|
|
31174
|
+
* How long ONE `move` pulse runs on this driver, ms.
|
|
31175
|
+
*
|
|
31176
|
+
* `move` is a self-terminating burst everywhere, but every provider hardcoded
|
|
31177
|
+
* its own duration in private -- 200 reolink, 350 amcrest, 500 hikvision,
|
|
31178
|
+
* 1000 onvif -- so no caller could read it. It is the DENOMINATOR of "how far
|
|
31179
|
+
* did the head travel per pulse": without it a measured displacement has no
|
|
31180
|
+
* gain to be divided into.
|
|
31181
|
+
*
|
|
31182
|
+
* `null` = the driver cannot say.
|
|
31183
|
+
*/
|
|
31184
|
+
moveImpulseMs: number().int().positive().nullable()
|
|
31185
|
+
});
|
|
31186
|
+
/**
|
|
31187
|
+
* Which preset `goHome` goes to, and WHO decided.
|
|
31188
|
+
*
|
|
31189
|
+
* `goHome` used to be three vendor "conventions" written in three comments --
|
|
31190
|
+
* Reolink preset 0, Hikvision preset '1', Dahua preset 1 -- and on this fleet
|
|
31191
|
+
* not one of the three cameras honours its own:
|
|
31192
|
+
*
|
|
31193
|
+
* 592 Videocamera camera Daniel (reolink) preset 0 holds "stanza"
|
|
31194
|
+
* 1438 Videocamera dispensa (hikvision) preset 1 holds "Credenza"
|
|
31195
|
+
* 3836 Videocamera studio (amcrest) preset 1 holds "Preset1"
|
|
31196
|
+
*
|
|
31197
|
+
* So `goHome` meant "go wherever the operator happened to save in slot 0 or 1",
|
|
31198
|
+
* and two of the three swallowed the failure in a bare `catch`. For autotrack
|
|
31199
|
+
* this is the HOTTEST path -- it runs every time a subject is released -- so it
|
|
31200
|
+
* needs an answer that is true per camera and audible when it is missing.
|
|
31201
|
+
*
|
|
31202
|
+
* `source` is what makes it honest:
|
|
31203
|
+
* - `operator` -- picked in the UI, stored by the provider.
|
|
31204
|
+
* - `firmware` -- the camera itself named a preset `fn: 'home'` (Hikvision's
|
|
31205
|
+
* "Back to origin"), adopted as the default with nothing to configure.
|
|
31206
|
+
* - `native` -- the driver homes WITHOUT a preset at all. ONVIF does:
|
|
31207
|
+
* `goHome` is `ptzAbsoluteMove({x:0, y:0, zoom:0})`. `presetId` is null and
|
|
31208
|
+
* that is not a failure -- there is nothing to pick, and the UI must offer
|
|
31209
|
+
* no picker.
|
|
31210
|
+
* - `none` -- nothing is configured and the camera names nothing.
|
|
31211
|
+
* `goHome` REFUSES rather than moving the head somewhere arbitrary.
|
|
31212
|
+
*
|
|
31213
|
+
* `source === 'none'` is the refusal condition, NOT `presetId === null`: the
|
|
31214
|
+
* native case has no preset and homes perfectly well.
|
|
31215
|
+
*/
|
|
31216
|
+
var PtzHomePresetSchema = object({
|
|
31217
|
+
presetId: string().nullable(),
|
|
31218
|
+
source: _enum([
|
|
31219
|
+
"operator",
|
|
31220
|
+
"firmware",
|
|
31221
|
+
"native",
|
|
31222
|
+
"none"
|
|
31223
|
+
])
|
|
31092
31224
|
});
|
|
31093
31225
|
var ptzCapability = {
|
|
31094
31226
|
name: "ptz",
|
|
@@ -31129,7 +31261,66 @@ var ptzCapability = {
|
|
|
31129
31261
|
auth: "admin"
|
|
31130
31262
|
}),
|
|
31131
31263
|
getOptions: method(object({ deviceId: number() }), PtzOptionsSchema),
|
|
31264
|
+
/**
|
|
31265
|
+
* Move to the camera's configured home preset. THROWS when
|
|
31266
|
+
* `getHomePreset()` resolves to `none` -- a head that did not move must
|
|
31267
|
+
* never look like a head that went home.
|
|
31268
|
+
*/
|
|
31132
31269
|
goHome: method(object({ deviceId: number() }), _void(), { kind: "mutation" }),
|
|
31270
|
+
/** Which preset `goHome` targets, and who decided it. */
|
|
31271
|
+
getHomePreset: method(object({ deviceId: number() }), PtzHomePresetSchema),
|
|
31272
|
+
/**
|
|
31273
|
+
* Pin the home preset for this camera. `null` clears the operator's choice
|
|
31274
|
+
* and falls back to whatever the firmware names, or to `none`.
|
|
31275
|
+
*/
|
|
31276
|
+
setHomePreset: method(object({
|
|
31277
|
+
deviceId: number(),
|
|
31278
|
+
presetId: string().nullable()
|
|
31279
|
+
}), _void(), {
|
|
31280
|
+
kind: "mutation",
|
|
31281
|
+
auth: "admin"
|
|
31282
|
+
}),
|
|
31283
|
+
/**
|
|
31284
|
+
* Make WHERE THE HEAD IS POINTING RIGHT NOW this camera's home.
|
|
31285
|
+
*
|
|
31286
|
+
* `setHomePreset` takes a preset id, and a preset id is the wrong shape for
|
|
31287
|
+
* the most natural thing an operator does: aim the camera, then say "here".
|
|
31288
|
+
* It is also the only shape a camera whose home is NOT a preset can accept
|
|
31289
|
+
* -- Reolink's home is a guard point, a stored position with no id at all,
|
|
31290
|
+
* and hiding the control for that case (as the UI briefly did) left the one
|
|
31291
|
+
* vendor with a native home unable to set it.
|
|
31292
|
+
*
|
|
31293
|
+
* Each provider does it its own way:
|
|
31294
|
+
* reolink `setGrd` with `needSetPos`, pinning the guard point here
|
|
31295
|
+
* hikvision / amcrest save a preset, then pin it as home
|
|
31296
|
+
* onvif REFUSES -- its home is the PTZ origin and cannot be moved
|
|
31297
|
+
*
|
|
31298
|
+
* A provider that cannot honour it throws rather than silently doing
|
|
31299
|
+
* nothing: an operator who pressed "set home here" and saw no error would
|
|
31300
|
+
* believe it took.
|
|
31301
|
+
*/
|
|
31302
|
+
captureHomeHere: method(object({ deviceId: number() }), _void(), {
|
|
31303
|
+
kind: "mutation",
|
|
31304
|
+
auth: "admin"
|
|
31305
|
+
}),
|
|
31306
|
+
/**
|
|
31307
|
+
* How long the camera waits, idle, before returning home on its own --
|
|
31308
|
+
* `null` when it has no such behaviour.
|
|
31309
|
+
*
|
|
31310
|
+
* Reolink's guard point carries one (`timeout`, freely settable in
|
|
31311
|
+
* seconds). It is a real function of the camera that we did not expose at
|
|
31312
|
+
* all, and it belongs next to the home it governs rather than in a vendor
|
|
31313
|
+
* settings page nobody associates with Home.
|
|
31314
|
+
*/
|
|
31315
|
+
getHomeReturnSeconds: method(object({ deviceId: number() }), number().int().nullable()),
|
|
31316
|
+
/** Set the idle-return delay. Refused by a camera that has no such timer. */
|
|
31317
|
+
setHomeReturnSeconds: method(object({
|
|
31318
|
+
deviceId: number(),
|
|
31319
|
+
seconds: number().int().min(0).max(3600)
|
|
31320
|
+
}), _void(), {
|
|
31321
|
+
kind: "mutation",
|
|
31322
|
+
auth: "admin"
|
|
31323
|
+
}),
|
|
31133
31324
|
/**
|
|
31134
31325
|
* Pull the current PTZ position. Redundant with the auto-injected
|
|
31135
31326
|
* `getStatus` method (see `status` below); kept for callers that
|
|
@@ -40791,6 +40982,12 @@ Object.freeze({
|
|
|
40791
40982
|
addonId: null,
|
|
40792
40983
|
access: "create"
|
|
40793
40984
|
},
|
|
40985
|
+
"ptz.captureHomeHere": {
|
|
40986
|
+
capName: "ptz",
|
|
40987
|
+
capScope: "device",
|
|
40988
|
+
addonId: null,
|
|
40989
|
+
access: "create"
|
|
40990
|
+
},
|
|
40794
40991
|
"ptz.continuousMove": {
|
|
40795
40992
|
capName: "ptz",
|
|
40796
40993
|
capScope: "device",
|
|
@@ -40803,6 +41000,18 @@ Object.freeze({
|
|
|
40803
41000
|
addonId: null,
|
|
40804
41001
|
access: "delete"
|
|
40805
41002
|
},
|
|
41003
|
+
"ptz.getHomePreset": {
|
|
41004
|
+
capName: "ptz",
|
|
41005
|
+
capScope: "device",
|
|
41006
|
+
addonId: null,
|
|
41007
|
+
access: "view"
|
|
41008
|
+
},
|
|
41009
|
+
"ptz.getHomeReturnSeconds": {
|
|
41010
|
+
capName: "ptz",
|
|
41011
|
+
capScope: "device",
|
|
41012
|
+
addonId: null,
|
|
41013
|
+
access: "view"
|
|
41014
|
+
},
|
|
40806
41015
|
"ptz.getOptions": {
|
|
40807
41016
|
capName: "ptz",
|
|
40808
41017
|
capScope: "device",
|
|
@@ -40851,6 +41060,18 @@ Object.freeze({
|
|
|
40851
41060
|
addonId: null,
|
|
40852
41061
|
access: "create"
|
|
40853
41062
|
},
|
|
41063
|
+
"ptz.setHomePreset": {
|
|
41064
|
+
capName: "ptz",
|
|
41065
|
+
capScope: "device",
|
|
41066
|
+
addonId: null,
|
|
41067
|
+
access: "create"
|
|
41068
|
+
},
|
|
41069
|
+
"ptz.setHomeReturnSeconds": {
|
|
41070
|
+
capName: "ptz",
|
|
41071
|
+
capScope: "device",
|
|
41072
|
+
addonId: null,
|
|
41073
|
+
access: "create"
|
|
41074
|
+
},
|
|
40854
41075
|
"ptz.stop": {
|
|
40855
41076
|
capName: "ptz",
|
|
40856
41077
|
capScope: "device",
|
|
@@ -44014,6 +44235,11 @@ Object.freeze({
|
|
|
44014
44235
|
form: "single",
|
|
44015
44236
|
optional: false
|
|
44016
44237
|
}],
|
|
44238
|
+
"ptz.captureHomeHere": [{
|
|
44239
|
+
name: "deviceId",
|
|
44240
|
+
form: "single",
|
|
44241
|
+
optional: false
|
|
44242
|
+
}],
|
|
44017
44243
|
"ptz.continuousMove": [{
|
|
44018
44244
|
name: "deviceId",
|
|
44019
44245
|
form: "single",
|
|
@@ -44024,6 +44250,16 @@ Object.freeze({
|
|
|
44024
44250
|
form: "single",
|
|
44025
44251
|
optional: false
|
|
44026
44252
|
}],
|
|
44253
|
+
"ptz.getHomePreset": [{
|
|
44254
|
+
name: "deviceId",
|
|
44255
|
+
form: "single",
|
|
44256
|
+
optional: false
|
|
44257
|
+
}],
|
|
44258
|
+
"ptz.getHomeReturnSeconds": [{
|
|
44259
|
+
name: "deviceId",
|
|
44260
|
+
form: "single",
|
|
44261
|
+
optional: false
|
|
44262
|
+
}],
|
|
44027
44263
|
"ptz.getOptions": [{
|
|
44028
44264
|
name: "deviceId",
|
|
44029
44265
|
form: "single",
|
|
@@ -44064,6 +44300,16 @@ Object.freeze({
|
|
|
44064
44300
|
form: "single",
|
|
44065
44301
|
optional: false
|
|
44066
44302
|
}],
|
|
44303
|
+
"ptz.setHomePreset": [{
|
|
44304
|
+
name: "deviceId",
|
|
44305
|
+
form: "single",
|
|
44306
|
+
optional: false
|
|
44307
|
+
}],
|
|
44308
|
+
"ptz.setHomeReturnSeconds": [{
|
|
44309
|
+
name: "deviceId",
|
|
44310
|
+
form: "single",
|
|
44311
|
+
optional: false
|
|
44312
|
+
}],
|
|
44067
44313
|
"ptz.stop": [{
|
|
44068
44314
|
name: "deviceId",
|
|
44069
44315
|
form: "single",
|
|
@@ -46139,6 +46385,17 @@ var amcrestDeviceCacheSchema = object({
|
|
|
46139
46385
|
* carries only the technical connection knobs.
|
|
46140
46386
|
*/
|
|
46141
46387
|
var amcrestCameraSchema = object({
|
|
46388
|
+
/**
|
|
46389
|
+
* Which preset `ptz.goHome` targets on THIS camera.
|
|
46390
|
+
*
|
|
46391
|
+
* Empty = nothing configured, and `goHome` then REFUSES. It used to go to a
|
|
46392
|
+
* hardcoded slot on a vendor "convention" this fleet contradicts: reolink
|
|
46393
|
+
* went to preset 0, which on device 592 holds "stanza"; amcrest went to
|
|
46394
|
+
* preset 1, which on device 3836 holds "Preset1". Both swallowed the failure
|
|
46395
|
+
* in a bare catch, so a head that never moved was indistinguishable from a
|
|
46396
|
+
* head that went home.
|
|
46397
|
+
*/
|
|
46398
|
+
homePresetId: string().default("").describe("Preset id ptz.goHome targets (empty = goHome refuses)"),
|
|
46142
46399
|
host: string().describe("Camera IP / hostname"),
|
|
46143
46400
|
port: number().int().min(1).max(65535).default(80).describe("HTTP CGI port"),
|
|
46144
46401
|
https: boolean().default(false).describe("Use HTTPS for CGI"),
|
|
@@ -46246,6 +46503,24 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
46246
46503
|
get channel() {
|
|
46247
46504
|
return this.config.get("channel") ?? 1;
|
|
46248
46505
|
}
|
|
46506
|
+
/**
|
|
46507
|
+
* Which preset `goHome` targets, and who decided it.
|
|
46508
|
+
*
|
|
46509
|
+
* Dahua names no home of its own that we have probed, so the operator's pick
|
|
46510
|
+
* is the only answer. With none, `none` -- and `goHome` refuses rather than
|
|
46511
|
+
* moving the head to whatever happens to sit in slot 1.
|
|
46512
|
+
*/
|
|
46513
|
+
async resolveHomePreset() {
|
|
46514
|
+
const chosen = this.config.get("homePresetId") ?? "";
|
|
46515
|
+
if (chosen === "") return {
|
|
46516
|
+
presetId: null,
|
|
46517
|
+
source: "none"
|
|
46518
|
+
};
|
|
46519
|
+
return {
|
|
46520
|
+
presetId: chosen,
|
|
46521
|
+
source: "operator"
|
|
46522
|
+
};
|
|
46523
|
+
}
|
|
46249
46524
|
ensureClient() {
|
|
46250
46525
|
if (this.client) return this.client;
|
|
46251
46526
|
this.client = this.buildClient();
|
|
@@ -46751,6 +47026,21 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
46751
47026
|
* auto-stop fires. The `usePTZ` UI hook already issues its own paired
|
|
46752
47027
|
* `stop` after a client-side pulse window; this timer is a backstop so
|
|
46753
47028
|
* a lone `move` (no following `stop`) can't leave the head gliding. */
|
|
47029
|
+
/**
|
|
47030
|
+
* Dahua's native PTZ speed scale is 1..8; `resolvePtzCodes` maps the cap's
|
|
47031
|
+
* normalized 0..1 onto it. Eight steps is COARSE -- three neighbouring
|
|
47032
|
+
* normalized requests round to the same one -- which is exactly why the
|
|
47033
|
+
* granularity is reported through `getOptions` instead of being hidden
|
|
47034
|
+
* behind a 0..1 float.
|
|
47035
|
+
*/
|
|
47036
|
+
/** Dahua exposes 25 fixed preset slots, 1-based. */
|
|
47037
|
+
static DAHUA_MAX_PRESETS = 25;
|
|
47038
|
+
static DAHUA_SPEED_STEPS = 8;
|
|
47039
|
+
/**
|
|
47040
|
+
* How long one `ptz.move` pulse runs, ms. The DENOMINATOR of "how far did
|
|
47041
|
+
* the head travel per pulse", so it leaves the provider through
|
|
47042
|
+
* `getOptions` rather than staying a private literal.
|
|
47043
|
+
*/
|
|
46754
47044
|
static PTZ_PULSE_MS = 350;
|
|
46755
47045
|
/** Short per-call deadline (ms) for the PTZ start/stop CGI so a slow or
|
|
46756
47046
|
* blocked camera round-trip fails fast instead of exceeding the
|
|
@@ -46876,7 +47166,9 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
46876
47166
|
if (deviceId !== this.id) return [];
|
|
46877
47167
|
return (await this.ensureClient().getPtzPresets(this.channel)).map((p) => ({
|
|
46878
47168
|
id: String(p.id),
|
|
46879
|
-
name: p.name
|
|
47169
|
+
name: p.name,
|
|
47170
|
+
fn: null,
|
|
47171
|
+
writable: true
|
|
46880
47172
|
}));
|
|
46881
47173
|
},
|
|
46882
47174
|
goToPreset: async ({ deviceId, presetId }) => {
|
|
@@ -46912,7 +47204,9 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
46912
47204
|
hasTilt: false,
|
|
46913
47205
|
hasZoom: false,
|
|
46914
47206
|
supportsPresets: false,
|
|
46915
|
-
hasAutofocus: false
|
|
47207
|
+
hasAutofocus: false,
|
|
47208
|
+
speedSteps: AmcrestCamera.DAHUA_SPEED_STEPS,
|
|
47209
|
+
moveImpulseMs: AmcrestCamera.PTZ_PULSE_MS
|
|
46916
47210
|
};
|
|
46917
47211
|
if (deviceId !== this.id) return cold;
|
|
46918
47212
|
const caps = await this.ensureClient().getPtzCaps(this.channel);
|
|
@@ -46923,26 +47217,64 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
46923
47217
|
hasZoom: caps.hasZoom,
|
|
46924
47218
|
supportsPresets: caps.supportsPreset,
|
|
46925
47219
|
hasAutofocus: false,
|
|
47220
|
+
speedSteps: AmcrestCamera.DAHUA_SPEED_STEPS,
|
|
47221
|
+
moveImpulseMs: AmcrestCamera.PTZ_PULSE_MS,
|
|
46926
47222
|
...caps.maxPresets !== null ? { maxPresets: caps.maxPresets } : {}
|
|
46927
47223
|
};
|
|
46928
47224
|
},
|
|
46929
47225
|
goHome: async ({ deviceId }) => {
|
|
46930
47226
|
if (deviceId !== this.id) return;
|
|
47227
|
+
const home = await this.resolveHomePreset();
|
|
47228
|
+
if (home.source === "none" || home.presetId === null) throw new Error("No home preset configured for this camera — set one in the PTZ settings");
|
|
47229
|
+
const id = Number.parseInt(home.presetId, 10);
|
|
47230
|
+
if (!Number.isFinite(id) || id < 1) throw new Error(`Configured home preset is not a Dahua preset id: ${home.presetId}`);
|
|
46931
47231
|
const client = this.ensureClient();
|
|
46932
|
-
this.enqueuePtz("goHome", () => client.ptzGotoPreset(this.channel,
|
|
47232
|
+
this.enqueuePtz("goHome", () => client.ptzGotoPreset(this.channel, id, AmcrestCamera.PTZ_CGI_TIMEOUT_MS));
|
|
47233
|
+
},
|
|
47234
|
+
getHomePreset: async ({ deviceId }) => {
|
|
47235
|
+
if (deviceId !== this.id) return {
|
|
47236
|
+
presetId: null,
|
|
47237
|
+
source: "none"
|
|
47238
|
+
};
|
|
47239
|
+
return this.resolveHomePreset();
|
|
47240
|
+
},
|
|
47241
|
+
captureHomeHere: async ({ deviceId }) => {
|
|
47242
|
+
if (deviceId !== this.id) return;
|
|
47243
|
+
const client = this.ensureClient();
|
|
47244
|
+
const ceiling = (await client.getPtzCaps(this.channel))?.maxPresets ?? AmcrestCamera.DAHUA_MAX_PRESETS;
|
|
47245
|
+
const used = new Set((await client.getPtzPresets(this.channel)).map((p) => p.id));
|
|
47246
|
+
let slot = 1;
|
|
47247
|
+
while (used.has(slot) && slot <= ceiling) slot += 1;
|
|
47248
|
+
if (slot > ceiling) throw new Error("No free preset slot on this camera to store a home position — free one first");
|
|
47249
|
+
await client.ptzSetPreset(this.channel, slot, AmcrestCamera.PTZ_CGI_TIMEOUT_MS);
|
|
47250
|
+
await this.config.setAll({ homePresetId: String(slot) });
|
|
47251
|
+
this.ctx.logger.info("amcrest: home pinned to the current position", {
|
|
47252
|
+
tags: { deviceId: this.id },
|
|
47253
|
+
meta: { presetId: slot }
|
|
47254
|
+
});
|
|
47255
|
+
},
|
|
47256
|
+
getHomeReturnSeconds: async () => {
|
|
47257
|
+
return null;
|
|
47258
|
+
},
|
|
47259
|
+
setHomeReturnSeconds: async () => {
|
|
47260
|
+
throw new Error("Amcrest: the idle-return timer is not wired on this driver");
|
|
47261
|
+
},
|
|
47262
|
+
setHomePreset: async ({ deviceId, presetId }) => {
|
|
47263
|
+
if (deviceId !== this.id) return;
|
|
47264
|
+
await this.config.setAll({ homePresetId: presetId ?? "" });
|
|
46933
47265
|
},
|
|
46934
47266
|
getPosition: async () => {
|
|
46935
47267
|
return {
|
|
46936
|
-
pan:
|
|
46937
|
-
tilt:
|
|
46938
|
-
zoom:
|
|
47268
|
+
pan: null,
|
|
47269
|
+
tilt: null,
|
|
47270
|
+
zoom: null
|
|
46939
47271
|
};
|
|
46940
47272
|
},
|
|
46941
47273
|
getStatus: async () => {
|
|
46942
47274
|
return {
|
|
46943
|
-
pan:
|
|
46944
|
-
tilt:
|
|
46945
|
-
zoom:
|
|
47275
|
+
pan: null,
|
|
47276
|
+
tilt: null,
|
|
47277
|
+
zoom: null,
|
|
46946
47278
|
autofocus: false
|
|
46947
47279
|
};
|
|
46948
47280
|
},
|
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).
|
|
@@ -31060,14 +31060,81 @@ function summarisePrivacyAudio(profiles) {
|
|
|
31060
31060
|
if (profiles.length === 0) return null;
|
|
31061
31061
|
return profiles.some((p) => p.audioEnabled);
|
|
31062
31062
|
}
|
|
31063
|
+
/**
|
|
31064
|
+
* What a preset id DOES on this camera.
|
|
31065
|
+
*
|
|
31066
|
+
* On several PTZ firmwares the preset namespace is shared: low ids are stored
|
|
31067
|
+
* positions, a reserved band triggers camera FUNCTIONS. The dispensa camera
|
|
31068
|
+
* (Hikvision) returns its own reserved band in the preset list, named by the
|
|
31069
|
+
* firmware:
|
|
31070
|
+
*
|
|
31071
|
+
* 34 Back to origin · 39 Day mode · 40 Night mode · 46 Day/Night Auto Mode
|
|
31072
|
+
* 92 Set manual limits · 93 Save manual limits · 94 Remote reboot
|
|
31073
|
+
* 95 Call OSD menu
|
|
31074
|
+
*
|
|
31075
|
+
* We used to hand all of these to the UI as ordinary presets. Saving over one
|
|
31076
|
+
* is what produced `PUT /ISAPI/PTZCtrl/channels/1/presets/34 -> HTTP 500`: the
|
|
31077
|
+
* camera refusing to let "Back to origin" be overwritten, surfaced to the
|
|
31078
|
+
* operator as a transport failure.
|
|
31079
|
+
*
|
|
31080
|
+
* A function preset stays fully usable as a DESTINATION -- `goToPreset('34')`
|
|
31081
|
+
* is the single most useful thing that camera can do. Only `savePreset` and
|
|
31082
|
+
* `deletePreset` are gated.
|
|
31083
|
+
*/
|
|
31084
|
+
var PtzPresetFunctionSchema = _enum([
|
|
31085
|
+
"home",
|
|
31086
|
+
"day-mode",
|
|
31087
|
+
"night-mode",
|
|
31088
|
+
"day-night-auto",
|
|
31089
|
+
"set-limits",
|
|
31090
|
+
"save-limits",
|
|
31091
|
+
"reboot",
|
|
31092
|
+
"osd-menu"
|
|
31093
|
+
]);
|
|
31063
31094
|
var PtzPresetSchema = object({
|
|
31064
31095
|
id: string(),
|
|
31065
|
-
name: string()
|
|
31096
|
+
name: string(),
|
|
31097
|
+
/**
|
|
31098
|
+
* The firmware function this id performs, when the provider KNOWS the id is
|
|
31099
|
+
* reserved. `null` = an ordinary stored position, OR a reserved id this
|
|
31100
|
+
* provider does not recognise -- the two are told apart by `writable`.
|
|
31101
|
+
*/
|
|
31102
|
+
fn: PtzPresetFunctionSchema.nullable(),
|
|
31103
|
+
/**
|
|
31104
|
+
* Whether `savePreset` / `deletePreset` may target this id. A provider sets
|
|
31105
|
+
* it false only for a band it KNOWS is reserved; where the firmware is not
|
|
31106
|
+
* documented it stays true and a refusal comes from the camera, named.
|
|
31107
|
+
*/
|
|
31108
|
+
writable: boolean()
|
|
31066
31109
|
});
|
|
31110
|
+
/**
|
|
31111
|
+
* Where the head is pointing -- per axis, and `null` when the driver cannot
|
|
31112
|
+
* read it.
|
|
31113
|
+
*
|
|
31114
|
+
* It used to be three REQUIRED numbers, which left a provider that cannot read
|
|
31115
|
+
* position no way to say so. All four said `0`:
|
|
31116
|
+
*
|
|
31117
|
+
* reolink `return { pan: 0, tilt: 0, zoom: 0 }` "until a position-query
|
|
31118
|
+
* path lands upstream"
|
|
31119
|
+
* hikvision stub, "firmware-dependent and frequently absent"
|
|
31120
|
+
* amcrest stub, "no generic absolute-position read for this model family"
|
|
31121
|
+
* onvif throws
|
|
31122
|
+
*
|
|
31123
|
+
* So every consumer asking where a camera points was told "perfectly centred"
|
|
31124
|
+
* by four cameras that had never looked. That is D393 -- a measurement that
|
|
31125
|
+
* FAILED is `null`, never `0`, and the type says so all the way to the
|
|
31126
|
+
* decision -- the same shape as the unreadable `statfs` folded into "0 bytes of
|
|
31127
|
+
* headroom", which evacuated a healthy disk.
|
|
31128
|
+
*
|
|
31129
|
+
* D393 also says to check for a narrower structural TWIN of the result type.
|
|
31130
|
+
* There is one: `PtzStatusSchema` extends this, so the lie had already
|
|
31131
|
+
* propagated into `getStatus`, which is the surface `getPosition`'s own comment
|
|
31132
|
+
* tells callers to migrate to.
|
|
31133
|
+
*/
|
|
31067
31134
|
var PtzPositionSchema = object({
|
|
31068
|
-
pan: number(),
|
|
31069
|
-
tilt: number(),
|
|
31070
|
-
zoom: number()
|
|
31135
|
+
pan: number().nullable(),
|
|
31136
|
+
tilt: number().nullable(),
|
|
31137
|
+
zoom: number().nullable()
|
|
31071
31138
|
});
|
|
31072
31139
|
var PtzMoveCommandSchema = object({
|
|
31073
31140
|
pan: number().optional(),
|
|
@@ -31089,7 +31156,72 @@ var PtzOptionsSchema = object({
|
|
|
31089
31156
|
maxPresets: number().optional(),
|
|
31090
31157
|
/** Whether the camera exposes a controllable autofocus toggle
|
|
31091
31158
|
* (boolean `hasX` per the getOptions availability convention). */
|
|
31092
|
-
hasAutofocus: boolean()
|
|
31159
|
+
hasAutofocus: boolean(),
|
|
31160
|
+
/**
|
|
31161
|
+
* How many distinct speeds this camera's NATIVE scale offers.
|
|
31162
|
+
*
|
|
31163
|
+
* The cap's `speed` is normalized 0..1 and each provider maps it down --
|
|
31164
|
+
* Baichuan 1..63, Dahua 1..8, ISAPI's percentage -100..100. The normalized
|
|
31165
|
+
* value hides how coarse that really is: asking an amcrest for `0.37` is
|
|
31166
|
+
* meaningless, because it has eight steps and three of them round to the
|
|
31167
|
+
* same one. A loop that tunes its own gain has to know the granularity of
|
|
31168
|
+
* the knob it is turning.
|
|
31169
|
+
*
|
|
31170
|
+
* `null` = the driver does not know, or the scale is continuous (ONVIF takes
|
|
31171
|
+
* a float). Never a made-up number.
|
|
31172
|
+
*/
|
|
31173
|
+
speedSteps: number().int().positive().nullable(),
|
|
31174
|
+
/**
|
|
31175
|
+
* How long ONE `move` pulse runs on this driver, ms.
|
|
31176
|
+
*
|
|
31177
|
+
* `move` is a self-terminating burst everywhere, but every provider hardcoded
|
|
31178
|
+
* its own duration in private -- 200 reolink, 350 amcrest, 500 hikvision,
|
|
31179
|
+
* 1000 onvif -- so no caller could read it. It is the DENOMINATOR of "how far
|
|
31180
|
+
* did the head travel per pulse": without it a measured displacement has no
|
|
31181
|
+
* gain to be divided into.
|
|
31182
|
+
*
|
|
31183
|
+
* `null` = the driver cannot say.
|
|
31184
|
+
*/
|
|
31185
|
+
moveImpulseMs: number().int().positive().nullable()
|
|
31186
|
+
});
|
|
31187
|
+
/**
|
|
31188
|
+
* Which preset `goHome` goes to, and WHO decided.
|
|
31189
|
+
*
|
|
31190
|
+
* `goHome` used to be three vendor "conventions" written in three comments --
|
|
31191
|
+
* Reolink preset 0, Hikvision preset '1', Dahua preset 1 -- and on this fleet
|
|
31192
|
+
* not one of the three cameras honours its own:
|
|
31193
|
+
*
|
|
31194
|
+
* 592 Videocamera camera Daniel (reolink) preset 0 holds "stanza"
|
|
31195
|
+
* 1438 Videocamera dispensa (hikvision) preset 1 holds "Credenza"
|
|
31196
|
+
* 3836 Videocamera studio (amcrest) preset 1 holds "Preset1"
|
|
31197
|
+
*
|
|
31198
|
+
* So `goHome` meant "go wherever the operator happened to save in slot 0 or 1",
|
|
31199
|
+
* and two of the three swallowed the failure in a bare `catch`. For autotrack
|
|
31200
|
+
* this is the HOTTEST path -- it runs every time a subject is released -- so it
|
|
31201
|
+
* needs an answer that is true per camera and audible when it is missing.
|
|
31202
|
+
*
|
|
31203
|
+
* `source` is what makes it honest:
|
|
31204
|
+
* - `operator` -- picked in the UI, stored by the provider.
|
|
31205
|
+
* - `firmware` -- the camera itself named a preset `fn: 'home'` (Hikvision's
|
|
31206
|
+
* "Back to origin"), adopted as the default with nothing to configure.
|
|
31207
|
+
* - `native` -- the driver homes WITHOUT a preset at all. ONVIF does:
|
|
31208
|
+
* `goHome` is `ptzAbsoluteMove({x:0, y:0, zoom:0})`. `presetId` is null and
|
|
31209
|
+
* that is not a failure -- there is nothing to pick, and the UI must offer
|
|
31210
|
+
* no picker.
|
|
31211
|
+
* - `none` -- nothing is configured and the camera names nothing.
|
|
31212
|
+
* `goHome` REFUSES rather than moving the head somewhere arbitrary.
|
|
31213
|
+
*
|
|
31214
|
+
* `source === 'none'` is the refusal condition, NOT `presetId === null`: the
|
|
31215
|
+
* native case has no preset and homes perfectly well.
|
|
31216
|
+
*/
|
|
31217
|
+
var PtzHomePresetSchema = object({
|
|
31218
|
+
presetId: string().nullable(),
|
|
31219
|
+
source: _enum([
|
|
31220
|
+
"operator",
|
|
31221
|
+
"firmware",
|
|
31222
|
+
"native",
|
|
31223
|
+
"none"
|
|
31224
|
+
])
|
|
31093
31225
|
});
|
|
31094
31226
|
var ptzCapability = {
|
|
31095
31227
|
name: "ptz",
|
|
@@ -31130,7 +31262,66 @@ var ptzCapability = {
|
|
|
31130
31262
|
auth: "admin"
|
|
31131
31263
|
}),
|
|
31132
31264
|
getOptions: method(object({ deviceId: number() }), PtzOptionsSchema),
|
|
31265
|
+
/**
|
|
31266
|
+
* Move to the camera's configured home preset. THROWS when
|
|
31267
|
+
* `getHomePreset()` resolves to `none` -- a head that did not move must
|
|
31268
|
+
* never look like a head that went home.
|
|
31269
|
+
*/
|
|
31133
31270
|
goHome: method(object({ deviceId: number() }), _void(), { kind: "mutation" }),
|
|
31271
|
+
/** Which preset `goHome` targets, and who decided it. */
|
|
31272
|
+
getHomePreset: method(object({ deviceId: number() }), PtzHomePresetSchema),
|
|
31273
|
+
/**
|
|
31274
|
+
* Pin the home preset for this camera. `null` clears the operator's choice
|
|
31275
|
+
* and falls back to whatever the firmware names, or to `none`.
|
|
31276
|
+
*/
|
|
31277
|
+
setHomePreset: method(object({
|
|
31278
|
+
deviceId: number(),
|
|
31279
|
+
presetId: string().nullable()
|
|
31280
|
+
}), _void(), {
|
|
31281
|
+
kind: "mutation",
|
|
31282
|
+
auth: "admin"
|
|
31283
|
+
}),
|
|
31284
|
+
/**
|
|
31285
|
+
* Make WHERE THE HEAD IS POINTING RIGHT NOW this camera's home.
|
|
31286
|
+
*
|
|
31287
|
+
* `setHomePreset` takes a preset id, and a preset id is the wrong shape for
|
|
31288
|
+
* the most natural thing an operator does: aim the camera, then say "here".
|
|
31289
|
+
* It is also the only shape a camera whose home is NOT a preset can accept
|
|
31290
|
+
* -- Reolink's home is a guard point, a stored position with no id at all,
|
|
31291
|
+
* and hiding the control for that case (as the UI briefly did) left the one
|
|
31292
|
+
* vendor with a native home unable to set it.
|
|
31293
|
+
*
|
|
31294
|
+
* Each provider does it its own way:
|
|
31295
|
+
* reolink `setGrd` with `needSetPos`, pinning the guard point here
|
|
31296
|
+
* hikvision / amcrest save a preset, then pin it as home
|
|
31297
|
+
* onvif REFUSES -- its home is the PTZ origin and cannot be moved
|
|
31298
|
+
*
|
|
31299
|
+
* A provider that cannot honour it throws rather than silently doing
|
|
31300
|
+
* nothing: an operator who pressed "set home here" and saw no error would
|
|
31301
|
+
* believe it took.
|
|
31302
|
+
*/
|
|
31303
|
+
captureHomeHere: method(object({ deviceId: number() }), _void(), {
|
|
31304
|
+
kind: "mutation",
|
|
31305
|
+
auth: "admin"
|
|
31306
|
+
}),
|
|
31307
|
+
/**
|
|
31308
|
+
* How long the camera waits, idle, before returning home on its own --
|
|
31309
|
+
* `null` when it has no such behaviour.
|
|
31310
|
+
*
|
|
31311
|
+
* Reolink's guard point carries one (`timeout`, freely settable in
|
|
31312
|
+
* seconds). It is a real function of the camera that we did not expose at
|
|
31313
|
+
* all, and it belongs next to the home it governs rather than in a vendor
|
|
31314
|
+
* settings page nobody associates with Home.
|
|
31315
|
+
*/
|
|
31316
|
+
getHomeReturnSeconds: method(object({ deviceId: number() }), number().int().nullable()),
|
|
31317
|
+
/** Set the idle-return delay. Refused by a camera that has no such timer. */
|
|
31318
|
+
setHomeReturnSeconds: method(object({
|
|
31319
|
+
deviceId: number(),
|
|
31320
|
+
seconds: number().int().min(0).max(3600)
|
|
31321
|
+
}), _void(), {
|
|
31322
|
+
kind: "mutation",
|
|
31323
|
+
auth: "admin"
|
|
31324
|
+
}),
|
|
31134
31325
|
/**
|
|
31135
31326
|
* Pull the current PTZ position. Redundant with the auto-injected
|
|
31136
31327
|
* `getStatus` method (see `status` below); kept for callers that
|
|
@@ -40792,6 +40983,12 @@ Object.freeze({
|
|
|
40792
40983
|
addonId: null,
|
|
40793
40984
|
access: "create"
|
|
40794
40985
|
},
|
|
40986
|
+
"ptz.captureHomeHere": {
|
|
40987
|
+
capName: "ptz",
|
|
40988
|
+
capScope: "device",
|
|
40989
|
+
addonId: null,
|
|
40990
|
+
access: "create"
|
|
40991
|
+
},
|
|
40795
40992
|
"ptz.continuousMove": {
|
|
40796
40993
|
capName: "ptz",
|
|
40797
40994
|
capScope: "device",
|
|
@@ -40804,6 +41001,18 @@ Object.freeze({
|
|
|
40804
41001
|
addonId: null,
|
|
40805
41002
|
access: "delete"
|
|
40806
41003
|
},
|
|
41004
|
+
"ptz.getHomePreset": {
|
|
41005
|
+
capName: "ptz",
|
|
41006
|
+
capScope: "device",
|
|
41007
|
+
addonId: null,
|
|
41008
|
+
access: "view"
|
|
41009
|
+
},
|
|
41010
|
+
"ptz.getHomeReturnSeconds": {
|
|
41011
|
+
capName: "ptz",
|
|
41012
|
+
capScope: "device",
|
|
41013
|
+
addonId: null,
|
|
41014
|
+
access: "view"
|
|
41015
|
+
},
|
|
40807
41016
|
"ptz.getOptions": {
|
|
40808
41017
|
capName: "ptz",
|
|
40809
41018
|
capScope: "device",
|
|
@@ -40852,6 +41061,18 @@ Object.freeze({
|
|
|
40852
41061
|
addonId: null,
|
|
40853
41062
|
access: "create"
|
|
40854
41063
|
},
|
|
41064
|
+
"ptz.setHomePreset": {
|
|
41065
|
+
capName: "ptz",
|
|
41066
|
+
capScope: "device",
|
|
41067
|
+
addonId: null,
|
|
41068
|
+
access: "create"
|
|
41069
|
+
},
|
|
41070
|
+
"ptz.setHomeReturnSeconds": {
|
|
41071
|
+
capName: "ptz",
|
|
41072
|
+
capScope: "device",
|
|
41073
|
+
addonId: null,
|
|
41074
|
+
access: "create"
|
|
41075
|
+
},
|
|
40855
41076
|
"ptz.stop": {
|
|
40856
41077
|
capName: "ptz",
|
|
40857
41078
|
capScope: "device",
|
|
@@ -44015,6 +44236,11 @@ Object.freeze({
|
|
|
44015
44236
|
form: "single",
|
|
44016
44237
|
optional: false
|
|
44017
44238
|
}],
|
|
44239
|
+
"ptz.captureHomeHere": [{
|
|
44240
|
+
name: "deviceId",
|
|
44241
|
+
form: "single",
|
|
44242
|
+
optional: false
|
|
44243
|
+
}],
|
|
44018
44244
|
"ptz.continuousMove": [{
|
|
44019
44245
|
name: "deviceId",
|
|
44020
44246
|
form: "single",
|
|
@@ -44025,6 +44251,16 @@ Object.freeze({
|
|
|
44025
44251
|
form: "single",
|
|
44026
44252
|
optional: false
|
|
44027
44253
|
}],
|
|
44254
|
+
"ptz.getHomePreset": [{
|
|
44255
|
+
name: "deviceId",
|
|
44256
|
+
form: "single",
|
|
44257
|
+
optional: false
|
|
44258
|
+
}],
|
|
44259
|
+
"ptz.getHomeReturnSeconds": [{
|
|
44260
|
+
name: "deviceId",
|
|
44261
|
+
form: "single",
|
|
44262
|
+
optional: false
|
|
44263
|
+
}],
|
|
44028
44264
|
"ptz.getOptions": [{
|
|
44029
44265
|
name: "deviceId",
|
|
44030
44266
|
form: "single",
|
|
@@ -44065,6 +44301,16 @@ Object.freeze({
|
|
|
44065
44301
|
form: "single",
|
|
44066
44302
|
optional: false
|
|
44067
44303
|
}],
|
|
44304
|
+
"ptz.setHomePreset": [{
|
|
44305
|
+
name: "deviceId",
|
|
44306
|
+
form: "single",
|
|
44307
|
+
optional: false
|
|
44308
|
+
}],
|
|
44309
|
+
"ptz.setHomeReturnSeconds": [{
|
|
44310
|
+
name: "deviceId",
|
|
44311
|
+
form: "single",
|
|
44312
|
+
optional: false
|
|
44313
|
+
}],
|
|
44068
44314
|
"ptz.stop": [{
|
|
44069
44315
|
name: "deviceId",
|
|
44070
44316
|
form: "single",
|
|
@@ -46140,6 +46386,17 @@ var amcrestDeviceCacheSchema = object({
|
|
|
46140
46386
|
* carries only the technical connection knobs.
|
|
46141
46387
|
*/
|
|
46142
46388
|
var amcrestCameraSchema = object({
|
|
46389
|
+
/**
|
|
46390
|
+
* Which preset `ptz.goHome` targets on THIS camera.
|
|
46391
|
+
*
|
|
46392
|
+
* Empty = nothing configured, and `goHome` then REFUSES. It used to go to a
|
|
46393
|
+
* hardcoded slot on a vendor "convention" this fleet contradicts: reolink
|
|
46394
|
+
* went to preset 0, which on device 592 holds "stanza"; amcrest went to
|
|
46395
|
+
* preset 1, which on device 3836 holds "Preset1". Both swallowed the failure
|
|
46396
|
+
* in a bare catch, so a head that never moved was indistinguishable from a
|
|
46397
|
+
* head that went home.
|
|
46398
|
+
*/
|
|
46399
|
+
homePresetId: string().default("").describe("Preset id ptz.goHome targets (empty = goHome refuses)"),
|
|
46143
46400
|
host: string().describe("Camera IP / hostname"),
|
|
46144
46401
|
port: number().int().min(1).max(65535).default(80).describe("HTTP CGI port"),
|
|
46145
46402
|
https: boolean().default(false).describe("Use HTTPS for CGI"),
|
|
@@ -46247,6 +46504,24 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
46247
46504
|
get channel() {
|
|
46248
46505
|
return this.config.get("channel") ?? 1;
|
|
46249
46506
|
}
|
|
46507
|
+
/**
|
|
46508
|
+
* Which preset `goHome` targets, and who decided it.
|
|
46509
|
+
*
|
|
46510
|
+
* Dahua names no home of its own that we have probed, so the operator's pick
|
|
46511
|
+
* is the only answer. With none, `none` -- and `goHome` refuses rather than
|
|
46512
|
+
* moving the head to whatever happens to sit in slot 1.
|
|
46513
|
+
*/
|
|
46514
|
+
async resolveHomePreset() {
|
|
46515
|
+
const chosen = this.config.get("homePresetId") ?? "";
|
|
46516
|
+
if (chosen === "") return {
|
|
46517
|
+
presetId: null,
|
|
46518
|
+
source: "none"
|
|
46519
|
+
};
|
|
46520
|
+
return {
|
|
46521
|
+
presetId: chosen,
|
|
46522
|
+
source: "operator"
|
|
46523
|
+
};
|
|
46524
|
+
}
|
|
46250
46525
|
ensureClient() {
|
|
46251
46526
|
if (this.client) return this.client;
|
|
46252
46527
|
this.client = this.buildClient();
|
|
@@ -46752,6 +47027,21 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
46752
47027
|
* auto-stop fires. The `usePTZ` UI hook already issues its own paired
|
|
46753
47028
|
* `stop` after a client-side pulse window; this timer is a backstop so
|
|
46754
47029
|
* a lone `move` (no following `stop`) can't leave the head gliding. */
|
|
47030
|
+
/**
|
|
47031
|
+
* Dahua's native PTZ speed scale is 1..8; `resolvePtzCodes` maps the cap's
|
|
47032
|
+
* normalized 0..1 onto it. Eight steps is COARSE -- three neighbouring
|
|
47033
|
+
* normalized requests round to the same one -- which is exactly why the
|
|
47034
|
+
* granularity is reported through `getOptions` instead of being hidden
|
|
47035
|
+
* behind a 0..1 float.
|
|
47036
|
+
*/
|
|
47037
|
+
/** Dahua exposes 25 fixed preset slots, 1-based. */
|
|
47038
|
+
static DAHUA_MAX_PRESETS = 25;
|
|
47039
|
+
static DAHUA_SPEED_STEPS = 8;
|
|
47040
|
+
/**
|
|
47041
|
+
* How long one `ptz.move` pulse runs, ms. The DENOMINATOR of "how far did
|
|
47042
|
+
* the head travel per pulse", so it leaves the provider through
|
|
47043
|
+
* `getOptions` rather than staying a private literal.
|
|
47044
|
+
*/
|
|
46755
47045
|
static PTZ_PULSE_MS = 350;
|
|
46756
47046
|
/** Short per-call deadline (ms) for the PTZ start/stop CGI so a slow or
|
|
46757
47047
|
* blocked camera round-trip fails fast instead of exceeding the
|
|
@@ -46877,7 +47167,9 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
46877
47167
|
if (deviceId !== this.id) return [];
|
|
46878
47168
|
return (await this.ensureClient().getPtzPresets(this.channel)).map((p) => ({
|
|
46879
47169
|
id: String(p.id),
|
|
46880
|
-
name: p.name
|
|
47170
|
+
name: p.name,
|
|
47171
|
+
fn: null,
|
|
47172
|
+
writable: true
|
|
46881
47173
|
}));
|
|
46882
47174
|
},
|
|
46883
47175
|
goToPreset: async ({ deviceId, presetId }) => {
|
|
@@ -46913,7 +47205,9 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
46913
47205
|
hasTilt: false,
|
|
46914
47206
|
hasZoom: false,
|
|
46915
47207
|
supportsPresets: false,
|
|
46916
|
-
hasAutofocus: false
|
|
47208
|
+
hasAutofocus: false,
|
|
47209
|
+
speedSteps: AmcrestCamera.DAHUA_SPEED_STEPS,
|
|
47210
|
+
moveImpulseMs: AmcrestCamera.PTZ_PULSE_MS
|
|
46917
47211
|
};
|
|
46918
47212
|
if (deviceId !== this.id) return cold;
|
|
46919
47213
|
const caps = await this.ensureClient().getPtzCaps(this.channel);
|
|
@@ -46924,26 +47218,64 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
46924
47218
|
hasZoom: caps.hasZoom,
|
|
46925
47219
|
supportsPresets: caps.supportsPreset,
|
|
46926
47220
|
hasAutofocus: false,
|
|
47221
|
+
speedSteps: AmcrestCamera.DAHUA_SPEED_STEPS,
|
|
47222
|
+
moveImpulseMs: AmcrestCamera.PTZ_PULSE_MS,
|
|
46927
47223
|
...caps.maxPresets !== null ? { maxPresets: caps.maxPresets } : {}
|
|
46928
47224
|
};
|
|
46929
47225
|
},
|
|
46930
47226
|
goHome: async ({ deviceId }) => {
|
|
46931
47227
|
if (deviceId !== this.id) return;
|
|
47228
|
+
const home = await this.resolveHomePreset();
|
|
47229
|
+
if (home.source === "none" || home.presetId === null) throw new Error("No home preset configured for this camera — set one in the PTZ settings");
|
|
47230
|
+
const id = Number.parseInt(home.presetId, 10);
|
|
47231
|
+
if (!Number.isFinite(id) || id < 1) throw new Error(`Configured home preset is not a Dahua preset id: ${home.presetId}`);
|
|
46932
47232
|
const client = this.ensureClient();
|
|
46933
|
-
this.enqueuePtz("goHome", () => client.ptzGotoPreset(this.channel,
|
|
47233
|
+
this.enqueuePtz("goHome", () => client.ptzGotoPreset(this.channel, id, AmcrestCamera.PTZ_CGI_TIMEOUT_MS));
|
|
47234
|
+
},
|
|
47235
|
+
getHomePreset: async ({ deviceId }) => {
|
|
47236
|
+
if (deviceId !== this.id) return {
|
|
47237
|
+
presetId: null,
|
|
47238
|
+
source: "none"
|
|
47239
|
+
};
|
|
47240
|
+
return this.resolveHomePreset();
|
|
47241
|
+
},
|
|
47242
|
+
captureHomeHere: async ({ deviceId }) => {
|
|
47243
|
+
if (deviceId !== this.id) return;
|
|
47244
|
+
const client = this.ensureClient();
|
|
47245
|
+
const ceiling = (await client.getPtzCaps(this.channel))?.maxPresets ?? AmcrestCamera.DAHUA_MAX_PRESETS;
|
|
47246
|
+
const used = new Set((await client.getPtzPresets(this.channel)).map((p) => p.id));
|
|
47247
|
+
let slot = 1;
|
|
47248
|
+
while (used.has(slot) && slot <= ceiling) slot += 1;
|
|
47249
|
+
if (slot > ceiling) throw new Error("No free preset slot on this camera to store a home position — free one first");
|
|
47250
|
+
await client.ptzSetPreset(this.channel, slot, AmcrestCamera.PTZ_CGI_TIMEOUT_MS);
|
|
47251
|
+
await this.config.setAll({ homePresetId: String(slot) });
|
|
47252
|
+
this.ctx.logger.info("amcrest: home pinned to the current position", {
|
|
47253
|
+
tags: { deviceId: this.id },
|
|
47254
|
+
meta: { presetId: slot }
|
|
47255
|
+
});
|
|
47256
|
+
},
|
|
47257
|
+
getHomeReturnSeconds: async () => {
|
|
47258
|
+
return null;
|
|
47259
|
+
},
|
|
47260
|
+
setHomeReturnSeconds: async () => {
|
|
47261
|
+
throw new Error("Amcrest: the idle-return timer is not wired on this driver");
|
|
47262
|
+
},
|
|
47263
|
+
setHomePreset: async ({ deviceId, presetId }) => {
|
|
47264
|
+
if (deviceId !== this.id) return;
|
|
47265
|
+
await this.config.setAll({ homePresetId: presetId ?? "" });
|
|
46934
47266
|
},
|
|
46935
47267
|
getPosition: async () => {
|
|
46936
47268
|
return {
|
|
46937
|
-
pan:
|
|
46938
|
-
tilt:
|
|
46939
|
-
zoom:
|
|
47269
|
+
pan: null,
|
|
47270
|
+
tilt: null,
|
|
47271
|
+
zoom: null
|
|
46940
47272
|
};
|
|
46941
47273
|
},
|
|
46942
47274
|
getStatus: async () => {
|
|
46943
47275
|
return {
|
|
46944
|
-
pan:
|
|
46945
|
-
tilt:
|
|
46946
|
-
zoom:
|
|
47276
|
+
pan: null,
|
|
47277
|
+
tilt: null,
|
|
47278
|
+
zoom: null,
|
|
46947
47279
|
autofocus: false
|
|
46948
47280
|
};
|
|
46949
47281
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-provider-amcrest",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.114",
|
|
4
4
|
"description": "Amcrest/Dahua camera device provider addon for CamStack — Dahua CGI over HTTP(S) with digest auth (snapshot, RTSP catalog, PTZ, image/day-night config)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|