@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.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-BEyvfshj.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(),
@@ -29176,11 +29338,39 @@ var NativeObjectDetectionStatusSchema = object({
29176
29338
  supportedClasses: array(NativeObjectClassEnum).readonly(),
29177
29339
  /**
29178
29340
  * Whether forwarding of onboard AI detections is enabled for this device.
29179
- * Default FALSE (opt-in, cold-start) — onboard AI pushes are noisy/sparse and
29180
- * churn the tracker, so forwarding stays off until the operator enables it.
29341
+ *
29342
+ * The COLD-START default is the provider's, and it is not one value for
29343
+ * every camera: a mains camera carries the boxed side-channel and its
29344
+ * detections are usable subjects, so it defaults ON; a battery camera never
29345
+ * attaches that channel, so the same toggle would buy it nothing but a write
29346
+ * — it defaults OFF and an operator who wants it turns it on knowing what it
29347
+ * costs. {@link NativeObjectDetectionOptionsSchema.geometry} is how the UI
29348
+ * says which of the two a camera is.
29181
29349
  */
29182
29350
  enabled: boolean()
29183
29351
  });
29352
+ /**
29353
+ * What a camera's onboard detections actually CARRY.
29354
+ *
29355
+ * `boxed` — the firmware ships a bounding box with the class, so a detection is
29356
+ * a subject the pipeline can track, crop and score.
29357
+ * `flag-only` — the firmware ships the class and nothing else. The pipeline
29358
+ * needs geometry to make a subject out of it, so such a push reaches the
29359
+ * tracker as motion and no further. On Reolink this is every battery camera:
29360
+ * the boxes ride the BcMedia sub-stream, which a battery camera never attaches.
29361
+ *
29362
+ * Reported and not inferred, because the operator's question in front of the
29363
+ * toggle is "what do I get", and "a class with no box" and "a subject" are
29364
+ * different answers (D14: the derived form is only as honest as `getOptions`).
29365
+ */
29366
+ var NativeObjectGeometryEnum = _enum(["boxed", "flag-only"]);
29367
+ var NativeObjectDetectionOptionsSchema = object({
29368
+ /** Classes this firmware can detect — the same list the status reports. */
29369
+ supportedClasses: array(NativeObjectClassEnum).readonly(),
29370
+ /** What a detection from this camera carries. */
29371
+ geometry: NativeObjectGeometryEnum
29372
+ });
29373
+ var NativeObjectDetectionSettingsPatchSchema = object({ enabled: boolean().optional() });
29184
29374
  var NativeObjectDetectionRuntimeStateSchema = NativeObjectDetectionStatusSchema.extend({
29185
29375
  /** Required by createRuntimeStateBridge — epoch ms of last refresh. */
29186
29376
  lastFetchedAt: number() });
