@camstack/addon-provider-hikvision 1.2.117 → 1.2.119

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