@camstack/addon-provider-reolink 1.2.129 → 1.2.131

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 +242 -42
  2. package/dist/addon.mjs +242 -42
  3. package/package.json +2 -2
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-BEyvfshj.mjs
5389
+ //#region ../types/dist/sleep-BDR76Ykr.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).
@@ -7271,6 +7271,40 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
7271
7271
  DeviceRole["GenericNotifier"] = "generic-notifier";
7272
7272
  return DeviceRole;
7273
7273
  }({});
7274
+ /**
7275
+ * Identity — preserves literal types for downstream inference.
7276
+ *
7277
+ * The constraint is `Record<string, unknown>` (not `CustomActionsSpec`) so
7278
+ * TypeScript does not widen each entry's literal `kind`/`auth` fields to
7279
+ * the broader unions declared on `CustomActionSpec`'s default generics.
7280
+ * Shape validity is enforced separately by the `customAction(...)` helper
7281
+ * whose return type is already a `CustomActionSpec<...>`.
7282
+ */
7283
+ function defineCustomActions(spec) {
7284
+ return spec;
7285
+ }
7286
+ function customAction(input, output, options) {
7287
+ return {
7288
+ input,
7289
+ output,
7290
+ kind: options?.kind ?? "query",
7291
+ auth: options?.auth ?? "protected",
7292
+ scope: options?.scope ?? { kind: "system" },
7293
+ ...options?.caller ? { caller: "required" } : {}
7294
+ };
7295
+ }
7296
+ function deviceCustomAction(input, output, options) {
7297
+ return {
7298
+ input,
7299
+ output,
7300
+ kind: options?.kind ?? "query",
7301
+ auth: options?.auth ?? "protected",
7302
+ scope: {
7303
+ kind: "device",
7304
+ ...options?.deviceTypes ? { deviceTypes: options.deviceTypes } : {}
7305
+ }
7306
+ };
7307
+ }
7274
7308
  var StaticDirOutputSchema = object({ staticDir: string() });
7275
7309
  var VersionOutputSchema = object({ version: string() });
7276
7310
  method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
