@camstack/addon-provider-reolink 1.2.138 → 1.2.139

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 +265 -29
  2. package/dist/addon.mjs +265 -29
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -5386,7 +5386,7 @@ var ZodIssueCode = {
5386
5386
  var ZodFirstPartyTypeKind;
5387
5387
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5388
5388
  //#endregion
5389
- //#region ../types/dist/sleep-BDR76Ykr.mjs
5389
+ //#region ../types/dist/sleep-B1y0Fo1U.mjs
5390
5390
  /**
5391
5391
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5392
5392
  * window to float samples (D455).
@@ -31657,14 +31657,81 @@ function summarisePrivacyAudio(profiles) {
31657
31657
  if (profiles.length === 0) return null;
31658
31658
  return profiles.some((p) => p.audioEnabled);
31659
31659
  }
31660
+ /**
31661
+ * What a preset id DOES on this camera.
31662
+ *
31663
+ * On several PTZ firmwares the preset namespace is shared: low ids are stored
31664
+ * positions, a reserved band triggers camera FUNCTIONS. The dispensa camera
31665
+ * (Hikvision) returns its own reserved band in the preset list, named by the
31666
+ * firmware:
31667
+ *
31668
+ * 34 Back to origin · 39 Day mode · 40 Night mode · 46 Day/Night Auto Mode
31669
+ * 92 Set manual limits · 93 Save manual limits · 94 Remote reboot
31670
+ * 95 Call OSD menu
31671
+ *
31672
+ * We used to hand all of these to the UI as ordinary presets. Saving over one
31673
+ * is what produced `PUT /ISAPI/PTZCtrl/channels/1/presets/34 -> HTTP 500`: the
31674
+ * camera refusing to let "Back to origin" be overwritten, surfaced to the
31675
+ * operator as a transport failure.
31676
+ *
31677
+ * A function preset stays fully usable as a DESTINATION -- `goToPreset('34')`
31678
+ * is the single most useful thing that camera can do. Only `savePreset` and
31679
+ * `deletePreset` are gated.
31680
+ */
31681
+ var PtzPresetFunctionSchema = _enum([
31682
+ "home",
31683
+ "day-mode",
31684
+ "night-mode",
31685
+ "day-night-auto",
31686
+ "set-limits",
31687
+ "save-limits",
31688
+ "reboot",
31689
+ "osd-menu"
31690
+ ]);
31660
31691
  var PtzPresetSchema = object({
31661
31692
  id: string(),
31662
- name: string()
31693
+ name: string(),
31694
+ /**
31695
+ * The firmware function this id performs, when the provider KNOWS the id is
31696
+ * reserved. `null` = an ordinary stored position, OR a reserved id this
31697
+ * provider does not recognise -- the two are told apart by `writable`.
31698
+ */
31699
+ fn: PtzPresetFunctionSchema.nullable(),
31700
+ /**
31701
+ * Whether `savePreset` / `deletePreset` may target this id. A provider sets
31702
+ * it false only for a band it KNOWS is reserved; where the firmware is not
31703
+ * documented it stays true and a refusal comes from the camera, named.
31704
+ */
31705
+ writable: boolean()
31663
31706
  });
31707
+ /**
31708
+ * Where the head is pointing -- per axis, and `null` when the driver cannot
31709
+ * read it.
31710
+ *
31711
+ * It used to be three REQUIRED numbers, which left a provider that cannot read
31712
+ * position no way to say so. All four said `0`:
31713
+ *
31714
+ * reolink `return { pan: 0, tilt: 0, zoom: 0 }` "until a position-query
31715
+ * path lands upstream"
31716
+ * hikvision stub, "firmware-dependent and frequently absent"
31717
+ * amcrest stub, "no generic absolute-position read for this model family"
31718
+ * onvif throws
31719
+ *
31720
+ * So every consumer asking where a camera points was told "perfectly centred"
31721
+ * by four cameras that had never looked. That is D393 -- a measurement that
31722
+ * FAILED is `null`, never `0`, and the type says so all the way to the
31723
+ * decision -- the same shape as the unreadable `statfs` folded into "0 bytes of
31724
+ * headroom", which evacuated a healthy disk.
31725
+ *
31726
+ * D393 also says to check for a narrower structural TWIN of the result type.
31727
+ * There is one: `PtzStatusSchema` extends this, so the lie had already
31728
+ * propagated into `getStatus`, which is the surface `getPosition`'s own comment
31729
+ * tells callers to migrate to.
31730
+ */
31664
31731
  var PtzPositionSchema = object({
31665
- pan: number(),
31666
- tilt: number(),
31667
- zoom: number()
31732
+ pan: number().nullable(),
31733
+ tilt: number().nullable(),
31734
+ zoom: number().nullable()
31668
31735
  });
31669
31736
  var PtzMoveCommandSchema = object({
31670
31737
  pan: number().optional(),
@@ -31686,7 +31753,72 @@ var PtzOptionsSchema = object({
31686
31753
  maxPresets: number().optional(),
31687
31754
  /** Whether the camera exposes a controllable autofocus toggle
31688
31755
  * (boolean `hasX` per the getOptions availability convention). */
31689
- hasAutofocus: boolean()
31756
+ hasAutofocus: boolean(),
31757
+ /**
31758
+ * How many distinct speeds this camera's NATIVE scale offers.
31759
+ *
31760
+ * The cap's `speed` is normalized 0..1 and each provider maps it down --
31761
+ * Baichuan 1..63, Dahua 1..8, ISAPI's percentage -100..100. The normalized
31762
+ * value hides how coarse that really is: asking an amcrest for `0.37` is
31763
+ * meaningless, because it has eight steps and three of them round to the
31764
+ * same one. A loop that tunes its own gain has to know the granularity of
31765
+ * the knob it is turning.
31766
+ *
31767
+ * `null` = the driver does not know, or the scale is continuous (ONVIF takes
31768
+ * a float). Never a made-up number.
31769
+ */
31770
+ speedSteps: number().int().positive().nullable(),
31771
+ /**
31772
+ * How long ONE `move` pulse runs on this driver, ms.
31773
+ *
31774
+ * `move` is a self-terminating burst everywhere, but every provider hardcoded
31775
+ * its own duration in private -- 200 reolink, 350 amcrest, 500 hikvision,
31776
+ * 1000 onvif -- so no caller could read it. It is the DENOMINATOR of "how far
31777
+ * did the head travel per pulse": without it a measured displacement has no
31778
+ * gain to be divided into.
31779
+ *
31780
+ * `null` = the driver cannot say.
31781
+ */
31782
+ moveImpulseMs: number().int().positive().nullable()
31783
+ });
31784
+ /**
31785
+ * Which preset `goHome` goes to, and WHO decided.
31786
+ *
31787
+ * `goHome` used to be three vendor "conventions" written in three comments --
31788
+ * Reolink preset 0, Hikvision preset '1', Dahua preset 1 -- and on this fleet
31789
+ * not one of the three cameras honours its own:
31790
+ *
31791
+ * 592 Videocamera camera Daniel (reolink) preset 0 holds "stanza"
31792
+ * 1438 Videocamera dispensa (hikvision) preset 1 holds "Credenza"
31793
+ * 3836 Videocamera studio (amcrest) preset 1 holds "Preset1"
31794
+ *
31795
+ * So `goHome` meant "go wherever the operator happened to save in slot 0 or 1",
31796
+ * and two of the three swallowed the failure in a bare `catch`. For autotrack
31797
+ * this is the HOTTEST path -- it runs every time a subject is released -- so it
31798
+ * needs an answer that is true per camera and audible when it is missing.
31799
+ *
31800
+ * `source` is what makes it honest:
31801
+ * - `operator` -- picked in the UI, stored by the provider.
31802
+ * - `firmware` -- the camera itself named a preset `fn: 'home'` (Hikvision's
31803
+ * "Back to origin"), adopted as the default with nothing to configure.
31804
+ * - `native` -- the driver homes WITHOUT a preset at all. ONVIF does:
31805
+ * `goHome` is `ptzAbsoluteMove({x:0, y:0, zoom:0})`. `presetId` is null and
31806
+ * that is not a failure -- there is nothing to pick, and the UI must offer
31807
+ * no picker.
31808
+ * - `none` -- nothing is configured and the camera names nothing.
31809
+ * `goHome` REFUSES rather than moving the head somewhere arbitrary.
31810
+ *
31811
+ * `source === 'none'` is the refusal condition, NOT `presetId === null`: the
31812
+ * native case has no preset and homes perfectly well.
31813
+ */
31814
+ var PtzHomePresetSchema = object({
31815
+ presetId: string().nullable(),
31816
+ source: _enum([
31817
+ "operator",
31818
+ "firmware",
31819
+ "native",
31820
+ "none"
31821
+ ])
31690
31822
  });
31691
31823
  var ptzCapability = {
31692
31824
  name: "ptz",
@@ -31727,7 +31859,25 @@ var ptzCapability = {
31727
31859
  auth: "admin"
31728
31860
  }),
31729
31861
  getOptions: method(object({ deviceId: number() }), PtzOptionsSchema),
31862
+ /**
31863
+ * Move to the camera's configured home preset. THROWS when
31864
+ * `getHomePreset()` resolves to `none` -- a head that did not move must
31865
+ * never look like a head that went home.
31866
+ */
31730
31867
  goHome: method(object({ deviceId: number() }), _void(), { kind: "mutation" }),
31868
+ /** Which preset `goHome` targets, and who decided it. */
31869
+ getHomePreset: method(object({ deviceId: number() }), PtzHomePresetSchema),
31870
+ /**
31871
+ * Pin the home preset for this camera. `null` clears the operator's choice
31872
+ * and falls back to whatever the firmware names, or to `none`.
31873
+ */
31874
+ setHomePreset: method(object({
31875
+ deviceId: number(),
31876
+ presetId: string().nullable()
31877
+ }), _void(), {
31878
+ kind: "mutation",
31879
+ auth: "admin"
31880
+ }),
31731
31881
  /**
31732
31882
  * Pull the current PTZ position. Redundant with the auto-injected
31733
31883
  * `getStatus` method (see `status` below); kept for callers that
@@ -41398,6 +41548,12 @@ Object.freeze({
41398
41548
  addonId: null,
41399
41549
  access: "delete"
41400
41550
  },
41551
+ "ptz.getHomePreset": {
41552
+ capName: "ptz",
41553
+ capScope: "device",
41554
+ addonId: null,
41555
+ access: "view"
41556
+ },
41401
41557
  "ptz.getOptions": {
41402
41558
  capName: "ptz",
41403
41559
  capScope: "device",
@@ -41446,6 +41602,12 @@ Object.freeze({
41446
41602
  addonId: null,
41447
41603
  access: "create"
41448
41604
  },
41605
+ "ptz.setHomePreset": {
41606
+ capName: "ptz",
41607
+ capScope: "device",
41608
+ addonId: null,
41609
+ access: "create"
41610
+ },
41449
41611
  "ptz.stop": {
41450
41612
  capName: "ptz",
41451
41613
  capScope: "device",
@@ -44619,6 +44781,11 @@ Object.freeze({
44619
44781
  form: "single",
44620
44782
  optional: false
44621
44783
  }],
44784
+ "ptz.getHomePreset": [{
44785
+ name: "deviceId",
44786
+ form: "single",
44787
+ optional: false
44788
+ }],
44622
44789
  "ptz.getOptions": [{
44623
44790
  name: "deviceId",
44624
44791
  form: "single",
@@ -44659,6 +44826,11 @@ Object.freeze({
44659
44826
  form: "single",
44660
44827
  optional: false
44661
44828
  }],
44829
+ "ptz.setHomePreset": [{
44830
+ name: "deviceId",
44831
+ form: "single",
44832
+ optional: false
44833
+ }],
44662
44834
  "ptz.stop": [{
44663
44835
  name: "deviceId",
44664
44836
  form: "single",
@@ -148323,6 +148495,17 @@ var ReolinkStreamProfileOptionsSchema = object({
148323
148495
  }).optional()
148324
148496
  });
148325
148497
  var reolinkCameraSchema = object({
148498
+ /**
148499
+ * Which preset `ptz.goHome` targets on THIS camera.
148500
+ *
148501
+ * Empty = nothing configured, and `goHome` then REFUSES. It used to go to a
148502
+ * hardcoded slot on a vendor "convention" this fleet contradicts: reolink
148503
+ * went to preset 0, which on device 592 holds "stanza"; amcrest went to
148504
+ * preset 1, which on device 3836 holds "Preset1". Both swallowed the failure
148505
+ * in a bare catch, so a head that never moved was indistinguishable from a
148506
+ * head that went home.
148507
+ */
148508
+ homePresetId: string().default("").describe("Preset id ptz.goHome targets (empty = goHome refuses)"),
148326
148509
  host: string().describe("Camera IP or hostname"),
148327
148510
  port: number().default(9e3).describe("Baichuan port (default 9000)"),
148328
148511
  username: string().default("admin").describe("Username"),
@@ -149613,6 +149796,20 @@ function coerceNumber(value) {
149613
149796
  return null;
149614
149797
  }
149615
149798
  /**
149799
+ * Baichuan's native PTZ speed scale is 1..63; `runPtz` maps the cap's
149800
+ * normalized 0..1 onto it. Named because the autotrack loop has to know the
149801
+ * GRANULARITY of the knob it turns -- a normalized 0.37 means something very
149802
+ * different on 63 steps than on amcrest's 8.
149803
+ */
149804
+ var BAICHUAN_SPEED_STEPS = 63;
149805
+ /**
149806
+ * How long one `ptz.move` pulse runs, ms -- the `autoStopMs` handed to
149807
+ * Baichuan. It is the DENOMINATOR of "how far did the head travel per pulse",
149808
+ * so it is exported through `getOptions` rather than left a literal no caller
149809
+ * could read.
149810
+ */
149811
+ var PTZ_MOVE_IMPULSE_MS = 200;
149812
+ /**
149616
149813
  * Per-device transient diagnostics blob populated from the lib's
149617
149814
  * `getOnlineUserSessionsForUi` + `getSocketPoolSummary` +
149618
149815
  * `getSocketPoolCooldownStatus` calls. NOT persisted — recomputed on
@@ -149954,6 +150151,24 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
149954
150151
  * during `adoptDevice`). Standalone cameras default to 0 so the
149955
150152
  * pre-Hub behavior is preserved verbatim.
149956
150153
  */
150154
+ /**
150155
+ * Which preset `goHome` targets, and who decided it.
150156
+ *
150157
+ * Baichuan names no home of its own, so the operator's pick is the only
150158
+ * answer there can be. With none, `none` -- and `goHome` refuses rather than
150159
+ * moving the head to whatever happens to sit in slot 0.
150160
+ */
150161
+ async resolveHomePreset() {
150162
+ const chosen = this.config.get("homePresetId") ?? "";
150163
+ if (chosen === "") return {
150164
+ presetId: null,
150165
+ source: "none"
150166
+ };
150167
+ return {
150168
+ presetId: chosen,
150169
+ source: "operator"
150170
+ };
150171
+ }
149957
150172
  getChannel() {
149958
150173
  const ch = this.config.get("channel");
149959
150174
  if (typeof ch === "number" && ch >= 0) return ch;
@@ -153331,7 +153546,9 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
153331
153546
  const channel = this.getChannel();
153332
153547
  return (await api.getPtzPresets(channel)).map((p) => ({
153333
153548
  id: String(p.id),
153334
- name: p.name
153549
+ name: p.name,
153550
+ fn: null,
153551
+ writable: true
153335
153552
  }));
153336
153553
  } catch {
153337
153554
  return [];
@@ -153367,7 +153584,9 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
153367
153584
  hasTilt: false,
153368
153585
  hasZoom: false,
153369
153586
  supportsPresets: false,
153370
- hasAutofocus: false
153587
+ hasAutofocus: false,
153588
+ speedSteps: BAICHUAN_SPEED_STEPS,
153589
+ moveImpulseMs: PTZ_MOVE_IMPULSE_MS
153371
153590
  };
153372
153591
  const hasAutofocus = this.config.get("deviceCache")?.autoFocusSnapshot?.supported === true;
153373
153592
  return this.resolveCapOptions({
@@ -153380,7 +153599,9 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
153380
153599
  hasTilt: capabilities.hasTilt,
153381
153600
  hasZoom: capabilities.hasZoom,
153382
153601
  supportsPresets: capabilities.hasPresets,
153383
- hasAutofocus
153602
+ hasAutofocus,
153603
+ speedSteps: BAICHUAN_SPEED_STEPS,
153604
+ moveImpulseMs: PTZ_MOVE_IMPULSE_MS
153384
153605
  };
153385
153606
  },
153386
153607
  fallback: () => {
@@ -153390,42 +153611,57 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
153390
153611
  hasTilt: hasPtz,
153391
153612
  hasZoom: hasPtz,
153392
153613
  supportsPresets: hasPtz,
153393
- hasAutofocus
153614
+ hasAutofocus,
153615
+ speedSteps: BAICHUAN_SPEED_STEPS,
153616
+ moveImpulseMs: PTZ_MOVE_IMPULSE_MS
153394
153617
  };
153395
153618
  }
153396
153619
  });
