@camstack/addon-post-analysis 1.2.219 → 1.2.221

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.
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-Cnlk6Tyz.js");
5
+ const require_dist = require("../dist-B6jHfZ6s.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_dist.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -20002,6 +20002,14 @@ function checkTimelapsePacing(input) {
20002
20002
  unservedWindows: []
20003
20003
  };
20004
20004
  }
20005
+ //#endregion
20006
+ //#region src/notification-center/timelapse/timelapse-renderer.ts
20007
+ /** `part / whole` to two decimals — the shape a log line is read in. An empty
20008
+ * whole is 0, never NaN: a ratio nobody can compare is not a measurement. */
20009
+ function ratio(part, whole) {
20010
+ if (whole <= 0) return 0;
20011
+ return Math.round(part / whole * 100) / 100;
20012
+ }
20005
20013
  /** Bound on the post-render pacing check. The check is best-effort by charter:
20006
20014
  * a byte read that hangs must delay delivery by at most this, never sink it. */
20007
20015
  var PACING_CHECK_TIMEOUT_MS = 2e4;
@@ -20025,25 +20033,6 @@ function profileCost(profile) {
20025
20033
  const i = PROFILE_BY_COST.indexOf(profile);
20026
20034
  return i === -1 ? PROFILE_BY_COST.length : i;
20027
20035
  }