@@ -29190,13 +29380,28 @@ var nativeObjectDetectionCapability = {
29190
29380
  deviceNative: true,
29191
29381
  mode: "singleton",
29192
29382
  deviceTypes: [DeviceType.Camera],
29193
- methods: { setEnabled: method(object({
29194
- deviceId: number(),
29195
- enabled: boolean()
29196
- }), _void(), {
29197
- kind: "mutation",
29198
- auth: "admin"
29199
- }) },
29383
+ deviceConfig: { ui: {
29384
+ kind: "derived-form",
29385
+ builderId: "native-object-detection",
29386
+ tab: "motion"
29387
+ } },
29388
+ methods: {
29389
+ getOptions: method(object({ deviceId: number() }), NativeObjectDetectionOptionsSchema),
29390
+ setSettings: method(object({
29391
+ deviceId: number(),
29392
+ settings: NativeObjectDetectionSettingsPatchSchema
29393
+ }), _void(), {
29394
+ kind: "mutation",
29395
+ auth: "admin"
29396
+ }),
29397
+ setEnabled: method(object({
29398
+ deviceId: number(),
29399
+ enabled: boolean()
29400
+ }), _void(), {
29401
+ kind: "mutation",
29402
+ auth: "admin"
29403
+ })
29404
+ },
29200
29405
  events: { onDetected: { data: object({
29201
29406
  deviceId: number(),
29202
29407
  detection: NativeDetectionSchema
@@ -34976,6 +35181,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
34976
35181
  smoke: smokeCapability,
34977
35182
  streamCatalog: streamCatalogCapability,
34978
35183
  streamParams: streamParamsCapability,
35184
+ streamSignals: streamSignalsCapability,
34979
35185
  switch: switchCapability,
34980
35186
  tamper: tamperCapability,
34981
35187
  temperatureSensor: temperatureSensorCapability,
@@ -39048,12 +39254,24 @@ Object.freeze({
39048
39254
  addonId: null,
39049
39255
  access: "create"
39050
39256
  },
39257
+ "nativeObjectDetection.getOptions": {
39258
+ capName: "native-object-detection",
39259
+ capScope: "device",
39260
+ addonId: null,
39261
+ access: "view"
39262
+ },
39051
39263
  "nativeObjectDetection.setEnabled": {
39052
39264
  capName: "native-object-detection",
39053
39265
  capScope: "device",
39054
39266
  addonId: null,
39055
39267
  access: "create"
39056
39268
  },
39269
+ "nativeObjectDetection.setSettings": {
39270
+ capName: "native-object-detection",
39271
+ capScope: "device",
39272
+ addonId: null,
39273
+ access: "create"
39274
+ },
39057
39275
  "navigation.getFeatures": {
39058
39276
  capName: "navigation",
39059
39277
  capScope: "device",
@@ -41712,6 +41930,12 @@ Object.freeze({
41712
41930
  addonId: null,
41713
41931
  access: "create"
41714
41932
  },
41933
+ "streamBroker.reportStreamSignal": {
41934
+ capName: "stream-broker",
41935
+ capScope: "system",
41936
+ addonId: null,
41937
+ access: "create"
41938
+ },
41715
41939
  "streamBroker.restartProfile": {
41716
41940
  capName: "stream-broker",
41717
41941
  capScope: "system",
@@ -41796,6 +42020,12 @@ Object.freeze({
41796
42020
  addonId: null,
41797
42021
  access: "create"
41798
42022
  },
42023
+ "streamSignals.listSignals": {
42024
+ capName: "stream-signals",
42025
+ capScope: "device",
42026
+ addonId: null,
42027
+ access: "view"
42028
+ },
41799
42029
  "switch.setState": {
41800
42030
  capName: "switch",
41801
42031
  capScope: "device",
@@ -43268,11 +43498,21 @@ Object.freeze({
43268
43498
  form: "single",
43269
43499
  optional: false
43270
43500
  }],
43501
+ "nativeObjectDetection.getOptions": [{
43502
+ name: "deviceId",
43503
+ form: "single",
43504
+ optional: false
43505
+ }],
43271
43506
  "nativeObjectDetection.setEnabled": [{
43272
43507
  name: "deviceId",
43273
43508
  form: "single",
43274
43509
  optional: false
43275
43510
  }],
43511
+ "nativeObjectDetection.setSettings": [{
43512
+ name: "deviceId",
43513
+ form: "single",
43514
+ optional: false
43515
+ }],
43276
43516
  "navigation.getFeatures": [{
43277
43517
  name: "deviceId",
43278
43518
  form: "single",
@@ -44168,6 +44408,11 @@ Object.freeze({
44168
44408
  form: "single",
44169
44409
  optional: false
44170
44410
  }],
44411
+ "streamBroker.reportStreamSignal": [{
44412
+ name: "deviceId",
44413
+ form: "single",
44414
+ optional: false
44415
+ }],
44171
44416
  "streamBroker.restartProfile": [{
44172
44417
  name: "deviceId",
44173
44418
  form: "single",
@@ -147447,10 +147692,22 @@ var REOLINK_ADDON_ID = "provider-reolink";
147447
147692
  * CameraNativeDetection class strings the rest of camstack consumes.
147448
147693
  * Keeping the loose-string contract — face/package etc are valid.
147449
147694
  */
147695
+ /**
147696
+ * Baichuan AI class name → the cap's `NativeObjectClass`.
147697
+ *
147698
+ * TWO vocabularies arrive here and both must be keyed. The simple-event push
147699
+ * (cmd 33 `<AItype>`) says `animal`; the capability probe (cmd 299
147700
+ * `getAiDetectTypes`) says `dog_cat` for the same class. Keying only the first
147701
+ * made `buildSupportedClasses` drop it, so camera 618 reported
147702
+ * `supportedClasses: ["person","vehicle"]` while its own `lastByClass` held a
147703
+ * live `animal` detection — the camera detected a class the system said it
147704
+ * could not. The two key-spaces do not collide, so one map serves both.
147705
+ */
147450
147706
  var AI_CLASS_MAP = {
147451
147707
  people: "person",
147452
147708
  vehicle: "vehicle",
147453
147709
  animal: "animal",
147710
+ dog_cat: "animal",
147454
147711
  face: "face",
147455
147712
  package: "package"
147456
147713
  };
@@ -148291,6 +148548,30 @@ function mapDetectionEvent(event, cameraId, nowMs) {
148291
148548
  function isNativeObjectForwardingEnabled(capState) {
148292
148549
  return capState?.enabled === true;
148293
148550
  }
148551
+ /**
148552
+ * The classes this firmware can detect, from the cmd-299 probe
148553
+ * (`deviceCache.aiDetectTypes`), mapped through {@link AI_CLASS_MAP}.
148554
+ *
148555
+ * Module-level and exported because the version of this that lived as a
148556
+ * closure inside `registerNativeObjectDetectionCap` had a test that
148557
+ * RE-IMPLEMENTED it — and the copy encoded the `dog_cat` gap as intent
148558
+ * ("skips unknown Baichuan type names"), so the suite stayed green while
148559
+ * camera 618 reported it could not detect a class it was detecting. One
148560
+ * function, one test, no copy.
148561
+ *
148562
+ * An absent probe is not an empty answer: it means nobody has asked the camera
148563
+ * yet, and the honest fallback is the full mapped value-set rather than "this
148564
+ * camera detects nothing".
148565
+ */
148566
+ function buildSupportedNativeClasses(aiDetectTypes, isSupportedClass) {
148567
+ if (!aiDetectTypes || aiDetectTypes.length === 0) return [...new Set(Object.values(AI_CLASS_MAP))].filter(isSupportedClass);
148568
+ const classes = [];
148569
+ for (const libName of aiDetectTypes) {
148570
+ const mapped = AI_CLASS_MAP[libName];
148571
+ if (mapped !== void 0 && isSupportedClass(mapped) && !classes.includes(mapped)) classes.push(mapped);
148572
+ }
148573
+ return classes;
148574
+ }
148294
148575
  function formatUserLevel(level) {
148295
148576
  if (level === void 0 || level === null) return "user";
148296
148577
  if (typeof level === "string" && level.length > 0) return level;
@@ -148976,6 +149257,13 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
148976
149257
  loginPromise = null;
148977
149258
  /** True once we've discovered (via getBatteryInfo) that this is a battery cam. */
148978
149259
  isBattery = false;
149260
+ /**
149261
+ * When each AI class last had its suppression reported, so the gate's drop is
149262
+ * on the record without one line per push. Per class because the vocabulary
149263
+ * is five values and the finding is WHICH class the camera is pushing that
149264
+ * nothing downstream receives.
149265
+ */
149266
+ nativeDetectionSuppressedAt = /* @__PURE__ */ new Map();
148979
149267
  /** Reconnect attempt counter — drives exponential backoff. */
148980
149268
  reconnectAttempts = 0;
148981
149269
  /** Pending reconnect timer; cleared on successful reconnect or removeDevice. */
@@ -152121,6 +152409,63 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
152121
152409
  this.ctx.logger.info("Reolink image-settings cap registered", { tags: { deviceId: this.id } });
152122
152410
  }
152123
152411
  /**
152412
+ * Tell the broker one of our declared signals just fired.
152413
+ *
152414
+ * Never awaited by the push path: a camera event must not wait on a stream
152415
+ * decision, and the broker's own answer (`woke` / `hold-extended` /
152416
+ * `no-consumer` / `not-enabled`) is the interesting part only when something
152417
+ * is actually watching. Logged at debug so a quiet camera does not fill the
152418
+ * log with refusals nobody asked about — the broker logs the wakes it acts
152419
+ * on, with `tags: { deviceId }`, and that is the operator-facing line.
152420
+ */
152421
+ async reportWakeSignal(code) {
152422
+ try {
152423
+ const result = await this.ctx.api.streamBroker.reportStreamSignal.mutate({
152424
+ deviceId: this.id,
152425
+ code
152426
+ });
152427
+ if (result.accepted) this.ctx.logger.info("wake signal accepted by the broker", {
152428
+ tags: { deviceId: this.id },
152429
+ meta: {
152430
+ code,
152431
+ reason: result.reason
152432
+ }
152433
+ });
152434
+ } catch (err) {
152435
+ this.ctx.logger.debug("wake signal not delivered", {
152436
+ tags: { deviceId: this.id },
152437
+ meta: {
152438
+ code,
152439
+ error: err instanceof Error ? err.message : String(err)
152440
+ }
152441
+ });
152442
+ }
152443
+ }
152444
+ /**
152445
+ * Declare the signals THIS camera can emit to wake its own stream.
152446
+ *
152447
+ * Two, and both recommended: a Reolink pushes firmware motion over Baichuan
152448
+ * and camera-native AI detections, both without a stream running, which is
152449
+ * exactly the property the wake needs — our own detections come from decoded
152450
+ * frames and would depend on the stream they are meant to justify.
152451
+ *
152452
+ * Declaring is not enabling: the operator picks per camera, and the broker
152453
+ * holds that choice alongside the cooldown. `recommended` only says which
152454
+ * ones this provider is confident in.
152455
+ */
152456
+ registerStreamSignalsCap() {
152457
+ const signals = [{
152458
+ code: "motion",
152459
+ label: "Motion (camera)",
152460
+ recommended: true
152461
+ }, {
152462
+ code: "detection",
152463
+ label: "Object detected (camera)",
152464
+ recommended: true
152465
+ }];
152466
+ this.ctx.registerNativeCap(streamSignalsCapability, { listSignals: async () => signals });
152467
+ }
152468
+ /**
152124
152469
  * Register the `native-object-detection` cap.
152125
152470
  *
152126
152471
  * The runtimeState holds three fields:
@@ -152131,53 +152476,84 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
152131
152476
  * yet (e.g. fresh device, no config cached).
152132
152477
  * - `lastByClass`: updated in `handleSimpleEvent` every time an AI-class
152133
152478
  * event fires, regardless of the `enabled` flag.
152134
- * - `enabled`: operator toggle. Cold-start default `true` detections
152135
- * flow unconditionally before a value is saved, preserving the existing
152136
- * behaviour.
152479
+ * - `enabled`: operator toggle. Cold-start default is the camera's power
152480
+ * source ON for a mains camera (its detections carry boxes and become
152481
+ * subjects), OFF for a battery one (no boxed side-channel, so the toggle
152482
+ * would buy it nothing). See `buildEmptyState`.
152137
152483
  *
152138
152484
  * Follows the trampoline pattern from `registerStreamParamsCap` — no
152139
152485
  * parallel private fields; all state lives in the kernel runtimeState
152140
152486
  * slice.
152141
152487
  */
152488
+ /**
152489
+ * Report — once a minute per class — that the forwarding gate dropped an
152490
+ * onboard AI detection. A branch that drops work names it (D391): without
152491
+ * this the toggle being off looks exactly like a camera that pushes nothing,
152492
+ * and the operator's only symptom is silence.
152493
+ */
152494
+ noteNativeDetectionSuppressed(aiClass) {
152495
+ const now = Date.now();
152496
+ if (now - (this.nativeDetectionSuppressedAt.get(aiClass) ?? 0) < 6e4) return;
152497
+ this.nativeDetectionSuppressedAt.set(aiClass, now);
152498
+ this.ctx.logger.info("Reolink onboard AI detection dropped — forwarding is off for this camera", {
152499
+ tags: { deviceId: this.id },
152500
+ meta: {
152501
+ class: aiClass,
152502
+ battery: this.isBattery
152503
+ }
152504
+ });
152505
+ }
152142
152506
  registerNativeObjectDetectionCap() {
152143
152507
  const CAP_NAME = "native-object-detection";
152144
- const buildSupportedClasses = () => {
152145
- const cached = this.config.get("deviceCache")?.aiDetectTypes;
152146
- if (!cached || cached.length === 0) return Object.values(AI_CLASS_MAP).filter(isNativeObjectClass);
152147
- const classes = [];
152148
- for (const libName of cached) {
152149
- const mapped = AI_CLASS_MAP[libName];
152150
- if (mapped !== void 0 && isNativeObjectClass(mapped)) classes.push(mapped);
152151
- }
152152
- return classes;
152153
- };
152508
+ const buildSupportedClasses = () => buildSupportedNativeClasses(this.config.get("deviceCache")?.aiDetectTypes, isNativeObjectClass).filter(isNativeObjectClass);
152154
152509
  const buildEmptyState = () => ({
152155
- enabled: false,
152510
+ enabled: !this.isBattery,
152156
152511
  lastByClass: {},
152157
152512
  supportedClasses: buildSupportedClasses(),
152158
152513
  lastFetchedAt: 0
152159
152514
  });
152515
+ const bridge = createRuntimeStateBridge({
152516
+ runtimeState: this.runtimeState,
152517
+ cap: nativeObjectDetectionCapability,
152518
+ ownDeviceId: this.id,
152519
+ refresh: async () => {},
152520
+ staleMs: Infinity,
152521
+ empty: buildEmptyState
152522
+ });
152523
+ /** Shared by `setEnabled` and the derived form's `setSettings`. */
152524
+ const applyEnabled = async (enabled) => {
152525
+ await this.runtimeState.patchCapState(CAP_NAME, { enabled });
152526
+ const api = this.api;
152527
+ if (!api) return;
152528
+ if (enabled) await this.resubscribeObjectDetections(api, "cap-enabled").catch((err) => {
152529
+ this.ctx.logger.debug("Reolink objectDetections re-arm on enable failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
152530
+ });
152531
+ else await this.unsubscribeObjectDetections(api);
152532
+ };
152160
152533
  const provider = {
152161
- getStatus: createRuntimeStateBridge({
152162
- runtimeState: this.runtimeState,
152163
- cap: nativeObjectDetectionCapability,
152164
- ownDeviceId: this.id,
152165
- refresh: async () => {},
152166
- staleMs: Infinity,
152167
- empty: buildEmptyState
152168
- }).getStatus,
152534
+ getStatus: bridge.getStatus,
152535
+ getOptions: async ({ deviceId }) => {
152536
+ if (deviceId !== this.id) return {
152537
+ supportedClasses: [],
152538
+ geometry: "flag-only"
152539
+ };
152540
+ return {
152541
+ supportedClasses: buildSupportedClasses(),
152542
+ geometry: this.isBattery ? "flag-only" : "boxed"
152543
+ };
152544
+ },
152545
+ setSettings: async ({ deviceId, settings }) => {
152546
+ if (deviceId !== this.id) return;
152547
+ if (settings.enabled === void 0) return;
152548
+ await applyEnabled(settings.enabled);
152549
+ },
152169
152550
  setEnabled: async ({ deviceId, enabled }) => {
152170
152551
  if (deviceId !== this.id) return;
152171
- await this.runtimeState.patchCapState(CAP_NAME, { enabled });
152172
- const api = this.api;
152173
- if (!api) return;
152174
- if (enabled) await this.resubscribeObjectDetections(api, "cap-enabled").catch((err) => {
152175
- this.ctx.logger.debug("Reolink objectDetections re-arm on enable failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
152176
- });
152177
- else await this.unsubscribeObjectDetections(api);
152552
+ await applyEnabled(enabled);
152178
152553
  }
152179
152554
  };
152180
152555
  this.ctx.registerNativeCap(nativeObjectDetectionCapability, provider);
152556
+ this.registerStreamSignalsCap();
152181
152557
  if (this.runtimeState.getCapState(CAP_NAME) === void 0) this.runtimeState.setCapState(CAP_NAME, {
152182
152558
  ...buildEmptyState(),
152183
152559
  lastFetchedAt: Date.now()
@@ -155532,6 +155908,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
155532
155908
  timestamp: now,
155533
155909
  source: "onboard"
155534
155910
  }));
155911
+ this.reportWakeSignal("motion");
155535
155912
  return;
155536
155913
  }
155537
155914
  const aiClass = AI_CLASS_MAP[event.type];
@@ -155542,14 +155919,17 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
155542
155919
  timestamp: now,
155543
155920
  confidence: void 0
155544
155921
  } } });
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
- }));
155922
+ if (isNativeObjectForwardingEnabled(this.runtimeState.getCapState("native-object-detection"))) {
155923
+ this.reportWakeSignal("detection");
155924
+ this.ctx.eventBus.emit(createEvent(EventCategory.DetectionCameraNative, eventSource, {
155925
+ cameraId: this.id,
155926
+ source: "onboard",
155927
+ detections: [{
155928
+ class: aiClass,
155929
+ timestamp: now
155930
+ }]
155931
+ }));
155932
+ } else this.noteNativeDetectionSuppressed(aiClass);
155553
155933
  this.ctx.eventBus.emit(createEvent(EventCategory.MotionOnMotionChanged, eventSource, {
155554
155934
  deviceId: this.id,
155555
155935
  detected: true,