@camstack/addon-provider-reolink 1.2.123 → 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 +419 -24
  2. package/dist/addon.mjs +419 -24
  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-CnLyvg3w.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).
@@ -7413,7 +7427,23 @@ var EncodeProfileSchema = object({
7413
7427
  "zerolatency",
7414
7428
  "film",
7415
7429
  "animation"
7416
- ]).optional()
7430
+ ]).optional(),
7431
+ /**
7432
+ * ONE slice per access unit.
7433
+ *
7434
+ * `-tune zerolatency` turns on x264's sliced threads, and a frame then leaves
7435
+ * the encoder as five NAL slices that share one RTP timestamp and carry one
7436
+ * marker bit. A libwebrtc depacketiser sees five frame-starts and one
7437
+ * frame-end per frame: the first pictures render and the video then freezes
7438
+ * for good while the audio, on its own plane, plays on. Measured on this hub
7439
+ * 2026-09-13 against the Echo, and reduced to one slice by this flag alone.
7440
+ *
7441
+ * A NAMED field and not a raw flag, because {@link EgressEncodeSchema} omits
7442
+ * `outputArgs` on purpose: an opaque array is part of the sharing key, so two
7443
+ * consumers meaning the same thing spelled differently would stop sharing one
7444
+ * child. Absent means "whatever the encoder does" — today's behaviour.
7445
+ */
7446
+ singleSlicePerFrame: boolean().optional()
7417
7447
  }),