153397
153620
  },
153398
153621
  goHome: async ({ deviceId }) => {
153399
153622
  if (deviceId !== this.id) return;
153400
- try {
153401
- const api = await this.ensureApi();
153402
- const channel = this.getChannel();
153403
- await api.moveToPtzPreset(channel, 0);
153404
- } catch {}
153623
+ const home = await this.resolveHomePreset();
153624
+ if (home.source === "none" || home.presetId === null) throw new Error("No home preset configured for this camera — set one in the PTZ settings");
153625
+ const id = Number(home.presetId);
153626
+ if (!Number.isFinite(id)) throw new Error(`Configured home preset is not a Baichuan id: ${home.presetId}`);
153627
+ const api = await this.ensureApi();
153628
+ const channel = this.getChannel();
153629
+ await api.moveToPtzPreset(channel, id);
153630
+ },
153631
+ getHomePreset: async ({ deviceId }) => {
153632
+ if (deviceId !== this.id) return {
153633
+ presetId: null,
153634
+ source: "none"
153635
+ };
153636
+ return this.resolveHomePreset();
153637
+ },
153638
+ setHomePreset: async ({ deviceId, presetId }) => {
153639
+ if (deviceId !== this.id) return;
153640
+ await this.config.setAll({ homePresetId: presetId ?? "" });
153405
153641
  },
