@camstack/addon-provider-reolink 1.2.125 → 1.2.126

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 +178 -40
  2. package/dist/addon.mjs +178 -40
  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).
@@ -29343,11 +29343,39 @@ var NativeObjectDetectionStatusSchema = object({
29343
29343
  supportedClasses: array(NativeObjectClassEnum).readonly(),
29344
29344
  /**
29345
29345
  * 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.
29346
+ *
29347
+ * The COLD-START default is the provider's, and it is not one value for
29348
+ * every camera: a mains camera carries the boxed side-channel and its
29349
+ * detections are usable subjects, so it defaults ON; a battery camera never
29350
+ * attaches that channel, so the same toggle would buy it nothing but a write
29351
+ * — it defaults OFF and an operator who wants it turns it on knowing what it
29352
+ * costs. {@link NativeObjectDetectionOptionsSchema.geometry} is how the UI
29353
+ * says which of the two a camera is.
29348
29354
  */
29349
29355
  enabled: boolean()
29350
29356
  });
29357
+ /**
29358
+ * What a camera's onboard detections actually CARRY.
29359
+ *
29360
+ * `boxed` — the firmware ships a bounding box with the class, so a detection is
29361
+ * a subject the pipeline can track, crop and score.
29362
+ * `flag-only` — the firmware ships the class and nothing else. The pipeline
29363
+ * needs geometry to make a subject out of it, so such a push reaches the
29364
+ * tracker as motion and no further. On Reolink this is every battery camera:
29365
+ * the boxes ride the BcMedia sub-stream, which a battery camera never attaches.
29366
+ *
29367
+ * Reported and not inferred, because the operator's question in front of the
29368
+ * toggle is "what do I get", and "a class with no box" and "a subject" are
29369
+ * different answers (D14: the derived form is only as honest as `getOptions`).
29370
+ */
29371
+ var NativeObjectGeometryEnum = _enum(["boxed", "flag-only"]);
29372
+ var NativeObjectDetectionOptionsSchema = object({
29373
+ /** Classes this firmware can detect — the same list the status reports. */
29374
+ supportedClasses: array(NativeObjectClassEnum).readonly(),
29375
+ /** What a detection from this camera carries. */
29376
+ geometry: NativeObjectGeometryEnum
29377
+ });
29378
+ var NativeObjectDetectionSettingsPatchSchema = object({ enabled: boolean().optional() });
29351
29379
  var NativeObjectDetectionRuntimeStateSchema = NativeObjectDetectionStatusSchema.extend({
29352
29380
  /** Required by createRuntimeStateBridge — epoch ms of last refresh. */
29353
29381
  lastFetchedAt: number() });