7418
7448
  audio: union([literal("passthrough"), object({
7419
7449
  codec: _enum([
@@ -7463,7 +7493,7 @@ var BASE_LIVE_EGRESS_PROFILE = {
7463
7493
  audio: "passthrough"
7464
7494
  };
7465
7495
  ({ ...BASE_LIVE_EGRESS_PROFILE }), { ...BASE_LIVE_EGRESS_PROFILE.video };
7466
- ({ ...BASE_LIVE_EGRESS_PROFILE });
7496
+ ({ ...BASE_LIVE_EGRESS_PROFILE }), { ...BASE_LIVE_EGRESS_PROFILE.video };
7467
7497
  /**
7468
7498
  * Deep wiring healthcheck — snapshot of active reachability probes across
7469
7499
  * every declared capability + widget of every installed plugin, on every
@@ -12044,6 +12074,72 @@ method(ListInputSchema, array(BrokerInfoSchema$1)), method(GetInputSchema, Broke
12044
12074
  auth: "admin"
12045
12075
  }), method(GetStateInputSchema, unknown().nullable()), method(_void(), RegistryStatusSchema);
12046
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
+ };
12047
12143
  /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
12048
12144
  var StreamFormatSchema = _enum([
12049
12145
  "webrtc",
@@ -12440,6 +12536,22 @@ var EgressTranscodeSchema = object({
12440
12536
  camStreamId: string().nullable()
12441
12537
  });
12442
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({
12443
12555
  deviceId: number().int().nonnegative(),
12444
12556
  camStreamId: string().min(1),
12445
12557
  kind: CamStreamKindSchema,
@@ -12694,6 +12806,16 @@ var PickStreamRequirementsSchema = object({
12694
12806
  acceptCodecs: array(StreamCodecSchema).readonly().optional(),
12695
12807
  /** Minimum vertical resolution. Streams shorter than this are dropped. */
12696
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(),
12697
12819
  /** Minimum horizontal resolution. */
12698
12820
  minWidth: number().int().positive().optional(),
12699
12821
  /**
@@ -12710,7 +12832,22 @@ var PickStreamRequirementsSchema = object({
12710
12832
  * transcoded" guard: if the device is already serving the consumer's
12711
12833
  * codec end-to-end, there's nothing to optimise.
12712
12834
  */
12713
- 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()
12714
12851
  }).readonly();
12715
12852
  var PickStreamPreferencesSchema = object({
12716
12853
  /**
@@ -12724,12 +12861,32 @@ var PickStreamPreferencesSchema = object({
12724
12861
  * picks the tallest stream; `'lowest'` picks the shortest (used by
12725
12862
  * memory-constrained consumers / Apple Home guest sessions).
12726
12863
  */
12727
- 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()
12728
12877
  }).readonly();
12729
12878
  var PickedCamStreamSchema = object({
12730
12879
  camStreamId: string(),
12731
12880
  codec: string().optional(),
12732
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(),
12733
12890
  /** One-line explanation of why this stream won — for logs / debug UI. */
12734
12891
  reason: string()
12735
12892
  });
@@ -17320,6 +17477,8 @@ var NcSystemEventKindSchema = _enum([
17320
17477
  "device-enabled",
17321
17478
  "device-battery-low",
17322
17479
  "device-battery-normal",
17480
+ "device-consumable-low",
17481
+ "device-consumable-normal",
17323
17482
  "stream-online",
17324
17483
  "stream-offline",
17325
17484
  "node-online",
@@ -17338,6 +17497,7 @@ var NcSystemEventKindSchema = _enum([
17338
17497
  "addon-updated",
17339
17498
  "server-updated",
17340
17499
  "export-completed",
17500
+ "backup-completed",
17341
17501
  "camera-online",
17342
17502
  "camera-offline",
17343
17503
  "camera-disabled",
@@ -20774,13 +20934,109 @@ var SummariesQueryInput = object({
20774
20934
  deviceIds: array(number()).min(1),
20775
20935
  since: number().optional(),
20776
20936
  until: number().optional(),
20777
- limit: number().int().min(1).max(200).default(50)
20937
+ limit: number().int().min(1).max(200).default(50),
20938
+ /** The same member-wise filter {@link listRecentSummaries} takes — one
20939
+ * meaning of "dog" on every summary surface. */
20940
+ classes: array(string().min(1).max(60)).max(40).optional()
20778
20941
  });
20779
- var SummariesPageSchema = object({ summaries: array(SummarySchema).readonly() });
20780
20942
  var SummaryDetailSchema = object({
20781
20943
  summary: SummarySchema,
20782
- /** Member track ids, in absorption order. */
20783
- trackIds: array(string()).readonly()
20944
+ /** Members in absorption order, oldest join first. */
20945
+ members: array(object({
20946
+ trackId: string(),
20947
+ /**
20948
+ * The track's class at the time it JOINED, or `null` when it was unresolved
20949
+ * and the track no longer exists to fill it.
20950
+ *
20951
+ * The join, not the latest opinion: class voting can re-decide a track's
20952
+ * class afterwards, and a group's composition is the history of who came
20953
+ * together. A consumer counts these to badge a thumbnail — "two people and a
20954
+ * dog" — rather than being handed a second, denormalised tally that would
20955
+ * drift from the members it claims to describe.
20956
+ */
20957
+ className: string().nullable(),
20958
+ /** `primary` is the one member that carries the group's identity. */
20959
+ role: _enum([
20960
+ "primary",
20961
+ "sibling",
20962
+ "child"
20963
+ ]).nullable(),
20964
+ joinedAt: number(),
20965
+ /** Tier 1 — the sub-class (`dog`, `van`), resolved from the member's track
20966
+ * at read time. Absent when nothing resolved, never an empty string. */
20967
+ label: string().optional(),
20968
+ /** Tier 2 — the instance: a person's name, a plate. Absent when the pipeline
20969
+ * recognised nobody, and absent for a member whose track has been pruned —
20970
+ * a thumbnail that names the wrong person is worse than one naming nobody. */
20971
+ subLabel: string().optional()
20972
+ })).readonly()
20973
+ });
20974
+ /**
20975
+ * What a LIST of groups carries: the envelope plus what it is MADE OF.
20976
+ *
20977
+ * Coarse on purpose — the operator's rule is "only the rough data on the list,
20978
+ * we extend when we notice we need more". Enough to draw the card ("two people
20979
+ * and a dog") and no more. The MEMBERS, their names and the per-member links
20980
+ * are on {@link getSummary}: shipping every member of every group so a client
20981
+ * can count them turns a page of two hundred groups into a thousand records to
20982
+ * draw two hundred badges.
20983
+ */
20984
+ var SummaryListItemSchema = object({
20985
+ summary: SummarySchema,
20986
+ /** Class counts, most numerous first; `className: null` is a member whose
20987
+ * class never resolved, counted so the counts add up to `memberCount`. */
20988
+ classes: array(object({
20989
+ className: string().nullable(),
20990
+ count: number().int().positive()
20991
+ })).readonly()
20992
+ });
20993
+ var SummariesPageSchema = object({ summaries: array(SummaryListItemSchema).readonly() });
20994
+ /**
20995
+ * The paged feed, deliberately the SAME shape as {@link RecentTracksQueryInput}.
20996
+ *
20997
+ * The reel on the cameras page and the Events gallery both ride a cursor, so a
20998
+ * summaries feed that could only answer "the last N for these cameras"
20999
+ * (`listSummaries`) would make groups available in the timeline and nowhere
21000
+ * else. Two feeds with different ordering contracts would be two ideas of what
21001
+ * "newest" means, on one screen.
21002
+ */
21003
+ var RecentSummariesQueryInput = object({
21004
+ /** Devices to merge. An empty array yields an empty page. */
21005
+ deviceIds: array(number()),
21006
+ /** Window lower bound on `lastActiveAt` (inclusive). */
21007
+ since: number().optional(),
21008
+ /** Window upper bound on `lastActiveAt` (inclusive). */
21009
+ until: number().optional(),
21010
+ limit: number().int().min(1).max(500).default(100),
21011
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
21012
+ cursor: string().optional(),
21013
+ /**
21014
+ * Restrict to these classes — THE SAME FIELD `listRecentTracks` takes, and
21015
+ * deliberately the same name: the events filter tree emits one list of ticked
21016
+ * taxonomy nodes, and a summaries feed that called it something else would
21017
+ * make the operator's one filter component produce two shapes.
21018
+ *
21019
+ * A question about the MEMBERS: a group matches when ANY of them does. "Show
21020
+ * me the arrivals with a dog" has to return the group of four people who had
21021
+ * one with them; a filter on the group's own fields could only ever answer
21022
+ * about its primary.
21023
+ *
21024
+ * Matched against all THREE tiers of a member — the class (`animal`), the
21025
+ * sub-class (`dog`) and the instance (a name, a plate) — because which tier a
21026
+ * ticked node lives on is the taxonomy's business, not the caller's. `dog` is
21027
+ * an event kind in the tree and a tier-1 label on an `animal` track; a filter
21028
+ * that read only the class would answer "animal" and never "dog".
21029
+ *
21030
+ * ABSENT or EMPTY means no filter — absent is not a selection of none.
21031
+ */
21032
+ classes: array(string().min(1).max(60)).max(40).optional()
21033
+ });
21034
+ var RecentSummariesPageSchema = object({
21035
+ /** Ordered by (`lastActiveAt` DESC, id DESC). Same light item as
21036
+ * {@link listSummaries} — one list shape, so a card renders the same
21037
+ * whichever feed drew it. */
21038
+ summaries: array(SummaryListItemSchema).readonly(),
21039
+ nextCursor: string().nullable()
20784
21040
  });
20785
21041
  var RecentTracksPageSchema = object({
20786
21042
  /** Merged page, ordered by (`lastSeen` DESC, `trackId` DESC). */
@@ -21194,7 +21450,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
21194
21450
  classes: array(string()).optional(),
21195
21451
  /** See {@link ExcludeSourcesDoc}. */
21196
21452
  excludeSources: array(TrackSourceSchema).optional()
21197
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(SummariesQueryInput, SummariesPageSchema), method(object({ id: string() }), SummaryDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
21453
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(SummariesQueryInput, SummariesPageSchema), method(RecentSummariesQueryInput, RecentSummariesPageSchema), method(object({ id: string() }), SummaryDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
21198
21454
  kind: "mutation",
21199
21455
  auth: "admin"
21200
21456
  }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
@@ -24627,10 +24883,28 @@ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), ar
24627
24883
  }), boolean()), method(object({
24628
24884
  deviceId: number().int().nonnegative(),
24629
24885
  sessionId: string()
24630
- }), object({ pendingRenegotiation: object({
24631
- target: WebrtcStreamTargetSchema,
24632
- epoch: number()
24633
- }).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
+ }));
24634
24908
  object({
24635
24909
  /** All accessory children of the parent. */
24636
24910
  childDeviceIds: array(number()).readonly(),
@@ -33356,7 +33630,29 @@ var LoggingSettingsPatchSchema = object({
33356
33630
  * disarmed the channels it did not mention would make the Levels page and
33357
33631
  * the Diagnostics page fight over the same value.
33358
33632
  */
33359
- channels: array(LogChannelWindowPatchSchema).readonly().optional()
33633
+ channels: array(LogChannelWindowPatchSchema).readonly().optional(),
33634
+ /**
33635
+ * How many days the analytics OPS LOG is kept — the audit trail of what
33636
+ * retention actually deleted.
33637
+ *
33638
+ * It lives HERE, on the logging document, and not on the analytics addon's
33639
+ * settings form, on the operator's instruction: every other log-ish window in
33640
+ * this system is configured through this one document, and a retention audit
33641
+ * configured somewhere else is a second place to look when the question is
33642
+ * "what happened to my data".
33643
+ *
33644
+ * That placement has a cost worth stating: this document ships in the
33645
+ * framework closure, so this knob needs a published `@camstack/server` while
33646
+ * the four windows beside it (D485) reach a node on `camstack deploy`.
33647
+ *
33648
+ * `retention-model.ts` argued this one must NOT follow a device window,
33649
+ * because the audit trail is the history of the SWEEP and not of the camera.
33650
+ * That argument stands and is untouched — what changes is that 30 days stops
33651
+ * being a constant nobody can reach. `0` keeps it forever. The floor is 1 day
33652
+ * rather than 0-as-delete: an audit log truncated to nothing is precisely the
33653
+ * state in which the next 9.5 GiB leak cannot be explained.
33654
+ */
33655
+ opsLogRetentionDays: number().int().min(0).max(3650).optional()
33360
33656
  });
33361
33657
  /**
33362
33658
  * Which LAYER of the hierarchy is addressed. Absent = the cluster layer.
@@ -33429,6 +33725,14 @@ var LoggingSettingsStateSchema = object({
33429
33725
  channels: array(LogChannelDescriptorSchema).readonly(),
33430
33726
  /** The channels ARMED right now, each with its deadline. */
33431
33727
  activeChannels: array(LogChannelWindowStateSchema).readonly(),
33728
+ /**
33729
+ * The ops-log window in days, as it is in force. `0` = kept forever.
33730
+ *
33731
+ * Reported rather than left to the caller's memory of what it wrote: this is
33732
+ * the value the analytics sweep will actually use, and an operator asking how
33733
+ * long their audit trail lives should not have to infer it from a default.
33734
+ */
33735
+ opsLogRetentionDays: number().int().min(0),
33432
33736
  persisted: boolean()
33433
33737
  });
33434
33738
  method(_void(), FeatureManifestSchema), method(_void(), HealthStatusSchema), method(_void(), FeatureManifestSchema), method(_void(), array(NetworkAddressSchema).readonly()), method(_void(), unknown().nullable(), { auth: "admin" }), method(record(string(), unknown()), _null(), {
@@ -34839,6 +35143,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
34839
35143
  smoke: smokeCapability,
34840
35144
  streamCatalog: streamCatalogCapability,
34841
35145
  streamParams: streamParamsCapability,
35146
+ streamSignals: streamSignalsCapability,
34842
35147
  switch: switchCapability,
34843
35148
  tamper: tamperCapability,
34844
35149
  temperatureSensor: temperatureSensorCapability,
@@ -39607,6 +39912,12 @@ Object.freeze({
39607
39912
  addonId: null,
39608
39913
  access: "view"
39609
39914
  },
39915
+ "pipelineAnalytics.listRecentSummaries": {
39916
+ capName: "pipeline-analytics",
39917
+ capScope: "device",
39918
+ addonId: null,
39919
+ access: "view"
39920
+ },
39610
39921
  "pipelineAnalytics.listRecentTracks": {
39611
39922
  capName: "pipeline-analytics",
39612
39923
  capScope: "device",
@@ -41569,6 +41880,12 @@ Object.freeze({
41569
41880
  addonId: null,
41570
41881
  access: "create"
41571
41882
  },
41883
+ "streamBroker.reportStreamSignal": {
41884
+ capName: "stream-broker",
41885
+ capScope: "system",
41886
+ addonId: null,
41887
+ access: "create"
41888
+ },
41572
41889
  "streamBroker.restartProfile": {
41573
41890
  capName: "stream-broker",
41574
41891
  capScope: "system",
@@ -41653,6 +41970,12 @@ Object.freeze({
41653
41970
  addonId: null,
41654
41971
  access: "create"
41655
41972
  },
41973
+ "streamSignals.listSignals": {
41974
+ capName: "stream-signals",
41975
+ capScope: "device",
41976
+ addonId: null,
41977
+ access: "view"
41978
+ },
41656
41979
  "switch.setState": {
41657
41980
  capName: "switch",
41658
41981
  capScope: "device",
@@ -43434,6 +43757,11 @@ Object.freeze({
43434
43757
  form: "single",
43435
43758
  optional: true
43436
43759
  }],
43760
+ "pipelineAnalytics.listRecentSummaries": [{
43761
+ name: "deviceIds",
43762
+ form: "array",
43763
+ optional: false
43764
+ }],
43437
43765
  "pipelineAnalytics.listRecentTracks": [{
43438
43766
  name: "deviceIds",
43439
43767
  form: "array",
@@ -44020,6 +44348,11 @@ Object.freeze({
44020
44348
  form: "single",
44021
44349
  optional: false
44022
44350
  }],
44351
+ "streamBroker.reportStreamSignal": [{
44352
+ name: "deviceId",
44353
+ form: "single",
44354
+ optional: false
44355
+ }],
44023
44356
  "streamBroker.restartProfile": [{
44024
44357
  name: "deviceId",
44025
44358
  form: "single",
@@ -151973,6 +152306,63 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
151973
152306
  this.ctx.logger.info("Reolink image-settings cap registered", { tags: { deviceId: this.id } });
151974
152307
  }
151975
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
+ /**
151976
152366
  * Register the `native-object-detection` cap.
151977
152367
  *
151978
152368
  * The runtimeState holds three fields:
@@ -152030,6 +152420,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
152030
152420
  }
152031
152421
  };
152032
152422
  this.ctx.registerNativeCap(nativeObjectDetectionCapability, provider);
152423
+ this.registerStreamSignalsCap();
152033
152424
  if (this.runtimeState.getCapState(CAP_NAME) === void 0) this.runtimeState.setCapState(CAP_NAME, {
152034
152425
  ...buildEmptyState(),
152035
152426
  lastFetchedAt: Date.now()
@@ -155384,6 +155775,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
155384
155775
  timestamp: now,
155385
155776
  source: "onboard"
155386
155777
  }));
155778
+ this.reportWakeSignal("motion");
155387
155779
  return;
155388
155780
  }
155389
155781
  const aiClass = AI_CLASS_MAP[event.type];
@@ -155394,14 +155786,17 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
155394
155786
  timestamp: now,
155395
155787
  confidence: void 0
155396
155788
  } } });
155397
- if (isNativeObjectForwardingEnabled(this.runtimeState.getCapState("native-object-detection"))) this.ctx.eventBus.emit(createEvent(EventCategory.DetectionCameraNative, eventSource, {
155398
- cameraId: this.id,
155399
- source: "onboard",
155400
- detections: [{
155401
- class: aiClass,
155402
- timestamp: now
155403
- }]
155404
- }));
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
+ }
155405
155800
  this.ctx.eventBus.emit(createEvent(EventCategory.MotionOnMotionChanged, eventSource, {
155406
155801
  deviceId: this.id,
155407
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-CnLyvg3w.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).
@@ -7408,7 +7422,23 @@ var EncodeProfileSchema = object({
7408
7422
  "zerolatency",
7409
7423
  "film",
7410
7424
  "animation"
7411
- ]).optional()
7425
+ ]).optional(),
7426
+ /**
7427
+ * ONE slice per access unit.
7428
+ *
7429
+ * `-tune zerolatency` turns on x264's sliced threads, and a frame then leaves
7430
+ * the encoder as five NAL slices that share one RTP timestamp and carry one
7431
+ * marker bit. A libwebrtc depacketiser sees five frame-starts and one
7432
+ * frame-end per frame: the first pictures render and the video then freezes
7433
+ * for good while the audio, on its own plane, plays on. Measured on this hub
7434
+ * 2026-09-13 against the Echo, and reduced to one slice by this flag alone.
7435
+ *
7436
+ * A NAMED field and not a raw flag, because {@link EgressEncodeSchema} omits
7437
+ * `outputArgs` on purpose: an opaque array is part of the sharing key, so two
7438
+ * consumers meaning the same thing spelled differently would stop sharing one
7439
+ * child. Absent means "whatever the encoder does" — today's behaviour.
7440
+ */
7441
+ singleSlicePerFrame: boolean().optional()
7412
7442
  }),
7413
7443
  audio: union([literal("passthrough"), object({
7414
7444
  codec: _enum([
@@ -7458,7 +7488,7 @@ var BASE_LIVE_EGRESS_PROFILE = {
7458
7488
  audio: "passthrough"
7459
7489
  };
7460
7490
  ({ ...BASE_LIVE_EGRESS_PROFILE }), { ...BASE_LIVE_EGRESS_PROFILE.video };
7461
- ({ ...BASE_LIVE_EGRESS_PROFILE });
7491
+ ({ ...BASE_LIVE_EGRESS_PROFILE }), { ...BASE_LIVE_EGRESS_PROFILE.video };
7462
7492
  /**
7463
7493
  * Deep wiring healthcheck — snapshot of active reachability probes across
7464
7494
  * every declared capability + widget of every installed plugin, on every
@@ -12039,6 +12069,72 @@ method(ListInputSchema, array(BrokerInfoSchema$1)), method(GetInputSchema, Broke
12039
12069
  auth: "admin"
12040
12070
  }), method(GetStateInputSchema, unknown().nullable()), method(_void(), RegistryStatusSchema);
12041
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
+ };
12042
12138
  /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
12043
12139
  var StreamFormatSchema = _enum([
12044
12140
  "webrtc",
@@ -12435,6 +12531,22 @@ var EgressTranscodeSchema = object({
12435
12531
  camStreamId: string().nullable()
12436
12532
  });
12437
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({
12438
12550
  deviceId: number().int().nonnegative(),
12439
12551
  camStreamId: string().min(1),
12440
12552
  kind: CamStreamKindSchema,
@@ -12689,6 +12801,16 @@ var PickStreamRequirementsSchema = object({
12689
12801
  acceptCodecs: array(StreamCodecSchema).readonly().optional(),
12690
12802
  /** Minimum vertical resolution. Streams shorter than this are dropped. */
12691
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(),
12692
12814
  /** Minimum horizontal resolution. */
12693
12815
  minWidth: number().int().positive().optional(),
12694
12816
  /**
@@ -12705,7 +12827,22 @@ var PickStreamRequirementsSchema = object({
12705
12827
  * transcoded" guard: if the device is already serving the consumer's
12706
12828
  * codec end-to-end, there's nothing to optimise.
12707
12829
  */
12708
- 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()
12709
12846
  }).readonly();
12710
12847
  var PickStreamPreferencesSchema = object({
12711
12848
  /**
@@ -12719,12 +12856,32 @@ var PickStreamPreferencesSchema = object({
12719
12856
  * picks the tallest stream; `'lowest'` picks the shortest (used by
12720
12857
  * memory-constrained consumers / Apple Home guest sessions).
12721
12858
  */
12722
- 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()
12723
12872
  }).readonly();
12724
12873
  var PickedCamStreamSchema = object({
12725
12874
  camStreamId: string(),
12726
12875
  codec: string().optional(),
12727
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(),
12728
12885
  /** One-line explanation of why this stream won — for logs / debug UI. */
12729
12886
  reason: string()
12730
12887
  });
@@ -17315,6 +17472,8 @@ var NcSystemEventKindSchema = _enum([
17315
17472
  "device-enabled",
17316
17473
  "device-battery-low",
17317
17474
  "device-battery-normal",
17475
+ "device-consumable-low",
17476
+ "device-consumable-normal",
17318
17477
  "stream-online",
17319
17478
  "stream-offline",
17320
17479
  "node-online",
@@ -17333,6 +17492,7 @@ var NcSystemEventKindSchema = _enum([
17333
17492
  "addon-updated",
17334
17493
  "server-updated",
17335
17494
  "export-completed",
17495
+ "backup-completed",
17336
17496
  "camera-online",
17337
17497
  "camera-offline",
17338
17498
  "camera-disabled",
@@ -20769,13 +20929,109 @@ var SummariesQueryInput = object({
20769
20929
  deviceIds: array(number()).min(1),
20770
20930
  since: number().optional(),
20771
20931
  until: number().optional(),
20772
- limit: number().int().min(1).max(200).default(50)
20932
+ limit: number().int().min(1).max(200).default(50),
20933
+ /** The same member-wise filter {@link listRecentSummaries} takes — one
20934
+ * meaning of "dog" on every summary surface. */
20935
+ classes: array(string().min(1).max(60)).max(40).optional()
20773
20936
  });
20774
- var SummariesPageSchema = object({ summaries: array(SummarySchema).readonly() });
20775
20937
  var SummaryDetailSchema = object({
20776
20938
  summary: SummarySchema,
20777
- /** Member track ids, in absorption order. */
20778
- trackIds: array(string()).readonly()
20939
+ /** Members in absorption order, oldest join first. */
20940
+ members: array(object({
20941
+ trackId: string(),
20942
+ /**
20943
+ * The track's class at the time it JOINED, or `null` when it was unresolved
20944
+ * and the track no longer exists to fill it.
20945
+ *
20946
+ * The join, not the latest opinion: class voting can re-decide a track's
20947
+ * class afterwards, and a group's composition is the history of who came
20948
+ * together. A consumer counts these to badge a thumbnail — "two people and a
20949
+ * dog" — rather than being handed a second, denormalised tally that would
20950
+ * drift from the members it claims to describe.
20951
+ */
20952
+ className: string().nullable(),
20953
+ /** `primary` is the one member that carries the group's identity. */
20954
+ role: _enum([
20955
+ "primary",
20956
+ "sibling",
20957
+ "child"
20958
+ ]).nullable(),
20959
+ joinedAt: number(),
20960
+ /** Tier 1 — the sub-class (`dog`, `van`), resolved from the member's track
20961
+ * at read time. Absent when nothing resolved, never an empty string. */
20962
+ label: string().optional(),
20963
+ /** Tier 2 — the instance: a person's name, a plate. Absent when the pipeline
20964
+ * recognised nobody, and absent for a member whose track has been pruned —
20965
+ * a thumbnail that names the wrong person is worse than one naming nobody. */
20966
+ subLabel: string().optional()
20967
+ })).readonly()
20968
+ });
20969
+ /**
20970
+ * What a LIST of groups carries: the envelope plus what it is MADE OF.
20971
+ *
20972
+ * Coarse on purpose — the operator's rule is "only the rough data on the list,
20973
+ * we extend when we notice we need more". Enough to draw the card ("two people
20974
+ * and a dog") and no more. The MEMBERS, their names and the per-member links
20975
+ * are on {@link getSummary}: shipping every member of every group so a client
20976
+ * can count them turns a page of two hundred groups into a thousand records to
20977
+ * draw two hundred badges.
20978
+ */
20979
+ var SummaryListItemSchema = object({
20980
+ summary: SummarySchema,
20981
+ /** Class counts, most numerous first; `className: null` is a member whose
20982
+ * class never resolved, counted so the counts add up to `memberCount`. */
20983
+ classes: array(object({
20984
+ className: string().nullable(),
20985
+ count: number().int().positive()
20986
+ })).readonly()
20987
+ });
20988
+ var SummariesPageSchema = object({ summaries: array(SummaryListItemSchema).readonly() });
20989
+ /**
20990
+ * The paged feed, deliberately the SAME shape as {@link RecentTracksQueryInput}.
20991
+ *
20992
+ * The reel on the cameras page and the Events gallery both ride a cursor, so a
20993
+ * summaries feed that could only answer "the last N for these cameras"
20994
+ * (`listSummaries`) would make groups available in the timeline and nowhere
20995
+ * else. Two feeds with different ordering contracts would be two ideas of what
20996
+ * "newest" means, on one screen.
20997
+ */
20998
+ var RecentSummariesQueryInput = object({
20999
+ /** Devices to merge. An empty array yields an empty page. */
21000
+ deviceIds: array(number()),
21001
+ /** Window lower bound on `lastActiveAt` (inclusive). */
21002
+ since: number().optional(),
21003
+ /** Window upper bound on `lastActiveAt` (inclusive). */
21004
+ until: number().optional(),
21005
+ limit: number().int().min(1).max(500).default(100),
21006
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
21007
+ cursor: string().optional(),
21008
+ /**
21009
+ * Restrict to these classes — THE SAME FIELD `listRecentTracks` takes, and
21010
+ * deliberately the same name: the events filter tree emits one list of ticked
21011
+ * taxonomy nodes, and a summaries feed that called it something else would
21012
+ * make the operator's one filter component produce two shapes.
21013
+ *
21014
+ * A question about the MEMBERS: a group matches when ANY of them does. "Show
21015
+ * me the arrivals with a dog" has to return the group of four people who had
21016
+ * one with them; a filter on the group's own fields could only ever answer
21017
+ * about its primary.
21018
+ *
21019
+ * Matched against all THREE tiers of a member — the class (`animal`), the
21020
+ * sub-class (`dog`) and the instance (a name, a plate) — because which tier a
21021
+ * ticked node lives on is the taxonomy's business, not the caller's. `dog` is
21022
+ * an event kind in the tree and a tier-1 label on an `animal` track; a filter
21023
+ * that read only the class would answer "animal" and never "dog".
21024
+ *
21025
+ * ABSENT or EMPTY means no filter — absent is not a selection of none.
21026
+ */
21027
+ classes: array(string().min(1).max(60)).max(40).optional()
21028
+ });
21029
+ var RecentSummariesPageSchema = object({
21030
+ /** Ordered by (`lastActiveAt` DESC, id DESC). Same light item as
21031
+ * {@link listSummaries} — one list shape, so a card renders the same
21032
+ * whichever feed drew it. */
21033
+ summaries: array(SummaryListItemSchema).readonly(),
21034
+ nextCursor: string().nullable()
20779
21035
  });
20780
21036
  var RecentTracksPageSchema = object({
20781
21037
  /** Merged page, ordered by (`lastSeen` DESC, `trackId` DESC). */
@@ -21189,7 +21445,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
21189
21445
  classes: array(string()).optional(),
21190
21446
  /** See {@link ExcludeSourcesDoc}. */
21191
21447
  excludeSources: array(TrackSourceSchema).optional()
21192
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(SummariesQueryInput, SummariesPageSchema), method(object({ id: string() }), SummaryDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
21448
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(SummariesQueryInput, SummariesPageSchema), method(RecentSummariesQueryInput, RecentSummariesPageSchema), method(object({ id: string() }), SummaryDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
21193
21449
  kind: "mutation",
21194
21450
  auth: "admin"
21195
21451
  }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
@@ -24622,10 +24878,28 @@ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), ar
24622
24878
  }), boolean()), method(object({
24623
24879
  deviceId: number().int().nonnegative(),
24624
24880
  sessionId: string()
24625
- }), object({ pendingRenegotiation: object({
24626
- target: WebrtcStreamTargetSchema,
24627
- epoch: number()
24628
- }).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
+ }));
24629
24903
  object({
24630
24904
  /** All accessory children of the parent. */
24631
24905
  childDeviceIds: array(number()).readonly(),
@@ -33351,7 +33625,29 @@ var LoggingSettingsPatchSchema = object({
33351
33625
  * disarmed the channels it did not mention would make the Levels page and
33352
33626
  * the Diagnostics page fight over the same value.
33353
33627
  */
33354
- channels: array(LogChannelWindowPatchSchema).readonly().optional()
33628
+ channels: array(LogChannelWindowPatchSchema).readonly().optional(),
33629
+ /**
33630
+ * How many days the analytics OPS LOG is kept — the audit trail of what
33631
+ * retention actually deleted.
33632
+ *
33633
+ * It lives HERE, on the logging document, and not on the analytics addon's
33634
+ * settings form, on the operator's instruction: every other log-ish window in
33635
+ * this system is configured through this one document, and a retention audit
33636
+ * configured somewhere else is a second place to look when the question is
33637
+ * "what happened to my data".
33638
+ *
33639
+ * That placement has a cost worth stating: this document ships in the
33640
+ * framework closure, so this knob needs a published `@camstack/server` while
33641
+ * the four windows beside it (D485) reach a node on `camstack deploy`.
33642
+ *
33643
+ * `retention-model.ts` argued this one must NOT follow a device window,
33644
+ * because the audit trail is the history of the SWEEP and not of the camera.
33645
+ * That argument stands and is untouched — what changes is that 30 days stops
33646
+ * being a constant nobody can reach. `0` keeps it forever. The floor is 1 day
33647
+ * rather than 0-as-delete: an audit log truncated to nothing is precisely the
33648
+ * state in which the next 9.5 GiB leak cannot be explained.
33649
+ */
33650
+ opsLogRetentionDays: number().int().min(0).max(3650).optional()
33355
33651
  });
33356
33652
  /**
33357
33653
  * Which LAYER of the hierarchy is addressed. Absent = the cluster layer.
@@ -33424,6 +33720,14 @@ var LoggingSettingsStateSchema = object({
33424
33720
  channels: array(LogChannelDescriptorSchema).readonly(),
33425
33721
  /** The channels ARMED right now, each with its deadline. */
33426
33722
  activeChannels: array(LogChannelWindowStateSchema).readonly(),
33723
+ /**
33724
+ * The ops-log window in days, as it is in force. `0` = kept forever.
33725
+ *
33726
+ * Reported rather than left to the caller's memory of what it wrote: this is
33727
+ * the value the analytics sweep will actually use, and an operator asking how
33728
+ * long their audit trail lives should not have to infer it from a default.
33729
+ */
33730
+ opsLogRetentionDays: number().int().min(0),
33427
33731
  persisted: boolean()
33428
33732
  });
33429
33733
  method(_void(), FeatureManifestSchema), method(_void(), HealthStatusSchema), method(_void(), FeatureManifestSchema), method(_void(), array(NetworkAddressSchema).readonly()), method(_void(), unknown().nullable(), { auth: "admin" }), method(record(string(), unknown()), _null(), {
@@ -34834,6 +35138,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
34834
35138
  smoke: smokeCapability,
34835
35139
  streamCatalog: streamCatalogCapability,
34836
35140
  streamParams: streamParamsCapability,
35141
+ streamSignals: streamSignalsCapability,
34837
35142
  switch: switchCapability,
34838
35143
  tamper: tamperCapability,
34839
35144
  temperatureSensor: temperatureSensorCapability,
@@ -39602,6 +39907,12 @@ Object.freeze({
39602
39907
  addonId: null,
39603
39908
  access: "view"
39604
39909
  },
39910
+ "pipelineAnalytics.listRecentSummaries": {
39911
+ capName: "pipeline-analytics",
39912
+ capScope: "device",
39913
+ addonId: null,
39914
+ access: "view"
39915
+ },
39605
39916
  "pipelineAnalytics.listRecentTracks": {
39606
39917
  capName: "pipeline-analytics",
39607
39918
  capScope: "device",
@@ -41564,6 +41875,12 @@ Object.freeze({
41564
41875
  addonId: null,
41565
41876
  access: "create"
41566
41877
  },
41878
+ "streamBroker.reportStreamSignal": {
41879
+ capName: "stream-broker",
41880
+ capScope: "system",
41881
+ addonId: null,
41882
+ access: "create"
41883
+ },
41567
41884
  "streamBroker.restartProfile": {
41568
41885
  capName: "stream-broker",
41569
41886
  capScope: "system",
@@ -41648,6 +41965,12 @@ Object.freeze({
41648
41965
  addonId: null,
41649
41966
  access: "create"
41650
41967
  },
41968
+ "streamSignals.listSignals": {
41969
+ capName: "stream-signals",
41970
+ capScope: "device",
41971
+ addonId: null,
41972
+ access: "view"
41973
+ },
41651
41974
  "switch.setState": {
41652
41975
  capName: "switch",
41653
41976
  capScope: "device",
@@ -43429,6 +43752,11 @@ Object.freeze({
43429
43752
  form: "single",
43430
43753
  optional: true
43431
43754
  }],
43755
+ "pipelineAnalytics.listRecentSummaries": [{
43756
+ name: "deviceIds",
43757
+ form: "array",
43758
+ optional: false
43759
+ }],
43432
43760
  "pipelineAnalytics.listRecentTracks": [{
43433
43761
  name: "deviceIds",
43434
43762
  form: "array",
@@ -44015,6 +44343,11 @@ Object.freeze({
44015
44343
  form: "single",
44016
44344
  optional: false
44017
44345
  }],
44346
+ "streamBroker.reportStreamSignal": [{
44347
+ name: "deviceId",
44348
+ form: "single",
44349
+ optional: false
44350
+ }],
44018
44351
  "streamBroker.restartProfile": [{
44019
44352
  name: "deviceId",
44020
44353
  form: "single",
@@ -151968,6 +152301,63 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
151968
152301
  this.ctx.logger.info("Reolink image-settings cap registered", { tags: { deviceId: this.id } });
151969
152302
  }
151970
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
+ /**
151971
152361
  * Register the `native-object-detection` cap.
151972
152362
  *
151973
152363
  * The runtimeState holds three fields:
@@ -152025,6 +152415,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
152025
152415
  }
152026
152416
  };
152027
152417
  this.ctx.registerNativeCap(nativeObjectDetectionCapability, provider);
152418
+ this.registerStreamSignalsCap();
152028
152419
  if (this.runtimeState.getCapState(CAP_NAME) === void 0) this.runtimeState.setCapState(CAP_NAME, {
152029
152420
  ...buildEmptyState(),
152030
152421
  lastFetchedAt: Date.now()
@@ -155379,6 +155770,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
155379
155770
  timestamp: now,
155380
155771
  source: "onboard"
155381
155772
  }));
155773
+ this.reportWakeSignal("motion");
155382
155774
  return;
155383
155775
  }
155384
155776
  const aiClass = AI_CLASS_MAP[event.type];
@@ -155389,14 +155781,17 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
155389
155781
  timestamp: now,
155390
155782
  confidence: void 0
155391
155783
  } } });
155392
- if (isNativeObjectForwardingEnabled(this.runtimeState.getCapState("native-object-detection"))) this.ctx.eventBus.emit(createEvent(EventCategory.DetectionCameraNative, eventSource, {
155393
- cameraId: this.id,
155394
- source: "onboard",
155395
- detections: [{
155396
- class: aiClass,
155397
- timestamp: now
155398
- }]
155399
- }));
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
+ }
155400
155795
  this.ctx.eventBus.emit(createEvent(EventCategory.MotionOnMotionChanged, eventSource, {
155401
155796
  deviceId: this.id,
155402
155797
  detected: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-reolink",
3
- "version": "1.2.123",
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
  }