@@ -12132,6 +12166,21 @@ var streamSignalsCapability = {
12132
12166
  mode: "singleton",
12133
12167
  deviceTypes: Object.values(DeviceType),
12134
12168
  runtimeState: StreamSignalsStatusSchema,
12169
+ /**
12170
+ * Runtime-state durability: **session** — mirrored in RAM, never written.
12171
+ *
12172
+ * The slice holds what the DEVICE says it can emit. That is a probed fact,
12173
+ * not an operator choice: the provider re-declares it on every registration,
12174
+ * so losing it loses nothing and persisting it would freeze an answer the
12175
+ * camera is entitled to change. Measured the same day on the sibling case —
12176
+ * `native-object-detection.supportedClasses` was persisted, and a firmware
12177
+ * class the camera really detected stayed missing for the life of the row
12178
+ * because the fix could not reach it.
12179
+ *
12180
+ * See `RuntimeStateDurability`. Enforced by
12181
+ * `scripts/check-runtime-state-durability.ts`.
12182
+ */
12183
+ durability: "session",
12135
12184
  methods: {
12136
12185
  /**
12137
12186
  * What this device can emit. Empty is a valid and common answer — most
@@ -13106,40 +13155,6 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
13106
13155
  content: string()
13107
13156
  })) }), { auth: "admin" });
13108
13157
  /**
13109
- * Identity — preserves literal types for downstream inference.
13110
- *
13111
- * The constraint is `Record<string, unknown>` (not `CustomActionsSpec`) so
13112
- * TypeScript does not widen each entry's literal `kind`/`auth` fields to
13113
- * the broader unions declared on `CustomActionSpec`'s default generics.
13114
- * Shape validity is enforced separately by the `customAction(...)` helper
13115
- * whose return type is already a `CustomActionSpec<...>`.
13116
- */
13117
- function defineCustomActions(spec) {
13118
- return spec;
13119
- }
13120
- function customAction(input, output, options) {
13121
- return {
13122
- input,
13123
- output,
13124
- kind: options?.kind ?? "query",
13125
- auth: options?.auth ?? "protected",
13126
- scope: options?.scope ?? { kind: "system" },
13127
- ...options?.caller ? { caller: "required" } : {}
13128
- };
13129
- }
13130
- function deviceCustomAction(input, output, options) {
13131
- return {
13132
- input,
13133
- output,
13134
- kind: options?.kind ?? "query",
13135
- auth: options?.auth ?? "protected",
13136
- scope: {
13137
- kind: "device",
13138
- ...options?.deviceTypes ? { deviceTypes: options.deviceTypes } : {}
13139
- }
13140
- };
13141
- }
13142
- /**
13143
13158
  * `custom-model-registry` — collection cap exposing operator-registered
13144
13159
  * custom detection models. Each provider (today: `addon-model-studio`)
13145
13160
  * contributes a list of `CustomModelDescriptor`s; the hub auto-concatenates
@@ -18820,6 +18835,48 @@ var NcAlarmConfigSchema = object({
18820
18835
  settings: NcAlarmSettingsSchema,
18821
18836
  coverage: array(NcAlarmModeCoverageSchema)
18822
18837
  });
18838
+ /**
18839
+ * ONE rule's demand on ONE camera's clip ring.
18840
+ *
18841
+ * A rule, not a camera: the broker takes the MAX over the asks that reach a
18842
+ * camera, so the answer stays a faithful description of the rules and the
18843
+ * bounding (the byte budget, the broker's own ceiling) stays where the cost
18844
+ * lives. A camera that appears in no ask is being told **nothing** — which is
18845
+ * a different thing from never having been told, and only the envelope
18846
+ * (`rulesLoaded`) can tell those apart.
18847
+ */
18848
+ var NcClipRetentionAskSchema = object({
18849
+ /**
18850
+ * The camera this ask is about. **Absent = every camera**, which is what a
18851
+ * rule with no `conditions.devices` means: it can fire anywhere, so it is
18852
+ * asking everywhere.
18853
+ */
18854
+ deviceId: number().int().nonnegative().optional(),
18855
+ /** Seconds of history the rule needs held for it. Never zero — a rule that
18856
+ * needs nothing produces no ask at all. */
18857
+ seconds: number().min(0).max(60),
18858
+ /** The profile the rule cuts from, when it named one. Absent = the cheapest
18859
+ * assigned, which is what the cut defaults to. */
18860
+ profile: CamProfileSchema.optional(),
18861
+ /** Who is asking — so a retention the operator did not expect names a rule. */
18862
+ ruleId: string(),
18863
+ ruleName: string(),
18864
+ /** Why this many seconds: the rule's own window, or the occupancy ceiling. */
18865
+ reason: _enum(["window", "occupancy"])
18866
+ });
18867
+ /**
18868
+ * The whole answer, with the one bit that keeps absence from reading as zero.
18869
+ *
18870
+ * `rulesLoaded` is false until the rule ledger has completed a load at least
18871
+ * once. A centre that is up but has not read its rules yet answers with an
18872
+ * empty ask list that means nothing at all — applying it would drop every
18873
+ * camera's ring to zero for the first events after a restart, which is the
18874
+ * footage this mechanism exists to keep.
18875
+ */
18876
+ var NcClipRetentionPlanSchema = object({
18877
+ rulesLoaded: boolean(),
18878
+ asks: array(NcClipRetentionAskSchema).readonly()
18879
+ });
18823
18880
  method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), method(object({ ruleId: string() }), object({ rule: NcRuleSchema.nullable() }), { auth: "admin" }), method(object({ rule: NcRuleInputSchema }), object({ rule: NcRuleSchema }), {
18824
18881
  kind: "mutation",
18825
18882
  auth: "admin",
@@ -18840,7 +18897,7 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
18840
18897
  }), object({ success: literal(true) }), {
18841
18898
  kind: "mutation",
18842
18899
  auth: "admin"
18843
- }), method(object({}), object({ mutedDeviceIds: array(number().int()).readonly() }), { auth: "admin" }), method(object({
18900
+ }), method(object({}), NcClipRetentionPlanSchema, { auth: "admin" }), method(object({}), object({ mutedDeviceIds: array(number().int()).readonly() }), { auth: "admin" }), method(object({
18844
18901
  deviceId: number().int(),
18845
18902
  muted: boolean()
18846
18903
  }), object({ success: literal(true) }), {
@@ -21414,6 +21471,33 @@ var RunReplayFrameProcessorResultSchema = object({ tracks: array(object({
21414
21471
  * solid is this track", cheaper than re-deriving it from a trajectory. */
21415
21472
  framesMatched: number().int()
21416
21473
  })).readonly() });
21474
+ /**
21475
+ * The long-term series a camera produces. A closed union so a typo cannot
21476
+ * invent one, and the ONE declaration of it — the addon's `AnalyticsLtsSeries`
21477
+ * is an alias of this, not a second list.
21478
+ */
21479
+ var AnalyticsLtsSeriesSchema = _enum([
21480
+ "motion",
21481
+ "audio-dbfs",
21482
+ "battery",
21483
+ "occupancy"
21484
+ ]);
21485
+ /**
21486
+ * One closed 5-minute bucket of a long-term series.
21487
+ *
21488
+ * `samples` is how many readings the bucket folded — it is the ACTIVITY for a
21489
+ * series like `motion`, and the confidence for one like `occupancy`. A bucket
21490
+ * that exists at all was measured; a bucket that is absent was not, which is a
21491
+ * different claim from zero and the reason this is a sparse series.
21492
+ */
21493
+ var LtsBucketSchema = object({
21494
+ scope: string(),
21495
+ bucketStart: number().int(),
21496
+ samples: number().int().nonnegative(),
21497
+ sum: number(),
21498
+ min: number(),
21499
+ max: number()
21500
+ });
21417
21501
  DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).readonly()), method(object({
21418
21502
  deviceId: number(),
21419
21503
  trackId: string()
@@ -21460,6 +21544,14 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
21460
21544
  kinds: array(string()).optional(),
21461
21545
  limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
21462
21546
  }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(KeyEventBatchQueryInput, array(KeyEventsForDeviceSchema).readonly()), method(object({
21547
+ deviceId: number(),
21548
+ series: AnalyticsLtsSeriesSchema,
21549
+ /** Omit for EVERY scope of this series in the range. */
21550
+ scope: string().optional(),
21551
+ from: number(),
21552
+ to: number(),
21553
+ limit: number().int().positive().optional()
21554
+ }), array(LtsBucketSchema).readonly()), method(object({
21463
21555
  deviceId: number(),
21464
21556
  since: number(),
21465
21557
  until: number(),
@@ -22446,7 +22538,47 @@ var RunnerLocalMetricsSchema = object({
22446
22538
  queueDepth: number(),
22447
22539
  frameLazy: FrameLazyMetricsSchema.optional()
22448
22540
  });
22541
+ /**
22542
+ * Pipeline Runner capability — runtime detection workhorse.
22543
+ *
22544
+ * One instance per node. Receives camera assignments from
22545
+ * `addon-pipeline-orchestrator`, subscribes to the local stream-broker for
22546
+ * decoded frames, drains motion + detection queues, calls the local
22547
+ * `motion-detection` and `pipeline-executor` capabilities, and emits typed
22548
+ * `pipeline.inference-result` and `detection.motion-analysis` events on
22549
+ * the bus.
22550
+ *
22551
+ * Distinct from `pipeline-orchestrator` (the hub-side load balancer) — the
22552
+ * runner has zero knowledge of other agents, no global state, and never
22553
+ * makes assignment decisions itself.
22554
+ */
22555
+ /**
22556
+ * Why a runner declined to look at a camera out of band. Named rather than a
22557
+ * bare `false`: a camera asleep on battery (D173), one attached elsewhere, one
22558
+ * already mid-session and one already bursting are four different facts, and a
22559
+ * caller told only "no" cannot tell a protection from a bug.
22560
+ */
22561
+ var OccupancyBurstRefusalSchema = _enum([
22562
+ "not-attached",
22563
+ "battery-asleep",
22564
+ "not-watching",
22565
+ "already-bursting"
22566
+ ]);
22449
22567
  method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mutation" }), method(object({ deviceId: number() }), object({ success: literal(true) }), { kind: "mutation" }), method(ReportMotionInputSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), RunnerLocalLoadSchema), method(_void(), RunnerLocalMetricsSchema), method(object({ deviceId: number() }), CameraMetricsSchema.nullable()), method(_void(), array(CameraMetricsWithDeviceIdSchema).readonly()), method(_void(), array(number()).readonly()), method(object({
22568
+ deviceId: number(),
22569
+ /** How many frames to collect. The caller sizes this against what its
22570
+ * own confirmation needs; the runner clamps it to the field's range. */
22571
+ frames: number().int().positive().optional(),
22572
+ /** Why, for the log. A burst nobody can attribute is a cost nobody can
22573
+ * defend — the periodic one at least has a timer to point at. */
22574
+ reason: string()
22575
+ }), object({
22576
+ started: boolean(),
22577
+ refusedBecause: OccupancyBurstRefusalSchema.optional()
22578
+ }), {
22579
+ auth: "admin",
22580
+ kind: "mutation"
22581
+ }), method(object({
22450
22582
  handle: FrameHandleSchema,
22451
22583
  bbox: NativeCropBboxSchema,
22452
22584
  maxWidth: number().int().positive().optional(),
@@ -34598,7 +34730,28 @@ var PerScopeBreakdownSchema = object({
34598
34730
  /** Total tracked objects in this scope (frame / zone / unzoned). */
34599
34731
  totalObjects: number().int().nonnegative(),
34600
34732
  /** Per-class count. Keys are macro class names (e.g. `person`, `car`). */
34601
- byClass: record(string(), number().int().nonnegative())
34733
+ byClass: record(string(), number().int().nonnegative()),
34734
+ /**
34735
+ * Of `totalObjects`, how many are STANDING — objects the census holds,
34736
+ * present but not going anywhere (a parked car, a bin, a statue).
34737
+ *
34738
+ * The operator's question about a scene has two halves and they answer
34739
+ * different things: "what is parked here" is stable for hours and must not
34740
+ * flap, while "who is walking through" is the thing an alert is about. One
34741
+ * number could only serve one of them, and `totalObjects` served the first
34742
+ * badly — a passer-by moved it and every occupancy rule saw a change.
34743
+ *
34744
+ * `standing + transient === totalObjects`, always. The total keeps its old
34745
+ * meaning exactly, so no rule written against it changes behaviour.
34746
+ *
34747
+ * ABSENT means UNKNOWN, never zero (D393): a snapshot from a node older than
34748
+ * this field cannot say, and a reader that folds absence in as 0 reports "no
34749
+ * parked objects here" about a camera whose lot is full.
34750
+ */
34751
+ standingObjects: number().int().nonnegative().optional(),
34752
+ /** Of `totalObjects`, how many are passing through. See `standingObjects` —
34753
+ * same absence rule. */
34754
+ transientObjects: number().int().nonnegative().optional()
34602
34755
  });
34603
34756
  var ZoneScopeBreakdownSchema = PerScopeBreakdownSchema.extend({
34604
34757
  zoneId: string(),
@@ -34721,6 +34874,23 @@ var HistoryPointSchema = object({
34721
34874
  * triple covers the three spatial scopes (per-zone, frame-wide,
34722
34875
  * outside-any-zone). Crossing them with `className?` gives the full
34723
34876
  * combinatorial coverage the operator UI requested.
34877
+ *
34878
+ * ## ⚠️ The three history methods are DEPRECATED
34879
+ *
34880
+ * They are served from a 60-MINUTE in-memory ring. It dies with the process,
34881
+ * so after a restart they report nothing about a night that did happen, and any
34882
+ * window past an hour is silently clamped — a caller cannot tell a clamp from
34883
+ * an empty scene. The presets in the occupancy chart existed to hide that
34884
+ * horizon rather than to answer a question.
34885
+ *
34886
+ * Use `pipelineAnalytics.readLongTermSeries({ series: 'occupancy', scope })`
34887
+ * instead. Those rows are already written for every camera, ~60 bytes per
34888
+ * (camera, scope, 5-minute bucket), kept for a year, and survive a restart.
34889
+ * Spell the scope with `occupancyScope(zoneId?, className?)` — the same
34890
+ * function the writer uses, so the two cannot drift.
34891
+ *
34892
+ * The ring stays until these three go, and it is the only thing left reading
34893
+ * it. Do not add a fourth caller.
34724
34894
  */
34725
34895
  var zoneAnalyticsCapability = {
34726
34896
  name: "zone-analytics",
@@ -39580,6 +39750,12 @@ Object.freeze({
39580
39750
  addonId: null,
39581
39751
  access: "view"
39582
39752
  },
39753
+ "notificationRules.getClipRetentionAsks": {
39754
+ capName: "notification-rules",
39755
+ capScope: "system",
39756
+ addonId: null,
39757
+ access: "view"
39758
+ },
39583
39759
  "notificationRules.getConditionCatalog": {
39584
39760
  capName: "notification-rules",
39585
39761
  capScope: "system",
@@ -40090,6 +40266,12 @@ Object.freeze({
40090
40266
  addonId: null,
40091
40267
  access: "create"
40092
40268
  },
40269
+ "pipelineAnalytics.readLongTermSeries": {
40270
+ capName: "pipeline-analytics",
40271
+ capScope: "device",
40272
+ addonId: null,
40273
+ access: "view"
40274
+ },
40093
40275
  "pipelineAnalytics.rebuildObjectEmbeddings": {
40094
40276
  capName: "pipeline-analytics",
40095
40277
  capScope: "device",
@@ -40762,6 +40944,12 @@ Object.freeze({
40762
40944
  addonId: null,
40763
40945
  access: "create"
40764
40946
  },
40947
+ "pipelineRunner.requestOccupancyBurst": {
40948
+ capName: "pipeline-runner",
40949
+ capScope: "system",
40950
+ addonId: null,
40951
+ access: "create"
40952
+ },
40765
40953
  "pipelineRunner.runDetailSubtree": {
40766
40954
  capName: "pipeline-runner",
40767
40955
  capScope: "system",
@@ -43906,6 +44094,11 @@ Object.freeze({
43906
44094
  form: "single",
43907
44095
  optional: false
43908
44096
  }],
44097
+ "pipelineAnalytics.readLongTermSeries": [{
44098
+ name: "deviceId",
44099
+ form: "single",
44100
+ optional: false
44101
+ }],
43909
44102
  "pipelineAnalytics.rebuildObjectEmbeddings": [{
43910
44103
  name: "deviceId",
43911
44104
  form: "single",
@@ -44096,6 +44289,11 @@ Object.freeze({
44096
44289
  form: "single",
44097
44290
  optional: false
44098
44291
  }],
44292
+ "pipelineRunner.requestOccupancyBurst": [{
44293
+ name: "deviceId",
44294
+ form: "single",
44295
+ optional: false
44296
+ }],
44099
44297
  "pipelineRunner.runDetailSubtree": [{
44100
44298
  name: "deviceId",
44101
44299
  form: "single",
@@ -152596,11 +152794,13 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
152596
152794
  };
152597
152795
  this.ctx.registerNativeCap(nativeObjectDetectionCapability, provider);
152598
152796
  this.registerStreamSignalsCap();
152599
- if (this.runtimeState.getCapState(CAP_NAME) === void 0) this.runtimeState.setCapState(CAP_NAME, {
152600
- ...buildEmptyState(),
152601
- lastFetchedAt: Date.now()
152602
- });
152603
- else this.runtimeState.patchCapState(CAP_NAME, { supportedClasses: buildSupportedClasses() });
152797
+ if (this.runtimeState.getCapState(CAP_NAME) === void 0) {
152798
+ this.runtimeState.setCapState(CAP_NAME, {
152799
+ ...buildEmptyState(),
152800
+ lastFetchedAt: Date.now()
152801
+ });
152802
+ this.ctx.logger.info("Reolink onboard AI: no restored state — cold-start default (OFF)", { tags: { deviceId: this.id } });
152803
+ } else this.runtimeState.patchCapState(CAP_NAME, { supportedClasses: buildSupportedClasses() });
152604
152804
  this.ctx.logger.info("Reolink native-object-detection cap registered", { tags: { deviceId: this.id } });
152605
152805
  }
152606
152806
  /**
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-BEyvfshj.mjs
5384
+ //#region ../types/dist/sleep-BDR76Ykr.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).
@@ -7266,6 +7266,40 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
7266
7266
  DeviceRole["GenericNotifier"] = "generic-notifier";
7267
7267
  return DeviceRole;
7268
7268
  }({});
7269
+ /**
7270
+ * Identity — preserves literal types for downstream inference.
7271
+ *
7272
+ * The constraint is `Record<string, unknown>` (not `CustomActionsSpec`) so
7273
+ * TypeScript does not widen each entry's literal `kind`/`auth` fields to
7274
+ * the broader unions declared on `CustomActionSpec`'s default generics.
7275
+ * Shape validity is enforced separately by the `customAction(...)` helper
7276
+ * whose return type is already a `CustomActionSpec<...>`.
7277
+ */
7278
+ function defineCustomActions(spec) {
7279
+ return spec;
7280
+ }
7281
+ function customAction(input, output, options) {
7282
+ return {
7283
+ input,
7284
+ output,
7285
+ kind: options?.kind ?? "query",
7286
+ auth: options?.auth ?? "protected",
7287
+ scope: options?.scope ?? { kind: "system" },
7288
+ ...options?.caller ? { caller: "required" } : {}
7289
+ };
7290
+ }
7291
+ function deviceCustomAction(input, output, options) {
7292
+ return {
7293
+ input,
7294
+ output,
7295
+ kind: options?.kind ?? "query",
7296
+ auth: options?.auth ?? "protected",
7297
+ scope: {
7298
+ kind: "device",
7299
+ ...options?.deviceTypes ? { deviceTypes: options.deviceTypes } : {}
7300
+ }
7301
+ };
7302
+ }
7269
7303
  var StaticDirOutputSchema = object({ staticDir: string() });
7270
7304
  var VersionOutputSchema = object({ version: string() });
7271
7305
  method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
@@ -12127,6 +12161,21 @@ var streamSignalsCapability = {
12127
12161
  mode: "singleton",
12128
12162
  deviceTypes: Object.values(DeviceType),
12129
12163
  runtimeState: StreamSignalsStatusSchema,
12164
+ /**
12165
+ * Runtime-state durability: **session** — mirrored in RAM, never written.
12166
+ *
12167
+ * The slice holds what the DEVICE says it can emit. That is a probed fact,
12168
+ * not an operator choice: the provider re-declares it on every registration,
12169
+ * so losing it loses nothing and persisting it would freeze an answer the
12170
+ * camera is entitled to change. Measured the same day on the sibling case —
12171
+ * `native-object-detection.supportedClasses` was persisted, and a firmware
12172
+ * class the camera really detected stayed missing for the life of the row
12173
+ * because the fix could not reach it.
12174
+ *
12175
+ * See `RuntimeStateDurability`. Enforced by
12176
+ * `scripts/check-runtime-state-durability.ts`.
12177
+ */
12178
+ durability: "session",
12130
12179
  methods: {
12131
12180
  /**
12132
12181
  * What this device can emit. Empty is a valid and common answer — most
@@ -13101,40 +13150,6 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
13101
13150
  content: string()
13102
13151
  })) }), { auth: "admin" });
13103
13152
  /**
13104
- * Identity — preserves literal types for downstream inference.
13105
- *
13106
- * The constraint is `Record<string, unknown>` (not `CustomActionsSpec`) so
13107
- * TypeScript does not widen each entry's literal `kind`/`auth` fields to
13108
- * the broader unions declared on `CustomActionSpec`'s default generics.
13109
- * Shape validity is enforced separately by the `customAction(...)` helper
13110
- * whose return type is already a `CustomActionSpec<...>`.
13111
- */
13112
- function defineCustomActions(spec) {
13113
- return spec;
13114
- }
13115
- function customAction(input, output, options) {
13116
- return {
13117
- input,
13118
- output,
13119
- kind: options?.kind ?? "query",
13120
- auth: options?.auth ?? "protected",
13121
- scope: options?.scope ?? { kind: "system" },
13122
- ...options?.caller ? { caller: "required" } : {}
13123
- };
13124
- }
13125
- function deviceCustomAction(input, output, options) {
13126
- return {
13127
- input,
13128
- output,
13129
- kind: options?.kind ?? "query",
13130
- auth: options?.auth ?? "protected",
13131
- scope: {
13132
- kind: "device",
13133
- ...options?.deviceTypes ? { deviceTypes: options.deviceTypes } : {}
13134
- }
13135
- };
13136
- }
13137
- /**
13138
13153
  * `custom-model-registry` — collection cap exposing operator-registered
13139
13154
  * custom detection models. Each provider (today: `addon-model-studio`)
13140
13155
  * contributes a list of `CustomModelDescriptor`s; the hub auto-concatenates
@@ -18815,6 +18830,48 @@ var NcAlarmConfigSchema = object({
18815
18830
  settings: NcAlarmSettingsSchema,
18816
18831
  coverage: array(NcAlarmModeCoverageSchema)
18817
18832
  });
18833
+ /**
18834
+ * ONE rule's demand on ONE camera's clip ring.
18835
+ *
18836
+ * A rule, not a camera: the broker takes the MAX over the asks that reach a
18837
+ * camera, so the answer stays a faithful description of the rules and the
18838
+ * bounding (the byte budget, the broker's own ceiling) stays where the cost
18839
+ * lives. A camera that appears in no ask is being told **nothing** — which is
18840
+ * a different thing from never having been told, and only the envelope
18841
+ * (`rulesLoaded`) can tell those apart.
18842
+ */
18843
+ var NcClipRetentionAskSchema = object({
18844
+ /**
18845
+ * The camera this ask is about. **Absent = every camera**, which is what a
18846
+ * rule with no `conditions.devices` means: it can fire anywhere, so it is
18847
+ * asking everywhere.
18848
+ */
18849
+ deviceId: number().int().nonnegative().optional(),
18850
+ /** Seconds of history the rule needs held for it. Never zero — a rule that
18851
+ * needs nothing produces no ask at all. */
18852
+ seconds: number().min(0).max(60),
18853
+ /** The profile the rule cuts from, when it named one. Absent = the cheapest
18854
+ * assigned, which is what the cut defaults to. */
18855
+ profile: CamProfileSchema.optional(),
18856
+ /** Who is asking — so a retention the operator did not expect names a rule. */
18857
+ ruleId: string(),
18858
+ ruleName: string(),
18859
+ /** Why this many seconds: the rule's own window, or the occupancy ceiling. */
18860
+ reason: _enum(["window", "occupancy"])
18861
+ });
18862
+ /**
18863
+ * The whole answer, with the one bit that keeps absence from reading as zero.
18864
+ *
18865
+ * `rulesLoaded` is false until the rule ledger has completed a load at least
18866
+ * once. A centre that is up but has not read its rules yet answers with an
18867
+ * empty ask list that means nothing at all — applying it would drop every
18868
+ * camera's ring to zero for the first events after a restart, which is the
18869
+ * footage this mechanism exists to keep.
18870
+ */
18871
+ var NcClipRetentionPlanSchema = object({
18872
+ rulesLoaded: boolean(),
18873
+ asks: array(NcClipRetentionAskSchema).readonly()
18874
+ });
18818
18875
  method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), method(object({ ruleId: string() }), object({ rule: NcRuleSchema.nullable() }), { auth: "admin" }), method(object({ rule: NcRuleInputSchema }), object({ rule: NcRuleSchema }), {
18819
18876
  kind: "mutation",
18820
18877
  auth: "admin",
@@ -18835,7 +18892,7 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
18835
18892
  }), object({ success: literal(true) }), {
18836
18893
  kind: "mutation",
18837
18894
  auth: "admin"
18838
- }), method(object({}), object({ mutedDeviceIds: array(number().int()).readonly() }), { auth: "admin" }), method(object({
18895
+ }), method(object({}), NcClipRetentionPlanSchema, { auth: "admin" }), method(object({}), object({ mutedDeviceIds: array(number().int()).readonly() }), { auth: "admin" }), method(object({
18839
18896
  deviceId: number().int(),
18840
18897
  muted: boolean()
18841
18898
  }), object({ success: literal(true) }), {
@@ -21409,6 +21466,33 @@ var RunReplayFrameProcessorResultSchema = object({ tracks: array(object({
21409
21466
  * solid is this track", cheaper than re-deriving it from a trajectory. */
21410
21467
  framesMatched: number().int()
21411
21468
  })).readonly() });
21469
+ /**
21470
+ * The long-term series a camera produces. A closed union so a typo cannot
21471
+ * invent one, and the ONE declaration of it — the addon's `AnalyticsLtsSeries`
21472
+ * is an alias of this, not a second list.
21473
+ */
21474
+ var AnalyticsLtsSeriesSchema = _enum([
21475
+ "motion",
21476
+ "audio-dbfs",
21477
+ "battery",
21478
+ "occupancy"
21479
+ ]);
21480
+ /**
21481
+ * One closed 5-minute bucket of a long-term series.
21482
+ *
21483
+ * `samples` is how many readings the bucket folded — it is the ACTIVITY for a
21484
+ * series like `motion`, and the confidence for one like `occupancy`. A bucket
21485
+ * that exists at all was measured; a bucket that is absent was not, which is a
21486
+ * different claim from zero and the reason this is a sparse series.
21487
+ */
21488
+ var LtsBucketSchema = object({
21489
+ scope: string(),
21490
+ bucketStart: number().int(),
21491
+ samples: number().int().nonnegative(),
21492
+ sum: number(),
21493
+ min: number(),
21494
+ max: number()
21495
+ });
21412
21496
  DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).readonly()), method(object({
21413
21497
  deviceId: number(),
21414
21498
  trackId: string()
@@ -21455,6 +21539,14 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
21455
21539
  kinds: array(string()).optional(),
21456
21540
  limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
21457
21541
  }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(KeyEventBatchQueryInput, array(KeyEventsForDeviceSchema).readonly()), method(object({
21542
+ deviceId: number(),
21543
+ series: AnalyticsLtsSeriesSchema,
21544
+ /** Omit for EVERY scope of this series in the range. */
21545
+ scope: string().optional(),
21546
+ from: number(),
21547
+ to: number(),
21548
+ limit: number().int().positive().optional()
21549
+ }), array(LtsBucketSchema).readonly()), method(object({
21458
21550
  deviceId: number(),
21459
21551
  since: number(),
21460
21552
  until: number(),
@@ -22441,7 +22533,47 @@ var RunnerLocalMetricsSchema = object({
22441
22533
  queueDepth: number(),
22442
22534
  frameLazy: FrameLazyMetricsSchema.optional()
22443
22535
  });
22536
+ /**
22537
+ * Pipeline Runner capability — runtime detection workhorse.
22538
+ *
22539
+ * One instance per node. Receives camera assignments from
22540
+ * `addon-pipeline-orchestrator`, subscribes to the local stream-broker for
22541
+ * decoded frames, drains motion + detection queues, calls the local
22542
+ * `motion-detection` and `pipeline-executor` capabilities, and emits typed
22543
+ * `pipeline.inference-result` and `detection.motion-analysis` events on
22544
+ * the bus.
22545
+ *
22546
+ * Distinct from `pipeline-orchestrator` (the hub-side load balancer) — the
22547
+ * runner has zero knowledge of other agents, no global state, and never
22548
+ * makes assignment decisions itself.
22549
+ */
22550
+ /**
22551
+ * Why a runner declined to look at a camera out of band. Named rather than a
22552
+ * bare `false`: a camera asleep on battery (D173), one attached elsewhere, one
22553
+ * already mid-session and one already bursting are four different facts, and a
22554
+ * caller told only "no" cannot tell a protection from a bug.
22555
+ */
22556
+ var OccupancyBurstRefusalSchema = _enum([
22557
+ "not-attached",
22558
+ "battery-asleep",
22559
+ "not-watching",
22560
+ "already-bursting"
22561
+ ]);
22444
22562
  method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mutation" }), method(object({ deviceId: number() }), object({ success: literal(true) }), { kind: "mutation" }), method(ReportMotionInputSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), RunnerLocalLoadSchema), method(_void(), RunnerLocalMetricsSchema), method(object({ deviceId: number() }), CameraMetricsSchema.nullable()), method(_void(), array(CameraMetricsWithDeviceIdSchema).readonly()), method(_void(), array(number()).readonly()), method(object({
22563
+ deviceId: number(),
22564
+ /** How many frames to collect. The caller sizes this against what its
22565
+ * own confirmation needs; the runner clamps it to the field's range. */
22566
+ frames: number().int().positive().optional(),
22567
+ /** Why, for the log. A burst nobody can attribute is a cost nobody can
22568
+ * defend — the periodic one at least has a timer to point at. */
22569
+ reason: string()
22570
+ }), object({
22571
+ started: boolean(),
22572
+ refusedBecause: OccupancyBurstRefusalSchema.optional()
22573
+ }), {
22574
+ auth: "admin",
22575
+ kind: "mutation"
22576
+ }), method(object({
22445
22577
  handle: FrameHandleSchema,
22446
22578
  bbox: NativeCropBboxSchema,
22447
22579
  maxWidth: number().int().positive().optional(),
@@ -34593,7 +34725,28 @@ var PerScopeBreakdownSchema = object({
34593
34725
  /** Total tracked objects in this scope (frame / zone / unzoned). */
34594
34726
  totalObjects: number().int().nonnegative(),
34595
34727
  /** Per-class count. Keys are macro class names (e.g. `person`, `car`). */
34596
- byClass: record(string(), number().int().nonnegative())
34728
+ byClass: record(string(), number().int().nonnegative()),
34729
+ /**
34730
+ * Of `totalObjects`, how many are STANDING — objects the census holds,
34731
+ * present but not going anywhere (a parked car, a bin, a statue).
34732
+ *
34733
+ * The operator's question about a scene has two halves and they answer
34734
+ * different things: "what is parked here" is stable for hours and must not
34735
+ * flap, while "who is walking through" is the thing an alert is about. One
34736
+ * number could only serve one of them, and `totalObjects` served the first
34737
+ * badly — a passer-by moved it and every occupancy rule saw a change.
34738
+ *
34739
+ * `standing + transient === totalObjects`, always. The total keeps its old
34740
+ * meaning exactly, so no rule written against it changes behaviour.
34741
+ *
34742
+ * ABSENT means UNKNOWN, never zero (D393): a snapshot from a node older than
34743
+ * this field cannot say, and a reader that folds absence in as 0 reports "no
34744
+ * parked objects here" about a camera whose lot is full.
34745
+ */
34746
+ standingObjects: number().int().nonnegative().optional(),
34747
+ /** Of `totalObjects`, how many are passing through. See `standingObjects` —
34748
+ * same absence rule. */
34749
+ transientObjects: number().int().nonnegative().optional()
34597
34750
  });
34598
34751
  var ZoneScopeBreakdownSchema = PerScopeBreakdownSchema.extend({
34599
34752
  zoneId: string(),
@@ -34716,6 +34869,23 @@ var HistoryPointSchema = object({
34716
34869
  * triple covers the three spatial scopes (per-zone, frame-wide,
34717
34870
  * outside-any-zone). Crossing them with `className?` gives the full
34718
34871
  * combinatorial coverage the operator UI requested.
34872
+ *
34873
+ * ## ⚠️ The three history methods are DEPRECATED
34874
+ *
34875
+ * They are served from a 60-MINUTE in-memory ring. It dies with the process,
34876
+ * so after a restart they report nothing about a night that did happen, and any
34877
+ * window past an hour is silently clamped — a caller cannot tell a clamp from
34878
+ * an empty scene. The presets in the occupancy chart existed to hide that
34879
+ * horizon rather than to answer a question.
34880
+ *
34881
+ * Use `pipelineAnalytics.readLongTermSeries({ series: 'occupancy', scope })`
34882
+ * instead. Those rows are already written for every camera, ~60 bytes per
34883
+ * (camera, scope, 5-minute bucket), kept for a year, and survive a restart.
34884
+ * Spell the scope with `occupancyScope(zoneId?, className?)` — the same
34885
+ * function the writer uses, so the two cannot drift.
34886
+ *
34887
+ * The ring stays until these three go, and it is the only thing left reading
34888
+ * it. Do not add a fourth caller.
34719
34889
  */
34720
34890
  var zoneAnalyticsCapability = {
34721
34891
  name: "zone-analytics",
@@ -39575,6 +39745,12 @@ Object.freeze({
39575
39745
  addonId: null,
39576
39746
  access: "view"
39577
39747
  },
39748
+ "notificationRules.getClipRetentionAsks": {
39749
+ capName: "notification-rules",
39750
+ capScope: "system",
39751
+ addonId: null,
39752
+ access: "view"
39753
+ },
39578
39754
  "notificationRules.getConditionCatalog": {
39579
39755
  capName: "notification-rules",
39580
39756
  capScope: "system",
@@ -40085,6 +40261,12 @@ Object.freeze({
40085
40261
  addonId: null,
40086
40262
  access: "create"
40087
40263
  },
40264
+ "pipelineAnalytics.readLongTermSeries": {
40265
+ capName: "pipeline-analytics",
40266
+ capScope: "device",
40267
+ addonId: null,
40268
+ access: "view"
40269
+ },
40088
40270
  "pipelineAnalytics.rebuildObjectEmbeddings": {
40089
40271
  capName: "pipeline-analytics",
40090
40272
  capScope: "device",
@@ -40757,6 +40939,12 @@ Object.freeze({
40757
40939
  addonId: null,
40758
40940
  access: "create"
40759
40941
  },
40942
+ "pipelineRunner.requestOccupancyBurst": {
40943
+ capName: "pipeline-runner",
40944
+ capScope: "system",
40945
+ addonId: null,
40946
+ access: "create"
40947
+ },
40760
40948
  "pipelineRunner.runDetailSubtree": {
40761
40949
  capName: "pipeline-runner",
40762
40950
  capScope: "system",
@@ -43901,6 +44089,11 @@ Object.freeze({
43901
44089
  form: "single",
43902
44090
  optional: false
43903
44091
  }],
44092
+ "pipelineAnalytics.readLongTermSeries": [{
44093
+ name: "deviceId",
44094
+ form: "single",
44095
+ optional: false
44096
+ }],
43904
44097
  "pipelineAnalytics.rebuildObjectEmbeddings": [{
43905
44098
  name: "deviceId",
43906
44099
  form: "single",
@@ -44091,6 +44284,11 @@ Object.freeze({
44091
44284
  form: "single",
44092
44285
  optional: false
44093
44286
  }],
44287
+ "pipelineRunner.requestOccupancyBurst": [{
44288
+ name: "deviceId",
44289
+ form: "single",
44290
+ optional: false
44291
+ }],
44094
44292
  "pipelineRunner.runDetailSubtree": [{
44095
44293
  name: "deviceId",
44096
44294
  form: "single",
@@ -152591,11 +152789,13 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
152591
152789
  };
152592
152790
  this.ctx.registerNativeCap(nativeObjectDetectionCapability, provider);
152593
152791
  this.registerStreamSignalsCap();
152594
- if (this.runtimeState.getCapState(CAP_NAME) === void 0) this.runtimeState.setCapState(CAP_NAME, {
152595
- ...buildEmptyState(),
152596
- lastFetchedAt: Date.now()
152597
- });
152598
- else this.runtimeState.patchCapState(CAP_NAME, { supportedClasses: buildSupportedClasses() });
152792
+ if (this.runtimeState.getCapState(CAP_NAME) === void 0) {
152793
+ this.runtimeState.setCapState(CAP_NAME, {
152794
+ ...buildEmptyState(),
152795
+ lastFetchedAt: Date.now()
152796
+ });
152797
+ this.ctx.logger.info("Reolink onboard AI: no restored state — cold-start default (OFF)", { tags: { deviceId: this.id } });
152798
+ } else this.runtimeState.patchCapState(CAP_NAME, { supportedClasses: buildSupportedClasses() });
152599
152799
  this.ctx.logger.info("Reolink native-object-detection cap registered", { tags: { deviceId: this.id } });
152600
152800
  }
152601
152801
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-reolink",
3
- "version": "1.2.129",
3
+ "version": "1.2.131",
4
4
  "description": "Reolink camera device provider addon for CamStack — native Baichuan protocol",
5
5
  "keywords": [
6
6
  "camstack",
@@ -85,7 +85,7 @@
85
85
  "build": "vite build",
86
86
  "dev": "vite build --watch",
87
87
  "typecheck": "tsc --noEmit",
88
- "publish": "npm publish --access public"
88
+ "publish:npm": "npm publish --access public"
89
89
  },
90
90
  "dependencies": {
91
91
  "@apocaliss92/nodelink-js": "^0.7.0"