@camstack/addon-provider-reolink 1.2.124 → 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 +434 -54
  2. package/dist/addon.mjs +434 -54
  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-BEyvfshj.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(),
@@ -29181,11 +29343,39 @@ var NativeObjectDetectionStatusSchema = object({
29181
29343
  supportedClasses: array(NativeObjectClassEnum).readonly(),
29182
29344
  /**
29183
29345
  * Whether forwarding of onboard AI detections is enabled for this device.
29184
- * Default FALSE (opt-in, cold-start) — onboard AI pushes are noisy/sparse and
29185
- * 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.
29186
29354
  */
29187
29355
  enabled: boolean()
29188
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() });
29189
29379
  var NativeObjectDetectionRuntimeStateSchema = NativeObjectDetectionStatusSchema.extend({
29190
29380
  /** Required by createRuntimeStateBridge — epoch ms of last refresh. */
29191
29381
  lastFetchedAt: number() });
@@ -29195,13 +29385,28 @@ var nativeObjectDetectionCapability = {
29195
29385
  deviceNative: true,
29196
29386
  mode: "singleton",
29197
29387
  deviceTypes: [DeviceType.Camera],
29198
- methods: { setEnabled: method(object({
29199
- deviceId: number(),
29200
- enabled: boolean()
29201
- }), _void(), {
29202
- kind: "mutation",
29203
- auth: "admin"
29204
- }) },
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
+ },
29205
29410
  events: { onDetected: { data: object({
29206
29411
  deviceId: number(),
29207
29412
  detection: NativeDetectionSchema
@@ -34981,6 +35186,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
34981
35186
  smoke: smokeCapability,
34982
35187
  streamCatalog: streamCatalogCapability,
34983
35188
  streamParams: streamParamsCapability,
35189
+ streamSignals: streamSignalsCapability,
34984
35190
  switch: switchCapability,
34985
35191
  tamper: tamperCapability,
34986
35192
  temperatureSensor: temperatureSensorCapability,
@@ -39053,12 +39259,24 @@ Object.freeze({
39053
39259
  addonId: null,
39054
39260
  access: "create"
39055
39261
  },
39262
+ "nativeObjectDetection.getOptions": {
39263
+ capName: "native-object-detection",
39264
+ capScope: "device",
39265
+ addonId: null,
39266
+ access: "view"
39267
+ },
39056
39268
  "nativeObjectDetection.setEnabled": {
39057
39269
  capName: "native-object-detection",
39058
39270
  capScope: "device",
39059
39271
  addonId: null,
39060
39272
  access: "create"
39061
39273
  },
39274
+ "nativeObjectDetection.setSettings": {
39275
+ capName: "native-object-detection",
39276
+ capScope: "device",
39277
+ addonId: null,
39278
+ access: "create"
39279
+ },
39062
39280
  "navigation.getFeatures": {
39063
39281
  capName: "navigation",
39064
39282
  capScope: "device",
@@ -41717,6 +41935,12 @@ Object.freeze({
41717
41935
  addonId: null,
41718
41936
  access: "create"
41719
41937
  },
41938
+ "streamBroker.reportStreamSignal": {
41939
+ capName: "stream-broker",
41940
+ capScope: "system",
41941
+ addonId: null,
41942
+ access: "create"
41943
+ },
41720
41944
  "streamBroker.restartProfile": {
41721
41945
  capName: "stream-broker",
41722
41946
  capScope: "system",
@@ -41801,6 +42025,12 @@ Object.freeze({
41801
42025
  addonId: null,
41802
42026
  access: "create"
41803
42027
  },
42028
+ "streamSignals.listSignals": {
42029
+ capName: "stream-signals",
42030
+ capScope: "device",
42031
+ addonId: null,
42032
+ access: "view"
42033
+ },
41804
42034
  "switch.setState": {
41805
42035
  capName: "switch",
41806
42036
  capScope: "device",
@@ -43273,11 +43503,21 @@ Object.freeze({
43273
43503
  form: "single",
43274
43504
  optional: false
43275
43505
  }],
43506
+ "nativeObjectDetection.getOptions": [{
43507
+ name: "deviceId",
43508
+ form: "single",
43509
+ optional: false
43510
+ }],
43276
43511
  "nativeObjectDetection.setEnabled": [{
43277
43512
  name: "deviceId",
43278
43513
  form: "single",
43279
43514
  optional: false
43280
43515
  }],
43516
+ "nativeObjectDetection.setSettings": [{
43517
+ name: "deviceId",
43518
+ form: "single",
43519
+ optional: false
43520
+ }],
43281
43521
  "navigation.getFeatures": [{
43282
43522
  name: "deviceId",
43283
43523
  form: "single",
@@ -44173,6 +44413,11 @@ Object.freeze({
44173
44413
  form: "single",
44174
44414
  optional: false
44175
44415
  }],
44416
+ "streamBroker.reportStreamSignal": [{
44417
+ name: "deviceId",
44418
+ form: "single",
44419
+ optional: false
44420
+ }],
44176
44421
  "streamBroker.restartProfile": [{
44177
44422
  name: "deviceId",
44178
44423
  form: "single",
@@ -147452,10 +147697,22 @@ var REOLINK_ADDON_ID = "provider-reolink";
147452
147697
  * CameraNativeDetection class strings the rest of camstack consumes.
147453
147698
  * Keeping the loose-string contract — face/package etc are valid.
147454
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
+ */
147455
147711
  var AI_CLASS_MAP = {
147456
147712
  people: "person",
147457
147713
  vehicle: "vehicle",
147458
147714
  animal: "animal",
147715
+ dog_cat: "animal",
147459
147716
  face: "face",
147460
147717
  package: "package"
147461
147718
  };
@@ -148296,6 +148553,30 @@ function mapDetectionEvent(event, cameraId, nowMs) {
148296
148553
  function isNativeObjectForwardingEnabled(capState) {
148297
148554
  return capState?.enabled === true;
148298
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
+ }
148299
148580
  function formatUserLevel(level) {
148300
148581
  if (level === void 0 || level === null) return "user";
148301
148582
  if (typeof level === "string" && level.length > 0) return level;
@@ -148981,6 +149262,13 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
148981
149262
  loginPromise = null;
148982
149263
  /** True once we've discovered (via getBatteryInfo) that this is a battery cam. */
148983
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();
148984
149272
  /** Reconnect attempt counter — drives exponential backoff. */
148985
149273
  reconnectAttempts = 0;
148986
149274
  /** Pending reconnect timer; cleared on successful reconnect or removeDevice. */
@@ -152126,6 +152414,63 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
152126
152414
  this.ctx.logger.info("Reolink image-settings cap registered", { tags: { deviceId: this.id } });
152127
152415
  }
152128
152416
  /**
152417
+ * Tell the broker one of our declared signals just fired.
152418
+ *
152419
+ * Never awaited by the push path: a camera event must not wait on a stream
152420
+ * decision, and the broker's own answer (`woke` / `hold-extended` /
152421
+ * `no-consumer` / `not-enabled`) is the interesting part only when something
152422
+ * is actually watching. Logged at debug so a quiet camera does not fill the
152423
+ * log with refusals nobody asked about — the broker logs the wakes it acts
152424
+ * on, with `tags: { deviceId }`, and that is the operator-facing line.
152425
+ */
152426
+ async reportWakeSignal(code) {
152427
+ try {
152428
+ const result = await this.ctx.api.streamBroker.reportStreamSignal.mutate({
152429
+ deviceId: this.id,
152430
+ code
152431
+ });
152432
+ if (result.accepted) this.ctx.logger.info("wake signal accepted by the broker", {
152433
+ tags: { deviceId: this.id },
152434
+ meta: {
152435
+ code,
152436
+ reason: result.reason
152437
+ }
152438
+ });
152439
+ } catch (err) {
152440
+ this.ctx.logger.debug("wake signal not delivered", {
152441
+ tags: { deviceId: this.id },
152442
+ meta: {
152443
+ code,
152444
+ error: err instanceof Error ? err.message : String(err)
152445
+ }
152446
+ });
152447
+ }
152448
+ }
152449
+ /**
152450
+ * Declare the signals THIS camera can emit to wake its own stream.
152451
+ *
152452
+ * Two, and both recommended: a Reolink pushes firmware motion over Baichuan
152453
+ * and camera-native AI detections, both without a stream running, which is
152454
+ * exactly the property the wake needs — our own detections come from decoded
152455
+ * frames and would depend on the stream they are meant to justify.
152456
+ *
152457
+ * Declaring is not enabling: the operator picks per camera, and the broker
152458
+ * holds that choice alongside the cooldown. `recommended` only says which
152459
+ * ones this provider is confident in.
152460
+ */
152461
+ registerStreamSignalsCap() {
152462
+ const signals = [{
152463
+ code: "motion",
152464
+ label: "Motion (camera)",
152465
+ recommended: true
152466
+ }, {
152467
+ code: "detection",
152468
+ label: "Object detected (camera)",
152469
+ recommended: true
152470
+ }];
152471
+ this.ctx.registerNativeCap(streamSignalsCapability, { listSignals: async () => signals });
152472
+ }
152473
+ /**
152129
152474
  * Register the `native-object-detection` cap.
152130
152475
  *
152131
152476
  * The runtimeState holds three fields:
@@ -152136,53 +152481,84 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
152136
152481
  * yet (e.g. fresh device, no config cached).
152137
152482
  * - `lastByClass`: updated in `handleSimpleEvent` every time an AI-class
152138
152483
  * event fires, regardless of the `enabled` flag.
152139
- * - `enabled`: operator toggle. Cold-start default `true` detections
152140
- * flow unconditionally before a value is saved, preserving the existing
152141
- * 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`.
152142
152488
  *
152143
152489
  * Follows the trampoline pattern from `registerStreamParamsCap` — no
152144
152490
  * parallel private fields; all state lives in the kernel runtimeState
152145
152491
  * slice.
152146
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
+ }
152147
152511
  registerNativeObjectDetectionCap() {
152148
152512
  const CAP_NAME = "native-object-detection";
152149
- const buildSupportedClasses = () => {
152150
- const cached = this.config.get("deviceCache")?.aiDetectTypes;
152151
- if (!cached || cached.length === 0) return Object.values(AI_CLASS_MAP).filter(isNativeObjectClass);
152152
- const classes = [];
152153
- for (const libName of cached) {
152154
- const mapped = AI_CLASS_MAP[libName];
152155
- if (mapped !== void 0 && isNativeObjectClass(mapped)) classes.push(mapped);
152156
- }
152157
- return classes;
152158
- };
152513
+ const buildSupportedClasses = () => buildSupportedNativeClasses(this.config.get("deviceCache")?.aiDetectTypes, isNativeObjectClass).filter(isNativeObjectClass);
152159
152514
  const buildEmptyState = () => ({
152160
- enabled: false,
152515
+ enabled: !this.isBattery,
152161
152516
  lastByClass: {},
152162
152517
  supportedClasses: buildSupportedClasses(),
152163
152518
  lastFetchedAt: 0
152164
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
+ };
152165
152538
  const provider = {
152166
- getStatus: createRuntimeStateBridge({
152167
- runtimeState: this.runtimeState,
152168
- cap: nativeObjectDetectionCapability,
152169
- ownDeviceId: this.id,
152170
- refresh: async () => {},
152171
- staleMs: Infinity,
152172
- empty: buildEmptyState
152173
- }).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
+ },
152174
152555
  setEnabled: async ({ deviceId, enabled }) => {
152175
152556
  if (deviceId !== this.id) return;
152176
- await this.runtimeState.patchCapState(CAP_NAME, { enabled });
152177
- const api = this.api;
152178
- if (!api) return;
152179
- if (enabled) await this.resubscribeObjectDetections(api, "cap-enabled").catch((err) => {
152180
- this.ctx.logger.debug("Reolink objectDetections re-arm on enable failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
152181
- });
152182
- else await this.unsubscribeObjectDetections(api);
152557
+ await applyEnabled(enabled);
152183
152558
  }
152184
152559
  };
152185
152560
  this.ctx.registerNativeCap(nativeObjectDetectionCapability, provider);
152561
+ this.registerStreamSignalsCap();
152186
152562
  if (this.runtimeState.getCapState(CAP_NAME) === void 0) this.runtimeState.setCapState(CAP_NAME, {
152187
152563
  ...buildEmptyState(),
152188
152564
  lastFetchedAt: Date.now()
@@ -155537,6 +155913,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
155537
155913
  timestamp: now,
155538
155914
  source: "onboard"
155539
155915
  }));
155916
+ this.reportWakeSignal("motion");
155540
155917
  return;
155541
155918
  }
155542
155919
  const aiClass = AI_CLASS_MAP[event.type];
@@ -155547,14 +155924,17 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
155547
155924
  timestamp: now,
155548
155925
  confidence: void 0
155549
155926
  } } });
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
- }));
155927
+ if (isNativeObjectForwardingEnabled(this.runtimeState.getCapState("native-object-detection"))) {
155928
+ this.reportWakeSignal("detection");
155929
+ this.ctx.eventBus.emit(createEvent(EventCategory.DetectionCameraNative, eventSource, {
155930
+ cameraId: this.id,
155931
+ source: "onboard",
155932
+ detections: [{
155933
+ class: aiClass,
155934
+ timestamp: now
155935
+ }]
155936
+ }));
155937
+ } else this.noteNativeDetectionSuppressed(aiClass);
155558
155938
  this.ctx.eventBus.emit(createEvent(EventCategory.MotionOnMotionChanged, eventSource, {
155559
155939
  deviceId: this.id,
155560
155940
  detected: true,