@camstack/addon-post-analysis 1.2.661 → 1.2.663

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 (16) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/{clip-model-registry-CVajwmEk.mjs → clip-model-registry-CwXJyrLg.mjs} +34398 -33387
  3. package/dist/{clip-model-registry-RvEPUqvS.js → clip-model-registry-Cz8LWYwU.js} +34398 -33387
  4. package/dist/embedding-encoder/index.js +1 -1
  5. package/dist/embedding-encoder/index.mjs +1 -1
  6. package/dist/pipeline-analytics/_stub.js +2 -2
  7. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CwKrui6c.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-jPBj20ez.mjs} +2 -2
  8. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Be6bZvOS.mjs +26 -0
  9. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B5Bw-VHt.mjs +26 -0
  10. package/dist/pipeline-analytics/{hostInit-DR75U6YM.mjs → hostInit-DOdK6KJ5.mjs} +2 -2
  11. package/dist/pipeline-analytics/index.js +391 -31
  12. package/dist/pipeline-analytics/index.mjs +391 -31
  13. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  14. package/package.json +2 -2
  15. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BDKliSzv.mjs +0 -26
  16. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BGdtpHOa.mjs +0 -26
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_clip_model_registry = require("../clip-model-registry-RvEPUqvS.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-Cz8LWYwU.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_clip_model_registry.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -8950,6 +8950,17 @@ var en_default = {
8950
8950
  "detection.subject.identity": "{{label}}",
8951
8951
  "detection.subject.plate": "{{classNoun}} {{label}}",
8952
8952
  "detection.inZones": " in {{zones}}",
8953
+ "appliance.finished.title": "{{device}} finished",
8954
+ "appliance.finished.body": "{{device}} finished{{afterDuration}}{{usedEnergy}}",
8955
+ "appliance.finished.duration": " after {{duration}}",
8956
+ "appliance.finished.energy": " and used {{energy}}",
8957
+ "appliance.duration.minutes": {
8958
+ "one": "{{minutes}} minute",
8959
+ "other": "{{minutes}} minutes"
8960
+ },
8961
+ "appliance.duration.hours": "{{hours}} h",
8962
+ "appliance.duration.hoursMinutes": "{{hours}} h {{minutes}} min",
8963
+ "appliance.energy.value": "{{value}} kWh",
8953
8964
  "class.person": {
8954
8965
  "one": "person",
8955
8966
  "other": "people"
@@ -9356,6 +9367,17 @@ var it_default = {
9356
9367
  "detection.subject.identity": "{{label}}",
9357
9368
  "detection.subject.plate": "{{classNoun}} {{label}}",
9358
9369
  "detection.inZones": " in {{zones}}",
9370
+ "appliance.finished.title": "{{device}} ha terminato",
9371
+ "appliance.finished.body": "{{device}} ha terminato{{afterDuration}}{{usedEnergy}}",
9372
+ "appliance.finished.duration": " dopo {{duration}}",
9373
+ "appliance.finished.energy": " e ha consumato {{energy}}",
9374
+ "appliance.duration.minutes": {
9375
+ "one": "{{minutes}} minuto",
9376
+ "other": "{{minutes}} minuti"
9377
+ },
9378
+ "appliance.duration.hours": "{{hours}} h",
9379
+ "appliance.duration.hoursMinutes": "{{hours}} h {{minutes}} min",
9380
+ "appliance.energy.value": "{{value}} kWh",
9359
9381
  "class.person": {
9360
9382
  "one": "persona",
9361
9383
  "other": "persone"
@@ -68703,6 +68725,109 @@ function createAnalyticsWidgetsProvider() {
68703
68725
  ] };
68704
68726
  }
68705
68727
  //#endregion
68728
+ //#region src/notification-center/appliance-cycle.ts
68729
+ var NC_UNKNOWN_CYCLE_FACTS = {
68730
+ durationMs: null,
68731
+ energyKwh: null
68732
+ };
68733
+ var NC_EMPTY_CYCLE_VARS = {
68734
+ duration: "",
68735
+ energy: "",
68736
+ afterDuration: "",
68737
+ usedEnergy: ""
68738
+ };
68739
+ /** `52 minutes` / `1 h 12 min` / `2 h`; `''` below one minute (never "0 minutes"). */
68740
+ function durationText(texts, durationMs) {
68741
+ if (durationMs === null || !Number.isFinite(durationMs)) return "";
68742
+ const totalMinutes = Math.round(durationMs / 6e4);
68743
+ if (totalMinutes < 1) return "";
68744
+ if (totalMinutes < 60) return texts.text({
68745
+ key: "appliance.duration.minutes",
68746
+ count: totalMinutes,
68747
+ vars: { minutes: `${totalMinutes}` }
68748
+ });
68749
+ const hours = Math.floor(totalMinutes / 60);
68750
+ const minutes = totalMinutes % 60;
68751
+ return minutes === 0 ? texts.text({
68752
+ key: "appliance.duration.hours",
68753
+ vars: { hours: `${hours}` }
68754
+ }) : texts.text({
68755
+ key: "appliance.duration.hoursMinutes",
68756
+ vars: {
68757
+ hours: `${hours}`,
68758
+ minutes: `${minutes}`
68759
+ }
68760
+ });
68761
+ }
68762
+ /** `0,84 kWh` (it) / `0.84 kWh` (en); `''` when unmeasured. */
68763
+ function energyText(texts, energyKwh) {
68764
+ if (energyKwh === null || !Number.isFinite(energyKwh) || energyKwh < 0) return "";
68765
+ const value = energyKwh.toLocaleString(texts.language, {
68766
+ minimumFractionDigits: 2,
68767
+ maximumFractionDigits: 2
68768
+ });
68769
+ return texts.text({
68770
+ key: "appliance.energy.value",
68771
+ vars: { value }
68772
+ });
68773
+ }
68774
+ /** The four variables of a finished cycle, fragments rendered from scalars only. */
68775
+ function ncApplianceCycleVars(texts, facts) {
68776
+ const duration = durationText(texts, facts.durationMs);
68777
+ const energy = energyText(texts, facts.energyKwh);
68778
+ return {
68779
+ duration,
68780
+ energy,
68781
+ afterDuration: duration === "" ? "" : texts.text({
68782
+ key: "appliance.finished.duration",
68783
+ vars: { duration }
68784
+ }),
68785
+ usedEnergy: energy === "" ? "" : texts.text({
68786
+ key: "appliance.finished.energy",
68787
+ vars: { energy }
68788
+ })
68789
+ };
68790
+ }
68791
+ /**
68792
+ * The frozen reference a FINISHED appliance cycle row carries (`{}` for every
68793
+ * other row): kind `appliance-cycle`, edge `cleared`. The appliance is the
68794
+ * sensor itself — a linked camera is only where the row was attributed.
68795
+ */
68796
+ function applianceCycleOf(subject) {
68797
+ if (subject.sensorKind !== "appliance-cycle" || subject.sensorRaised !== false) return {};
68798
+ return { applianceCycle: { deviceId: subject.linkedSensorId ?? subject.sourceDeviceId ?? subject.deviceId } };
68799
+ }
68800
+ //#endregion
68801
+ //#region src/notification-center/appliance-cycle-runs.ts
68802
+ var CYCLE_FACTS_WAIT_MS = 3e3;
68803
+ var CYCLE_FACTS_POLL_MS = 250;
68804
+ /** A closed run belongs to this finish when it ended within this much of it. */
68805
+ var CYCLE_RUN_MATCH_MS = 10 * 6e4;
68806
+ function factsOfClosed(run, finishedAtMs) {
68807
+ if (run.endedAt === null || Math.abs(run.endedAt - finishedAtMs) > CYCLE_RUN_MATCH_MS) return NC_UNKNOWN_CYCLE_FACTS;
68808
+ return {
68809
+ durationMs: run.durationMs,
68810
+ energyKwh: run.detail.energyKwh
68811
+ };
68812
+ }
68813
+ function applianceCycleFactsFromRuns(ports) {
68814
+ return async ({ deviceId, finishedAtMs }) => {
68815
+ let waited = 0;
68816
+ for (;;) {
68817
+ const run = await ports.newestRun(deviceId);
68818
+ if (run === null) return NC_UNKNOWN_CYCLE_FACTS;
68819
+ if (run.state === "closed") return factsOfClosed(run, finishedAtMs);
68820
+ if (run.startedAt > finishedAtMs) return NC_UNKNOWN_CYCLE_FACTS;
68821
+ if (waited >= CYCLE_FACTS_WAIT_MS) return {
68822
+ durationMs: finishedAtMs - run.startedAt,
68823
+ energyKwh: null
68824
+ };
68825
+ await ports.sleep(CYCLE_FACTS_POLL_MS);
68826
+ waited += CYCLE_FACTS_POLL_MS;
68827
+ }
68828
+ };
68829
+ }
68830
+ //#endregion
68706
68831
  //#region src/pipeline-analytics/runtime/wire-subscriptions.ts
68707
68832
  /**
68708
68833
  * Every category the Notification Center's system-event intake can turn into a
@@ -70664,6 +70789,8 @@ Object.freeze({
70664
70789
  "page": 1,
70665
70790
  "pageSize": 50
70666
70791
  } },
70792
+ "device-runs": { "listRuns": { "limit": 50 } },
70793
+ "fan-presets": { "capturePreset": { "steps": [] } },
70667
70794
  "llm": { "upsertProfile": { "profile": { "__nested__": {
70668
70795
  "timeoutMs": 6e4,
70669
70796
  "connectTimeoutMs": 1e4,
@@ -70904,6 +71031,10 @@ Object.freeze({
70904
71031
  "speed": 2
70905
71032
  }
70906
71033
  },
71034
+ "system": { "debugWatch": {
71035
+ "channels": [],
71036
+ "deviceIds": []
71037
+ } },
70907
71038
  "terminal-session": { "pullOutput": { "waitMs": 0 } },
70908
71039
  "training": {
70909
71040
  "planJob": { "selection": { "__nested__": { "workState": "complete" } } },
@@ -70950,6 +71081,7 @@ Object.freeze({
70950
71081
  ],
70951
71082
  "device-removal-hook": ["listDeviceData"],
70952
71083
  "failure-contribution": ["list"],
71084
+ "integration-instances": ["listInstances"],
70953
71085
  "llm": [
70954
71086
  "getDefaults",
70955
71087
  "getUsage",
@@ -70991,6 +71123,7 @@ Object.freeze({
70991
71123
  "listSupportedEntityKinds",
70992
71124
  "unexposeEntity"
70993
71125
  ],
71126
+ "integration-instances": ["testInstance"],
70994
71127
  "log-destination": [
70995
71128
  "cancelRead",
70996
71129
  "describeRead",
@@ -85089,6 +85222,9 @@ async function renderMosaic(request, logger) {
85089
85222
  * one composition are decisions about the set, and none of them belongs in a
85090
85223
  * shelf that photographs one camera for an absence.
85091
85224
  *
85225
+ * A SLEEPING battery camera is woken by one forced capture when the rule leaves
85226
+ * it allowed (D998) — bounded, once per gap per camera, never by a poll.
85227
+ *
85092
85228
  * Two reads can only ever ADD a camera back, never take one away on their own
85093
85229
  * failure (D49): a wrapper-state read (run in parallel with the capture) that
85094
85230
  * misses or throws keeps the tile, and a freshness read that misses or throws
@@ -85102,6 +85238,15 @@ var NC_LINKED_CAMERA_PREFIX = "nc-linked:";
85102
85238
  function isLinkedStillId(id) {
85103
85239
  return id.startsWith(NC_LINKED_MOSAIC_PREFIX) || id.startsWith(NC_LINKED_CAMERA_PREFIX);
85104
85240
  }
85241
+ /**
85242
+ * D998 — the one forced capture that wakes a sleeping battery camera for a
85243
+ * sensor notification: the bound it may take, the pause between re-polls of a
85244
+ * camera another wake already started, and the gap per camera before it is
85245
+ * woken again (a flapping contact must not drain a battery).
85246
+ */
85247
+ var NC_LINKED_WAKE_CAPTURE_TIMEOUT_MS = 15e3;
85248
+ var NC_LINKED_WAKE_POLL_MS = 1e3;
85249
+ var NC_LINKED_WAKE_MIN_GAP_MS = 6e4;
85105
85250
  /** Per camera — the marker's own bound (`SENSOR_MARKER_SNAPSHOT_TIMEOUT_MS`). */
85106
85251
  var SENSOR_LINKED_CAPTURE_TIMEOUT_MS = 2500;
85107
85252
  /** The capture-free wrapper-state read, per camera. It runs IN PARALLEL with
@@ -85134,11 +85279,14 @@ var NcLinkedStillShelf = class {
85134
85279
  deps;
85135
85280
  sets = /* @__PURE__ */ new Map();
85136
85281
  owners = /* @__PURE__ */ new Map();
85282
+ /** setId → when its in-flight wake gives up (the dispatcher waits that long). */
85283
+ pendingWakes = /* @__PURE__ */ new Map();
85284
+ lastWakeAt = /* @__PURE__ */ new Map();
85137
85285
  constructor(deps) {
85138
85286
  this.deps = deps;
85139
85287
  }
85140
85288
  /** SYNCHRONOUS: the evaluation that follows names these owners. */
85141
- captureSet(sensorId, cameraIds, atMs) {
85289
+ captureSet(sensorId, cameraIds, atMs, options = { wakeSleeping: false }) {
85142
85290
  const now = this.deps.now();
85143
85291
  this.prune(now);
85144
85292
  const setId = (0, node_crypto.randomUUID)();
@@ -85166,7 +85314,7 @@ var NcLinkedStillShelf = class {
85166
85314
  camera: cameraId
85167
85315
  });
85168
85316
  });
85169
- this.run(setId, stills, atMs, now).catch((err) => {
85317
+ this.run(setId, stills, atMs, now, options).catch((err) => {
85170
85318
  this.deps.logger.warn("sensor notification pictures failed — it ships text-only", {
85171
85319
  tags: { deviceId: sensorId },
85172
85320
  meta: { error: String(err) }
@@ -85174,6 +85322,15 @@ var NcLinkedStillShelf = class {
85174
85322
  });
85175
85323
  return stills;
85176
85324
  }
85325
+ /**
85326
+ * While a wake of one of this owner's cameras is in flight: the instant it
85327
+ * gives up (its bound plus one poll). `undefined` otherwise — the dispatcher
85328
+ * then keeps its ordinary still-wait.
85329
+ */
85330
+ pendingUntil(ownerId) {
85331
+ const ref = this.owners.get(ownerId);
85332
+ return ref === void 0 ? void 0 : this.pendingWakes.get(ref.setId);
85333
+ }
85177
85334
  /** `undefined` = not an owner this shelf minted (or pruned); `[]` = nothing (yet). */
85178
85335
  get(ownerId) {
85179
85336
  const ref = this.owners.get(ownerId);
@@ -85195,20 +85352,37 @@ var NcLinkedStillShelf = class {
85195
85352
  clear() {
85196
85353
  this.sets.clear();
85197
85354
  this.owners.clear();
85355
+ this.pendingWakes.clear();
85198
85356
  }
85199
- async run(setId, stills, atMs, startedAt) {
85200
- const results = await Promise.all(stills.cameraIds.map((id) => this.captureOne(id, startedAt)));
85357
+ async run(setId, stills, atMs, startedAt, options) {
85358
+ const ctx = {
85359
+ setId,
85360
+ sensorId: stills.sensorId,
85361
+ startedAt,
85362
+ options
85363
+ };
85364
+ let results;
85365
+ try {
85366
+ results = await Promise.all(stills.cameraIds.map((id) => this.captureOne(id, ctx)));
85367
+ } finally {
85368
+ this.pendingWakes.delete(setId);
85369
+ }
85201
85370
  const judged = await this.judgeFreshness(stills.sensorId, results, atMs);
85202
85371
  for (const r of judged) this.logTile(stills.sensorId, r);
85203
85372
  const landed = judged.filter(isLandedTile);
85204
85373
  const composed = await this.compose(stills.sensorId, landed, atMs);
85205
85374
  const undrawable = landed.length - composed.drawn;
85375
+ const woken = judged.filter((r) => r.woken === true).map((r) => r.cameraId);
85206
85376
  this.deps.logger.info(composed.mosaic.length === 0 ? "sensor notification has no linked picture — it ships text-only" : "sensor notification pictures ready", {
85207
85377
  tags: { deviceId: stills.sensorId },
85208
85378
  meta: {
85209
85379
  cameras: stills.cameraIds.length,
85210
85380
  landed: composed.drawn,
85211
85381
  ...undrawable > 0 ? { undrawable } : {},
85382
+ ...woken.length > 0 ? {
85383
+ woken,
85384
+ waitedMs: this.deps.now() - startedAt
85385
+ } : {},
85212
85386
  dropped: Object.fromEntries(judged.filter((r) => r.outcome !== "landed").map((r) => [String(r.cameraId), r.outcome])),
85213
85387
  tookMs: this.deps.now() - startedAt
85214
85388
  }
@@ -85221,21 +85395,25 @@ var NcLinkedStillShelf = class {
85221
85395
  mosaic: composed.mosaic
85222
85396
  });
85223
85397
  }
85224
- async captureOne(cameraId, startedAt) {
85225
- const took = () => this.deps.now() - startedAt;
85398
+ async captureOne(cameraId, ctx) {
85399
+ const took = () => this.deps.now() - ctx.startedAt;
85226
85400
  if (this.deps.isOnline(cameraId) === false) return {
85227
85401
  cameraId,
85228
85402
  outcome: "excluded",
85229
85403
  reason: "offline",
85230
85404
  tookMs: took()
85231
85405
  };
85232
- const [verdict, capture] = await Promise.all([this.wrapperVerdict(cameraId), this.captureStill(cameraId)]);
85233
- if (verdict.reason !== null) return {
85406
+ const [verdict, capture] = await Promise.all([this.wrapperVerdict(cameraId), this.captureStill(cameraId, false, SENSOR_LINKED_CAPTURE_TIMEOUT_MS)]);
85407
+ const excluded = (reason) => ({
85234
85408
  cameraId,
85235
85409
  outcome: "excluded",
85236
- reason: verdict.reason,
85410
+ reason,
85237
85411
  tookMs: took()
85238
- };
85412
+ });
85413
+ if (verdict.reason === "sleeping") return this.wakeOne(cameraId, ctx);
85414
+ if (verdict.reason === "waking" && ctx.options.wakeSleeping) return this.awaitWaking(cameraId, ctx);
85415
+ if (verdict.reason === "unreachable" && ctx.options.wakeSleeping) return excluded("wake-skipped:unreachable");
85416
+ if (verdict.reason !== null) return excluded(verdict.reason);
85239
85417
  return {
85240
85418
  cameraId,
85241
85419
  ...capture,
@@ -85243,9 +85421,84 @@ var NcLinkedStillShelf = class {
85243
85421
  tookMs: took()
85244
85422
  };
85245
85423
  }
85246
- async captureStill(cameraId) {
85424
+ /** D998: one forced capture, by the rule's leave, at most once per gap per camera. */
85425
+ async wakeOne(cameraId, ctx) {
85426
+ const took = () => this.deps.now() - ctx.startedAt;
85427
+ const skipped = (reason) => ({
85428
+ cameraId,
85429
+ outcome: "excluded",
85430
+ reason,
85431
+ tookMs: took()
85432
+ });
85433
+ if (!ctx.options.wakeSleeping) return skipped("wake-skipped:policy");
85434
+ const last = this.lastWakeAt.get(cameraId);
85435
+ if (last !== void 0 && ctx.startedAt - last < NC_LINKED_WAKE_MIN_GAP_MS) return skipped("wake-skipped:recent-wake");
85436
+ this.lastWakeAt.set(cameraId, ctx.startedAt);
85437
+ this.markPending(ctx);
85438
+ this.deps.logger.info("linked battery camera woken for the sensor notification", {
85439
+ tags: { deviceId: cameraId },
85440
+ meta: {
85441
+ sensorId: ctx.sensorId,
85442
+ ruleIds: ctx.options.ruleIds ?? [],
85443
+ boundMs: NC_LINKED_WAKE_CAPTURE_TIMEOUT_MS
85444
+ }
85445
+ });
85446
+ const capture = await this.captureStill(cameraId, true, NC_LINKED_WAKE_CAPTURE_TIMEOUT_MS);
85447
+ return {
85448
+ cameraId,
85449
+ ...this.asWakeResult(capture),
85450
+ woken: true,
85451
+ tookMs: took()
85452
+ };
85453
+ }
85454
+ /** A camera another wake already started: never forced, re-polled within the bound. */
85455
+ async awaitWaking(cameraId, ctx) {
85456
+ const took = () => this.deps.now() - ctx.startedAt;
85457
+ this.markPending(ctx);
85458
+ const polls = Math.floor(NC_LINKED_WAKE_CAPTURE_TIMEOUT_MS / NC_LINKED_WAKE_POLL_MS);
85459
+ for (let i = 0; i < polls; i++) {
85460
+ await new Promise((resolve) => setTimeout(resolve, NC_LINKED_WAKE_POLL_MS));
85461
+ const verdict = await this.wrapperVerdict(cameraId);
85462
+ if (verdict.reason === "waking") continue;
85463
+ if (verdict.reason !== null) return {
85464
+ cameraId,
85465
+ outcome: "excluded",
85466
+ reason: verdict.reason,
85467
+ tookMs: took()
85468
+ };
85469
+ const capture = await this.captureStill(cameraId, false, SENSOR_LINKED_CAPTURE_TIMEOUT_MS);
85470
+ return {
85471
+ cameraId,
85472
+ ...this.asWakeResult(capture),
85473
+ woken: true,
85474
+ tookMs: took()
85475
+ };
85476
+ }
85477
+ return {
85478
+ cameraId,
85479
+ outcome: "wake-timeout",
85480
+ woken: true,
85481
+ tookMs: took()
85482
+ };
85483
+ }
85484
+ markPending(ctx) {
85485
+ const until = ctx.startedAt + NC_LINKED_WAKE_CAPTURE_TIMEOUT_MS + NC_LINKED_WAKE_POLL_MS;
85486
+ this.pendingWakes.set(ctx.setId, Math.max(this.pendingWakes.get(ctx.setId) ?? 0, until));
85487
+ }
85488
+ /** A wake that brought nothing back names itself, not the generic `empty`. */
85489
+ asWakeResult(capture) {
85490
+ if (capture.outcome === "timeout") return {
85491
+ ...capture,
85492
+ outcome: "wake-timeout"
85493
+ };
85494
+ return capture.outcome === "empty" || capture.outcome === "error" && capture.reason !== "undecodable" ? {
85495
+ ...capture,
85496
+ outcome: "wake-refused"
85497
+ } : capture;
85498
+ }
85499
+ async captureStill(cameraId, force, timeoutMs) {
85247
85500
  try {
85248
- const shot = await withTimeout$2(this.deps.getSnapshot(cameraId), SENSOR_LINKED_CAPTURE_TIMEOUT_MS, "snapshot cap");
85501
+ const shot = await withTimeout$2(force ? this.deps.getSnapshot(cameraId, { force: true }) : this.deps.getSnapshot(cameraId), timeoutMs, "snapshot cap");
85249
85502
  if (shot === null || shot.base64.length === 0) return { outcome: "empty" };
85250
85503
  const jpeg = await this.decodableStill(Buffer.from(shot.base64, "base64"));
85251
85504
  if (jpeg === null) return {
@@ -86804,6 +87057,13 @@ var NC_RECORDED_FOOTAGE_WAIT_MS = 3e4;
86804
87057
  var NC_STILL_WAIT_MS = 3e3;
86805
87058
  var NC_STILL_WAIT_INTERVAL_MS = 500;
86806
87059
  /**
87060
+ * D998 — the longest a sensor notification holds for a linked battery camera
87061
+ * that is being woken (`stillDeadline`). A `sent` row is final, so the picture
87062
+ * cannot follow the push: it is waited for here, bounded, or never attached.
87063
+ * Owners with no wake in flight keep {@link NC_STILL_WAIT_MS}.
87064
+ */
87065
+ var NC_LINKED_WAKE_WAIT_MS = 15e3;
87066
+ /**
86807
87067
  * How far a still's OWN instant may sit from the trigger and still count as
86808
87068
  * being ABOUT the trigger, for a `delivery: 'immediate'` rule.
86809
87069
  *
@@ -87772,6 +88032,36 @@ var NcDispatcher = class {
87772
88032
  });
87773
88033
  }
87774
88034
  /**
88035
+ * The facts of a finished appliance cycle; unknown on every other row and on
88036
+ * any failure (a part that cannot be read is omitted, never a blocked send).
88037
+ */
88038
+ async applianceCycleFacts(entry) {
88039
+ const subject = entry.payload.subject;
88040
+ const resolve = this.deps.resolveApplianceCycleFacts;
88041
+ const deviceId = subject.applianceCycle?.deviceId;
88042
+ if (resolve === void 0 || deviceId === void 0) return NC_UNKNOWN_CYCLE_FACTS;
88043
+ try {
88044
+ const facts = await resolve({
88045
+ deviceId,
88046
+ finishedAtMs: subject.timestamp
88047
+ });
88048
+ if (facts.durationMs === null || facts.energyKwh === null) this.deps.logger.debug("appliance cycle part omitted — unknown", {
88049
+ tags: { deviceId },
88050
+ meta: {
88051
+ duration: facts.durationMs === null ? "unknown" : "known",
88052
+ energy: facts.energyKwh === null ? "unknown" : "known"
88053
+ }
88054
+ });
88055
+ return facts;
88056
+ } catch (err) {
88057
+ this.deps.logger.debug("appliance cycle facts failed — parts omitted", {
88058
+ tags: { deviceId },
88059
+ meta: { error: err instanceof Error ? err.message : String(err) }
88060
+ });
88061
+ return NC_UNKNOWN_CYCLE_FACTS;
88062
+ }
88063
+ }
88064
+ /**
87775
88065
  * Builds the notification AND returns the artefact ids its media resolve
87776
88066
  * indexed — see {@link withArtifactUrls}. A tuple rather than a field on
87777
88067
  * `NcNotificationInput`: the ids are internal (outbox provenance), never
@@ -87783,7 +88073,8 @@ var NcDispatcher = class {
87783
88073
  const deviceName = await notificationDeviceName(entry, this.deps.getDeviceName);
87784
88074
  const alarmSource = systemEvent?.alarmSource;
87785
88075
  const zoneLabels = systemEvent === void 0 ? await resolveZoneLabels(this.deps.getZoneNames, subject.deviceId, subject.zones) : alarmSource?.zones !== void 0 && alarmSource.zones.length > 0 ? await resolveZoneLabels(this.deps.getZoneNames, alarmSource.deviceId, alarmSource.zones) : [];
87786
- const vars = buildTemplateVars(this.texts, entry, deviceName, zoneLabels);
88076
+ const cycleFacts = await this.applianceCycleFacts(entry);
88077
+ const vars = buildTemplateVars(this.texts, entry, deviceName, zoneLabels, void 0, cycleFacts);
87787
88078
  this.warnDeprecatedVars(entry);
87788
88079
  const systemCount = systemEvent?.textCount !== void 0 ? { count: systemEvent.textCount } : {};
87789
88080
  const title = renderRowTitle(this.texts, entry, vars);
@@ -87989,7 +88280,15 @@ var NcDispatcher = class {
87989
88280
  const firstLook = groupTiles !== null ? null : await this.resolveAttachment(entry, canStillGainMedia(entry) ? "trigger-only" : "any");
87990
88281
  const footagePending = this.renderFootage(entry);
87991
88282
  const mosaic = groupView !== void 0 && groupTiles !== null ? await this.groupedMosaic(entry, groupView, groupTiles) : null;
87992
- const still = mosaic !== null ? mosaic.attachment : firstLook !== null || entry.payload.media === "none" ? firstLook : await this.waitForStill(entry);
88283
+ let waitedMs = 0;
88284
+ let still;
88285
+ if (mosaic !== null) still = mosaic.attachment;
88286
+ else if (firstLook !== null || entry.payload.media === "none") still = firstLook;
88287
+ else {
88288
+ const waited = await this.waitForStill(entry);
88289
+ still = waited.still;
88290
+ waitedMs = waited.waitedMs;
88291
+ }
87993
88292
  const grouped = groupView !== void 0 && groupMembers !== null ? {
87994
88293
  windowKey: groupView.key,
87995
88294
  members: groupView.members.length,
@@ -88014,6 +88313,7 @@ var NcDispatcher = class {
88014
88313
  recordKind: entry.recordKind,
88015
88314
  recordId: entry.recordId,
88016
88315
  policy: entry.payload.media,
88316
+ waitedMs,
88017
88317
  ...entry.payload.mediaImage !== void 0 ? { image: entry.payload.mediaImage } : {},
88018
88318
  ...memberTrackIds.length > 1 ? { memberTrackIds } : {},
88019
88319
  owners: [...entry.payload.subject.eventId !== void 0 ? [`object:${entry.payload.subject.eventId}`] : [], ...entry.payload.subject.trackId !== void 0 ? [`track:${entry.payload.subject.trackId}`] : []]
@@ -88040,7 +88340,13 @@ var NcDispatcher = class {
88040
88340
  const subject = entry.payload.subject;
88041
88341
  const out = [];
88042
88342
  for (const { ownerId, cameraId } of subject.extraStills ?? []) {
88043
- const file = await this.linkedStillFile(entry, ownerId, cameraId);
88343
+ let file = await this.linkedStillFile(entry, ownerId, cameraId);
88344
+ for (let polls = 0; file === void 0 && polls < NC_LINKED_WAKE_WAIT_MS / NC_STILL_WAIT_INTERVAL_MS; polls += 1) {
88345
+ const deadline = this.deps.stillDeadline?.(ownerId);
88346
+ if (deadline === void 0 || this.now() >= deadline) break;
88347
+ await this.sleep(NC_STILL_WAIT_INTERVAL_MS);
88348
+ file = await this.linkedStillFile(entry, ownerId, cameraId);
88349
+ }
88044
88350
  const bytes = file === void 0 ? null : decodeStillBytes(file.base64);
88045
88351
  if (file === void 0 || bytes === null) {
88046
88352
  this.deps.logger.info("linked camera picture missing at send — skipped", {
@@ -88527,11 +88833,19 @@ var NcDispatcher = class {
88527
88833
  */
88528
88834
  async waitForStill(entry) {
88529
88835
  const subject = entry.payload.subject;
88530
- if (this.now() - subject.timestamp > NC_STILL_WAIT_MAX_AGE_MS) return this.resolveAttachment(entry, "any");
88531
- if (subject.eventId === void 0 && subject.trackId === void 0) return null;
88532
- const attempts = Math.floor(NC_STILL_WAIT_MS / NC_STILL_WAIT_INTERVAL_MS);
88533
- for (let i = 0; i < attempts; i += 1) {
88836
+ if (this.now() - subject.timestamp > NC_STILL_WAIT_MAX_AGE_MS) return {
88837
+ still: await this.resolveAttachment(entry, "any"),
88838
+ waitedMs: 0
88839
+ };
88840
+ if (subject.eventId === void 0 && subject.trackId === void 0) return {
88841
+ still: null,
88842
+ waitedMs: 0
88843
+ };
88844
+ const startedAt = this.now();
88845
+ let waitedMs = 0;
88846
+ for (let i = 0; waitedMs < this.stillWaitBoundMs(entry, startedAt); i += 1) {
88534
88847
  await this.sleep(NC_STILL_WAIT_INTERVAL_MS);
88848
+ waitedMs += NC_STILL_WAIT_INTERVAL_MS;
88535
88849
  const still = await this.resolveAttachment(entry, "trigger-only");
88536
88850
  if (still !== null) {
88537
88851
  this.deps.logger.info("the still landed during the bounded wait", {
@@ -88543,10 +88857,32 @@ var NcDispatcher = class {
88543
88857
  kind: still.name
88544
88858
  }
88545
88859
  });
88546
- return still;
88860
+ return {
88861
+ still,
88862
+ waitedMs
88863
+ };
88547
88864
  }
88548
88865
  }
88549
- return this.resolveAttachment(entry, "any");
88866
+ return {
88867
+ still: await this.resolveAttachment(entry, "any"),
88868
+ waitedMs
88869
+ };
88870
+ }
88871
+ /**
88872
+ * How long this row's still poll may run: {@link NC_STILL_WAIT_MS}, or — while
88873
+ * the shelf says a linked camera of this row is being woken — until that
88874
+ * wake gives up, at most {@link NC_LINKED_WAKE_WAIT_MS} (D998).
88875
+ */
88876
+ stillWaitBoundMs(entry, startedAt) {
88877
+ const subject = entry.payload.subject;
88878
+ const owners = [...subject.trackId !== void 0 ? [subject.trackId] : [], ...(subject.extraStills ?? []).map((extra) => extra.ownerId)];
88879
+ let deadline;
88880
+ for (const ownerId of owners) {
88881
+ const until = this.deps.stillDeadline?.(ownerId);
88882
+ if (until !== void 0 && (deadline === void 0 || until > deadline)) deadline = until;
88883
+ }
88884
+ if (deadline === void 0) return NC_STILL_WAIT_MS;
88885
+ return Math.max(NC_STILL_WAIT_MS, Math.min(NC_LINKED_WAKE_WAIT_MS, deadline - startedAt));
88550
88886
  }
88551
88887
  /**
88552
88888
  * The grouped MOSAIC (D692 §6.2): one tile per member, at most
@@ -88694,8 +89030,8 @@ var NcDispatcher = class {
88694
89030
  return ts !== void 0 && this.now() - ts <= NC_STILL_WAIT_MAX_AGE_MS;
88695
89031
  });
88696
89032
  await look("trigger-only", pollable());
88697
- const attempts = Math.floor(NC_STILL_WAIT_MS / NC_STILL_WAIT_INTERVAL_MS);
88698
- for (let i = 0; i < attempts && pollable().length > 0; i += 1) {
89033
+ const startedAt = this.now();
89034
+ for (let waited = 0; waited < this.stillWaitBoundMs(entry, startedAt) && pollable().length > 0; waited += NC_STILL_WAIT_INTERVAL_MS) {
88699
89035
  await this.sleep(NC_STILL_WAIT_INTERVAL_MS);
88700
89036
  await look("trigger-only", pollable());
88701
89037
  }
@@ -88950,7 +89286,7 @@ function systemEventParamsForTemplate(params) {
88950
89286
  const { form: _form, attention: _attention, ...rest } = params;
88951
89287
  return rest;
88952
89288
  }
88953
- function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
89289
+ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone, cycle) {
88954
89290
  const subject = entry.payload.subject;
88955
89291
  const occupancy = subject.occupancy;
88956
89292
  const systemEvent = subject.systemEvent;
@@ -88986,6 +89322,7 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
88986
89322
  hitPercent: subject.audio?.hitPercent !== void 0 ? `${Math.round(subject.audio.hitPercent)}` : "",
88987
89323
  db: subject.audio?.peakDbfs !== void 0 ? `${Math.round(subject.audio.peakDbfs)}` : "",
88988
89324
  eventType: subject.eventType ?? "",
89325
+ ...cycle !== void 0 ? ncApplianceCycleVars(texts, cycle) : NC_EMPTY_CYCLE_VARS,
88989
89326
  rule: entry.payload.ruleName,
88990
89327
  count: occupancy !== void 0 ? `${occupancy.count}` : group !== void 0 && group.members.length > 1 ? `${group.members.length}` : "",
88991
89328
  capacity: occupancy !== void 0 ? `${occupancy.capacity}` : "",
@@ -89012,6 +89349,7 @@ function bodyKeyFor(entry) {
89012
89349
  const subject = entry.payload.subject;
89013
89350
  if (subject.systemEvent !== void 0) return subject.systemEvent.bodyKey ?? "";
89014
89351
  if (subject.occupancy !== void 0) return "occupancy.body";
89352
+ if (subject.applianceCycle !== void 0) return "appliance.finished.body";
89015
89353
  if (subject.packagePhase === "delivered" && subject.packagePossible === true) return "package.delivered.possible.body";
89016
89354
  if (subject.packagePhase !== void 0) return `package.${subject.packagePhase}.body`;
89017
89355
  return entry.recordKind === "track-end" ? "detection.body.trackEnd" : "detection.body";
@@ -89049,7 +89387,7 @@ function renderRowTitle(texts, entry, vars) {
89049
89387
  const systemEvent = entry.payload.subject.systemEvent;
89050
89388
  const intrusion = entry.payload.alarmIntrusion;
89051
89389
  const count = systemEvent?.textCount;
89052
- return renderRowText(texts, {
89390
+ const rendered = renderRowText(texts, {
89053
89391
  template: intrusion !== void 0 ? void 0 : entry.payload.template?.title,
89054
89392
  key: titleKeyFor(entry),
89055
89393
  vars: intrusion !== void 0 ? {
@@ -89059,11 +89397,13 @@ function renderRowTitle(texts, entry, vars) {
89059
89397
  ...count !== void 0 ? { count } : {},
89060
89398
  frozen: systemEvent?.title ?? entry.payload.ruleName
89061
89399
  });
89400
+ return titleKeyFor(entry) === "appliance.finished.title" && entry.payload.template?.title === void 0 ? ncSentenceCaseBody(rendered) : rendered;
89062
89401
  }
89063
89402
  /** The default TITLE key — same shape, same pre-catalog reasoning. */
89064
89403
  function titleKeyFor(entry) {
89065
89404
  const systemEvent = entry.payload.subject.systemEvent;
89066
89405
  if (systemEvent !== void 0) return systemEvent.titleKey ?? "";
89406
+ if (entry.payload.subject.applianceCycle !== void 0) return "appliance.finished.title";
89067
89407
  if (entry.payload.alarmIntrusion !== void 0) return "alarm.intrusion.title";
89068
89408
  const packagePhase = entry.payload.subject.packagePhase;
89069
89409
  if (packagePhase === "delivered" && entry.payload.subject.packagePossible === true) return "package.delivered.possible.title";
@@ -102135,6 +102475,7 @@ function buildEntries(host, rule, subject, kind, matchedOn, userTargets = [], or
102135
102475
  ...subject.audioWindow.peakDbfs !== void 0 ? { peakDbfs: subject.audioWindow.peakDbfs } : {}
102136
102476
  } } : {},
102137
102477
  ...subject.eventType !== void 0 ? { eventType: subject.eventType } : {},
102478
+ ...applianceCycleOf(subject),
102138
102479
  ...subject.systemEvent !== void 0 ? { systemEvent: subject.systemEvent } : {}
102139
102480
  }
102140
102481
  };
@@ -104210,7 +104551,7 @@ function beginLinkedStills(host, request) {
104210
104551
  if (rowCameraIds.length > 0 && rowShown.length === 0) return void 0;
104211
104552
  const shown = host.unmutedCameras(cameraIds);
104212
104553
  const involved = [sensorId, ...rowShown];
104213
- if (!host.rules.listEnabled("device-event").some((rule) => {
104554
+ const wanting = host.rules.listEnabled("device-event").filter((rule) => {
104214
104555
  if (ruleShipsNoStill(rule)) return false;
104215
104556
  if (!deviceEventRuleMayFireOnKind(rule, sensorKind, sensorRaised)) return false;
104216
104557
  const excluded = rule.conditions.devicesExclude;
@@ -104221,9 +104562,14 @@ function beginLinkedStills(host, request) {
104221
104562
  })) return false;
104222
104563
  const devices = rule.conditions.devices;
104223
104564
  return devices === void 0 || devices.length === 0 || devices.some((id) => involved.includes(id));
104224
- })) return void 0;
104565
+ });
104566
+ if (wanting.length === 0) return void 0;
104567
+ const wakeSleeping = wanting.some((rule) => rule.media?.wakeSleepingCamera !== false);
104225
104568
  for (const cameraId of cameraIds) if (!shown.includes(cameraId)) host.reportLinkedCameraMuted(cameraId, sensorId);
104226
- return shown.length > 0 ? shelf.captureSet(sensorId, shown, atMs) : void 0;
104569
+ return shown.length > 0 ? shelf.captureSet(sensorId, shown, atMs, {
104570
+ wakeSleeping,
104571
+ ruleIds: wanting.map((rule) => rule.id)
104572
+ }) : void 0;
104227
104573
  }
104228
104574
  /**
104229
104575
  * A muted camera left out of a linked set — one INFO line per camera per
@@ -106946,6 +107292,7 @@ var NotificationCenter = class {
106946
107292
  ...deps.dispatcher,
106947
107293
  logger: this.logger.child("dispatch"),
106948
107294
  texts: this.texts,
107295
+ stillDeadline: (ownerId) => isLinkedStillId(ownerId) ? this.linkedStills?.pendingUntil(ownerId) : void 0,
106949
107296
  getMediaForOwner: async (ownerKind, ownerId, eventScope) => {
106950
107297
  const held = isSyntheticId(ownerId) ? this.syntheticMedia.get(ownerId) : void 0;
106951
107298
  if (held !== void 0) return held.files;
@@ -109529,7 +109876,10 @@ var BuildNotificationCenter = class extends BuildNotificationCenterBase {
109529
109876
  },
109530
109877
  readAlerts: () => api.alerts.list.query({ limit: 5e3 }),
109531
109878
  emitBusEvent: (event) => this.ctx.eventBus.emit(event),
109532
- getSnapshot: (deviceId) => api.snapshot.getSnapshot.query({ deviceId }),
109879
+ getSnapshot: (deviceId, opts) => api.snapshot.getSnapshot.query(opts?.force === true ? {
109880
+ deviceId,
109881
+ force: true
109882
+ } : { deviceId }),
109533
109883
  recognitionOutlook: ({ deviceId, trackId, className, labelKind, now }) => recognitionOutlook({
109534
109884
  className,
109535
109885
  ...labelKind !== void 0 ? { labelKind } : {},
@@ -109872,6 +110222,7 @@ var BuildNotificationCenter = class extends BuildNotificationCenterBase {
109872
110222
  }));
109873
110223
  },
109874
110224
  getDeviceName,
110225
+ resolveApplianceCycleFacts: this.deps.resolveApplianceCycleFacts,
109875
110226
  getDeviceLocation,
109876
110227
  buildActions: (input) => this.notificationCenter?.mintButtons(input) ?? Promise.resolve([])
109877
110228
  },
@@ -123743,7 +124094,16 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
123743
124094
  buildTimelapsePorts: (api, stores) => this.buildTimelapsePorts(api, stores),
123744
124095
  buildSummaryPorts: (api, stores) => this.buildSummaryPorts(api, stores),
123745
124096
  wireAlarmPanel: (center) => this.wireAlarmPanel(center),
123746
- serveNcActionPlane: (center) => this.serveNcActionPlane(center)
124097
+ serveNcActionPlane: (center) => this.serveNcActionPlane(center),
124098
+ resolveApplianceCycleFacts: applianceCycleFactsFromRuns({
124099
+ newestRun: async (deviceId) => {
124100
+ return (await this.ctx.api.deviceRuns.listRuns.query({
124101
+ deviceId,
124102
+ limit: 1
124103
+ })).runs[0] ?? null;
124104
+ },
124105
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
124106
+ })
123747
124107
  });
123748
124108
  return this.notificationCenterBootInstance;
123749
124109
  }