20028
- /**
20029
- * The CHEAPEST profile that has footage over the window.
20030
- *
20031
- * It used to be the profile with the MOST coverage, ties broken by name — and
20032
- * that is a rule which picks `high` almost every time, because the high band is
20033
- * the one that records continuously. On 2026-09-03 it did: a test render of a
20034
- * ten-hour window chose `profile=high` at 97% coverage, which on this hub means
20035
- * 4K read from a USB spinning disk. Decoding it made the machine stop answering
20036
- * SSH and its own web UI for half an hour, and it had to be power-cycled.
20037
- *
20038
- * A timelapse is watched accelerated. Resolution buys nothing at 12 fps over a
20039
- * night, and the cost difference between the bands is one to two orders of
20040
- * magnitude in both bytes read and frames decoded. So there is no trade to
20041
- * weigh: take the cheapest band that has anything, every time.
20042
- *
20043
- * Coverage no longer decides WHICH — only whether a band qualifies at all. A
20044
- * band with less footage is still the right source; the gaps show as a shorter
20045
- * video, not as a reason to read twenty times the bytes.
20046
- */
20047
20036
  function pickProfile(ranges, window) {
20048
20037
  const byProfile = /* @__PURE__ */ new Map();
20049
20038
  for (const r of ranges) {
@@ -20290,18 +20279,20 @@ var TimelapseRenderer = class {
20290
20279
  const finished = await this.awaitReady(queued, tags);
20291
20280
  const published = await this.publish(finished.id, request, tags);
20292
20281
  const downloadUrl = await this.ports.resolveDownloadUrl?.({ exportId: finished.id }).catch(() => null);
20282
+ const archive = await this.measureArchive(finished.id, request, chosen, tags);
20283
+ const deliveredRows = archive?.rows ?? availability.filter((r) => r.profile === chosen.profile);
20293
20284
  await this.verifyPacing({
20294
20285
  exportId: finished.id,
20295
20286
  request,
20296
20287
  dense,
20297
20288
  subjects,
20298
- availability: availability.filter((r) => r.profile === chosen.profile),
20289
+ availability: deliveredRows,
20299
20290
  tags
20300
20291
  });
20301
20292
  return {
20302
20293
  exportId: finished.id,
20303
20294
  profile: chosen.profile,
20304
- coverage: chosen.coverage,
20295
+ coverage: archive?.coverage ?? chosen.coverage,
20305
20296
  plan,
20306
20297
  detections,
20307
20298
  artifact: published.video,
@@ -20409,6 +20400,96 @@ var TimelapseRenderer = class {
20409
20400
  throw new TimelapseRenderError("export-timeout", `export ${queued.id} timed out after ${this.timeoutMs}ms` + (pollFailures > 0 ? ` (${pollFailures} poll failures, last: ${lastPollError})` : ""));
20410
20401
  }
20411
20402
  /**
20403
+ * The window against the archive, measured AFTER the export and always said.
20404
+ *
20405
+ * Three figures, one line: the window's length, what the PLAN was cut from
20406
+ * (the availability read before the export) and what the ARCHIVE holds (the
20407
+ * same read after it — the export's verified walk has repaired the index the
20408
+ * calendar serves from by then). `plannedOfArchive` is the ratio D445 found
20409
+ * at 0.20 on camera 4374 and nothing reported; a healthy night reads 1.00
20410
+ * because both reads return the same rows.
20411
+ *
20412
+ * WHY THE THRESHOLD IS A SLACK AND NOT A RATIO. The two reads are the SAME
20413
+ * query against the SAME index, so on a healthy night they return the same
20414
+ * rows and the shortfall is exactly 0 — there is no distribution to pick a
20415
+ * percentile out of. It is non-zero only when something CHANGED the index
20416
+ * between them, and the export's verified walk is the only thing that does.
20417
+ * So the bar is not "how short is too short" (the robot's night was 0.20 and
20418
+ * a milder one would be just as wrong); it is only "is this a real change or
20419
+ * a boundary artefact". One artefact exists: the window is closed, but a
20420
+ * segment straddling its end can finalize while the export runs and extend
20421
+ * the last range by its own length. {@link COVERAGE_COMPLETE_SLACK_MS} (30 s)
20422
+ * is the slack the coverage figure already allows at exactly those edges,
20423
+ * and it is several segments wide. No second constant, and nothing between
20424
+ * "unchanged" and "the planner rendered from a short answer".
20425
+ *
20426
+ * Never throws. A re-read that fails is SAID, the plan's figure stands, and
20427
+ * every figure that needed the archive is `null` rather than a stand-in.
20428
+ */
20429
+ async measureArchive(exportId, request, chosen, tags) {
20430
+ const { window } = request;
20431
+ const windowSec = Math.round((window.endMs - window.startMs) / 1e3);
20432
+ const plannedSec = Math.round(chosen.coverage.coveredMs / 1e3);
20433
+ let rows;
20434
+ try {
20435
+ rows = (await this.ports.getAvailability({
20436
+ deviceId: request.deviceId,
20437
+ fromMs: window.startMs,
20438
+ toMs: window.endMs,
20439
+ profile: chosen.profile
20440
+ })).ranges.filter((r) => r.profile === chosen.profile);
20441
+ } catch (err) {
20442
+ this.ports.logger.warn("timelapse coverage: archive re-read failed — the window is reported against the plan only", {
20443
+ tags,
20444
+ meta: {
20445
+ exportId,
20446
+ ruleId: request.ruleId,
20447
+ profile: chosen.profile,
20448
+ windowSec,
20449
+ plannedSec,
20450
+ plannedOfWindow: ratio(chosen.coverage.coveredMs, window.endMs - window.startMs),
20451
+ archiveSec: null,
20452
+ plannedOfArchive: null,
20453
+ archiveOfWindow: null,
20454
+ error: String(err)
20455
+ }
20456
+ });
20457
+ return null;
20458
+ }
20459
+ const coverage = footageCoverage(rows, window.startMs, window.endMs);
20460
+ const shortfallMs = coverage.coveredMs - chosen.coverage.coveredMs;
20461
+ const meta = {
20462
+ exportId,
20463
+ ruleId: request.ruleId,
20464
+ profile: chosen.profile,
20465
+ windowSec,
20466
+ plannedSec,
20467
+ archiveSec: Math.round(coverage.coveredMs / 1e3),
20468
+ /** What the operator ASKED for against what the file HOLDS — the ratio
20469
+ * the robot's night made someone file a report over: ten hours in,
20470
+ * 6.4 s of video out. Low is not by itself a defect (a lazy source is
20471
+ * legitimately idle most of a night), which is why it never gates. */
20472
+ plannedOfWindow: ratio(chosen.coverage.coveredMs, window.endMs - window.startMs),
20473
+ archiveOfWindow: ratio(coverage.coveredMs, window.endMs - window.startMs),
20474
+ /** The one that IS a defect: what the planner saw against what the
20475
+ * export's verified walk found. 0.20 on camera 4374, 1.00 when healthy. */
20476
+ plannedOfArchive: ratio(chosen.coverage.coveredMs, coverage.coveredMs),
20477
+ shortfallSec: Math.round(shortfallMs / 1e3)
20478
+ };
20479
+ if (shortfallMs > 3e4) this.ports.logger.warn("timelapse coverage: the plan was cut from less footage than the archive holds", {
20480
+ tags,
20481
+ meta
20482
+ });
20483
+ else this.ports.logger.info("timelapse coverage: the window against the archive", {
20484
+ tags,
20485
+ meta
20486
+ });
20487
+ return {
20488
+ rows,
20489
+ coverage
20490
+ };
20491
+ }
20492
+ /**
20412
20493
  * Run the pacing check against the finished file, and SAY what it found.
20413
20494
  *
20414
20495
  * Never throws and never blocks past its bound: the check is telemetry about
@@ -28431,6 +28512,34 @@ var MEDIA_INDEXES = [
28431
28512
  "deviceId",
28432
28513
  "sizeBytes"
28433
28514
  ]
28515
+ }),
28516
+ (
28517
+ /**
28518
+ * The per-LOCATION footprint — `COUNT(*)`/`SUM(sizeBytes) WHERE locationId
28519
+ * = ?`, and the legacy `locationId IS NULL` half of the same question. See
28520
+ * {@link MediaStore.footprintByLocation} (D388) and D447.
28521
+ *
28522
+ * **Why not `idx_media_location`.** It leads with `locationId`, so it finds
28523
+ * the range — and then, exactly as the `idx_media_device_size` note above
28524
+ * describes for `deviceId`, fetches the table row behind every entry to read
28525
+ * `sizeBytes`. Every row of the store is on SOME location, so the range is
28526
+ * the table: measured 2026-09-10/11 on `hub/sqlite-settings`, 4 128–6 962 ms
28527
+ * per statement, every five minutes (the occupancy refresh), all night —
28528
+ * `plan=["SEARCH … USING INDEX idx_media_location (locationId=?)"]` on every
28529
+ * WARN. That thread is synchronous, so every configuration read in the
28530
+ * cluster queued behind it: the orchestrator's `configMs=6640` at 23:26:11
28531
+ * is this statement's 6 002 ms at 23:26:10.
28532
+ *
28533
+ * With `sizeBytes` in the index the planner never touches the table
28534
+ * (`USING COVERING INDEX idx_media_location_size`), and the seal gate keeps
28535
+ * its own narrower index. Same shape, same one-off caveat as its two
28536
+ * siblings: ~30 bytes a row, one more b-tree per insert, and the first
28537
+ * `declareCollection` after this ships builds it over every existing row —
28538
+ * do not deploy it beside a running drain or seal.
28539
+ */
28540
+ {
28541
+ name: "idx_media_location_size",
28542
+ columns: ["locationId", "sizeBytes"]
28434
28543
  })
28435
28544
  ];
28436
28545
  /** The storage location of a media row/record: its stamped `locationId`, or
@@ -46470,6 +46579,17 @@ var AudioDetectionSettingsSchema = require_dist.object({
46470
46579
  audioConfirmHits: require_dist.number().int().min(1).default(2),
46471
46580
  /** Width of the corroboration window. Ignored when `audioConfirmHits` is 1. */
46472
46581
  audioConfirmWindowSec: require_dist.number().positive().default(10),
46582
+ /**
46583
+ * How far above the camera's own 10 s mean a sample must sit to be a peak.
46584
+ *
46585
+ * A DIFFERENCE between two readings on the same camera, so a uniform scale
46586
+ * offset cancels exactly and the D459 correction did not touch it. Proved,
46587
+ * not assumed — D459 enumerates this alongside `audioMarkerMinDeviationDb`,
46588
+ * `audioMarkerMinDeviations` and the EWMA/MAD pair. "Correcting" it by
46589
+ * 12 dB would be introducing the very error the epoch removed.
46590
+ *
46591
+ * @dbfs-era relative
46592
+ */
46473
46593
  levelDeviationDb: require_dist.number().positive().default(10),
46474
46594
  levelWindowSec: require_dist.number().positive().default(10),
46475
46595
  levelMaxWaitSec: require_dist.number().positive().default(20),
@@ -46479,14 +46599,44 @@ var AudioDetectionSettingsSchema = require_dist.object({
46479
46599
  /** EWMA time constant for the camera's "normal" loudness, in seconds. Also
46480
46600
  * the warm-up: no marker until one time constant of audio has been heard. */
46481
46601
  audioMarkerBaselineSec: require_dist.number().positive().default(300),
46482
- /** "High volume", absolute. A sample quieter than this is never a marker
46483
- * however surprising it is for the camera. */
46602
+ /**
46603
+ * "High volume", absolute. A sample quieter than this is never a marker
46604
+ * however surprising it is for the camera.
46605
+ *
46606
+ * NOT shifted at the D459 epoch, for the same reason as `SILENCE_FLOOR_DBFS`
46607
+ * and with one extra argument of its own. `-25 dBFS is loud` is a statement
46608
+ * about the scale, not a measurement taken through the old decoder: nobody
46609
+ * read it off a meter, it is what "loud" means on a 16-bit full-scale range.
46610
+ *
46611
+ * The extra argument is that on the old scale this default was effectively
46612
+ * DEAD on the 19 mis-decoded cameras — a reading of -25 there required a
46613
+ * true level of -13 dBFS, which is a sound close to clipping. Bar 1 almost
46614
+ * never bound on them, so the feature has been running on two of its three
46615
+ * bars. At the epoch it starts working as designed rather than becoming too
46616
+ * permissive.
46617
+ *
46618
+ * That it also ships OFF by default (`audioMarkerEnabled`) is what makes
46619
+ * this safe to leave alone: the operator sizes the rate from the `wouldFire`
46620
+ * counter before paying for it, and that counter is now measuring the real
46621
+ * thing for the first time.
46622
+ *
46623
+ * @dbfs-era itu
46624
+ */
46484
46625
  audioMarkerMinDbfs: require_dist.number().max(0).default(-25),
46485
46626
  /** "Out of the ordinary": how many mean-absolute-deviations of the camera's
46486
46627
  * OWN jitter the sample must sit above its own mean. */
46487
46628
  audioMarkerMinDeviations: require_dist.number().positive().default(4),
46488
- /** Floor in dB on the distance above the camera's own mean — what holds
46489
- * when a dead-steady camera's deviation unit collapses toward zero. */
46629
+ /**
46630
+ * Floor in dB on the distance above the camera's own mean what holds when
46631
+ * a dead-steady camera's deviation unit collapses toward zero.
46632
+ *
46633
+ * A distance between two readings on the same camera: scale-invariant, and
46634
+ * untouched by D459. The numerical coincidence with the PCMU correction
46635
+ * (12 dB) is exactly that — a coincidence, and the reason this note exists
46636
+ * is so nobody reads it as one of the shifted numbers.
46637
+ *
46638
+ * @dbfs-era relative
46639
+ */
46490
46640
  audioMarkerMinDeviationDb: require_dist.number().positive().default(12),
46491
46641
  /** Audio matters most when nothing visual is happening. A marker is
46492
46642
  * suppressed while a track is alive on the camera, or motion was reported,
@@ -65000,6 +65150,10 @@ var STALE_BASELINE_FACTOR = 5;
65000
65150
  * has MAD 0 and would make every deviation infinite. 1 dB is below the
65001
65151
  * quantisation of any real level meter, so it only ever binds on a synthetic
65002
65152
  * or a genuinely dead-steady signal — where bar 2 is what should decide.
65153
+ *
65154
+ * A floor on a DEVIATION, not on a level: scale-invariant, untouched by D459.
65155
+ *
65156
+ * @dbfs-era relative
65003
65157
  */
65004
65158
  var MIN_MAD_DB = 1;
65005
65159
  function emptyAudioBaseline() {
@@ -65283,6 +65437,44 @@ function buildAudioLabels(input) {
65283
65437
  }];
65284
65438
  }
65285
65439
  //#endregion
65440
+ //#region src/pipeline-analytics/services/event-kinds-config.ts
65441
+ /** The honest answer for a pipeline that could not be read: nothing advertised. */
65442
+ var NO_EMITTABLE = {
65443
+ macroClasses: [],
65444
+ audioEnabled: false
65445
+ };
65446
+ /** Depth-first walk of the step tree; the detector is a root today, but the
65447
+ * question is "is it enabled anywhere", not "is it a root". */
65448
+ function* walkSteps(steps) {
65449
+ for (const step of steps) {
65450
+ yield step;
65451
+ if (step.children !== void 0) yield* walkSteps(step.children);
65452
+ }
65453
+ }
65454
+ /**
65455
+ * The macro classes an enabled `object-detection` step emits. Order follows
65456
+ * the authority, not the stored array, so two cameras narrowed to the same
65457
+ * set render the same lanes in the same order.
65458
+ */
65459
+ function macroClassesOf(step) {
65460
+ const stored = step.settings?.["enabledMacroClasses"];
65461
+ if (!Array.isArray(stored) || stored.length === 0) return [...require_dist.FIRST_LEVEL_MACRO_CLASSES];
65462
+ const chosen = new Set(stored.filter((v) => typeof v === "string"));
65463
+ return require_dist.FIRST_LEVEL_MACRO_CLASSES.filter((macro) => chosen.has(macro));
65464
+ }
65465
+ function emittableFromPipelineConfig(config) {
65466
+ let macroClasses = [];
65467
+ for (const step of walkSteps(config.steps)) {
65468
+ if (step.addonId !== "object-detection" || step.enabled === false) continue;
65469
+ macroClasses = macroClassesOf(step);
65470
+ break;
65471
+ }
65472
+ return {
65473
+ macroClasses,
65474
+ audioEnabled: config.audio?.enabled === true
65475
+ };
65476
+ }
65477
+ //#endregion
65286
65478
  //#region src/pipeline-analytics/services/event-kinds.ts
65287
65479
  /**
65288
65480
  * Extensible per-device event kinds (Part B).
@@ -78190,28 +78382,26 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
78190
78382
  };
78191
78383
  }
78192
78384
  /**
78193
- * Every event kind the device can produce: built-ins (motion + audio),
78194
- * detection classes actually observed on the device, and sensor kinds
78195
- * from LINKED devices (device-manager `getLinkedDevices`, binding-driven
78196
- * per linked device). Degrades to the built-ins on error.
78385
+ * Every event kind the device can produce: built-ins (motion + audio), the
78386
+ * detection classes its CONFIGURED pipeline can emit, and sensor kinds from
78387
+ * LINKED devices (device-manager `getLinkedDevices`, binding-driven per
78388
+ * linked device). Degrades to the built-ins on error.
78389
+ *
78390
+ * The detection and audio facts are read from `resolvePipeline` — the
78391
+ * configuration — and never from `getCameraStatus`, which is RUNTIME: an
78392
+ * on-motion camera holds no session between motion events
78393
+ * (`reasons.detection: 'armed:on-motion'`, `detection: null`) and was being
78394
+ * advertised as detecting nothing while its tracks said person / vehicle /
78395
+ * animal ([D449](../../../../docs/decisions/adr-0449-the-timeline-taxonomy-is-the-configured-one-raw-forward-has-no-switch-and-a-one-row-write-waits-on-the-disk.md)).
78197
78396
  */
78198
78397
  async listEventKinds(input) {
78199
78398
  const api = this.ctx.api;
78200
- const status = await api.pipelineOrchestrator.getCameraStatus.query({ deviceId: input.deviceId }).catch((err) => {
78201
- this.ctx.logger.warn("listEventKinds: getCameraStatus failed", {
78202
- tags: { deviceId: input.deviceId },
78203
- meta: { error: require_dist.errMsg(err) }
78204
- });
78205
- return null;
78206
- });
78207
- const detectionOn = status?.detection != null;
78208
- const audioOn = status?.audio?.enabled === true;
78209
- const detectionMacros = require_dist.MACRO_LABELS.map((l) => l.id).filter((id) => id !== "package");
78399
+ const emittable = await this.emittableFromConfig(input.deviceId, "listEventKinds");
78210
78400
  return composeEventKinds({
78211
78401
  linkedDevices: { getLinkedDevices: (i) => api.deviceManager.getLinkedDevices.query(i) },
78212
78402
  bindings: { getBindings: (i) => api.deviceManager.getBindings.query(i) },
78213
- enabledMacroClasses: async () => detectionOn ? detectionMacros : [],
78214
- audioEnabled: async () => audioOn,
78403
+ enabledMacroClasses: async () => emittable.macroClasses,
78404
+ audioEnabled: async () => emittable.audioEnabled,
78215
78405
  packageZonesEnabled: async (deviceId) => {
78216
78406
  return (await this.resolveDevicePackageRules(deviceId)).some((r) => r.enabled !== false);
78217
78407
  },
@@ -78225,6 +78415,24 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
78225
78415
  }, input.deviceId);
78226
78416
  }
78227
78417
  /**
78418
+ * What one camera's configured pipeline can emit (`resolvePipeline`), or
78419
+ * the honest fallback when the orchestrator could not answer: nothing
78420
+ * advertised, and a WARN saying so. The cap's answer is a plain list, so it
78421
+ * has no room for "unknown" — that residual of D315 is named in D449; the
78422
+ * steady state (an armed camera with no session) is no longer in it.
78423
+ */
78424
+ async emittableFromConfig(deviceId, scope) {
78425
+ try {
78426
+ return emittableFromPipelineConfig(await this.ctx.api.pipelineOrchestrator.resolvePipeline.query({ deviceId }));
78427
+ } catch (err) {
78428
+ this.ctx.logger.warn(`${scope}: resolvePipeline failed — no ML kinds advertised`, {
78429
+ tags: { deviceId },
78430
+ meta: { error: require_dist.errMsg(err) }
78431
+ });
78432
+ return NO_EMITTABLE;
78433
+ }
78434
+ }
78435
+ /**
78228
78436
  * `listEventKinds` for many cameras, in one call.
78229
78437
  *
78230
78438
  * The saving is not the round-trips the CALLER avoids — it is the fan-out
@@ -78236,11 +78444,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
78236
78444
  * `getLinkedDevices` alone took 4110 ms — each one runs a full-fleet
78237
78445
  * `listAll` inside device-manager and thirty of them QUEUE.
78238
78446
  *
78239
- * Now three calls for the whole set, whatever N is: one
78240
- * `getCameraStatuses`, then `prefetchDeviceTopology`'s
78241
- * `getLinkedDevicesBatch` + `getBindingsBatch`. `composeEventKinds` is
78242
- * unchanged it is handed the same per-device client interfaces, backed by
78243
- * maps instead of RPC.
78447
+ * Now two calls for the whole topology, whatever N is
78448
+ * `prefetchDeviceTopology`'s `getLinkedDevicesBatch` + `getBindingsBatch`
78449
+ * plus one `resolvePipeline` per camera, all in flight at once (D449; there
78450
+ * is no batch form of it). `composeEventKinds` is unchanged it is handed
78451
+ * the same per-device client interfaces, backed by maps instead of RPC.
78244
78452
  *
78245
78453
  * A camera whose composition fails yields an EMPTY list rather than dropping
78246
78454
  * out of the response: a caller that asked for twelve and got eleven cannot
@@ -78249,15 +78457,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
78249
78457
  async listEventKindsBatch(input) {
78250
78458
  const api = this.ctx.api;
78251
78459
  const deviceIds = [...new Set(input.deviceIds)];
78252
- const statuses = await api.pipelineOrchestrator.getCameraStatuses.query({ deviceIds }).catch((err) => {
78253
- this.ctx.logger.warn("listEventKindsBatch: getCameraStatuses failed", { meta: {
78254
- deviceCount: deviceIds.length,
78255
- error: require_dist.errMsg(err)
78256
- } });
78257
- return [];
78258
- });
78259
- const statusByDevice = new Map(statuses.map((st) => [st.deviceId, st]));
78260
- const detectionMacros = require_dist.MACRO_LABELS.map((l) => l.id).filter((id) => id !== "package");
78460
+ const emittableByDevice = new Map(await Promise.all(deviceIds.map(async (deviceId) => [deviceId, await this.emittableFromConfig(deviceId, "listEventKindsBatch")])));
78261
78461
  const topology = await prefetchDeviceTopology({
78262
78462
  getLinkedDevicesBatch: (i) => api.deviceManager.getLinkedDevicesBatch.query(i),
78263
78463
  getBindingsBatch: (i) => api.deviceManager.getBindingsBatch.query(i)
@@ -78269,14 +78469,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
78269
78469
  return null;
78270
78470
  });
78271
78471
  return composeEventKindsForDevices(deviceIds, async (deviceId) => {
78272
- const status = statusByDevice.get(deviceId) ?? null;
78273
- const detectionOn = status?.detection != null;
78274
- const audioOn = status?.audio?.enabled === true;
78472
+ const emittable = emittableByDevice.get(deviceId) ?? NO_EMITTABLE;
78275
78473
  return composeEventKinds({
78276
78474
  linkedDevices: topology?.linkedDevices ?? { getLinkedDevices: (i) => api.deviceManager.getLinkedDevices.query(i) },
78277
78475
  bindings: topology?.bindings ?? { getBindings: (i) => api.deviceManager.getBindings.query(i) },
78278
- enabledMacroClasses: async () => detectionOn ? detectionMacros : [],
78279
- audioEnabled: async () => audioOn,
78476
+ enabledMacroClasses: async () => emittable.macroClasses,
78477
+ audioEnabled: async () => emittable.audioEnabled,
78280
78478
  packageZonesEnabled: async (id) => {
78281
78479
  return (await this.resolveDevicePackageRules(id)).some((r) => r.enabled !== false);
78282
78480
  },