@camstack/addon-provider-reolink 1.2.124 → 1.2.125

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 +258 -16
  2. package/dist/addon.mjs +258 -16
  3. package/package.json +4 -1
package/dist/addon.js CHANGED
@@ -25,7 +25,7 @@ let fs_promises = require("fs/promises");
25
25
  fs_promises = require_chunk.__toESM(fs_promises, 1);
26
26
  let node_os = require("node:os");
27
27
  node_os = require_chunk.__toESM(node_os);
28
- //#region ../types/dist/event-category-ZyX6jcse.mjs
28
+ //#region ../types/dist/event-category-BVDXG4tB.mjs
29
29
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
30
30
  EventCategory["SystemBoot"] = "system.boot";
31
31
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -642,6 +642,20 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
642
642
  * pull-reconcile from the provider's `getStatus` on reconnect. */
643
643
  EventCategory["MeshNetworkChanged"] = "network.mesh.changed";
644
644
  EventCategory["BackupCompleted"] = "backup.completed";
645
+ /**
646
+ * A whole backup RUN finished — every destination attempted, win or lose.
647
+ *
648
+ * `backup.completed` fires once per DESTINATION, which is the right grain for
649
+ * a progress UI and the wrong one for a notification: an operator with three
650
+ * destinations would be told three times. And a run where two of three
651
+ * destinations succeeded is not a clean success — a single "backup
652
+ * completed" that hid the failed one would be a lie, so the count of each is
653
+ * carried here and the message says both.
654
+ *
655
+ * Emitted by the backup orchestrator only after the destination loop, so a
656
+ * run that dies during the BUILD phase produces no completion at all.
657
+ */
658
+ EventCategory["BackupRunCompleted"] = "backup.run-completed";
645
659
  EventCategory["BackupRestored"] = "backup.restored";
646
660
  EventCategory["NotificationDispatched"] = "notification.dispatched";
647
661
  EventCategory["NotificationFailed"] = "notification.failed";
