@camstack/addon-provider-reolink 1.2.125 → 1.2.127

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 +232 -43
  2. package/dist/addon.mjs +232 -43
  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-DEuj7E3j.mjs
5389
+ //#region ../types/dist/sleep-BEyvfshj.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).
@@ -22078,6 +22078,14 @@ var MotionSourceEnum = _enum([
22078
22078
  */
22079
22079
  var MotionSourcesSchema = array(MotionSourceEnum);
22080
22080
  /**
22081
+ * Which root detectors a camera runs. Deliberately the SAME vocabulary
22082
+ * post-analysis already tags every detection with (`DetectionSource`) rather
22083
+ * than a second spelling of the same two ideas — the value an operator picks
22084
+ * here is the value that comes back on the track, the overlay and the debug
22085
+ * row.
22086
+ */
22087
+ var DetectionSourcesSchema = array(DetectionSourceSchema);
22088
+ /**
22081
22089
  * Input shape for `pipeline-runner.reportMotion` cap method. Exported
22082
22090
  * so cap-side consumers (the orchestrator forward, the runner addon's
22083
22091
  * cap implementation, tests) can reuse the type instead of redeclaring
@@ -22228,6 +22236,35 @@ var RunnerCameraConfigSchema = object({
22228
22236
  * events; the orchestrator forwards to `reportMotion`.
22229
22237
  */
22230
22238
  motionSources: MotionSourcesSchema.default(["analyzer"]),
22239
+ /**
22240
+ * WHICH root detector runs for this camera. The detection counterpart of
22241
+ * {@link motionSources}, same shape and same discipline: a per-camera list
22242
+ * of sources the system understands, vendor-agnostic, owned here.
22243
+ *
22244
+ * - `pipeline` — this runner's own root step on decoded pixels (`steps`).
22245
+ * - `onboard` — the CAMERA's own detector, admitted only when it is named
22246
+ * here. Its boxes reach post-analysis on the same
22247
+ * `PipelineInferenceResult` payload as the pipeline's, so an onboard-born
22248
+ * track gets the same tracker, the same overlay and the same detail
22249
+ * subtree (face / plate / embedding).
22250
+ *
22251
+ * `['onboard']` alone is the REPLACEMENT: this runner's root step does not
22252
+ * run, and that is where a camera with a usable onboard detector buys back
22253
+ * its share of the accelerator. It saves the root inference, not the decode
22254
+ * — the detail subtree still needs pixels to cut crops from.
22255
+ *
22256
+ * A camera whose onboard detections carry no geometry cannot serve as a root
22257
+ * step at all (there is nothing to track), and the cap that knows this says
22258
+ * so on `native-object-detection.getOptions().geometry`. On Reolink that is
22259
+ * every battery camera: the boxes ride a sub-stream a battery camera never
22260
+ * attaches, so such a camera stays on `['pipeline']` and its onboard AI is
22261
+ * worth exactly what it already is — a wake signal with a class.
22262
+ *
22263
+ * Defaults to `['pipeline']`, which is byte-for-byte today's behaviour: a
22264
+ * vendor never turns a detector on for the operator, and neither does a
22265
+ * default.
22266
+ */
22267
+ detectionSources: DetectionSourcesSchema.default(["pipeline"]),
22231
22268
  pipelineEnabled: boolean().default(true),
22232
22269
  /** Ordered tree of video steps. Absent → runner skips video detection. */
22233
22270
  steps: array(PipelineStepInputSchema).readonly().optional(),
@@ -29343,11 +29380,46 @@ var NativeObjectDetectionStatusSchema = object({
29343
29380
  supportedClasses: array(NativeObjectClassEnum).readonly(),
29344
29381
  /**
29345
29382
  * Whether forwarding of onboard AI detections is enabled for this device.
29346
- * Default FALSE (opt-in, cold-start) — onboard AI pushes are noisy/sparse and
29347
- * churn the tracker, so forwarding stays off until the operator enables it.
29383
+ *
29384
+ * Cold-start OFF on every camera. A vendor does not decide which detector a
29385
+ * camera runs — that is the operator's choice, and it is made where every
29386
+ * other detection choice is made. `geometry` on
29387
+ * {@link NativeObjectDetectionOptionsSchema} is how the form says what the
29388
+ * choice would buy this particular camera.
29348
29389
  */
29349
29390
  enabled: boolean()
29350
29391
  });
29392
+ /**
29393
+ * WHEN a camera's onboard detections carry geometry.
29394
+ *
29395
+ * - `boxed` — always. The provider holds a standing channel for the boxes, so
29396
+ * a detection is a trackable subject whenever the camera sees one.
29397
+ * - `while-streaming` — only while some stream of this camera is already open.
29398
+ * The boxes ride the video's own side-channel rather than a feed of their
29399
+ * own, so they cost nothing and they exist only when something is pulling.
29400
+ * On Reolink this is every BATTERY camera: a persistent second feed is real
29401
+ * radio drain, but a camera that is awake is awake precisely because
29402
+ * something is in front of it, and its stream is already being pulled.
29403
+ * - `flag-only` — never. The firmware ships the class and nothing else; the
29404
+ * pipeline needs geometry to make a subject, so such a push reaches the
29405
+ * tracker as motion and no further.
29406
+ *
29407
+ * Reported and not inferred, because the operator's question in front of the
29408
+ * picker is "what do I get", and these are three different answers (D14: the
29409
+ * derived form is only as honest as `getOptions`).
29410
+ */
29411
+ var NativeObjectGeometryEnum = _enum([
29412
+ "boxed",
29413
+ "while-streaming",
29414
+ "flag-only"
29415
+ ]);
29416
+ var NativeObjectDetectionOptionsSchema = object({
29417
+ /** Classes this firmware can detect — the same list the status reports. */
29418
+ supportedClasses: array(NativeObjectClassEnum).readonly(),
29419
+ /** What a detection from this camera carries. */
29420
+ geometry: NativeObjectGeometryEnum
29421
+ });
29422
+ var NativeObjectDetectionSettingsPatchSchema = object({ enabled: boolean().optional() });
29351
29423
  var NativeObjectDetectionRuntimeStateSchema = NativeObjectDetectionStatusSchema.extend({
29352
29424
  /** Required by createRuntimeStateBridge — epoch ms of last refresh. */
29353
29425
  lastFetchedAt: number() });
@@ -29357,13 +29429,23 @@ var nativeObjectDetectionCapability = {
29357
29429
  deviceNative: true,
29358
29430
  mode: "singleton",
29359
29431
  deviceTypes: [DeviceType.Camera],
29360
- methods: { setEnabled: method(object({
29361
- deviceId: number(),
29362
- enabled: boolean()
29363
- }), _void(), {
29364
- kind: "mutation",
29365
- auth: "admin"
29366
- }) },
29432
+ methods: {
29433
+ getOptions: method(object({ deviceId: number() }), NativeObjectDetectionOptionsSchema),
29434
+ setSettings: method(object({
29435
+ deviceId: number(),
29436
+ settings: NativeObjectDetectionSettingsPatchSchema
29437
+ }), _void(), {
29438
+ kind: "mutation",
29439
+ auth: "admin"
29440
+ }),
29441
+ setEnabled: method(object({
29442
+ deviceId: number(),
29443
+ enabled: boolean()
29444
+ }), _void(), {
29445
+ kind: "mutation",
29446
+ auth: "admin"
29447
+ })
29448
+ },
29367
29449
  events: { onDetected: { data: object({
29368
29450
  deviceId: number(),
29369
29451
  detection: NativeDetectionSchema
@@ -39216,12 +39298,24 @@ Object.freeze({
39216
39298
  addonId: null,
39217
39299
  access: "create"
39218
39300
  },
39301
+ "nativeObjectDetection.getOptions": {
39302
+ capName: "native-object-detection",
39303
+ capScope: "device",
39304
+ addonId: null,
39305
+ access: "view"
39306
+ },
39219
39307
  "nativeObjectDetection.setEnabled": {
39220
39308
  capName: "native-object-detection",
39221
39309
  capScope: "device",
39222
39310
  addonId: null,
39223
39311
  access: "create"
39224
39312
  },
39313
+ "nativeObjectDetection.setSettings": {
39314
+ capName: "native-object-detection",
39315
+ capScope: "device",
39316
+ addonId: null,
39317
+ access: "create"
39318
+ },
39225
39319
  "navigation.getFeatures": {
39226
39320
  capName: "navigation",
39227
39321
  capScope: "device",
@@ -43448,11 +43542,21 @@ Object.freeze({
43448
43542
  form: "single",
43449
43543
  optional: false
43450
43544
  }],
43545
+ "nativeObjectDetection.getOptions": [{
43546
+ name: "deviceId",
43547
+ form: "single",
43548
+ optional: false
43549
+ }],
43451
43550
  "nativeObjectDetection.setEnabled": [{
43452
43551
  name: "deviceId",
43453
43552
  form: "single",
43454
43553
  optional: false
43455
43554
  }],
43555
+ "nativeObjectDetection.setSettings": [{
43556
+ name: "deviceId",
43557
+ form: "single",
43558
+ optional: false
43559
+ }],
43456
43560
  "navigation.getFeatures": [{
43457
43561
  name: "deviceId",
43458
43562
  form: "single",
@@ -147632,10 +147736,22 @@ var REOLINK_ADDON_ID = "provider-reolink";
147632
147736
  * CameraNativeDetection class strings the rest of camstack consumes.
147633
147737
  * Keeping the loose-string contract — face/package etc are valid.
147634
147738
  */
147739
+ /**
147740
+ * Baichuan AI class name → the cap's `NativeObjectClass`.
147741
+ *
147742
+ * TWO vocabularies arrive here and both must be keyed. The simple-event push
147743
+ * (cmd 33 `<AItype>`) says `animal`; the capability probe (cmd 299
147744
+ * `getAiDetectTypes`) says `dog_cat` for the same class. Keying only the first
147745
+ * made `buildSupportedClasses` drop it, so camera 618 reported
147746
+ * `supportedClasses: ["person","vehicle"]` while its own `lastByClass` held a
147747
+ * live `animal` detection — the camera detected a class the system said it
147748
+ * could not. The two key-spaces do not collide, so one map serves both.
147749
+ */
147635
147750
  var AI_CLASS_MAP = {
147636
147751
  people: "person",
147637
147752
  vehicle: "vehicle",
147638
147753
  animal: "animal",
147754
+ dog_cat: "animal",
147639
147755
  face: "face",
147640
147756
  package: "package"
147641
147757
  };
@@ -148476,6 +148592,30 @@ function mapDetectionEvent(event, cameraId, nowMs) {
148476
148592
  function isNativeObjectForwardingEnabled(capState) {
148477
148593
  return capState?.enabled === true;
148478
148594
  }
148595
+ /**
148596
+ * The classes this firmware can detect, from the cmd-299 probe
148597
+ * (`deviceCache.aiDetectTypes`), mapped through {@link AI_CLASS_MAP}.
148598
+ *
148599
+ * Module-level and exported because the version of this that lived as a
148600
+ * closure inside `registerNativeObjectDetectionCap` had a test that
148601
+ * RE-IMPLEMENTED it — and the copy encoded the `dog_cat` gap as intent
148602
+ * ("skips unknown Baichuan type names"), so the suite stayed green while
148603
+ * camera 618 reported it could not detect a class it was detecting. One
148604
+ * function, one test, no copy.
148605
+ *
148606
+ * An absent probe is not an empty answer: it means nobody has asked the camera
148607
+ * yet, and the honest fallback is the full mapped value-set rather than "this
148608
+ * camera detects nothing".
148609
+ */
148610
+ function buildSupportedNativeClasses(aiDetectTypes, isSupportedClass) {
148611
+ if (!aiDetectTypes || aiDetectTypes.length === 0) return [...new Set(Object.values(AI_CLASS_MAP))].filter(isSupportedClass);
148612
+ const classes = [];
148613
+ for (const libName of aiDetectTypes) {
148614
+ const mapped = AI_CLASS_MAP[libName];
148615
+ if (mapped !== void 0 && isSupportedClass(mapped) && !classes.includes(mapped)) classes.push(mapped);
148616
+ }
148617
+ return classes;
148618
+ }
148479
148619
  function formatUserLevel(level) {
148480
148620
  if (level === void 0 || level === null) return "user";
148481
148621
  if (typeof level === "string" && level.length > 0) return level;
@@ -149161,6 +149301,13 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
149161
149301
  loginPromise = null;
149162
149302
  /** True once we've discovered (via getBatteryInfo) that this is a battery cam. */
149163
149303
  isBattery = false;
149304
+ /**
149305
+ * When each AI class last had its suppression reported, so the gate's drop is
149306
+ * on the record without one line per push. Per class because the vocabulary
149307
+ * is five values and the finding is WHICH class the camera is pushing that
149308
+ * nothing downstream receives.
149309
+ */
149310
+ nativeDetectionSuppressedAt = /* @__PURE__ */ new Map();
149164
149311
  /** Reconnect attempt counter — drives exponential backoff. */
149165
149312
  reconnectAttempts = 0;
149166
149313
  /** Pending reconnect timer; cleared on successful reconnect or removeDevice. */
@@ -152373,50 +152520,78 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
152373
152520
  * yet (e.g. fresh device, no config cached).
152374
152521
  * - `lastByClass`: updated in `handleSimpleEvent` every time an AI-class
152375
152522
  * event fires, regardless of the `enabled` flag.
152376
- * - `enabled`: operator toggle. Cold-start default `true` detections
152377
- * flow unconditionally before a value is saved, preserving the existing
152378
- * behaviour.
152523
+ * - `enabled`: operator toggle, cold-start OFF on every camera. A vendor
152524
+ * never turns a detector on for the operator; see `buildEmptyState`.
152379
152525
  *
152380
152526
  * Follows the trampoline pattern from `registerStreamParamsCap` — no
152381
152527
  * parallel private fields; all state lives in the kernel runtimeState
152382
152528
  * slice.
152383
152529
  */
152530
+ /**
152531
+ * Report — once a minute per class — that the forwarding gate dropped an
152532
+ * onboard AI detection. A branch that drops work names it (D391): without
152533
+ * this the toggle being off looks exactly like a camera that pushes nothing,
152534
+ * and the operator's only symptom is silence.
152535
+ */
152536
+ noteNativeDetectionSuppressed(aiClass) {
152537
+ const now = Date.now();
152538
+ if (now - (this.nativeDetectionSuppressedAt.get(aiClass) ?? 0) < 6e4) return;
152539
+ this.nativeDetectionSuppressedAt.set(aiClass, now);
152540
+ this.ctx.logger.info("Reolink onboard AI detection dropped — forwarding is off for this camera", {
152541
+ tags: { deviceId: this.id },
152542
+ meta: {
152543
+ class: aiClass,
152544
+ battery: this.isBattery
152545
+ }
152546
+ });
152547
+ }
152384
152548
  registerNativeObjectDetectionCap() {
152385
152549
  const CAP_NAME = "native-object-detection";
152386
- const buildSupportedClasses = () => {
152387
- const cached = this.config.get("deviceCache")?.aiDetectTypes;
152388
- if (!cached || cached.length === 0) return Object.values(AI_CLASS_MAP).filter(isNativeObjectClass);
152389
- const classes = [];
152390
- for (const libName of cached) {
152391
- const mapped = AI_CLASS_MAP[libName];
152392
- if (mapped !== void 0 && isNativeObjectClass(mapped)) classes.push(mapped);
152393
- }
152394
- return classes;
152395
- };
152550
+ const buildSupportedClasses = () => buildSupportedNativeClasses(this.config.get("deviceCache")?.aiDetectTypes, isNativeObjectClass).filter(isNativeObjectClass);
152396
152551
  const buildEmptyState = () => ({
152397
152552
  enabled: false,
152398
152553
  lastByClass: {},
152399
152554
  supportedClasses: buildSupportedClasses(),
152400
152555
  lastFetchedAt: 0
152401
152556
  });
152557
+ const bridge = createRuntimeStateBridge({
152558
+ runtimeState: this.runtimeState,
152559
+ cap: nativeObjectDetectionCapability,
152560
+ ownDeviceId: this.id,
152561
+ refresh: async () => {},
152562
+ staleMs: Infinity,
152563
+ empty: buildEmptyState
152564
+ });
152565
+ /** Shared by `setEnabled` and the derived form's `setSettings`. */
152566
+ const applyEnabled = async (enabled) => {
152567
+ await this.runtimeState.patchCapState(CAP_NAME, { enabled });
152568
+ const api = this.api;
152569
+ if (!api) return;
152570
+ if (enabled) await this.resubscribeObjectDetections(api, "cap-enabled").catch((err) => {
152571
+ this.ctx.logger.debug("Reolink objectDetections re-arm on enable failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
152572
+ });
152573
+ else await this.unsubscribeObjectDetections(api);
152574
+ };
152402
152575
  const provider = {
152403
- getStatus: createRuntimeStateBridge({
152404
- runtimeState: this.runtimeState,
152405
- cap: nativeObjectDetectionCapability,
152406
- ownDeviceId: this.id,
152407
- refresh: async () => {},
152408
- staleMs: Infinity,
152409
- empty: buildEmptyState
152410
- }).getStatus,
152576
+ getStatus: bridge.getStatus,
152577
+ getOptions: async ({ deviceId }) => {
152578
+ if (deviceId !== this.id) return {
152579
+ supportedClasses: [],
152580
+ geometry: "flag-only"
152581
+ };
152582
+ return {
152583
+ supportedClasses: buildSupportedClasses(),
152584
+ geometry: this.isBattery ? "while-streaming" : "boxed"
152585
+ };
152586
+ },
152587
+ setSettings: async ({ deviceId, settings }) => {
152588
+ if (deviceId !== this.id) return;
152589
+ if (settings.enabled === void 0) return;
152590
+ await applyEnabled(settings.enabled);
152591
+ },
152411
152592
  setEnabled: async ({ deviceId, enabled }) => {
152412
152593
  if (deviceId !== this.id) return;
152413
- await this.runtimeState.patchCapState(CAP_NAME, { enabled });
152414
- const api = this.api;
152415
- if (!api) return;
152416
- if (enabled) await this.resubscribeObjectDetections(api, "cap-enabled").catch((err) => {
152417
- this.ctx.logger.debug("Reolink objectDetections re-arm on enable failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
152418
- });
152419
- else await this.unsubscribeObjectDetections(api);
152594
+ await applyEnabled(enabled);
152420
152595
  }
152421
152596
  };
152422
152597
  this.ctx.registerNativeCap(nativeObjectDetectionCapability, provider);
@@ -154047,14 +154222,22 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
154047
154222
  });
154048
154223
  } catch {}
154049
154224
  this.objectDetectionsSubscribed = false;
154050
- if (this.isBattery) {
154051
- this.ctx.logger.debug("Reolink objectDetections subscribe skipped — battery cam", { meta: { reason } });
154052
- return;
154053
- }
154054
154225
  if (!isNativeObjectForwardingEnabled(this.runtimeState.getCapState("native-object-detection"))) {
154055
154226
  this.ctx.logger.debug("Reolink objectDetections subscribe skipped — cap disabled", { meta: { reason } });
154056
154227
  return;
154057
154228
  }
154229
+ if (this.isBattery) {
154230
+ api.onDetection(this.handleObjectDetectionsBound);
154231
+ this.objectDetectionsSubscribed = true;
154232
+ this.ctx.logger.info("Reolink onboard boxes ride the open stream (battery cam)", {
154233
+ tags: { deviceId: this.id },
154234
+ meta: {
154235
+ reason,
154236
+ channel
154237
+ }
154238
+ });
154239
+ return;
154240
+ }
154058
154241
  if (!api.client.isSocketConnected() || !api.client.loggedIn) {
154059
154242
  this.ctx.logger.debug("Reolink objectDetections subscribe skipped — socket not ready", { meta: {
154060
154243
  reason,
@@ -154080,6 +154263,11 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
154080
154263
  */
154081
154264
  async unsubscribeObjectDetections(api) {
154082
154265
  if (!this.objectDetectionsSubscribed) return;
154266
+ if (this.isBattery) {
154267
+ api.offDetection(this.handleObjectDetectionsBound);
154268
+ this.objectDetectionsSubscribed = false;
154269
+ return;
154270
+ }
154083
154271
  try {
154084
154272
  await api.offObjectDetections(this.handleObjectDetectionsBound, {
154085
154273
  channel: this.getChannel(),
@@ -154109,6 +154297,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
154109
154297
  */
154110
154298
  handleObjectDetections(event) {
154111
154299
  if (!isNativeObjectForwardingEnabled(this.runtimeState.getCapState("native-object-detection"))) return;
154300
+ if (event.channel !== this.getChannel()) return;
154112
154301
  if (event.boxes.length === 0) return;
154113
154302
  const now = Date.now();
154114
154303
  const payload = mapDetectionEvent(event, this.id, now);
@@ -155796,7 +155985,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
155796
155985
  timestamp: now
155797
155986
  }]
155798
155987
  }));
155799
- }
155988
+ } else this.noteNativeDetectionSuppressed(aiClass);
155800
155989
  this.ctx.eventBus.emit(createEvent(EventCategory.MotionOnMotionChanged, eventSource, {
155801
155990
  deviceId: this.id,
155802
155991
  detected: true,
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-DEuj7E3j.mjs
5384
+ //#region ../types/dist/sleep-BEyvfshj.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).
@@ -22073,6 +22073,14 @@ var MotionSourceEnum = _enum([
22073
22073
  */
22074
22074
  var MotionSourcesSchema = array(MotionSourceEnum);
22075
22075
  /**
22076
+ * Which root detectors a camera runs. Deliberately the SAME vocabulary
22077
+ * post-analysis already tags every detection with (`DetectionSource`) rather
22078
+ * than a second spelling of the same two ideas — the value an operator picks
22079
+ * here is the value that comes back on the track, the overlay and the debug
22080
+ * row.
22081
+ */
22082
+ var DetectionSourcesSchema = array(DetectionSourceSchema);
22083
+ /**
22076
22084
  * Input shape for `pipeline-runner.reportMotion` cap method. Exported
22077
22085
  * so cap-side consumers (the orchestrator forward, the runner addon's
22078
22086
  * cap implementation, tests) can reuse the type instead of redeclaring
@@ -22223,6 +22231,35 @@ var RunnerCameraConfigSchema = object({
22223
22231
  * events; the orchestrator forwards to `reportMotion`.
22224
22232
  */
22225
22233
  motionSources: MotionSourcesSchema.default(["analyzer"]),
22234
+ /**
22235
+ * WHICH root detector runs for this camera. The detection counterpart of
22236
+ * {@link motionSources}, same shape and same discipline: a per-camera list
22237
+ * of sources the system understands, vendor-agnostic, owned here.
22238
+ *
22239
+ * - `pipeline` — this runner's own root step on decoded pixels (`steps`).
22240
+ * - `onboard` — the CAMERA's own detector, admitted only when it is named
22241
+ * here. Its boxes reach post-analysis on the same
22242
+ * `PipelineInferenceResult` payload as the pipeline's, so an onboard-born
22243
+ * track gets the same tracker, the same overlay and the same detail
22244
+ * subtree (face / plate / embedding).
22245
+ *
22246
+ * `['onboard']` alone is the REPLACEMENT: this runner's root step does not
22247
+ * run, and that is where a camera with a usable onboard detector buys back
22248
+ * its share of the accelerator. It saves the root inference, not the decode
22249
+ * — the detail subtree still needs pixels to cut crops from.
22250
+ *
22251
+ * A camera whose onboard detections carry no geometry cannot serve as a root
22252
+ * step at all (there is nothing to track), and the cap that knows this says
22253
+ * so on `native-object-detection.getOptions().geometry`. On Reolink that is
22254
+ * every battery camera: the boxes ride a sub-stream a battery camera never
22255
+ * attaches, so such a camera stays on `['pipeline']` and its onboard AI is
22256
+ * worth exactly what it already is — a wake signal with a class.
22257
+ *
22258
+ * Defaults to `['pipeline']`, which is byte-for-byte today's behaviour: a
22259
+ * vendor never turns a detector on for the operator, and neither does a
22260
+ * default.
22261
+ */
22262
+ detectionSources: DetectionSourcesSchema.default(["pipeline"]),
22226
22263
  pipelineEnabled: boolean().default(true),
22227
22264
  /** Ordered tree of video steps. Absent → runner skips video detection. */
22228
22265
  steps: array(PipelineStepInputSchema).readonly().optional(),
@@ -29338,11 +29375,46 @@ var NativeObjectDetectionStatusSchema = object({
29338
29375
  supportedClasses: array(NativeObjectClassEnum).readonly(),
29339
29376
  /**
29340
29377
  * Whether forwarding of onboard AI detections is enabled for this device.
29341
- * Default FALSE (opt-in, cold-start) — onboard AI pushes are noisy/sparse and
29342
- * churn the tracker, so forwarding stays off until the operator enables it.
29378
+ *
29379
+ * Cold-start OFF on every camera. A vendor does not decide which detector a
29380
+ * camera runs — that is the operator's choice, and it is made where every
29381
+ * other detection choice is made. `geometry` on
29382
+ * {@link NativeObjectDetectionOptionsSchema} is how the form says what the
29383
+ * choice would buy this particular camera.
29343
29384
  */
29344
29385
  enabled: boolean()
29345
29386
  });
29387
+ /**
29388
+ * WHEN a camera's onboard detections carry geometry.
29389
+ *
29390
+ * - `boxed` — always. The provider holds a standing channel for the boxes, so
29391
+ * a detection is a trackable subject whenever the camera sees one.
29392
+ * - `while-streaming` — only while some stream of this camera is already open.
29393
+ * The boxes ride the video's own side-channel rather than a feed of their
29394
+ * own, so they cost nothing and they exist only when something is pulling.
29395
+ * On Reolink this is every BATTERY camera: a persistent second feed is real
29396
+ * radio drain, but a camera that is awake is awake precisely because
29397
+ * something is in front of it, and its stream is already being pulled.
29398
+ * - `flag-only` — never. The firmware ships the class and nothing else; the
29399
+ * pipeline needs geometry to make a subject, so such a push reaches the
29400
+ * tracker as motion and no further.
29401
+ *
29402
+ * Reported and not inferred, because the operator's question in front of the
29403
+ * picker is "what do I get", and these are three different answers (D14: the
29404
+ * derived form is only as honest as `getOptions`).
29405
+ */
29406
+ var NativeObjectGeometryEnum = _enum([
29407
+ "boxed",
29408
+ "while-streaming",
29409
+ "flag-only"
29410
+ ]);
29411
+ var NativeObjectDetectionOptionsSchema = object({
29412
+ /** Classes this firmware can detect — the same list the status reports. */
29413
+ supportedClasses: array(NativeObjectClassEnum).readonly(),
29414
+ /** What a detection from this camera carries. */
29415
+ geometry: NativeObjectGeometryEnum
29416
+ });
29417
+ var NativeObjectDetectionSettingsPatchSchema = object({ enabled: boolean().optional() });
29346
29418
  var NativeObjectDetectionRuntimeStateSchema = NativeObjectDetectionStatusSchema.extend({
29347
29419
  /** Required by createRuntimeStateBridge — epoch ms of last refresh. */
29348
29420
  lastFetchedAt: number() });
@@ -29352,13 +29424,23 @@ var nativeObjectDetectionCapability = {
29352
29424
  deviceNative: true,
29353
29425
  mode: "singleton",
29354
29426
  deviceTypes: [DeviceType.Camera],
29355
- methods: { setEnabled: method(object({
29356
- deviceId: number(),
29357
- enabled: boolean()
29358
- }), _void(), {
29359
- kind: "mutation",
29360
- auth: "admin"
29361
- }) },
29427
+ methods: {
29428
+ getOptions: method(object({ deviceId: number() }), NativeObjectDetectionOptionsSchema),
29429
+ setSettings: method(object({
29430
+ deviceId: number(),
29431
+ settings: NativeObjectDetectionSettingsPatchSchema
29432
+ }), _void(), {
29433
+ kind: "mutation",
29434
+ auth: "admin"
29435
+ }),
29436
+ setEnabled: method(object({
29437
+ deviceId: number(),
29438
+ enabled: boolean()
29439
+ }), _void(), {
29440
+ kind: "mutation",
29441
+ auth: "admin"
29442
+ })
29443
+ },
29362
29444
  events: { onDetected: { data: object({
29363
29445
  deviceId: number(),
29364
29446
  detection: NativeDetectionSchema
@@ -39211,12 +39293,24 @@ Object.freeze({
39211
39293
  addonId: null,
39212
39294
  access: "create"
39213
39295
  },
39296
+ "nativeObjectDetection.getOptions": {
39297
+ capName: "native-object-detection",
39298
+ capScope: "device",
39299
+ addonId: null,
39300
+ access: "view"
39301
+ },
39214
39302
  "nativeObjectDetection.setEnabled": {
39215
39303
  capName: "native-object-detection",
39216
39304
  capScope: "device",
39217
39305
  addonId: null,
39218
39306
  access: "create"
39219
39307
  },
39308
+ "nativeObjectDetection.setSettings": {
39309
+ capName: "native-object-detection",
39310
+ capScope: "device",
39311
+ addonId: null,
39312
+ access: "create"
39313
+ },
39220
39314
  "navigation.getFeatures": {
39221
39315
  capName: "navigation",
39222
39316
  capScope: "device",
@@ -43443,11 +43537,21 @@ Object.freeze({
43443
43537
  form: "single",
43444
43538
  optional: false
43445
43539
  }],
43540
+ "nativeObjectDetection.getOptions": [{
43541
+ name: "deviceId",
43542
+ form: "single",
43543
+ optional: false
43544
+ }],
43446
43545
  "nativeObjectDetection.setEnabled": [{
43447
43546
  name: "deviceId",
43448
43547
  form: "single",
43449
43548
  optional: false
43450
43549
  }],
43550
+ "nativeObjectDetection.setSettings": [{
43551
+ name: "deviceId",
43552
+ form: "single",
43553
+ optional: false
43554
+ }],
43451
43555
  "navigation.getFeatures": [{
43452
43556
  name: "deviceId",
43453
43557
  form: "single",
@@ -147627,10 +147731,22 @@ var REOLINK_ADDON_ID = "provider-reolink";
147627
147731
  * CameraNativeDetection class strings the rest of camstack consumes.
147628
147732
  * Keeping the loose-string contract — face/package etc are valid.
147629
147733
  */
147734
+ /**
147735
+ * Baichuan AI class name → the cap's `NativeObjectClass`.
147736
+ *
147737
+ * TWO vocabularies arrive here and both must be keyed. The simple-event push
147738
+ * (cmd 33 `<AItype>`) says `animal`; the capability probe (cmd 299
147739
+ * `getAiDetectTypes`) says `dog_cat` for the same class. Keying only the first
147740
+ * made `buildSupportedClasses` drop it, so camera 618 reported
147741
+ * `supportedClasses: ["person","vehicle"]` while its own `lastByClass` held a
147742
+ * live `animal` detection — the camera detected a class the system said it
147743
+ * could not. The two key-spaces do not collide, so one map serves both.
147744
+ */
147630
147745
  var AI_CLASS_MAP = {
147631
147746
  people: "person",
147632
147747
  vehicle: "vehicle",
147633
147748
  animal: "animal",
147749
+ dog_cat: "animal",
147634
147750
  face: "face",
147635
147751
  package: "package"
147636
147752
  };
@@ -148471,6 +148587,30 @@ function mapDetectionEvent(event, cameraId, nowMs) {
148471
148587
  function isNativeObjectForwardingEnabled(capState) {
148472
148588
  return capState?.enabled === true;
148473
148589
  }
148590
+ /**
148591
+ * The classes this firmware can detect, from the cmd-299 probe
148592
+ * (`deviceCache.aiDetectTypes`), mapped through {@link AI_CLASS_MAP}.
148593
+ *
148594
+ * Module-level and exported because the version of this that lived as a
148595
+ * closure inside `registerNativeObjectDetectionCap` had a test that
148596
+ * RE-IMPLEMENTED it — and the copy encoded the `dog_cat` gap as intent
148597
+ * ("skips unknown Baichuan type names"), so the suite stayed green while
148598
+ * camera 618 reported it could not detect a class it was detecting. One
148599
+ * function, one test, no copy.
148600
+ *
148601
+ * An absent probe is not an empty answer: it means nobody has asked the camera
148602
+ * yet, and the honest fallback is the full mapped value-set rather than "this
148603
+ * camera detects nothing".
148604
+ */
148605
+ function buildSupportedNativeClasses(aiDetectTypes, isSupportedClass) {
148606
+ if (!aiDetectTypes || aiDetectTypes.length === 0) return [...new Set(Object.values(AI_CLASS_MAP))].filter(isSupportedClass);
148607
+ const classes = [];
148608
+ for (const libName of aiDetectTypes) {
148609
+ const mapped = AI_CLASS_MAP[libName];
148610
+ if (mapped !== void 0 && isSupportedClass(mapped) && !classes.includes(mapped)) classes.push(mapped);
148611
+ }
148612
+ return classes;
148613
+ }
148474
148614
  function formatUserLevel(level) {
148475
148615
  if (level === void 0 || level === null) return "user";
148476
148616
  if (typeof level === "string" && level.length > 0) return level;
@@ -149156,6 +149296,13 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
149156
149296
  loginPromise = null;
149157
149297
  /** True once we've discovered (via getBatteryInfo) that this is a battery cam. */
149158
149298
  isBattery = false;
149299
+ /**
149300
+ * When each AI class last had its suppression reported, so the gate's drop is
149301
+ * on the record without one line per push. Per class because the vocabulary
149302
+ * is five values and the finding is WHICH class the camera is pushing that
149303
+ * nothing downstream receives.
149304
+ */
149305
+ nativeDetectionSuppressedAt = /* @__PURE__ */ new Map();
149159
149306
  /** Reconnect attempt counter — drives exponential backoff. */
149160
149307
  reconnectAttempts = 0;
149161
149308
  /** Pending reconnect timer; cleared on successful reconnect or removeDevice. */
@@ -152368,50 +152515,78 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
152368
152515
  * yet (e.g. fresh device, no config cached).
152369
152516
  * - `lastByClass`: updated in `handleSimpleEvent` every time an AI-class
152370
152517
  * event fires, regardless of the `enabled` flag.
152371
- * - `enabled`: operator toggle. Cold-start default `true` detections
152372
- * flow unconditionally before a value is saved, preserving the existing
152373
- * behaviour.
152518
+ * - `enabled`: operator toggle, cold-start OFF on every camera. A vendor
152519
+ * never turns a detector on for the operator; see `buildEmptyState`.
152374
152520
  *
152375
152521
  * Follows the trampoline pattern from `registerStreamParamsCap` — no
152376
152522
  * parallel private fields; all state lives in the kernel runtimeState
152377
152523
  * slice.
152378
152524
  */
152525
+ /**
152526
+ * Report — once a minute per class — that the forwarding gate dropped an
152527
+ * onboard AI detection. A branch that drops work names it (D391): without
152528
+ * this the toggle being off looks exactly like a camera that pushes nothing,
152529
+ * and the operator's only symptom is silence.
152530
+ */
152531
+ noteNativeDetectionSuppressed(aiClass) {
152532
+ const now = Date.now();
152533
+ if (now - (this.nativeDetectionSuppressedAt.get(aiClass) ?? 0) < 6e4) return;
152534
+ this.nativeDetectionSuppressedAt.set(aiClass, now);
152535
+ this.ctx.logger.info("Reolink onboard AI detection dropped — forwarding is off for this camera", {
152536
+ tags: { deviceId: this.id },
152537
+ meta: {
152538
+ class: aiClass,
152539
+ battery: this.isBattery
152540
+ }
152541
+ });
152542
+ }
152379
152543
  registerNativeObjectDetectionCap() {
152380
152544
  const CAP_NAME = "native-object-detection";
152381
- const buildSupportedClasses = () => {
152382
- const cached = this.config.get("deviceCache")?.aiDetectTypes;
152383
- if (!cached || cached.length === 0) return Object.values(AI_CLASS_MAP).filter(isNativeObjectClass);
152384
- const classes = [];
152385
- for (const libName of cached) {
152386
- const mapped = AI_CLASS_MAP[libName];
152387
- if (mapped !== void 0 && isNativeObjectClass(mapped)) classes.push(mapped);
152388
- }
152389
- return classes;
152390
- };
152545
+ const buildSupportedClasses = () => buildSupportedNativeClasses(this.config.get("deviceCache")?.aiDetectTypes, isNativeObjectClass).filter(isNativeObjectClass);
152391
152546
  const buildEmptyState = () => ({
152392
152547
  enabled: false,
152393
152548
  lastByClass: {},
152394
152549
  supportedClasses: buildSupportedClasses(),
152395
152550
  lastFetchedAt: 0
152396
152551
  });
152552
+ const bridge = createRuntimeStateBridge({
152553
+ runtimeState: this.runtimeState,
152554
+ cap: nativeObjectDetectionCapability,
152555
+ ownDeviceId: this.id,
152556
+ refresh: async () => {},
152557
+ staleMs: Infinity,
152558
+ empty: buildEmptyState
152559
+ });
152560
+ /** Shared by `setEnabled` and the derived form's `setSettings`. */
152561
+ const applyEnabled = async (enabled) => {
152562
+ await this.runtimeState.patchCapState(CAP_NAME, { enabled });
152563
+ const api = this.api;
152564
+ if (!api) return;
152565
+ if (enabled) await this.resubscribeObjectDetections(api, "cap-enabled").catch((err) => {
152566
+ this.ctx.logger.debug("Reolink objectDetections re-arm on enable failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
152567
+ });
152568
+ else await this.unsubscribeObjectDetections(api);
152569
+ };
152397
152570
  const provider = {
152398
- getStatus: createRuntimeStateBridge({
152399
- runtimeState: this.runtimeState,
152400
- cap: nativeObjectDetectionCapability,
152401
- ownDeviceId: this.id,
152402
- refresh: async () => {},
152403
- staleMs: Infinity,
152404
- empty: buildEmptyState
152405
- }).getStatus,
152571
+ getStatus: bridge.getStatus,
152572
+ getOptions: async ({ deviceId }) => {
152573
+ if (deviceId !== this.id) return {
152574
+ supportedClasses: [],
152575
+ geometry: "flag-only"
152576
+ };
152577
+ return {
152578
+ supportedClasses: buildSupportedClasses(),
152579
+ geometry: this.isBattery ? "while-streaming" : "boxed"
152580
+ };
152581
+ },
152582
+ setSettings: async ({ deviceId, settings }) => {
152583
+ if (deviceId !== this.id) return;
152584
+ if (settings.enabled === void 0) return;
152585
+ await applyEnabled(settings.enabled);
152586
+ },
152406
152587
  setEnabled: async ({ deviceId, enabled }) => {
152407
152588
  if (deviceId !== this.id) return;
152408
- await this.runtimeState.patchCapState(CAP_NAME, { enabled });
152409
- const api = this.api;
152410
- if (!api) return;
152411
- if (enabled) await this.resubscribeObjectDetections(api, "cap-enabled").catch((err) => {
152412
- this.ctx.logger.debug("Reolink objectDetections re-arm on enable failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
152413
- });
152414
- else await this.unsubscribeObjectDetections(api);
152589
+ await applyEnabled(enabled);
152415
152590
  }
152416
152591
  };
152417
152592
  this.ctx.registerNativeCap(nativeObjectDetectionCapability, provider);
@@ -154042,14 +154217,22 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
154042
154217
  });
154043
154218
  } catch {}
154044
154219
  this.objectDetectionsSubscribed = false;
154045
- if (this.isBattery) {
154046
- this.ctx.logger.debug("Reolink objectDetections subscribe skipped — battery cam", { meta: { reason } });
154047
- return;
154048
- }
154049
154220
  if (!isNativeObjectForwardingEnabled(this.runtimeState.getCapState("native-object-detection"))) {
154050
154221
  this.ctx.logger.debug("Reolink objectDetections subscribe skipped — cap disabled", { meta: { reason } });
154051
154222
  return;
154052
154223
  }
154224
+ if (this.isBattery) {
154225
+ api.onDetection(this.handleObjectDetectionsBound);
154226
+ this.objectDetectionsSubscribed = true;
154227
+ this.ctx.logger.info("Reolink onboard boxes ride the open stream (battery cam)", {
154228
+ tags: { deviceId: this.id },
154229
+ meta: {
154230
+ reason,
154231
+ channel
154232
+ }
154233
+ });
154234
+ return;
154235
+ }
154053
154236
  if (!api.client.isSocketConnected() || !api.client.loggedIn) {
154054
154237
  this.ctx.logger.debug("Reolink objectDetections subscribe skipped — socket not ready", { meta: {
154055
154238
  reason,
@@ -154075,6 +154258,11 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
154075
154258
  */
154076
154259
  async unsubscribeObjectDetections(api) {
154077
154260
  if (!this.objectDetectionsSubscribed) return;
154261
+ if (this.isBattery) {
154262
+ api.offDetection(this.handleObjectDetectionsBound);
154263
+ this.objectDetectionsSubscribed = false;
154264
+ return;
154265
+ }
154078
154266
  try {
154079
154267
  await api.offObjectDetections(this.handleObjectDetectionsBound, {
154080
154268
  channel: this.getChannel(),
@@ -154104,6 +154292,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
154104
154292
  */
154105
154293
  handleObjectDetections(event) {
154106
154294
  if (!isNativeObjectForwardingEnabled(this.runtimeState.getCapState("native-object-detection"))) return;
154295
+ if (event.channel !== this.getChannel()) return;
154107
154296
  if (event.boxes.length === 0) return;
154108
154297
  const now = Date.now();
154109
154298
  const payload = mapDetectionEvent(event, this.id, now);
@@ -155791,7 +155980,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
155791
155980
  timestamp: now
155792
155981
  }]
155793
155982
  }));
155794
- }
155983
+ } else this.noteNativeDetectionSuppressed(aiClass);
155795
155984
  this.ctx.eventBus.emit(createEvent(EventCategory.MotionOnMotionChanged, eventSource, {
155796
155985
  deviceId: this.id,
155797
155986
  detected: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-reolink",
3
- "version": "1.2.125",
3
+ "version": "1.2.127",
4
4
  "description": "Reolink camera device provider addon for CamStack — native Baichuan protocol",
5
5
  "keywords": [
6
6
  "camstack",