@@ -29357,13 +29385,28 @@ var nativeObjectDetectionCapability = {
29357
29385
  deviceNative: true,
29358
29386
  mode: "singleton",
29359
29387
  deviceTypes: [DeviceType.Camera],
29360
- methods: { setEnabled: method(object({
29361
- deviceId: number(),
29362
- enabled: boolean()
29363
- }), _void(), {
29364
- kind: "mutation",
29365
- auth: "admin"
29366
- }) },
29388
+ deviceConfig: { ui: {
29389
+ kind: "derived-form",
29390
+ builderId: "native-object-detection",
29391
+ tab: "motion"
29392
+ } },
29393
+ methods: {
29394
+ getOptions: method(object({ deviceId: number() }), NativeObjectDetectionOptionsSchema),
29395
+ setSettings: method(object({
29396
+ deviceId: number(),
29397
+ settings: NativeObjectDetectionSettingsPatchSchema
29398
+ }), _void(), {
29399
+ kind: "mutation",
29400
+ auth: "admin"
29401
+ }),
29402
+ setEnabled: method(object({
29403
+ deviceId: number(),
29404
+ enabled: boolean()
29405
+ }), _void(), {
29406
+ kind: "mutation",
29407
+ auth: "admin"
29408
+ })
29409
+ },
29367
29410
  events: { onDetected: { data: object({
29368
29411
  deviceId: number(),
29369
29412
  detection: NativeDetectionSchema
@@ -39216,12 +39259,24 @@ Object.freeze({
39216
39259
  addonId: null,
39217
39260
  access: "create"
39218
39261
  },
39262
+ "nativeObjectDetection.getOptions": {
39263
+ capName: "native-object-detection",
39264
+ capScope: "device",
39265
+ addonId: null,
39266
+ access: "view"
39267
+ },
39219
39268
  "nativeObjectDetection.setEnabled": {
39220
39269
  capName: "native-object-detection",
39221
39270
  capScope: "device",
39222
39271
  addonId: null,
39223
39272
  access: "create"
39224
39273
  },
39274
+ "nativeObjectDetection.setSettings": {
39275
+ capName: "native-object-detection",
39276
+ capScope: "device",
39277
+ addonId: null,
39278
+ access: "create"
39279
+ },
39225
39280
  "navigation.getFeatures": {
39226
39281
  capName: "navigation",
39227
39282
  capScope: "device",
@@ -43448,11 +43503,21 @@ Object.freeze({
43448
43503
  form: "single",
43449
43504
  optional: false
43450
43505
  }],
43506
+ "nativeObjectDetection.getOptions": [{
43507
+ name: "deviceId",
43508
+ form: "single",
43509
+ optional: false
43510
+ }],
43451
43511
  "nativeObjectDetection.setEnabled": [{
43452
43512
  name: "deviceId",
43453
43513
  form: "single",
43454
43514
  optional: false
43455
43515
  }],
43516
+ "nativeObjectDetection.setSettings": [{
43517
+ name: "deviceId",
43518
+ form: "single",
43519
+ optional: false
43520
+ }],
43456
43521
  "navigation.getFeatures": [{
43457
43522
  name: "deviceId",
43458
43523
  form: "single",
@@ -147632,10 +147697,22 @@ var REOLINK_ADDON_ID = "provider-reolink";
147632
147697
  * CameraNativeDetection class strings the rest of camstack consumes.
147633
147698
  * Keeping the loose-string contract — face/package etc are valid.
147634
147699
  */
147700
+ /**
147701
+ * Baichuan AI class name → the cap's `NativeObjectClass`.
147702
+ *
147703
+ * TWO vocabularies arrive here and both must be keyed. The simple-event push
147704
+ * (cmd 33 `<AItype>`) says `animal`; the capability probe (cmd 299
147705
+ * `getAiDetectTypes`) says `dog_cat` for the same class. Keying only the first
147706
+ * made `buildSupportedClasses` drop it, so camera 618 reported
147707
+ * `supportedClasses: ["person","vehicle"]` while its own `lastByClass` held a
147708
+ * live `animal` detection — the camera detected a class the system said it
147709
+ * could not. The two key-spaces do not collide, so one map serves both.
147710
+ */
147635
147711
  var AI_CLASS_MAP = {
147636
147712
  people: "person",
147637
147713
  vehicle: "vehicle",
147638
147714
  animal: "animal",
147715
+ dog_cat: "animal",
147639
147716
  face: "face",
147640
147717
  package: "package"
147641
147718
  };
@@ -148476,6 +148553,30 @@ function mapDetectionEvent(event, cameraId, nowMs) {
148476
148553
  function isNativeObjectForwardingEnabled(capState) {
148477
148554
  return capState?.enabled === true;
148478
148555
  }
148556
+ /**
148557
+ * The classes this firmware can detect, from the cmd-299 probe
148558
+ * (`deviceCache.aiDetectTypes`), mapped through {@link AI_CLASS_MAP}.
148559
+ *
148560
+ * Module-level and exported because the version of this that lived as a
148561
+ * closure inside `registerNativeObjectDetectionCap` had a test that
148562
+ * RE-IMPLEMENTED it — and the copy encoded the `dog_cat` gap as intent
148563
+ * ("skips unknown Baichuan type names"), so the suite stayed green while
148564
+ * camera 618 reported it could not detect a class it was detecting. One
148565
+ * function, one test, no copy.
148566
+ *
148567
+ * An absent probe is not an empty answer: it means nobody has asked the camera
148568
+ * yet, and the honest fallback is the full mapped value-set rather than "this
148569
+ * camera detects nothing".
148570
+ */
148571
+ function buildSupportedNativeClasses(aiDetectTypes, isSupportedClass) {
148572
+ if (!aiDetectTypes || aiDetectTypes.length === 0) return [...new Set(Object.values(AI_CLASS_MAP))].filter(isSupportedClass);
148573
+ const classes = [];
148574
+ for (const libName of aiDetectTypes) {
148575
+ const mapped = AI_CLASS_MAP[libName];
148576
+ if (mapped !== void 0 && isSupportedClass(mapped) && !classes.includes(mapped)) classes.push(mapped);
148577
+ }
148578
+ return classes;
148579
+ }
148479
148580
  function formatUserLevel(level) {
148480
148581
  if (level === void 0 || level === null) return "user";
148481
148582
  if (typeof level === "string" && level.length > 0) return level;
@@ -149161,6 +149262,13 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
149161
149262
  loginPromise = null;
149162
149263
  /** True once we've discovered (via getBatteryInfo) that this is a battery cam. */
149163
149264
  isBattery = false;
149265
+ /**
149266
+ * When each AI class last had its suppression reported, so the gate's drop is
149267
+ * on the record without one line per push. Per class because the vocabulary
149268
+ * is five values and the finding is WHICH class the camera is pushing that
149269
+ * nothing downstream receives.
149270
+ */
149271
+ nativeDetectionSuppressedAt = /* @__PURE__ */ new Map();
149164
149272
  /** Reconnect attempt counter — drives exponential backoff. */
149165
149273
  reconnectAttempts = 0;
149166
149274
  /** Pending reconnect timer; cleared on successful reconnect or removeDevice. */
@@ -152373,50 +152481,80 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
152373
152481
  * yet (e.g. fresh device, no config cached).
152374
152482
  * - `lastByClass`: updated in `handleSimpleEvent` every time an AI-class
152375
152483
  * 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.
152484
+ * - `enabled`: operator toggle. Cold-start default is the camera's power
152485
+ * source ON for a mains camera (its detections carry boxes and become
152486
+ * subjects), OFF for a battery one (no boxed side-channel, so the toggle
152487
+ * would buy it nothing). See `buildEmptyState`.
152379
152488
  *
152380
152489
  * Follows the trampoline pattern from `registerStreamParamsCap` — no
152381
152490
  * parallel private fields; all state lives in the kernel runtimeState
152382
152491
  * slice.
152383
152492
  */
152493
+ /**
152494
+ * Report — once a minute per class — that the forwarding gate dropped an
152495
+ * onboard AI detection. A branch that drops work names it (D391): without
152496
+ * this the toggle being off looks exactly like a camera that pushes nothing,
152497
+ * and the operator's only symptom is silence.
152498
+ */
152499
+ noteNativeDetectionSuppressed(aiClass) {
152500
+ const now = Date.now();
152501
+ if (now - (this.nativeDetectionSuppressedAt.get(aiClass) ?? 0) < 6e4) return;
152502
+ this.nativeDetectionSuppressedAt.set(aiClass, now);
152503
+ this.ctx.logger.info("Reolink onboard AI detection dropped — forwarding is off for this camera", {
152504
+ tags: { deviceId: this.id },
152505
+ meta: {
152506
+ class: aiClass,
152507
+ battery: this.isBattery
152508
+ }
152509
+ });
152510
+ }
152384
152511
  registerNativeObjectDetectionCap() {
152385
152512
  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
- };
152513
+ const buildSupportedClasses = () => buildSupportedNativeClasses(this.config.get("deviceCache")?.aiDetectTypes, isNativeObjectClass).filter(isNativeObjectClass);
152396
152514
  const buildEmptyState = () => ({
152397
- enabled: false,
152515
+ enabled: !this.isBattery,
152398
152516
  lastByClass: {},
152399
152517
  supportedClasses: buildSupportedClasses(),
152400
152518
  lastFetchedAt: 0
152401
152519
  });
152520
+ const bridge = createRuntimeStateBridge({
152521
+ runtimeState: this.runtimeState,
152522
+ cap: nativeObjectDetectionCapability,
152523
+ ownDeviceId: this.id,
152524
+ refresh: async () => {},
152525
+ staleMs: Infinity,
152526
+ empty: buildEmptyState
152527
+ });
152528
+ /** Shared by `setEnabled` and the derived form's `setSettings`. */
152529
+ const applyEnabled = async (enabled) => {
152530
+ await this.runtimeState.patchCapState(CAP_NAME, { enabled });
152531
+ const api = this.api;
152532
+ if (!api) return;
152533
+ if (enabled) await this.resubscribeObjectDetections(api, "cap-enabled").catch((err) => {
152534
+ this.ctx.logger.debug("Reolink objectDetections re-arm on enable failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
152535
+ });
152536
+ else await this.unsubscribeObjectDetections(api);
152537
+ };
152402
152538
  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,
152539
+ getStatus: bridge.getStatus,
152540
+ getOptions: async ({ deviceId }) => {
152541
+ if (deviceId !== this.id) return {
152542
+ supportedClasses: [],
152543
+ geometry: "flag-only"
152544
+ };
152545
+ return {
152546
+ supportedClasses: buildSupportedClasses(),
152547
+ geometry: this.isBattery ? "flag-only" : "boxed"
152548
+ };
152549
+ },
152550
+ setSettings: async ({ deviceId, settings }) => {
152551
+ if (deviceId !== this.id) return;
152552
+ if (settings.enabled === void 0) return;
152553
+ await applyEnabled(settings.enabled);
152554
+ },
152411
152555
  setEnabled: async ({ deviceId, enabled }) => {
152412
152556
  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);
152557
+ await applyEnabled(enabled);
152420
152558
  }
152421
152559
  };
152422
152560
  this.ctx.registerNativeCap(nativeObjectDetectionCapability, provider);
@@ -155796,7 +155934,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
155796
155934
  timestamp: now
155797
155935
  }]
155798
155936
  }));
155799
- }
155937
+ } else this.noteNativeDetectionSuppressed(aiClass);
155800
155938
  this.ctx.eventBus.emit(createEvent(EventCategory.MotionOnMotionChanged, eventSource, {
155801
155939
  deviceId: this.id,
155802
155940
  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).
@@ -29338,11 +29338,39 @@ var NativeObjectDetectionStatusSchema = object({
29338
29338
  supportedClasses: array(NativeObjectClassEnum).readonly(),
29339
29339
  /**
29340
29340
  * 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.
29341
+ *
29342
+ * The COLD-START default is the provider's, and it is not one value for
29343
+ * every camera: a mains camera carries the boxed side-channel and its
29344
+ * detections are usable subjects, so it defaults ON; a battery camera never
29345
+ * attaches that channel, so the same toggle would buy it nothing but a write
29346
+ * — it defaults OFF and an operator who wants it turns it on knowing what it
29347
+ * costs. {@link NativeObjectDetectionOptionsSchema.geometry} is how the UI
29348
+ * says which of the two a camera is.
29343
29349
  */
29344
29350
  enabled: boolean()
29345
29351
  });
29352
+ /**
29353
+ * What a camera's onboard detections actually CARRY.
29354
+ *
29355
+ * `boxed` — the firmware ships a bounding box with the class, so a detection is
29356
+ * a subject the pipeline can track, crop and score.
29357
+ * `flag-only` — the firmware ships the class and nothing else. The pipeline
29358
+ * needs geometry to make a subject out of it, so such a push reaches the
29359
+ * tracker as motion and no further. On Reolink this is every battery camera:
29360
+ * the boxes ride the BcMedia sub-stream, which a battery camera never attaches.
29361
+ *
29362
+ * Reported and not inferred, because the operator's question in front of the
29363
+ * toggle is "what do I get", and "a class with no box" and "a subject" are
29364
+ * different answers (D14: the derived form is only as honest as `getOptions`).
29365
+ */
29366
+ var NativeObjectGeometryEnum = _enum(["boxed", "flag-only"]);
29367
+ var NativeObjectDetectionOptionsSchema = object({
29368
+ /** Classes this firmware can detect — the same list the status reports. */
29369
+ supportedClasses: array(NativeObjectClassEnum).readonly(),
29370
+ /** What a detection from this camera carries. */
29371
+ geometry: NativeObjectGeometryEnum
29372
+ });
29373
+ var NativeObjectDetectionSettingsPatchSchema = object({ enabled: boolean().optional() });
29346
29374
  var NativeObjectDetectionRuntimeStateSchema = NativeObjectDetectionStatusSchema.extend({
29347
29375
  /** Required by createRuntimeStateBridge — epoch ms of last refresh. */
29348
29376
  lastFetchedAt: number() });
@@ -29352,13 +29380,28 @@ var nativeObjectDetectionCapability = {
29352
29380
  deviceNative: true,
29353
29381
  mode: "singleton",
29354
29382
  deviceTypes: [DeviceType.Camera],
29355
- methods: { setEnabled: method(object({
29356
- deviceId: number(),
29357
- enabled: boolean()
29358
- }), _void(), {
29359
- kind: "mutation",
29360
- auth: "admin"
29361
- }) },
29383
+ deviceConfig: { ui: {
29384
+ kind: "derived-form",
29385
+ builderId: "native-object-detection",
29386
+ tab: "motion"
29387
+ } },
29388
+ methods: {
29389
+ getOptions: method(object({ deviceId: number() }), NativeObjectDetectionOptionsSchema),
29390
+ setSettings: method(object({
29391
+ deviceId: number(),
29392
+ settings: NativeObjectDetectionSettingsPatchSchema
29393
+ }), _void(), {
29394
+ kind: "mutation",
29395
+ auth: "admin"
29396
+ }),
29397
+ setEnabled: method(object({
29398
+ deviceId: number(),
29399
+ enabled: boolean()
29400
+ }), _void(), {
29401
+ kind: "mutation",
29402
+ auth: "admin"
29403
+ })
29404
+ },
29362
29405
  events: { onDetected: { data: object({
29363
29406
  deviceId: number(),
29364
29407
  detection: NativeDetectionSchema
@@ -39211,12 +39254,24 @@ Object.freeze({
39211
39254
  addonId: null,
39212
39255
  access: "create"
39213
39256
  },
39257
+ "nativeObjectDetection.getOptions": {
39258
+ capName: "native-object-detection",
39259
+ capScope: "device",
39260
+ addonId: null,
39261
+ access: "view"
39262
+ },
39214
39263
  "nativeObjectDetection.setEnabled": {
39215
39264
  capName: "native-object-detection",
39216
39265
  capScope: "device",
39217
39266
  addonId: null,
39218
39267
  access: "create"
39219
39268
  },
39269
+ "nativeObjectDetection.setSettings": {
39270
+ capName: "native-object-detection",
39271
+ capScope: "device",
39272
+ addonId: null,
39273
+ access: "create"
39274
+ },
39220
39275
  "navigation.getFeatures": {
39221
39276
  capName: "navigation",
39222
39277
  capScope: "device",
@@ -43443,11 +43498,21 @@ Object.freeze({
43443
43498
  form: "single",
43444
43499
  optional: false
43445
43500
  }],
43501
+ "nativeObjectDetection.getOptions": [{
43502
+ name: "deviceId",
43503
+ form: "single",
43504
+ optional: false
43505
+ }],
43446
43506
  "nativeObjectDetection.setEnabled": [{
43447
43507
  name: "deviceId",
43448
43508
  form: "single",
43449
43509
  optional: false
43450
43510
  }],
43511
+ "nativeObjectDetection.setSettings": [{
43512
+ name: "deviceId",
43513
+ form: "single",
43514
+ optional: false
43515
+ }],
43451
43516
  "navigation.getFeatures": [{
43452
43517
  name: "deviceId",
43453
43518
  form: "single",
@@ -147627,10 +147692,22 @@ var REOLINK_ADDON_ID = "provider-reolink";
147627
147692
  * CameraNativeDetection class strings the rest of camstack consumes.
147628
147693
  * Keeping the loose-string contract — face/package etc are valid.
147629
147694
  */
147695
+ /**
147696
+ * Baichuan AI class name → the cap's `NativeObjectClass`.
147697
+ *
147698
+ * TWO vocabularies arrive here and both must be keyed. The simple-event push
147699
+ * (cmd 33 `<AItype>`) says `animal`; the capability probe (cmd 299
147700
+ * `getAiDetectTypes`) says `dog_cat` for the same class. Keying only the first
147701
+ * made `buildSupportedClasses` drop it, so camera 618 reported
147702
+ * `supportedClasses: ["person","vehicle"]` while its own `lastByClass` held a
147703
+ * live `animal` detection — the camera detected a class the system said it
147704
+ * could not. The two key-spaces do not collide, so one map serves both.
147705
+ */
147630
147706
  var AI_CLASS_MAP = {
147631
147707
  people: "person",
147632
147708
  vehicle: "vehicle",
147633
147709
  animal: "animal",
147710
+ dog_cat: "animal",
147634
147711
  face: "face",
147635
147712
  package: "package"
147636
147713
  };
@@ -148471,6 +148548,30 @@ function mapDetectionEvent(event, cameraId, nowMs) {
148471
148548
  function isNativeObjectForwardingEnabled(capState) {
148472
148549
  return capState?.enabled === true;
148473
148550
  }
148551
+ /**
148552
+ * The classes this firmware can detect, from the cmd-299 probe
148553
+ * (`deviceCache.aiDetectTypes`), mapped through {@link AI_CLASS_MAP}.
148554
+ *
148555
+ * Module-level and exported because the version of this that lived as a
148556
+ * closure inside `registerNativeObjectDetectionCap` had a test that
148557
+ * RE-IMPLEMENTED it — and the copy encoded the `dog_cat` gap as intent
148558
+ * ("skips unknown Baichuan type names"), so the suite stayed green while
148559
+ * camera 618 reported it could not detect a class it was detecting. One
148560
+ * function, one test, no copy.
148561
+ *
148562
+ * An absent probe is not an empty answer: it means nobody has asked the camera
148563
+ * yet, and the honest fallback is the full mapped value-set rather than "this
148564
+ * camera detects nothing".
148565
+ */
148566
+ function buildSupportedNativeClasses(aiDetectTypes, isSupportedClass) {
148567
+ if (!aiDetectTypes || aiDetectTypes.length === 0) return [...new Set(Object.values(AI_CLASS_MAP))].filter(isSupportedClass);
148568
+ const classes = [];
148569
+ for (const libName of aiDetectTypes) {
148570
+ const mapped = AI_CLASS_MAP[libName];
148571
+ if (mapped !== void 0 && isSupportedClass(mapped) && !classes.includes(mapped)) classes.push(mapped);
148572
+ }
148573
+ return classes;
148574
+ }
148474
148575
  function formatUserLevel(level) {
148475
148576
  if (level === void 0 || level === null) return "user";
148476
148577
  if (typeof level === "string" && level.length > 0) return level;
@@ -149156,6 +149257,13 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
149156
149257
  loginPromise = null;
149157
149258
  /** True once we've discovered (via getBatteryInfo) that this is a battery cam. */
149158
149259
  isBattery = false;
149260
+ /**
149261
+ * When each AI class last had its suppression reported, so the gate's drop is
149262
+ * on the record without one line per push. Per class because the vocabulary
149263
+ * is five values and the finding is WHICH class the camera is pushing that
149264
+ * nothing downstream receives.
149265
+ */
149266
+ nativeDetectionSuppressedAt = /* @__PURE__ */ new Map();
149159
149267
  /** Reconnect attempt counter — drives exponential backoff. */
149160
149268
  reconnectAttempts = 0;
149161
149269
  /** Pending reconnect timer; cleared on successful reconnect or removeDevice. */
@@ -152368,50 +152476,80 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
152368
152476
  * yet (e.g. fresh device, no config cached).
152369
152477
  * - `lastByClass`: updated in `handleSimpleEvent` every time an AI-class
152370
152478
  * 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.
152479
+ * - `enabled`: operator toggle. Cold-start default is the camera's power
152480
+ * source ON for a mains camera (its detections carry boxes and become
152481
+ * subjects), OFF for a battery one (no boxed side-channel, so the toggle
152482
+ * would buy it nothing). See `buildEmptyState`.
152374
152483
  *
152375
152484
  * Follows the trampoline pattern from `registerStreamParamsCap` — no
152376
152485
  * parallel private fields; all state lives in the kernel runtimeState
152377
152486
  * slice.
152378
152487
  */
152488
+ /**
152489
+ * Report — once a minute per class — that the forwarding gate dropped an
152490
+ * onboard AI detection. A branch that drops work names it (D391): without
152491
+ * this the toggle being off looks exactly like a camera that pushes nothing,
152492
+ * and the operator's only symptom is silence.
152493
+ */
152494
+ noteNativeDetectionSuppressed(aiClass) {
152495
+ const now = Date.now();
152496
+ if (now - (this.nativeDetectionSuppressedAt.get(aiClass) ?? 0) < 6e4) return;
152497
+ this.nativeDetectionSuppressedAt.set(aiClass, now);
152498
+ this.ctx.logger.info("Reolink onboard AI detection dropped — forwarding is off for this camera", {
152499
+ tags: { deviceId: this.id },
152500
+ meta: {
152501
+ class: aiClass,
152502
+ battery: this.isBattery
152503
+ }
152504
+ });
152505
+ }
152379
152506
  registerNativeObjectDetectionCap() {
152380
152507
  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
- };
152508
+ const buildSupportedClasses = () => buildSupportedNativeClasses(this.config.get("deviceCache")?.aiDetectTypes, isNativeObjectClass).filter(isNativeObjectClass);
152391
152509
  const buildEmptyState = () => ({
152392
- enabled: false,
152510
+ enabled: !this.isBattery,
152393
152511
  lastByClass: {},
152394
152512
  supportedClasses: buildSupportedClasses(),
152395
152513
  lastFetchedAt: 0
152396
152514
  });
152515
+ const bridge = createRuntimeStateBridge({
152516
+ runtimeState: this.runtimeState,
152517
+ cap: nativeObjectDetectionCapability,
152518
+ ownDeviceId: this.id,
152519
+ refresh: async () => {},
152520
+ staleMs: Infinity,
152521
+ empty: buildEmptyState
152522
+ });
152523
+ /** Shared by `setEnabled` and the derived form's `setSettings`. */
152524
+ const applyEnabled = async (enabled) => {
152525
+ await this.runtimeState.patchCapState(CAP_NAME, { enabled });
152526
+ const api = this.api;
152527
+ if (!api) return;
152528
+ if (enabled) await this.resubscribeObjectDetections(api, "cap-enabled").catch((err) => {
152529
+ this.ctx.logger.debug("Reolink objectDetections re-arm on enable failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
152530
+ });
152531
+ else await this.unsubscribeObjectDetections(api);
152532
+ };
152397
152533
  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,
152534
+ getStatus: bridge.getStatus,
152535
+ getOptions: async ({ deviceId }) => {
152536
+ if (deviceId !== this.id) return {
152537
+ supportedClasses: [],
152538
+ geometry: "flag-only"
152539
+ };
152540
+ return {
152541
+ supportedClasses: buildSupportedClasses(),
152542
+ geometry: this.isBattery ? "flag-only" : "boxed"
152543
+ };
152544
+ },
152545
+ setSettings: async ({ deviceId, settings }) => {
152546
+ if (deviceId !== this.id) return;
152547
+ if (settings.enabled === void 0) return;
152548
+ await applyEnabled(settings.enabled);
152549
+ },
152406
152550
  setEnabled: async ({ deviceId, enabled }) => {
152407
152551
  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);
152552
+ await applyEnabled(enabled);
152415
152553
  }
152416
152554
  };
152417
152555
  this.ctx.registerNativeCap(nativeObjectDetectionCapability, provider);
@@ -155791,7 +155929,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
155791
155929
  timestamp: now
155792
155930
  }]
155793
155931
  }));
155794
- }
155932
+ } else this.noteNativeDetectionSuppressed(aiClass);
155795
155933
  this.ctx.eventBus.emit(createEvent(EventCategory.MotionOnMotionChanged, eventSource, {
155796
155934
  deviceId: this.id,
155797
155935
  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.126",
4
4
  "description": "Reolink camera device provider addon for CamStack — native Baichuan protocol",
5
5
  "keywords": [
6
6
  "camstack",