153406
153642
  getPosition: async ({ deviceId }) => {
153407
153643
  if (deviceId !== this.id) return {
153408
- pan: 0,
153409
- tilt: 0,
153410
- zoom: 0
153644
+ pan: null,
153645
+ tilt: null,
153646
+ zoom: null
153411
153647
  };
153412
153648
  return {
153413
- pan: 0,
153414
- tilt: 0,
153415
- zoom: 0
153649
+ pan: null,
153650
+ tilt: null,
153651
+ zoom: null
153416
153652
  };
153417
153653
  },
153418
153654
  getStatus: async ({ deviceId }) => {
153419
153655
  if (deviceId !== this.id) return {
153420
- pan: 0,
153421
- tilt: 0,
153422
- zoom: 0,
153656
+ pan: null,
153657
+ tilt: null,
153658
+ zoom: null,
153423
153659
  autofocus: false
153424
153660
  };
153425
153661
  return {
153426
- pan: 0,
153427
- tilt: 0,
153428
- zoom: 0,
153662
+ pan: null,
153663
+ tilt: null,
153664
+ zoom: null,
153429
153665
  autofocus: (this.config.get("deviceCache")?.autoFocusSnapshot)?.enabled === true
153430
153666
  };
153431
153667
  },
@@ -153476,13 +153712,13 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
153476
153712
  if (tilt !== void 0 && Math.abs(tilt) > .01) cmds.push(tilt > 0 ? "Up" : "Down");