@@ -5372,7 +5386,7 @@ var ZodIssueCode = {
5372
5386
  var ZodFirstPartyTypeKind;
5373
5387
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5374
5388
  //#endregion
5375
- //#region ../types/dist/sleep-DBKu2-U5.mjs
5389
+ //#region ../types/dist/sleep-DEuj7E3j.mjs
5376
5390
  /**
5377
5391
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5378
5392
  * window to float samples (D455).
@@ -12060,6 +12074,72 @@ method(ListInputSchema, array(BrokerInfoSchema$1)), method(GetInputSchema, Broke
12060
12074
  auth: "admin"
12061
12075
  }), method(GetStateInputSchema, unknown().nullable()), method(_void(), RegistryStatusSchema);
12062
12076
  DeviceType.Camera;
12077
+ /**
12078
+ * The signals a device can emit to WAKE its own stream.
12079
+ *
12080
+ * A camera whose stream is built on demand sleeps until something asks for it,
12081
+ * and "something" cannot be a consumer that is merely attached — a Frigate-style
12082
+ * puller holds a session open for ever, and treating that as demand would keep
12083
+ * a battery camera awake for ever, which is the whole thing the battery is for
12084
+ * (D173). So the wake has to come from the CAMERA: an event it noticed by
12085
+ * itself, with no stream running.
12086
+ *
12087
+ * ## The vocabulary is the PROVIDER'S, not ours
12088
+ *
12089
+ * Like `consumables`, this cap declares no vocabulary of its own. A provider
12090
+ * names each signal with a `code` it chooses and a `label` an operator reads.
12091
+ * Reolink offers motion and camera-native detection; another provider may offer
12092
+ * a tamper, a doorbell press, a PIR, or something no camera in this fleet has
12093
+ * yet. A fixed enum here would mean every new signal is a framework release.
12094
+ *
12095
+ * It is deliberately NOT derived from the caps a device already binds. Whether
12096
+ * a camera CAN push firmware motion is expressed by `motionSources` containing
12097
+ * `'onboard'`, and whether it does AI on-camera by the `native-object-detection`
12098
+ * binding — but both answer "what drives the detection pipeline", which is a
12099
+ * different question from "what may wake a sleeping stream". A camera can do
12100
+ * the first and not be trusted with the second, and the operator picks per
12101
+ * camera. Two questions, two authorities.
12102
+ *
12103
+ * ## Availability is not permission
12104
+ *
12105
+ * `listSignals` says what the device CAN emit. Whether a given signal actually
12106
+ * wakes the stream is the operator's per-camera choice, held by the broker
12107
+ * alongside the cooldown — see the stream-broker cap's wake settings. A
12108
+ * provider declaring a signal is not a provider enabling it.
12109
+ */
12110
+ /** One signal a device can emit. */
12111
+ var StreamSignalSchema = object({
12112
+ /** Stable id chosen by the provider, e.g. `'motion'`, `'person'`, `'tamper'`. */
12113
+ code: string().min(1),
12114
+ /** What an operator reads in the picker. The provider's own wording. */
12115
+ label: string().min(1),
12116
+ /**
12117
+ * Whether the provider recommends this signal ON when a camera is first set
12118
+ * up. A provider knows which of its signals are cheap and reliable; an
12119
+ * operator should not have to discover that by trial. Reolink recommends
12120
+ * both of its own.
12121
+ */
12122
+ recommended: boolean()
12123
+ });
12124
+ var StreamSignalsStatusSchema = object({
12125
+ signals: array(StreamSignalSchema),
12126
+ lastFetchedAt: number()
12127
+ });
12128
+ var streamSignalsCapability = {
12129
+ name: "stream-signals",
12130
+ scope: "device",
12131
+ deviceNative: true,
12132
+ mode: "singleton",
12133
+ deviceTypes: Object.values(DeviceType),
12134
+ runtimeState: StreamSignalsStatusSchema,
12135
+ methods: {
12136
+ /**
12137
+ * What this device can emit. Empty is a valid and common answer — most
12138
+ * cameras have nothing to offer here, and an empty list is what makes the
12139
+ * broker's picker show nothing rather than a false choice.
12140
+ */
12141
+ listSignals: method(_void(), array(StreamSignalSchema).readonly()) }
12142
+ };
12063
12143
  /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
12064
12144
  var StreamFormatSchema = _enum([
12065
12145
  "webrtc",
@@ -12456,6 +12536,22 @@ var EgressTranscodeSchema = object({
12456
12536
  camStreamId: string().nullable()
12457
12537
  });
12458
12538
  method(object({
12539
+ deviceId: number().int().nonnegative(),
12540
+ /** The provider's signal code. */
12541
+ code: string().min(1),
12542
+ /** Ms epoch. Absent ⇒ now. */
12543
+ at: number().optional()
12544
+ }), object({
12545
+ /** Whether the broker acted on it, and if not, why. */
12546
+ accepted: boolean(),
12547
+ reason: _enum([
12548
+ "woke",
12549
+ "hold-extended",
12550
+ "not-enabled",
12551
+ "no-consumer",
12552
+ "unknown-code"
12553
+ ])
12554
+ }), { kind: "mutation" }), method(object({
12459
12555
  deviceId: number().int().nonnegative(),
12460
12556
  camStreamId: string().min(1),
12461
12557
  kind: CamStreamKindSchema,
@@ -12710,6 +12806,16 @@ var PickStreamRequirementsSchema = object({
12710
12806
  acceptCodecs: array(StreamCodecSchema).readonly().optional(),
12711
12807
  /** Minimum vertical resolution. Streams shorter than this are dropped. */
12712
12808
  minHeight: number().int().positive().optional(),
12809
+ /**
12810
+ * Maximum vertical resolution. Streams TALLER than this are dropped.
12811
+ *
12812
+ * A consumer can have a ceiling as real as its floor: Alexa documents
12813
+ * 480p to 1080p, and a 4K stream is as unusable to an Echo as a 360p one.
12814
+ * Without this the ceiling had to be re-implemented by every caller — and
12815
+ * one caller implementing it privately is how a second scoring authority
12816
+ * gets born.
12817
+ */
12818
+ maxHeight: number().int().positive().optional(),
12713
12819
  /** Minimum horizontal resolution. */
12714
12820
  minWidth: number().int().positive().optional(),
12715
12821
  /**
@@ -12726,7 +12832,22 @@ var PickStreamRequirementsSchema = object({
12726
12832
  * transcoded" guard: if the device is already serving the consumer's
12727
12833
  * codec end-to-end, there's nothing to optimise.
12728
12834
  */
12729
- requireSiblingCodec: array(StreamCodecSchema).readonly().optional()
12835
+ requireSiblingCodec: array(StreamCodecSchema).readonly().optional(),
12836
+ /**
12837
+ * Whether a stream the consumer CANNOT decode may still be picked, on the
12838
+ * understanding that it will be transcoded.
12839
+ *
12840
+ * Default `false` — today's behaviour, and the right one for a bypass
12841
+ * question ("is there a stream I can forward untouched?"). Set `true` to
12842
+ * ask the larger question: "what is the best stream for me, transcoding if
12843
+ * I must?" A stream that satisfies `acceptCodecs` always outranks one that
12844
+ * does not, so a passthrough is never lost to a transcode; the answer says
12845
+ * which it is in {@link PickedCamStreamSchema.transcodes}.
12846
+ *
12847
+ * This is what lets one picker serve both the bypass and the full source
12848
+ * choice, instead of a consumer scoring privately when the bypass misses.
12849
+ */
12850
+ allowTranscode: boolean().optional()
12730
12851
  }).readonly();
12731
12852
  var PickStreamPreferencesSchema = object({
12732
12853
  /**
@@ -12740,12 +12861,32 @@ var PickStreamPreferencesSchema = object({
12740
12861
  * picks the tallest stream; `'lowest'` picks the shortest (used by
12741
12862
  * memory-constrained consumers / Apple Home guest sessions).
12742
12863
  */
12743
- resolutionPreference: _enum(["highest", "lowest"]).optional()
12864
+ resolutionPreference: _enum(["highest", "lowest"]).optional(),
12865
+ /**
12866
+ * The height the consumer actually wants to DELIVER.
12867
+ *
12868
+ * Not a constraint — an ordering. With it set, the SMALLEST stream at or
12869
+ * above the target wins, and only if nothing reaches it does the tallest
12870
+ * take over. Pulling 1296 lines to draw 720 on a 1280x800 Echo panel costs
12871
+ * a decode and buys nothing, and `resolutionPreference: 'highest'` cannot
12872
+ * express that: it says "as big as possible", which is a different wish.
12873
+ *
12874
+ * Ignored when absent, so every existing caller keeps its ordering.
12875
+ */
12876
+ targetHeight: number().int().positive().optional()
12744
12877
  }).readonly();
12745
12878
  var PickedCamStreamSchema = object({
12746
12879
  camStreamId: string(),
12747
12880
  codec: string().optional(),
12748
12881
  resolution: CamStreamResolutionSchema.optional(),
12882
+ /**
12883
+ * Whether serving this stream requires a decode + re-encode.
12884
+ *
12885
+ * `false` is a stream the consumer can take as it stands. Only ever `true`
12886
+ * when the caller asked for it with `allowTranscode`, so a caller that did
12887
+ * not ask cannot be handed a cost it never agreed to pay.
12888
+ */
12889
+ transcodes: boolean(),
12749
12890
  /** One-line explanation of why this stream won — for logs / debug UI. */
12750
12891
  reason: string()
12751
12892
  });
@@ -17336,6 +17477,8 @@ var NcSystemEventKindSchema = _enum([
17336
17477
  "device-enabled",
17337
17478
  "device-battery-low",
17338
17479
  "device-battery-normal",
17480
+ "device-consumable-low",
17481
+ "device-consumable-normal",
17339
17482
  "stream-online",
17340
17483
  "stream-offline",
17341
17484
  "node-online",
@@ -17354,6 +17497,7 @@ var NcSystemEventKindSchema = _enum([
17354
17497
  "addon-updated",
17355
17498
  "server-updated",
17356
17499
  "export-completed",
17500
+ "backup-completed",
17357
17501
  "camera-online",
17358
17502
  "camera-offline",
17359
17503
  "camera-disabled",
@@ -24739,10 +24883,28 @@ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), ar
24739
24883
  }), boolean()), method(object({
24740
24884
  deviceId: number().int().nonnegative(),
24741
24885
  sessionId: string()
24742
- }), object({ pendingRenegotiation: object({
24743
- target: WebrtcStreamTargetSchema,
24744
- epoch: number()
24745
- }).nullable() }));
24886
+ }), object({
24887
+ pendingRenegotiation: object({
24888
+ target: WebrtcStreamTargetSchema,
24889
+ epoch: number()
24890
+ }).nullable(),
24891
+ /**
24892
+ * Whether the session still EXISTS.
24893
+ *
24894
+ * A consumer that holds a resource for the life of a session needs to
24895
+ * be able to ask, because a session does not always end the way it
24896
+ * began. Measured on this hub 2026-09-13: an Echo that got stuck never
24897
+ * sent `SessionDisconnected`, so the Alexa exporter's
24898
+ * `releaseEgressTranscode` never ran, and a 2304x1296 HEVC to 720p
24899
+ * H.264 transcode kept running for NOBODY for more than twenty
24900
+ * minutes. The WebRTC session had logged `WebRTC session closed`
24901
+ * minutes earlier — the broker knew; the holder had no way to ask.
24902
+ *
24903
+ * `false` for a session id the provider has never heard of, which is
24904
+ * the same answer as "it ended": either way nothing is holding it up.
24905
+ */
24906
+ alive: boolean()
24907
+ }));
24746
24908
  object({
24747
24909
  /** All accessory children of the parent. */
24748
24910
  childDeviceIds: array(number()).readonly(),
@@ -34981,6 +35143,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
34981
35143
  smoke: smokeCapability,
34982
35144
  streamCatalog: streamCatalogCapability,
34983
35145
  streamParams: streamParamsCapability,
35146
+ streamSignals: streamSignalsCapability,
34984
35147
  switch: switchCapability,
34985
35148
  tamper: tamperCapability,
34986
35149
  temperatureSensor: temperatureSensorCapability,
@@ -41717,6 +41880,12 @@ Object.freeze({
41717
41880
  addonId: null,
41718
41881
  access: "create"
41719
41882
  },
41883
+ "streamBroker.reportStreamSignal": {
41884
+ capName: "stream-broker",
41885
+ capScope: "system",
41886
+ addonId: null,
41887
+ access: "create"
41888
+ },
41720
41889
  "streamBroker.restartProfile": {
41721
41890
  capName: "stream-broker",
41722
41891
  capScope: "system",
@@ -41801,6 +41970,12 @@ Object.freeze({
41801
41970
  addonId: null,
41802
41971
  access: "create"
41803
41972
  },
41973
+ "streamSignals.listSignals": {
41974
+ capName: "stream-signals",
41975
+ capScope: "device",
41976
+ addonId: null,
41977
+ access: "view"
41978
+ },
41804
41979
  "switch.setState": {
41805
41980
  capName: "switch",
41806
41981
  capScope: "device",
@@ -44173,6 +44348,11 @@ Object.freeze({
44173
44348
  form: "single",
44174
44349
  optional: false
44175
44350
  }],
44351
+ "streamBroker.reportStreamSignal": [{
44352
+ name: "deviceId",
44353
+ form: "single",
44354
+ optional: false
44355
+ }],
44176
44356
  "streamBroker.restartProfile": [{
44177
44357
  name: "deviceId",
44178
44358
  form: "single",
@@ -152126,6 +152306,63 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
152126
152306
  this.ctx.logger.info("Reolink image-settings cap registered", { tags: { deviceId: this.id } });
152127
152307
  }
152128
152308
  /**
152309
+ * Tell the broker one of our declared signals just fired.
152310
+ *
152311
+ * Never awaited by the push path: a camera event must not wait on a stream
152312
+ * decision, and the broker's own answer (`woke` / `hold-extended` /
152313
+ * `no-consumer` / `not-enabled`) is the interesting part only when something
152314
+ * is actually watching. Logged at debug so a quiet camera does not fill the
152315
+ * log with refusals nobody asked about — the broker logs the wakes it acts
152316
+ * on, with `tags: { deviceId }`, and that is the operator-facing line.
152317
+ */
152318
+ async reportWakeSignal(code) {
152319
+ try {
152320
+ const result = await this.ctx.api.streamBroker.reportStreamSignal.mutate({
152321
+ deviceId: this.id,
152322
+ code
152323
+ });
152324
+ if (result.accepted) this.ctx.logger.info("wake signal accepted by the broker", {
152325
+ tags: { deviceId: this.id },
152326
+ meta: {
152327
+ code,
152328
+ reason: result.reason
152329
+ }
152330
+ });
152331
+ } catch (err) {
152332
+ this.ctx.logger.debug("wake signal not delivered", {
152333
+ tags: { deviceId: this.id },
152334
+ meta: {
152335
+ code,
152336
+ error: err instanceof Error ? err.message : String(err)
152337
+ }
152338
+ });
152339
+ }
152340
+ }
152341
+ /**
152342
+ * Declare the signals THIS camera can emit to wake its own stream.
152343
+ *
152344
+ * Two, and both recommended: a Reolink pushes firmware motion over Baichuan
152345
+ * and camera-native AI detections, both without a stream running, which is
152346
+ * exactly the property the wake needs — our own detections come from decoded
152347
+ * frames and would depend on the stream they are meant to justify.
152348
+ *
152349
+ * Declaring is not enabling: the operator picks per camera, and the broker
152350
+ * holds that choice alongside the cooldown. `recommended` only says which
152351
+ * ones this provider is confident in.
152352
+ */
152353
+ registerStreamSignalsCap() {
152354
+ const signals = [{
152355
+ code: "motion",
152356
+ label: "Motion (camera)",
152357
+ recommended: true
152358
+ }, {
152359
+ code: "detection",
152360
+ label: "Object detected (camera)",
152361
+ recommended: true
152362
+ }];
152363
+ this.ctx.registerNativeCap(streamSignalsCapability, { listSignals: async () => signals });
152364
+ }
152365
+ /**
152129
152366
  * Register the `native-object-detection` cap.
152130
152367
  *
152131
152368
  * The runtimeState holds three fields:
@@ -152183,6 +152420,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
152183
152420
  }
152184
152421
  };
152185
152422
  this.ctx.registerNativeCap(nativeObjectDetectionCapability, provider);
152423
+ this.registerStreamSignalsCap();
152186
152424
  if (this.runtimeState.getCapState(CAP_NAME) === void 0) this.runtimeState.setCapState(CAP_NAME, {
152187
152425
  ...buildEmptyState(),
152188
152426
  lastFetchedAt: Date.now()
@@ -155537,6 +155775,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
155537
155775
  timestamp: now,
155538
155776
  source: "onboard"
155539
155777
  }));
155778
+ this.reportWakeSignal("motion");
155540
155779
  return;
155541
155780
  }
155542
155781
  const aiClass = AI_CLASS_MAP[event.type];
@@ -155547,14 +155786,17 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
155547
155786
  timestamp: now,
155548
155787
  confidence: void 0
155549
155788
  } } });
155550
- if (isNativeObjectForwardingEnabled(this.runtimeState.getCapState("native-object-detection"))) this.ctx.eventBus.emit(createEvent(EventCategory.DetectionCameraNative, eventSource, {
155551
- cameraId: this.id,
155552
- source: "onboard",
155553
- detections: [{
155554
- class: aiClass,
155555
- timestamp: now
155556
- }]
155557
- }));
155789
+ if (isNativeObjectForwardingEnabled(this.runtimeState.getCapState("native-object-detection"))) {
155790
+ this.reportWakeSignal("detection");
155791
+ this.ctx.eventBus.emit(createEvent(EventCategory.DetectionCameraNative, eventSource, {
155792
+ cameraId: this.id,
155793
+ source: "onboard",
155794
+ detections: [{
155795
+ class: aiClass,
155796
+ timestamp: now
155797
+ }]
155798
+ }));
155799
+ }
155558
155800
  this.ctx.eventBus.emit(createEvent(EventCategory.MotionOnMotionChanged, eventSource, {
155559
155801
  deviceId: this.id,
155560
155802
  detected: true,
package/dist/addon.mjs CHANGED
@@ -20,7 +20,7 @@ import * as net2 from "net";
20
20
  import netImpl from "net";
21
21
  import { mkdir } from "fs/promises";
22
22
  import os from "node:os";
23
- //#region ../types/dist/event-category-ZyX6jcse.mjs
23
+ //#region ../types/dist/event-category-BVDXG4tB.mjs
24
24
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
25
25
  EventCategory["SystemBoot"] = "system.boot";
26
26
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -637,6 +637,20 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
637
637
  * pull-reconcile from the provider's `getStatus` on reconnect. */
638
638
  EventCategory["MeshNetworkChanged"] = "network.mesh.changed";
639
639
  EventCategory["BackupCompleted"] = "backup.completed";
640
+ /**
641
+ * A whole backup RUN finished — every destination attempted, win or lose.
642
+ *
643
+ * `backup.completed` fires once per DESTINATION, which is the right grain for
644
+ * a progress UI and the wrong one for a notification: an operator with three
645
+ * destinations would be told three times. And a run where two of three
646
+ * destinations succeeded is not a clean success — a single "backup
647
+ * completed" that hid the failed one would be a lie, so the count of each is
648
+ * carried here and the message says both.
649
+ *
650
+ * Emitted by the backup orchestrator only after the destination loop, so a
651
+ * run that dies during the BUILD phase produces no completion at all.
652
+ */
653
+ EventCategory["BackupRunCompleted"] = "backup.run-completed";
640
654
  EventCategory["BackupRestored"] = "backup.restored";
641
655
  EventCategory["NotificationDispatched"] = "notification.dispatched";
642
656
  EventCategory["NotificationFailed"] = "notification.failed";
@@ -5367,7 +5381,7 @@ var ZodIssueCode = {
5367
5381
  var ZodFirstPartyTypeKind;
5368
5382
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5369
5383
  //#endregion
5370
- //#region ../types/dist/sleep-DBKu2-U5.mjs
5384
+ //#region ../types/dist/sleep-DEuj7E3j.mjs
5371
5385
  /**
5372
5386
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5373
5387
  * window to float samples (D455).
@@ -12055,6 +12069,72 @@ method(ListInputSchema, array(BrokerInfoSchema$1)), method(GetInputSchema, Broke
12055
12069
  auth: "admin"
12056
12070
  }), method(GetStateInputSchema, unknown().nullable()), method(_void(), RegistryStatusSchema);
12057
12071
  DeviceType.Camera;
12072
+ /**
12073
+ * The signals a device can emit to WAKE its own stream.
12074
+ *
12075
+ * A camera whose stream is built on demand sleeps until something asks for it,
12076
+ * and "something" cannot be a consumer that is merely attached — a Frigate-style
12077
+ * puller holds a session open for ever, and treating that as demand would keep
12078
+ * a battery camera awake for ever, which is the whole thing the battery is for
12079
+ * (D173). So the wake has to come from the CAMERA: an event it noticed by
12080
+ * itself, with no stream running.
12081
+ *
12082
+ * ## The vocabulary is the PROVIDER'S, not ours
12083
+ *
12084
+ * Like `consumables`, this cap declares no vocabulary of its own. A provider
12085
+ * names each signal with a `code` it chooses and a `label` an operator reads.
12086
+ * Reolink offers motion and camera-native detection; another provider may offer
12087
+ * a tamper, a doorbell press, a PIR, or something no camera in this fleet has
12088
+ * yet. A fixed enum here would mean every new signal is a framework release.
12089
+ *
12090
+ * It is deliberately NOT derived from the caps a device already binds. Whether
12091
+ * a camera CAN push firmware motion is expressed by `motionSources` containing
12092
+ * `'onboard'`, and whether it does AI on-camera by the `native-object-detection`
12093
+ * binding — but both answer "what drives the detection pipeline", which is a
12094
+ * different question from "what may wake a sleeping stream". A camera can do
12095
+ * the first and not be trusted with the second, and the operator picks per
12096
+ * camera. Two questions, two authorities.
12097
+ *
12098
+ * ## Availability is not permission
12099
+ *
12100
+ * `listSignals` says what the device CAN emit. Whether a given signal actually
12101
+ * wakes the stream is the operator's per-camera choice, held by the broker
12102
+ * alongside the cooldown — see the stream-broker cap's wake settings. A
12103
+ * provider declaring a signal is not a provider enabling it.
12104
+ */
12105
+ /** One signal a device can emit. */
12106
+ var StreamSignalSchema = object({
12107
+ /** Stable id chosen by the provider, e.g. `'motion'`, `'person'`, `'tamper'`. */
12108
+ code: string().min(1),
12109
+ /** What an operator reads in the picker. The provider's own wording. */
12110
+ label: string().min(1),
12111
+ /**
12112
+ * Whether the provider recommends this signal ON when a camera is first set
12113
+ * up. A provider knows which of its signals are cheap and reliable; an
12114
+ * operator should not have to discover that by trial. Reolink recommends
12115
+ * both of its own.
12116
+ */
12117
+ recommended: boolean()
12118
+ });
12119
+ var StreamSignalsStatusSchema = object({
12120
+ signals: array(StreamSignalSchema),
12121
+ lastFetchedAt: number()
12122
+ });
12123
+ var streamSignalsCapability = {
12124
+ name: "stream-signals",
12125
+ scope: "device",
12126
+ deviceNative: true,
12127
+ mode: "singleton",
12128
+ deviceTypes: Object.values(DeviceType),
12129
+ runtimeState: StreamSignalsStatusSchema,
12130
+ methods: {
12131
+ /**
12132
+ * What this device can emit. Empty is a valid and common answer — most
12133
+ * cameras have nothing to offer here, and an empty list is what makes the
12134
+ * broker's picker show nothing rather than a false choice.
12135
+ */
12136
+ listSignals: method(_void(), array(StreamSignalSchema).readonly()) }
12137
+ };
12058
12138
  /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
12059
12139
  var StreamFormatSchema = _enum([
12060
12140
  "webrtc",
@@ -12451,6 +12531,22 @@ var EgressTranscodeSchema = object({
12451
12531
  camStreamId: string().nullable()
12452
12532
  });
12453
12533
  method(object({
12534
+ deviceId: number().int().nonnegative(),
12535
+ /** The provider's signal code. */
12536
+ code: string().min(1),
12537
+ /** Ms epoch. Absent ⇒ now. */
12538
+ at: number().optional()
12539
+ }), object({
12540
+ /** Whether the broker acted on it, and if not, why. */
12541
+ accepted: boolean(),
12542
+ reason: _enum([
12543
+ "woke",
12544
+ "hold-extended",
12545
+ "not-enabled",
12546
+ "no-consumer",
12547
+ "unknown-code"
12548
+ ])
12549
+ }), { kind: "mutation" }), method(object({
12454
12550
  deviceId: number().int().nonnegative(),
12455
12551
  camStreamId: string().min(1),
12456
12552
  kind: CamStreamKindSchema,
@@ -12705,6 +12801,16 @@ var PickStreamRequirementsSchema = object({
12705
12801
  acceptCodecs: array(StreamCodecSchema).readonly().optional(),
12706
12802
  /** Minimum vertical resolution. Streams shorter than this are dropped. */
12707
12803
  minHeight: number().int().positive().optional(),
12804
+ /**
12805
+ * Maximum vertical resolution. Streams TALLER than this are dropped.
12806
+ *
12807
+ * A consumer can have a ceiling as real as its floor: Alexa documents
12808
+ * 480p to 1080p, and a 4K stream is as unusable to an Echo as a 360p one.
12809
+ * Without this the ceiling had to be re-implemented by every caller — and
12810
+ * one caller implementing it privately is how a second scoring authority
12811
+ * gets born.
12812
+ */
12813
+ maxHeight: number().int().positive().optional(),
12708
12814
  /** Minimum horizontal resolution. */
12709
12815
  minWidth: number().int().positive().optional(),
12710
12816
  /**
@@ -12721,7 +12827,22 @@ var PickStreamRequirementsSchema = object({
12721
12827
  * transcoded" guard: if the device is already serving the consumer's
12722
12828
  * codec end-to-end, there's nothing to optimise.
12723
12829
  */
12724
- requireSiblingCodec: array(StreamCodecSchema).readonly().optional()
12830
+ requireSiblingCodec: array(StreamCodecSchema).readonly().optional(),
12831
+ /**
12832
+ * Whether a stream the consumer CANNOT decode may still be picked, on the
12833
+ * understanding that it will be transcoded.
12834
+ *
12835
+ * Default `false` — today's behaviour, and the right one for a bypass
12836
+ * question ("is there a stream I can forward untouched?"). Set `true` to
12837
+ * ask the larger question: "what is the best stream for me, transcoding if
12838
+ * I must?" A stream that satisfies `acceptCodecs` always outranks one that
12839
+ * does not, so a passthrough is never lost to a transcode; the answer says
12840
+ * which it is in {@link PickedCamStreamSchema.transcodes}.
12841
+ *
12842
+ * This is what lets one picker serve both the bypass and the full source
12843
+ * choice, instead of a consumer scoring privately when the bypass misses.
12844
+ */
12845
+ allowTranscode: boolean().optional()
12725
12846
  }).readonly();
12726
12847
  var PickStreamPreferencesSchema = object({
12727
12848
  /**
@@ -12735,12 +12856,32 @@ var PickStreamPreferencesSchema = object({
12735
12856
  * picks the tallest stream; `'lowest'` picks the shortest (used by
12736
12857
  * memory-constrained consumers / Apple Home guest sessions).
12737
12858
  */
12738
- resolutionPreference: _enum(["highest", "lowest"]).optional()
12859
+ resolutionPreference: _enum(["highest", "lowest"]).optional(),
12860
+ /**
12861
+ * The height the consumer actually wants to DELIVER.
12862
+ *
12863
+ * Not a constraint — an ordering. With it set, the SMALLEST stream at or
12864
+ * above the target wins, and only if nothing reaches it does the tallest
12865
+ * take over. Pulling 1296 lines to draw 720 on a 1280x800 Echo panel costs
12866
+ * a decode and buys nothing, and `resolutionPreference: 'highest'` cannot
12867
+ * express that: it says "as big as possible", which is a different wish.
12868
+ *
12869
+ * Ignored when absent, so every existing caller keeps its ordering.
12870
+ */
12871
+ targetHeight: number().int().positive().optional()
12739
12872
  }).readonly();
12740
12873
  var PickedCamStreamSchema = object({
12741
12874
  camStreamId: string(),
12742
12875
  codec: string().optional(),
12743
12876
  resolution: CamStreamResolutionSchema.optional(),
12877
+ /**
12878
+ * Whether serving this stream requires a decode + re-encode.
12879
+ *
12880
+ * `false` is a stream the consumer can take as it stands. Only ever `true`
12881
+ * when the caller asked for it with `allowTranscode`, so a caller that did
12882
+ * not ask cannot be handed a cost it never agreed to pay.
12883
+ */
12884
+ transcodes: boolean(),
12744
12885
  /** One-line explanation of why this stream won — for logs / debug UI. */
12745
12886
  reason: string()
12746
12887
  });
@@ -17331,6 +17472,8 @@ var NcSystemEventKindSchema = _enum([
17331
17472
  "device-enabled",
17332
17473
  "device-battery-low",
17333
17474
  "device-battery-normal",
17475
+ "device-consumable-low",
17476
+ "device-consumable-normal",
17334
17477
  "stream-online",
17335
17478
  "stream-offline",
17336
17479
  "node-online",
@@ -17349,6 +17492,7 @@ var NcSystemEventKindSchema = _enum([
17349
17492
  "addon-updated",
17350
17493
  "server-updated",
17351
17494
  "export-completed",
17495
+ "backup-completed",
17352
17496
  "camera-online",
17353
17497
  "camera-offline",
17354
17498
  "camera-disabled",
@@ -24734,10 +24878,28 @@ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), ar
24734
24878
  }), boolean()), method(object({
24735
24879
  deviceId: number().int().nonnegative(),
24736
24880
  sessionId: string()
24737
- }), object({ pendingRenegotiation: object({
24738
- target: WebrtcStreamTargetSchema,
24739
- epoch: number()
24740
- }).nullable() }));
24881
+ }), object({
24882
+ pendingRenegotiation: object({
24883
+ target: WebrtcStreamTargetSchema,
24884
+ epoch: number()
24885
+ }).nullable(),
24886
+ /**
24887
+ * Whether the session still EXISTS.
24888
+ *
24889
+ * A consumer that holds a resource for the life of a session needs to
24890
+ * be able to ask, because a session does not always end the way it
24891
+ * began. Measured on this hub 2026-09-13: an Echo that got stuck never
24892
+ * sent `SessionDisconnected`, so the Alexa exporter's
24893
+ * `releaseEgressTranscode` never ran, and a 2304x1296 HEVC to 720p
24894
+ * H.264 transcode kept running for NOBODY for more than twenty
24895
+ * minutes. The WebRTC session had logged `WebRTC session closed`
24896
+ * minutes earlier — the broker knew; the holder had no way to ask.
24897
+ *
24898
+ * `false` for a session id the provider has never heard of, which is
24899
+ * the same answer as "it ended": either way nothing is holding it up.
24900
+ */
24901
+ alive: boolean()
24902
+ }));
24741
24903
  object({
24742
24904
  /** All accessory children of the parent. */
24743
24905
  childDeviceIds: array(number()).readonly(),
@@ -34976,6 +35138,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
34976
35138
  smoke: smokeCapability,
34977
35139
  streamCatalog: streamCatalogCapability,
34978
35140
  streamParams: streamParamsCapability,
35141
+ streamSignals: streamSignalsCapability,
34979
35142
  switch: switchCapability,
34980
35143
  tamper: tamperCapability,
34981
35144
  temperatureSensor: temperatureSensorCapability,
@@ -41712,6 +41875,12 @@ Object.freeze({
41712
41875
  addonId: null,
41713
41876
  access: "create"
41714
41877
  },
41878
+ "streamBroker.reportStreamSignal": {
41879
+ capName: "stream-broker",
41880
+ capScope: "system",
41881
+ addonId: null,
41882
+ access: "create"
41883
+ },
41715
41884
  "streamBroker.restartProfile": {
41716
41885
  capName: "stream-broker",
41717
41886
  capScope: "system",
@@ -41796,6 +41965,12 @@ Object.freeze({
41796
41965
  addonId: null,
41797
41966
  access: "create"
41798
41967
  },
41968
+ "streamSignals.listSignals": {
41969
+ capName: "stream-signals",
41970
+ capScope: "device",
41971
+ addonId: null,
41972
+ access: "view"
41973
+ },
41799
41974
  "switch.setState": {
41800
41975
  capName: "switch",
41801
41976
  capScope: "device",
@@ -44168,6 +44343,11 @@ Object.freeze({
44168
44343
  form: "single",
44169
44344
  optional: false
44170
44345
  }],
44346
+ "streamBroker.reportStreamSignal": [{
44347
+ name: "deviceId",
44348
+ form: "single",
44349
+ optional: false
44350
+ }],
44171
44351
  "streamBroker.restartProfile": [{
44172
44352
  name: "deviceId",
44173
44353
  form: "single",
@@ -152121,6 +152301,63 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
152121
152301
  this.ctx.logger.info("Reolink image-settings cap registered", { tags: { deviceId: this.id } });
152122
152302
  }
152123
152303
  /**
152304
+ * Tell the broker one of our declared signals just fired.
152305
+ *
152306
+ * Never awaited by the push path: a camera event must not wait on a stream
152307
+ * decision, and the broker's own answer (`woke` / `hold-extended` /
152308
+ * `no-consumer` / `not-enabled`) is the interesting part only when something
152309
+ * is actually watching. Logged at debug so a quiet camera does not fill the
152310
+ * log with refusals nobody asked about — the broker logs the wakes it acts
152311
+ * on, with `tags: { deviceId }`, and that is the operator-facing line.
152312
+ */
152313
+ async reportWakeSignal(code) {
152314
+ try {
152315
+ const result = await this.ctx.api.streamBroker.reportStreamSignal.mutate({
152316
+ deviceId: this.id,
152317
+ code
152318
+ });
152319
+ if (result.accepted) this.ctx.logger.info("wake signal accepted by the broker", {
152320
+ tags: { deviceId: this.id },
152321
+ meta: {
152322
+ code,
152323
+ reason: result.reason
152324
+ }
152325
+ });
152326
+ } catch (err) {
152327
+ this.ctx.logger.debug("wake signal not delivered", {
152328
+ tags: { deviceId: this.id },
152329
+ meta: {
152330
+ code,
152331
+ error: err instanceof Error ? err.message : String(err)
152332
+ }
152333
+ });
152334
+ }
152335
+ }
152336
+ /**
152337
+ * Declare the signals THIS camera can emit to wake its own stream.
152338
+ *
152339
+ * Two, and both recommended: a Reolink pushes firmware motion over Baichuan
152340
+ * and camera-native AI detections, both without a stream running, which is
152341
+ * exactly the property the wake needs — our own detections come from decoded
152342
+ * frames and would depend on the stream they are meant to justify.
152343
+ *
152344
+ * Declaring is not enabling: the operator picks per camera, and the broker
152345
+ * holds that choice alongside the cooldown. `recommended` only says which
152346
+ * ones this provider is confident in.
152347
+ */
152348
+ registerStreamSignalsCap() {
152349
+ const signals = [{
152350
+ code: "motion",
152351
+ label: "Motion (camera)",
152352
+ recommended: true
152353
+ }, {
152354
+ code: "detection",
152355
+ label: "Object detected (camera)",
152356
+ recommended: true
152357
+ }];
152358
+ this.ctx.registerNativeCap(streamSignalsCapability, { listSignals: async () => signals });
152359
+ }
152360
+ /**
152124
152361
  * Register the `native-object-detection` cap.
152125
152362
  *
152126
152363
  * The runtimeState holds three fields:
@@ -152178,6 +152415,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
152178
152415
  }
152179
152416
  };
152180
152417
  this.ctx.registerNativeCap(nativeObjectDetectionCapability, provider);
152418
+ this.registerStreamSignalsCap();
152181
152419
  if (this.runtimeState.getCapState(CAP_NAME) === void 0) this.runtimeState.setCapState(CAP_NAME, {
152182
152420
  ...buildEmptyState(),
152183
152421
  lastFetchedAt: Date.now()
@@ -155532,6 +155770,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
155532
155770
  timestamp: now,
155533
155771
  source: "onboard"
155534
155772
  }));
155773
+ this.reportWakeSignal("motion");
155535
155774
  return;
155536
155775
  }
155537
155776
  const aiClass = AI_CLASS_MAP[event.type];
@@ -155542,14 +155781,17 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
155542
155781
  timestamp: now,
155543
155782
  confidence: void 0
155544
155783
  } } });
155545
- if (isNativeObjectForwardingEnabled(this.runtimeState.getCapState("native-object-detection"))) this.ctx.eventBus.emit(createEvent(EventCategory.DetectionCameraNative, eventSource, {
155546
- cameraId: this.id,
155547
- source: "onboard",
155548
- detections: [{
155549
- class: aiClass,
155550
- timestamp: now
155551
- }]
155552
- }));
155784
+ if (isNativeObjectForwardingEnabled(this.runtimeState.getCapState("native-object-detection"))) {
155785
+ this.reportWakeSignal("detection");
155786
+ this.ctx.eventBus.emit(createEvent(EventCategory.DetectionCameraNative, eventSource, {
155787
+ cameraId: this.id,
155788
+ source: "onboard",
155789
+ detections: [{
155790
+ class: aiClass,
155791
+ timestamp: now
155792
+ }]
155793
+ }));
155794
+ }
155553
155795
  this.ctx.eventBus.emit(createEvent(EventCategory.MotionOnMotionChanged, eventSource, {
155554
155796
  deviceId: this.id,
155555
155797
  detected: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-reolink",
3
- "version": "1.2.124",
3
+ "version": "1.2.125",
4
4
  "description": "Reolink camera device provider addon for CamStack — native Baichuan protocol",
5
5
  "keywords": [
6
6
  "camstack",
@@ -65,6 +65,9 @@
65
65
  },
66
66
  {
67
67
  "name": "failure-contribution"
68
+ },
69
+ {
70
+ "name": "stream-signals"
68
71
  }
69
72
  ]
70
73
  }