@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.
- package/dist/{dist-Cnlk6Tyz.js → dist-B6jHfZ6s.js} +242 -30
- package/dist/{dist-D0l3xZqa.mjs → dist-Clp3AeIp.mjs} +237 -25
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-C8d8YCvw.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BVv-ddKp.mjs} +3 -3
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dgmog5ky.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BCU_bvyw.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-BsYVmiZ9.mjs → hostInit-EskKIFx1.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +259 -61
- package/dist/pipeline-analytics/index.mjs +259 -61
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -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-
|
|
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:
|
|
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
|
-
/**
|
|
46483
|
-
*
|
|
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
|
-
/**
|
|
46489
|
-
*
|
|
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
|
|
78195
|
-
*
|
|
78196
|
-
*
|
|
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
|
|
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 () =>
|
|
78214
|
-
audioEnabled: async () =>
|
|
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
|
|
78240
|
-
* `
|
|
78241
|
-
* `
|
|
78242
|
-
*
|
|
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
|
|
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
|
|
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 () =>
|
|
78279
|
-
audioEnabled: async () =>
|
|
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
|
},
|