153477
153713
  if (zoom !== void 0 && Math.abs(zoom) > .01) await this.runZoom(api, channel, zoom);
153478
153714
  if (cmds.length === 0) return;
153479
- const baichuanSpeed = speed === void 0 ? 32 : Math.max(1, Math.min(63, Math.round(speed * 63)));
153715
+ const baichuanSpeed = speed === void 0 ? Math.round(BAICHUAN_SPEED_STEPS / 2) : Math.max(1, Math.min(BAICHUAN_SPEED_STEPS, Math.round(speed * BAICHUAN_SPEED_STEPS)));
153480
153716
  for (const command of cmds) try {
153481
153717
  await api.ptz(channel, {
153482
153718
  action: "start",
153483
153719
  command,
153484
153720
  speed: baichuanSpeed,
153485
- ...continuous ? {} : { autoStopMs: 200 }
153721
+ ...continuous ? {} : { autoStopMs: PTZ_MOVE_IMPULSE_MS }
153486
153722
  });
153487
153723
  } catch (err) {
153488
153724
  this.ctx.logger.warn("Baichuan ptz command failed", {
package/dist/addon.mjs CHANGED
@@ -5381,7 +5381,7 @@ var ZodIssueCode = {
5381
5381
  var ZodFirstPartyTypeKind;
5382
5382
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5383
5383
  //#endregion
5384
- //#region ../types/dist/sleep-BDR76Ykr.mjs
5384
+ //#region ../types/dist/sleep-B1y0Fo1U.mjs
5385
5385
  /**
5386
5386
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5387
5387
  * window to float samples (D455).
@@ -31652,14 +31652,81 @@ function summarisePrivacyAudio(profiles) {
31652
31652
  if (profiles.length === 0) return null;
31653
31653
  return profiles.some((p) => p.audioEnabled);
31654
31654
  }
31655
+ /**
31656
+ * What a preset id DOES on this camera.
31657
+ *
31658
+ * On several PTZ firmwares the preset namespace is shared: low ids are stored
31659
+ * positions, a reserved band triggers camera FUNCTIONS. The dispensa camera
31660
+ * (Hikvision) returns its own reserved band in the preset list, named by the
31661
+ * firmware:
31662
+ *
31663
+ * 34 Back to origin · 39 Day mode · 40 Night mode · 46 Day/Night Auto Mode
31664
+ * 92 Set manual limits · 93 Save manual limits · 94 Remote reboot
31665
+ * 95 Call OSD menu
31666
+ *
31667
+ * We used to hand all of these to the UI as ordinary presets. Saving over one
31668
+ * is what produced `PUT /ISAPI/PTZCtrl/channels/1/presets/34 -> HTTP 500`: the
31669
+ * camera refusing to let "Back to origin" be overwritten, surfaced to the
31670
+ * operator as a transport failure.
31671
+ *
31672
+ * A function preset stays fully usable as a DESTINATION -- `goToPreset('34')`
31673
+ * is the single most useful thing that camera can do. Only `savePreset` and
31674
+ * `deletePreset` are gated.
31675
+ */
31676
+ var PtzPresetFunctionSchema = _enum([
31677
+ "home",
31678
+ "day-mode",
31679
+ "night-mode",
31680
+ "day-night-auto",
31681
+ "set-limits",
31682
+ "save-limits",
31683
+ "reboot",
31684
+ "osd-menu"
31685
+ ]);
31655
31686
  var PtzPresetSchema = object({
31656
31687
  id: string(),
31657
- name: string()
31688
+ name: string(),
31689
+ /**
31690
+ * The firmware function this id performs, when the provider KNOWS the id is
31691
+ * reserved. `null` = an ordinary stored position, OR a reserved id this
31692
+ * provider does not recognise -- the two are told apart by `writable`.
31693
+ */
31694
+ fn: PtzPresetFunctionSchema.nullable(),
31695
+ /**
31696
+ * Whether `savePreset` / `deletePreset` may target this id. A provider sets
31697
+ * it false only for a band it KNOWS is reserved; where the firmware is not
31698
+ * documented it stays true and a refusal comes from the camera, named.
31699
+ */
31700
+ writable: boolean()
31658
31701
  });
31702
+ /**
31703
+ * Where the head is pointing -- per axis, and `null` when the driver cannot
31704
+ * read it.
31705
+ *
31706
+ * It used to be three REQUIRED numbers, which left a provider that cannot read
31707
+ * position no way to say so. All four said `0`:
31708
+ *
31709
+ * reolink `return { pan: 0, tilt: 0, zoom: 0 }` "until a position-query
31710
+ * path lands upstream"
31711
+ * hikvision stub, "firmware-dependent and frequently absent"
31712
+ * amcrest stub, "no generic absolute-position read for this model family"
31713
+ * onvif throws
31714
+ *
31715
+ * So every consumer asking where a camera points was told "perfectly centred"
31716
+ * by four cameras that had never looked. That is D393 -- a measurement that
31717
+ * FAILED is `null`, never `0`, and the type says so all the way to the
31718
+ * decision -- the same shape as the unreadable `statfs` folded into "0 bytes of
31719
+ * headroom", which evacuated a healthy disk.
31720
+ *
31721
+ * D393 also says to check for a narrower structural TWIN of the result type.
31722
+ * There is one: `PtzStatusSchema` extends this, so the lie had already
31723
+ * propagated into `getStatus`, which is the surface `getPosition`'s own comment
31724
+ * tells callers to migrate to.
31725
+ */
31659
31726
  var PtzPositionSchema = object({
31660
- pan: number(),
31661
- tilt: number(),
31662
- zoom: number()
31727
+ pan: number().nullable(),
31728
+ tilt: number().nullable(),
31729
+ zoom: number().nullable()
31663
31730
  });
31664
31731
  var PtzMoveCommandSchema = object({
31665
31732
  pan: number().optional(),
@@ -31681,7 +31748,72 @@ var PtzOptionsSchema = object({
31681
31748
  maxPresets: number().optional(),
31682
31749
  /** Whether the camera exposes a controllable autofocus toggle
31683
31750
  * (boolean `hasX` per the getOptions availability convention). */
31684
- hasAutofocus: boolean()
31751
+ hasAutofocus: boolean(),
31752
+ /**
31753
+ * How many distinct speeds this camera's NATIVE scale offers.
31754
+ *
31755
+ * The cap's `speed` is normalized 0..1 and each provider maps it down --
31756
+ * Baichuan 1..63, Dahua 1..8, ISAPI's percentage -100..100. The normalized
31757
+ * value hides how coarse that really is: asking an amcrest for `0.37` is
31758
+ * meaningless, because it has eight steps and three of them round to the
31759
+ * same one. A loop that tunes its own gain has to know the granularity of
31760
+ * the knob it is turning.
31761
+ *
31762
+ * `null` = the driver does not know, or the scale is continuous (ONVIF takes
31763
+ * a float). Never a made-up number.
31764
+ */
31765
+ speedSteps: number().int().positive().nullable(),
31766
+ /**
31767
+ * How long ONE `move` pulse runs on this driver, ms.
31768
+ *
31769
+ * `move` is a self-terminating burst everywhere, but every provider hardcoded
31770
+ * its own duration in private -- 200 reolink, 350 amcrest, 500 hikvision,
31771
+ * 1000 onvif -- so no caller could read it. It is the DENOMINATOR of "how far
31772
+ * did the head travel per pulse": without it a measured displacement has no
31773
+ * gain to be divided into.
31774
+ *
31775
+ * `null` = the driver cannot say.
31776
+ */
31777
+ moveImpulseMs: number().int().positive().nullable()
31778
+ });
31779
+ /**
31780
+ * Which preset `goHome` goes to, and WHO decided.
31781
+ *
31782
+ * `goHome` used to be three vendor "conventions" written in three comments --
31783
+ * Reolink preset 0, Hikvision preset '1', Dahua preset 1 -- and on this fleet
31784
+ * not one of the three cameras honours its own:
31785
+ *
31786
+ * 592 Videocamera camera Daniel (reolink) preset 0 holds "stanza"
31787
+ * 1438 Videocamera dispensa (hikvision) preset 1 holds "Credenza"
31788
+ * 3836 Videocamera studio (amcrest) preset 1 holds "Preset1"
31789
+ *
31790
+ * So `goHome` meant "go wherever the operator happened to save in slot 0 or 1",
31791
+ * and two of the three swallowed the failure in a bare `catch`. For autotrack
31792
+ * this is the HOTTEST path -- it runs every time a subject is released -- so it
31793
+ * needs an answer that is true per camera and audible when it is missing.
31794
+ *
31795
+ * `source` is what makes it honest:
31796
+ * - `operator` -- picked in the UI, stored by the provider.
31797
+ * - `firmware` -- the camera itself named a preset `fn: 'home'` (Hikvision's
31798
+ * "Back to origin"), adopted as the default with nothing to configure.
31799
+ * - `native` -- the driver homes WITHOUT a preset at all. ONVIF does:
31800
+ * `goHome` is `ptzAbsoluteMove({x:0, y:0, zoom:0})`. `presetId` is null and
31801
+ * that is not a failure -- there is nothing to pick, and the UI must offer
31802
+ * no picker.
31803
+ * - `none` -- nothing is configured and the camera names nothing.
31804
+ * `goHome` REFUSES rather than moving the head somewhere arbitrary.
31805
+ *
31806
+ * `source === 'none'` is the refusal condition, NOT `presetId === null`: the
31807
+ * native case has no preset and homes perfectly well.
31808
+ */
31809
+ var PtzHomePresetSchema = object({
31810
+ presetId: string().nullable(),
31811
+ source: _enum([
31812
+ "operator",
31813
+ "firmware",
31814
+ "native",
31815
+ "none"
31816
+ ])
31685
31817
  });
31686
31818
  var ptzCapability = {
31687
31819
  name: "ptz",
@@ -31722,7 +31854,25 @@ var ptzCapability = {
31722
31854
  auth: "admin"
31723
31855
  }),
31724
31856
  getOptions: method(object({ deviceId: number() }), PtzOptionsSchema),
31857
+ /**
31858
+ * Move to the camera's configured home preset. THROWS when
31859
+ * `getHomePreset()` resolves to `none` -- a head that did not move must
31860
+ * never look like a head that went home.
31861
+ */
31725
31862
  goHome: method(object({ deviceId: number() }), _void(), { kind: "mutation" }),
31863
+ /** Which preset `goHome` targets, and who decided it. */
31864
+ getHomePreset: method(object({ deviceId: number() }), PtzHomePresetSchema),
31865
+ /**
31866
+ * Pin the home preset for this camera. `null` clears the operator's choice
31867
+ * and falls back to whatever the firmware names, or to `none`.
31868
+ */
31869
+ setHomePreset: method(object({
31870
+ deviceId: number(),
31871
+ presetId: string().nullable()
31872
+ }), _void(), {
31873
+ kind: "mutation",
31874
+ auth: "admin"
31875
+ }),
31726
31876
  /**
31727
31877
  * Pull the current PTZ position. Redundant with the auto-injected
31728
31878
  * `getStatus` method (see `status` below); kept for callers that
@@ -41393,6 +41543,12 @@ Object.freeze({
41393
41543
  addonId: null,
41394
41544
  access: "delete"
41395
41545
  },
41546
+ "ptz.getHomePreset": {
41547
+ capName: "ptz",
41548
+ capScope: "device",
41549
+ addonId: null,
41550
+ access: "view"
41551
+ },
41396
41552
  "ptz.getOptions": {
41397
41553
  capName: "ptz",
41398
41554
  capScope: "device",
@@ -41441,6 +41597,12 @@ Object.freeze({
41441
41597
  addonId: null,
41442
41598
  access: "create"
41443
41599
  },
41600
+ "ptz.setHomePreset": {
41601
+ capName: "ptz",
41602
+ capScope: "device",
41603
+ addonId: null,
41604
+ access: "create"
41605
+ },
41444
41606
  "ptz.stop": {
41445
41607
  capName: "ptz",
41446
41608
  capScope: "device",
@@ -44614,6 +44776,11 @@ Object.freeze({
44614
44776
  form: "single",
44615
44777
  optional: false
44616
44778
  }],
44779
+ "ptz.getHomePreset": [{
44780
+ name: "deviceId",
44781
+ form: "single",
44782
+ optional: false
44783
+ }],
44617
44784
  "ptz.getOptions": [{
44618
44785
  name: "deviceId",
44619
44786
  form: "single",
@@ -44654,6 +44821,11 @@ Object.freeze({
44654
44821
  form: "single",
44655
44822
  optional: false
44656
44823
  }],
44824
+ "ptz.setHomePreset": [{
44825
+ name: "deviceId",
44826
+ form: "single",
44827
+ optional: false
44828
+ }],
44657
44829
  "ptz.stop": [{
44658
44830
  name: "deviceId",
44659
44831
  form: "single",
@@ -148318,6 +148490,17 @@ var ReolinkStreamProfileOptionsSchema = object({
148318
148490
  }).optional()
148319
148491
  });
148320
148492
  var reolinkCameraSchema = object({
148493
+ /**
148494
+ * Which preset `ptz.goHome` targets on THIS camera.
148495
+ *
148496
+ * Empty = nothing configured, and `goHome` then REFUSES. It used to go to a
148497
+ * hardcoded slot on a vendor "convention" this fleet contradicts: reolink
148498
+ * went to preset 0, which on device 592 holds "stanza"; amcrest went to
148499
+ * preset 1, which on device 3836 holds "Preset1". Both swallowed the failure
148500
+ * in a bare catch, so a head that never moved was indistinguishable from a
148501
+ * head that went home.
148502
+ */
148503
+ homePresetId: string().default("").describe("Preset id ptz.goHome targets (empty = goHome refuses)"),
148321
148504
  host: string().describe("Camera IP or hostname"),
148322
148505
  port: number().default(9e3).describe("Baichuan port (default 9000)"),
148323
148506
  username: string().default("admin").describe("Username"),
@@ -149608,6 +149791,20 @@ function coerceNumber(value) {
149608
149791
  return null;
149609
149792
  }
149610
149793
  /**
149794
+ * Baichuan's native PTZ speed scale is 1..63; `runPtz` maps the cap's
149795
+ * normalized 0..1 onto it. Named because the autotrack loop has to know the
149796
+ * GRANULARITY of the knob it turns -- a normalized 0.37 means something very
149797
+ * different on 63 steps than on amcrest's 8.
149798
+ */
149799
+ var BAICHUAN_SPEED_STEPS = 63;
149800
+ /**
149801
+ * How long one `ptz.move` pulse runs, ms -- the `autoStopMs` handed to
149802
+ * Baichuan. It is the DENOMINATOR of "how far did the head travel per pulse",
149803
+ * so it is exported through `getOptions` rather than left a literal no caller
149804
+ * could read.
149805
+ */
149806
+ var PTZ_MOVE_IMPULSE_MS = 200;
149807
+ /**
149611
149808
  * Per-device transient diagnostics blob populated from the lib's
149612
149809
  * `getOnlineUserSessionsForUi` + `getSocketPoolSummary` +
149613
149810
  * `getSocketPoolCooldownStatus` calls. NOT persisted — recomputed on
@@ -149949,6 +150146,24 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
149949
150146
  * during `adoptDevice`). Standalone cameras default to 0 so the
149950
150147
  * pre-Hub behavior is preserved verbatim.
149951
150148
  */
150149
+ /**
150150
+ * Which preset `goHome` targets, and who decided it.
150151
+ *
150152
+ * Baichuan names no home of its own, so the operator's pick is the only
150153
+ * answer there can be. With none, `none` -- and `goHome` refuses rather than
150154
+ * moving the head to whatever happens to sit in slot 0.
150155
+ */
150156
+ async resolveHomePreset() {
150157
+ const chosen = this.config.get("homePresetId") ?? "";
150158
+ if (chosen === "") return {
150159
+ presetId: null,
150160
+ source: "none"
150161
+ };
150162
+ return {
150163
+ presetId: chosen,
150164
+ source: "operator"
150165
+ };
150166
+ }
149952
150167
  getChannel() {
149953
150168
  const ch = this.config.get("channel");
149954
150169
  if (typeof ch === "number" && ch >= 0) return ch;
@@ -153326,7 +153541,9 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
153326
153541
  const channel = this.getChannel();
153327
153542
  return (await api.getPtzPresets(channel)).map((p) => ({
153328
153543
  id: String(p.id),
153329
- name: p.name
153544
+ name: p.name,
153545
+ fn: null,
153546
+ writable: true
153330
153547
  }));
153331
153548
  } catch {
153332
153549
  return [];
@@ -153362,7 +153579,9 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
153362
153579
  hasTilt: false,
153363
153580
  hasZoom: false,
153364
153581
  supportsPresets: false,
153365
- hasAutofocus: false
153582
+ hasAutofocus: false,
153583
+ speedSteps: BAICHUAN_SPEED_STEPS,
153584
+ moveImpulseMs: PTZ_MOVE_IMPULSE_MS
153366
153585
  };
153367
153586
  const hasAutofocus = this.config.get("deviceCache")?.autoFocusSnapshot?.supported === true;
153368
153587
  return this.resolveCapOptions({
@@ -153375,7 +153594,9 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
153375
153594
  hasTilt: capabilities.hasTilt,
153376
153595
  hasZoom: capabilities.hasZoom,
153377
153596
  supportsPresets: capabilities.hasPresets,
153378
- hasAutofocus
153597
+ hasAutofocus,
153598
+ speedSteps: BAICHUAN_SPEED_STEPS,
153599
+ moveImpulseMs: PTZ_MOVE_IMPULSE_MS
153379
153600
  };
153380
153601
  },
153381
153602
  fallback: () => {
@@ -153385,42 +153606,57 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
153385
153606
  hasTilt: hasPtz,
153386
153607
  hasZoom: hasPtz,
153387
153608
  supportsPresets: hasPtz,
153388
- hasAutofocus
153609
+ hasAutofocus,
153610
+ speedSteps: BAICHUAN_SPEED_STEPS,
153611
+ moveImpulseMs: PTZ_MOVE_IMPULSE_MS
153389
153612
  };
153390
153613
  }
153391
153614
  });
153392
153615
  },
153393
153616
  goHome: async ({ deviceId }) => {
153394
153617
  if (deviceId !== this.id) return;
153395
- try {
153396
- const api = await this.ensureApi();
153397
- const channel = this.getChannel();
153398
- await api.moveToPtzPreset(channel, 0);
153399
- } catch {}
153618
+ const home = await this.resolveHomePreset();
153619
+ if (home.source === "none" || home.presetId === null) throw new Error("No home preset configured for this camera — set one in the PTZ settings");
153620
+ const id = Number(home.presetId);
153621
+ if (!Number.isFinite(id)) throw new Error(`Configured home preset is not a Baichuan id: ${home.presetId}`);
153622
+ const api = await this.ensureApi();
153623
+ const channel = this.getChannel();
153624
+ await api.moveToPtzPreset(channel, id);
153625
+ },
153626
+ getHomePreset: async ({ deviceId }) => {
153627
+ if (deviceId !== this.id) return {
153628
+ presetId: null,
153629
+ source: "none"
153630
+ };
153631
+ return this.resolveHomePreset();
153632
+ },
153633
+ setHomePreset: async ({ deviceId, presetId }) => {
153634
+ if (deviceId !== this.id) return;
153635
+ await this.config.setAll({ homePresetId: presetId ?? "" });
153400
153636
  },
153401
153637
  getPosition: async ({ deviceId }) => {
153402
153638
  if (deviceId !== this.id) return {
153403
- pan: 0,
153404
- tilt: 0,
153405
- zoom: 0
153639
+ pan: null,
153640
+ tilt: null,
153641
+ zoom: null
153406
153642
  };
153407
153643
  return {
153408
- pan: 0,
153409
- tilt: 0,
153410
- zoom: 0
153644
+ pan: null,
153645
+ tilt: null,
153646
+ zoom: null
153411
153647
  };
153412
153648
  },
153413
153649
  getStatus: async ({ deviceId }) => {
153414
153650
  if (deviceId !== this.id) return {
153415
- pan: 0,
153416
- tilt: 0,
153417
- zoom: 0,
153651
+ pan: null,
153652
+ tilt: null,
153653
+ zoom: null,
153418
153654
  autofocus: false
153419
153655
  };
153420
153656
  return {
153421
- pan: 0,
153422
- tilt: 0,
153423
- zoom: 0,
153657
+ pan: null,
153658
+ tilt: null,
153659
+ zoom: null,
153424
153660
  autofocus: (this.config.get("deviceCache")?.autoFocusSnapshot)?.enabled === true
153425
153661
  };
153426
153662
  },
@@ -153471,13 +153707,13 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
153471
153707
  if (tilt !== void 0 && Math.abs(tilt) > .01) cmds.push(tilt > 0 ? "Up" : "Down");
153472
153708
  if (zoom !== void 0 && Math.abs(zoom) > .01) await this.runZoom(api, channel, zoom);
153473
153709
  if (cmds.length === 0) return;
153474
- const baichuanSpeed = speed === void 0 ? 32 : Math.max(1, Math.min(63, Math.round(speed * 63)));
153710
+ const baichuanSpeed = speed === void 0 ? Math.round(BAICHUAN_SPEED_STEPS / 2) : Math.max(1, Math.min(BAICHUAN_SPEED_STEPS, Math.round(speed * BAICHUAN_SPEED_STEPS)));
153475
153711
  for (const command of cmds) try {
153476
153712
  await api.ptz(channel, {
153477
153713
  action: "start",
153478
153714
  command,
153479
153715
  speed: baichuanSpeed,
153480
- ...continuous ? {} : { autoStopMs: 200 }
153716
+ ...continuous ? {} : { autoStopMs: PTZ_MOVE_IMPULSE_MS }
153481
153717
  });
153482
153718
  } catch (err) {
153483
153719
  this.ctx.logger.warn("Baichuan ptz command failed", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-reolink",
3
- "version": "1.2.138",
3
+ "version": "1.2.139",
4
4
  "description": "Reolink camera device provider addon for CamStack — native Baichuan protocol",
5
5
  "keywords": [
6
6
  "camstack",