@camstack/addon-post-analysis 1.2.309 → 1.2.311

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 (14) hide show
  1. package/dist/{clip-model-registry-CrBn6Qvt.mjs → clip-model-registry-DPqDIZrG.mjs} +409 -200
  2. package/dist/{clip-model-registry-B3M4ZI17.js → clip-model-registry-X457TLXn.js} +456 -199
  3. package/dist/embedding-encoder/index.js +1 -1
  4. package/dist/embedding-encoder/index.mjs +1 -1
  5. package/dist/pipeline-analytics/_stub.js +2 -2
  6. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Bkf0-pki.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-D0JVXXRd.mjs} +2 -2
  7. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Ds6vT2FJ.mjs +26 -0
  8. package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B8qeS8Da.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B_KS96RA.mjs} +1 -1
  9. package/dist/pipeline-analytics/{hostInit-Dj0aaMhv.mjs → hostInit-Da_2XVg_.mjs} +2 -2
  10. package/dist/pipeline-analytics/index.js +411 -34
  11. package/dist/pipeline-analytics/index.mjs +411 -34
  12. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  13. package/package.json +1 -1
  14. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DWg2WU0a.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-B3M4ZI17.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-X457TLXn.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");
@@ -1396,11 +1396,85 @@ var NcAlarmExclusions = class {
1396
1396
  }
1397
1397
  };
1398
1398
  //#endregion
1399
+ //#region src/notification-center/alarm/alarm-mode-delays.ts
1400
+ /**
1401
+ * The persisted per-mode delay overrides, read through a guard (D697).
1402
+ *
1403
+ * The panel's config schema keeps `mode` a plain string so a blob written by a later release
1404
+ * with a mode this one does not know cannot fail the whole config. This is where that string is
1405
+ * narrowed: an entry that cannot be trusted is dropped and REPORTED, never repaired. An entry of an unknown
1406
+ * mode is also handed back raw (`foreign`) so a write can keep it.
1407
+ */
1408
+ function inBounds(value, max) {
1409
+ return Number.isInteger(value) && value >= 0 && value <= max;
1410
+ }
1411
+ /** Read the persisted overrides through a guard, never a cast (like `nonBlockingFromPersisted`). */
1412
+ function modeDelaysFromPersisted(raw) {
1413
+ const seen = /* @__PURE__ */ new Set();
1414
+ const entries = [];
1415
+ const dropped = [];
1416
+ const foreign = [];
1417
+ for (const item of raw) {
1418
+ const mode = require_clip_model_registry.AlarmArmModeSchema.safeParse(item.mode);
1419
+ if (!mode.success) {
1420
+ dropped.push({
1421
+ mode: item.mode,
1422
+ why: "unknown-mode"
1423
+ });
1424
+ foreign.push(item);
1425
+ continue;
1426
+ }
1427
+ if (seen.has(mode.data)) {
1428
+ dropped.push({
1429
+ mode: item.mode,
1430
+ why: "duplicate-mode"
1431
+ });
1432
+ continue;
1433
+ }
1434
+ seen.add(mode.data);
1435
+ const kept = { mode: mode.data };
1436
+ const fields = [
1437
+ {
1438
+ field: "exitDelaySec",
1439
+ max: require_clip_model_registry.NC_ALARM_DELAY_MAX_SEC
1440
+ },
1441
+ {
1442
+ field: "entryDelaySec",
1443
+ max: require_clip_model_registry.NC_ALARM_DELAY_MAX_SEC
1444
+ },
1445
+ {
1446
+ field: "triggeredDurationSec",
1447
+ max: require_clip_model_registry.NC_ALARM_TRIGGERED_MAX_SEC
1448
+ }
1449
+ ];
1450
+ for (const { field, max } of fields) {
1451
+ const value = item[field];
1452
+ if (value === void 0) continue;
1453
+ if (!inBounds(value, max)) {
1454
+ dropped.push({
1455
+ mode: item.mode,
1456
+ why: "out-of-bounds",
1457
+ field
1458
+ });
1459
+ continue;
1460
+ }
1461
+ kept[field] = value;
1462
+ }
1463
+ if (kept.exitDelaySec !== void 0 || kept.entryDelaySec !== void 0 || kept.triggeredDurationSec !== void 0) entries.push(kept);
1464
+ }
1465
+ return {
1466
+ entries,
1467
+ dropped,
1468
+ foreign
1469
+ };
1470
+ }
1471
+ //#endregion
1399
1472
  //#region src/notification-center/alarm/alarm-state-machine.ts
1400
1473
  /**
1401
- * Defaults an operator can change per panel. `home` has no exit delay on
1402
- * purpose — nobody is leaving — but the shared value is applied to every mode
1403
- * until per-mode delays are asked for.
1474
+ * The GLOBAL defaults an operator can change per panel — what a mode inherits
1475
+ * when it overrides nothing. A mode can override each of the three since D697
1476
+ * (`resolveModeDelays`): `home` has no exit delay on purpose — nobody is
1477
+ * leaving — and says so with its own entry, not by this shared value.
1404
1478
  *
1405
1479
  * `triggeredDurationSec: 0` = "sound until somebody disarms it". That is what
1406
1480
  * the panel did before the field existed, so an install that never opens the
@@ -1815,6 +1889,19 @@ var ncAlarmPanelSchema = require_clip_model_registry.object({
1815
1889
  nonBlocking: require_clip_model_registry.array(require_clip_model_registry.object({
1816
1890
  mode: require_clip_model_registry.string(),
1817
1891
  deviceIds: require_clip_model_registry.array(require_clip_model_registry.number().int())
1892
+ })).default([]),
1893
+ /**
1894
+ * Per arm mode, the delays that override the global ones (D697). Loose on purpose — `mode` is
1895
+ * a string and the numbers are unbounded here — and read back through
1896
+ * {@link modeDelaysFromPersisted}, never a cast: an entry this release cannot trust is dropped
1897
+ * and logged at boot, never the whole config. A blob written before D697 has no key and
1898
+ * parses to no overrides.
1899
+ */
1900
+ modeDelays: require_clip_model_registry.array(require_clip_model_registry.object({
1901
+ mode: require_clip_model_registry.string(),
1902
+ exitDelaySec: require_clip_model_registry.number().optional(),
1903
+ entryDelaySec: require_clip_model_registry.number().optional(),
1904
+ triggeredDurationSec: require_clip_model_registry.number().optional()
1818
1905
  })).default([])
1819
1906
  });
1820
1907
  /**
@@ -1885,7 +1972,6 @@ function machineFromPersisted(raw) {
1885
1972
  var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
1886
1973
  features = [];
1887
1974
  machine = DISARMED;
1888
- delays = DEFAULT_DELAYS;
1889
1975
  ticker = null;
1890
1976
  onPublish = null;
1891
1977
  /**
@@ -1916,7 +2002,7 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
1916
2002
  refusalSeq = 0;
1917
2003
  constructor(ctx) {
1918
2004
  super(ctx, ncAlarmPanelSchema, { type: require_clip_model_registry.DeviceType.AlarmPanel });
1919
- this.delays = this.readDelays();
2005
+ this.logDroppedModeDelays();
1920
2006
  this.restore();
1921
2007
  this.registerAlarmCap();
1922
2008
  this.publish();
@@ -1928,30 +2014,148 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
1928
2014
  ...this.readDelays(),
1929
2015
  announceArm: this.config.get("announceArm"),
1930
2016
  announceTargets: [...this.config.get("announceTargets")],
1931
- nonBlocking: nonBlockingFromPersisted(this.config.get("nonBlocking"))
2017
+ nonBlocking: nonBlockingFromPersisted(this.config.get("nonBlocking")),
2018
+ modeDelays: modeDelaysFromPersisted(this.config.get("modeDelays")).entries
1932
2019
  };
1933
2020
  }
1934
2021
  /**
1935
2022
  * Apply an editor's patch and return what the panel settled on.
1936
2023
  *
1937
2024
  * The new delays take effect on the NEXT transition, never retroactively: a
1938
- * machine already counting down keeps the instants it was given. Re-deriving
1939
- * `armedAt` from a delay the operator changed mid-exit would move the arm
1940
- * instant under somebody who is walking out of the door.
2025
+ * machine already counting down keeps the instants it was given (the exit
2026
+ * delay is read at arm, the entry delay and siren duration at trigger, and
2027
+ * the machine stores the instants, not the durations). Re-deriving `armedAt`
2028
+ * from a delay the operator changed mid-exit would move the arm instant under
2029
+ * somebody who is walking out of the door.
2030
+ *
2031
+ * Two phases, so a refused patch writes NOTHING:
2032
+ * 1. every refusal is checked first (a mode this panel does not offer, an
2033
+ * add to a full non-blocking list) — it logs why and throws;
2034
+ * 2. every LIST write is CALLED before the first `await`. `DeviceConfig.set`
2035
+ * replaces its in-memory value synchronously, so a list computed after an
2036
+ * await could be based on a list another call has since replaced — and a
2037
+ * whole-list write (the deleted-device prune sends one, computed
2038
+ * synchronously before calling: `device-prune-runner.ts` `applyAlarm`)
2039
+ * would erase the membership edit that landed in between.
1941
2040
  */
1942
2041
  async applySettings(patch) {
2042
+ const edits = patch.modeDelays ?? [];
2043
+ const membership = patch.nonBlockingEdit;
2044
+ for (const edit of edits) this.refuseUnofferedMode(edit.mode);
2045
+ if (membership !== void 0) {
2046
+ this.refuseUnofferedMode(membership.mode, membership.deviceId);
2047
+ const verdict = require_clip_model_registry.applyNonBlockingEdit(patch.nonBlocking !== void 0 ? patch.nonBlocking : nonBlockingFromPersisted(this.config.get("nonBlocking")), membership);
2048
+ if (verdict.kind === "refused") {
2049
+ this.ctx.logger.info("alarm settings refused", {
2050
+ tags: { deviceId: membership.deviceId },
2051
+ meta: {
2052
+ panelId: this.id,
2053
+ reason: "list-full",
2054
+ mode: verdict.mode
2055
+ }
2056
+ });
2057
+ throw new Error(`the ${verdict.mode} non-blocking list already holds ${verdict.max} devices — remove one first`);
2058
+ }
2059
+ }
2060
+ const listWrites = [];
2061
+ if (patch.nonBlocking !== void 0) listWrites.push(this.config.set("nonBlocking", patch.nonBlocking.map((l) => ({
2062
+ mode: l.mode,
2063
+ deviceIds: [...l.deviceIds]
2064
+ }))));
2065
+ let membershipChanged = null;
2066
+ if (membership !== void 0) {
2067
+ const result = require_clip_model_registry.applyNonBlockingEdit(nonBlockingFromPersisted(this.config.get("nonBlocking")), membership);
2068
+ if (result.kind === "applied") {
2069
+ membershipChanged = result.changed;
2070
+ if (result.changed) listWrites.push(this.config.set("nonBlocking", result.lists.map((l) => ({
2071
+ mode: l.mode,
2072
+ deviceIds: [...l.deviceIds]
2073
+ }))));
2074
+ }
2075
+ }
2076
+ const persistedDelays = modeDelaysFromPersisted(this.config.get("modeDelays"));
2077
+ const beforeDelays = persistedDelays.entries;
2078
+ const afterDelays = edits.length > 0 ? require_clip_model_registry.applyModeDelaysEdits(beforeDelays, edits) : beforeDelays;
2079
+ if (edits.length > 0) {
2080
+ for (const drop of persistedDelays.dropped) {
2081
+ if (drop.why === "unknown-mode") continue;
2082
+ this.ctx.logger.info("alarm per-mode delay discarded on write", {
2083
+ tags: { deviceId: this.id },
2084
+ meta: {
2085
+ mode: drop.mode,
2086
+ why: drop.why,
2087
+ field: drop.field
2088
+ }
2089
+ });
2090
+ }
2091
+ listWrites.push(this.config.set("modeDelays", [...afterDelays, ...persistedDelays.foreign]));
2092
+ }
2093
+ await Promise.all(listWrites);
2094
+ this.logModeDelayChanges(beforeDelays, afterDelays, edits);
1943
2095
  if (patch.exitDelaySec !== void 0) await this.config.set("exitDelaySec", patch.exitDelaySec);
1944
2096
  if (patch.entryDelaySec !== void 0) await this.config.set("entryDelaySec", patch.entryDelaySec);
1945
2097
  if (patch.triggeredDurationSec !== void 0) await this.config.set("triggeredDurationSec", patch.triggeredDurationSec);
1946
2098
  if (patch.announceArm !== void 0) await this.config.set("announceArm", patch.announceArm);
1947
2099
  if (patch.announceTargets !== void 0) await this.config.set("announceTargets", [...patch.announceTargets]);
1948
- if (patch.nonBlocking !== void 0) await this.config.set("nonBlocking", patch.nonBlocking.map((l) => ({
1949
- mode: l.mode,
1950
- deviceIds: [...l.deviceIds]
1951
- })));
1952
- this.delays = this.readDelays();
2100
+ if (membership !== void 0 && membershipChanged !== null) this.ctx.logger.info("alarm non-blocking list changed", {
2101
+ tags: { deviceId: membership.deviceId },
2102
+ meta: {
2103
+ panelId: this.id,
2104
+ mode: membership.mode,
2105
+ nonBlocking: membership.nonBlocking,
2106
+ changed: membershipChanged
2107
+ }
2108
+ });
1953
2109
  return this.settings();
1954
2110
  }
2111
+ /**
2112
+ * Refuse a mode this panel does not offer — logged by reason, then thrown. A membership edit
2113
+ * passes its SENSOR as `deviceId` (the tag); a per-mode delay edit defaults to the panel.
2114
+ */
2115
+ refuseUnofferedMode(mode, deviceId = this.id) {
2116
+ if (NC_ALARM_MODES.includes(mode)) return;
2117
+ this.ctx.logger.info("alarm settings refused", {
2118
+ tags: { deviceId },
2119
+ meta: {
2120
+ panelId: this.id,
2121
+ reason: "mode-not-offered",
2122
+ mode
2123
+ }
2124
+ });
2125
+ throw new Error(`alarm mode '${mode}' is not offered by this panel`);
2126
+ }
2127
+ /** One INFO line per mode-delay field an edit really changed (`null` = inherit). */
2128
+ logModeDelayChanges(before, after, edits) {
2129
+ for (const edit of edits) {
2130
+ const was = before.find((e) => e.mode === edit.mode);
2131
+ const now = after.find((e) => e.mode === edit.mode);
2132
+ for (const field of require_clip_model_registry.NC_ALARM_DELAY_FIELDS) {
2133
+ const from = was?.[field] ?? null;
2134
+ const to = now?.[field] ?? null;
2135
+ if (from === to) continue;
2136
+ this.ctx.logger.info("alarm mode delays changed", {
2137
+ tags: { deviceId: this.id },
2138
+ meta: {
2139
+ mode: edit.mode,
2140
+ field,
2141
+ from,
2142
+ to
2143
+ }
2144
+ });
2145
+ }
2146
+ }
2147
+ }
2148
+ /** One INFO line per persisted per-mode delay entry the guard refused to trust. */
2149
+ logDroppedModeDelays() {
2150
+ for (const drop of modeDelaysFromPersisted(this.config.get("modeDelays")).dropped) this.ctx.logger.info("alarm per-mode delay dropped at boot", {
2151
+ tags: { deviceId: this.id },
2152
+ meta: {
2153
+ mode: drop.mode,
2154
+ why: drop.why,
2155
+ field: drop.field
2156
+ }
2157
+ });
2158
+ }
1955
2159
  /** Wire the state mirror and the transition notifications. See
1956
2160
  * {@link NcAlarmPublishHook}. */
1957
2161
  setPublishHook(hook) {
@@ -2234,7 +2438,17 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2234
2438
  else this.exclusions.clear();
2235
2439
  this.persist();
2236
2440
  }
2237
- this.machine = arm(mode, this.delays, now);
2441
+ const resolved = this.delaysFor(mode);
2442
+ this.machine = arm(mode, resolved, now);
2443
+ this.ctx.logger.info("alarm delays applied", {
2444
+ tags: { deviceId: this.id },
2445
+ meta: {
2446
+ mode,
2447
+ phase: "arm",
2448
+ exitDelaySec: resolved.exitDelaySec,
2449
+ source: resolved.source.exitDelaySec
2450
+ }
2451
+ });
2238
2452
  this.attribution.clear();
2239
2453
  this.publish(now);
2240
2454
  for (const { device, why } of exclusions ?? []) this.ctx.logger.info("alarm armed with an open device excluded", {
@@ -2334,7 +2548,9 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2334
2548
  this.announceIgnoredTrigger(source, now);
2335
2549
  return;
2336
2550
  }
2337
- const next = trigger(this.machine, this.delays, now);
2551
+ const armed = armModeOf(this.machine.target, NC_ALARM_MODES);
2552
+ const resolved = this.delaysFor(armed);
2553
+ const next = trigger(this.machine, resolved, now);
2338
2554
  if (next === this.machine) {
2339
2555
  const state = stateAt(this.machine, now);
2340
2556
  this.ctx.logger.info(ignoredTriggerMessage(state), {
@@ -2352,6 +2568,19 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2352
2568
  }
2353
2569
  });
2354
2570
  this.machine = next;
2571
+ this.ctx.logger.info("alarm delays applied", {
2572
+ tags: { deviceId: this.id },
2573
+ meta: {
2574
+ mode: armed,
2575
+ phase: "trigger",
2576
+ entryDelaySec: resolved.entryDelaySec,
2577
+ triggeredDurationSec: resolved.triggeredDurationSec,
2578
+ source: {
2579
+ entry: resolved.source.entryDelaySec,
2580
+ siren: resolved.source.triggeredDurationSec
2581
+ }
2582
+ }
2583
+ });
2355
2584
  this.publish(now);
2356
2585
  }
2357
2586
  });
@@ -2487,7 +2716,32 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2487
2716
  });
2488
2717
  }
2489
2718
  }
2490
- /** The three durations, read from the persisted config. */
2719
+ /** The global durations plus the per-mode overrides, read from the persisted config NOW. */
2720
+ delaySettings() {
2721
+ return {
2722
+ ...this.readDelays(),
2723
+ modeDelays: modeDelaysFromPersisted(this.config.get("modeDelays")).entries
2724
+ };
2725
+ }
2726
+ /**
2727
+ * What `mode` runs on, read at the transition that uses it — never cached. `null` (no armed
2728
+ * mode to look at) is the global values, every field `default`.
2729
+ */
2730
+ delaysFor(mode) {
2731
+ const settings = this.delaySettings();
2732
+ if (mode === null) return {
2733
+ exitDelaySec: settings.exitDelaySec,
2734
+ entryDelaySec: settings.entryDelaySec,
2735
+ triggeredDurationSec: settings.triggeredDurationSec,
2736
+ source: {
2737
+ exitDelaySec: "default",
2738
+ entryDelaySec: "default",
2739
+ triggeredDurationSec: "default"
2740
+ }
2741
+ };
2742
+ return require_clip_model_registry.resolveModeDelays(settings, mode);
2743
+ }
2744
+ /** The three global durations, read from the persisted config. */
2491
2745
  readDelays() {
2492
2746
  return {
2493
2747
  exitDelaySec: this.config.get("exitDelaySec"),
@@ -8353,6 +8607,79 @@ function sameWord(a, b) {
8353
8607
  return a !== void 0 && a.trim().toLowerCase() === b.trim().toLowerCase();
8354
8608
  }
8355
8609
  //#endregion
8610
+ //#region src/notification-center/alarm/alarm-sensors.ts
8611
+ /**
8612
+ * One id's state as the table shows it — spec §3.4's hub table, nothing else.
8613
+ *
8614
+ * Built ON `classifyCoveredOpening`, the arm check's classifier: what is
8615
+ * unknown, open or skipped is decided there, and this only splits a skip into
8616
+ * `closed` / `not-opening` for the table. A later change to the classifier
8617
+ * therefore moves the table with the arm.
8618
+ */
8619
+ function alarmSensorStateOf(deviceId, opening, online) {
8620
+ const verdict = classifyCoveredOpening(opening);
8621
+ switch (verdict.kind) {
8622
+ case "unknown": return {
8623
+ deviceId,
8624
+ reading: "unknown",
8625
+ state: null,
8626
+ unknownWhy: verdict.why,
8627
+ online
8628
+ };
8629
+ case "open": return {
8630
+ deviceId,
8631
+ reading: "open",
8632
+ state: verdict.state,
8633
+ unknownWhy: null,
8634
+ online
8635
+ };
8636
+ case "skip": return opening?.kind === "reading" ? {
8637
+ deviceId,
8638
+ reading: "closed",
8639
+ state: opening.state,
8640
+ unknownWhy: null,
8641
+ online
8642
+ } : {
8643
+ deviceId,
8644
+ reading: "not-opening",
8645
+ state: null,
8646
+ unknownWhy: null,
8647
+ online
8648
+ };
8649
+ }
8650
+ }
8651
+ /** The asked ids, de-duplicated and ascending. */
8652
+ function alarmSensorReadIds(ids) {
8653
+ return [...new Set(ids)].toSorted((a, b) => a - b);
8654
+ }
8655
+ /**
8656
+ * The sensors read's health, logged on CHANGE only (the call is polled every
8657
+ * 5-15 s by every open tab). "Wholesale failure" = ids were asked and not one
8658
+ * was answered (the device manager is unreachable): WARN once when it starts,
8659
+ * ONE INFO when answers return. A partial answer is not a health change — those
8660
+ * ids already leave their own debug line and show as `unknown`.
8661
+ */
8662
+ var NcAlarmSensorsReadLog = class {
8663
+ failing = false;
8664
+ next(asked, answered) {
8665
+ const wholesale = asked > 0 && answered === 0;
8666
+ if (wholesale === this.failing) return null;
8667
+ this.failing = wholesale;
8668
+ return wholesale ? {
8669
+ level: "warn",
8670
+ message: "alarm sensors read: no asked device answered — every row is unknown",
8671
+ meta: { asked }
8672
+ } : {
8673
+ level: "info",
8674
+ message: "alarm sensors read: devices answer again",
8675
+ meta: {
8676
+ asked,
8677
+ answered
8678
+ }
8679
+ };
8680
+ }
8681
+ };
8682
+ //#endregion
8356
8683
  //#region src/notification-center/alarm/alarm-live.ts
8357
8684
  /** Every covered id across all modes, ascending, once — what the one read asks for. */
8358
8685
  function alarmLiveReadIds(covered) {
@@ -17027,7 +17354,8 @@ var ncActions = require_clip_model_registry.defineCustomActions({
17027
17354
  * `getStatus`; this one opens up once it filters to the caller's grants.
17028
17355
  * The config pair is the admin's, as `getAlarmConfig` / `setAlarmConfig` are
17029
17356
  * on the cap: it carries the delays, the recipients and the non-blocking
17030
- * lists.
17357
+ * lists. `getAlarmSensors` is the admin's too: it names which of the asked
17358
+ * devices are open.
17031
17359
  */
17032
17360
  "nc.getAlarmStatus": require_clip_model_registry.customAction(require_clip_model_registry.object({}), NcAlarmStatusSchema, {
17033
17361
  auth: "admin",
@@ -17068,6 +17396,10 @@ var ncActions = require_clip_model_registry.defineCustomActions({
17068
17396
  auth: "admin",
17069
17397
  caller: "required"
17070
17398
  }),
17399
+ "nc.getAlarmSensors": require_clip_model_registry.customAction(require_clip_model_registry.NcAlarmSensorsInputSchema, require_clip_model_registry.NcAlarmSensorsSchema, {
17400
+ auth: "admin",
17401
+ caller: "required"
17402
+ }),
17071
17403
  "nc.setAlarmConfig": require_clip_model_registry.customAction(require_clip_model_registry.object({ patch: require_clip_model_registry.NcAlarmSettingsPatchSchema }), require_clip_model_registry.NcAlarmConfigSchema, {
17072
17404
  kind: "mutation",
17073
17405
  auth: "admin",
@@ -17860,6 +18192,10 @@ function makeNcActionHandlers(deps) {
17860
18192
  requireAdmin(caller);
17861
18193
  return requireAlarm("nc.getAlarmConfig").getConfig();
17862
18194
  },
18195
+ "nc.getAlarmSensors": async (input, caller) => {
18196
+ requireAdmin(caller);
18197
+ return requireAlarm("nc.getAlarmSensors").getSensors(input);
18198
+ },
17863
18199
  "nc.setAlarmConfig": async (input, caller) => {
17864
18200
  requireAdmin(caller);
17865
18201
  return requireAlarm("nc.setAlarmConfig").setConfig(input.patch);
@@ -32694,7 +33030,8 @@ function consumablesSliceOf(event) {
32694
33030
  var NC_ALARM_SETTINGS_FALLBACK = {
32695
33031
  ...DEFAULT_DELAYS,
32696
33032
  announceTargets: [],
32697
- nonBlocking: []
33033
+ nonBlocking: [],
33034
+ modeDelays: []
32698
33035
  };
32699
33036
  /**
32700
33037
  * How often the "matched NO rule" report may fire per device. Long enough that
@@ -33076,6 +33413,7 @@ var NotificationCenter = class NotificationCenter {
33076
33413
  alarmPanel = null;
33077
33414
  /** The live view's read, logged on change only — see `alarm-live-read-log.ts`. */
33078
33415
  alarmLiveReadLog = new NcAlarmLiveReadLog();
33416
+ alarmSensorsReadLog = new NcAlarmSensorsReadLog();
33079
33417
  /**
33080
33418
  * The words the panel answers the `deviceState` gate with — its state, plus
33081
33419
  * the armed mode a `pending`/`triggered` belongs to (D630). Refreshed on
@@ -35394,6 +35732,7 @@ var NotificationCenter = class NotificationCenter {
35394
35732
  return { success: true };
35395
35733
  },
35396
35734
  getAlarmConfig: async () => this.alarmConfig(),
35735
+ getAlarmSensors: async (input) => this.alarmSensors(input),
35397
35736
  setAlarmConfig: async ({ patch }) => this.setAlarmConfig(patch)
35398
35737
  };
35399
35738
  }
@@ -35621,6 +35960,7 @@ var NotificationCenter = class NotificationCenter {
35621
35960
  });
35622
35961
  },
35623
35962
  getConfig: () => this.alarmConfig(),
35963
+ getSensors: (input) => this.alarmSensors(input),
35624
35964
  setConfig: (patch) => this.setAlarmConfig(patch)
35625
35965
  };
35626
35966
  }
@@ -35634,7 +35974,8 @@ var NotificationCenter = class NotificationCenter {
35634
35974
  exitDelaySec: settings.exitDelaySec,
35635
35975
  entryDelaySec: settings.entryDelaySec,
35636
35976
  triggeredDurationSec: settings.triggeredDurationSec,
35637
- announceTargets: settings.announceTargets.length
35977
+ announceTargets: settings.announceTargets.length,
35978
+ modeDelays: settings.modeDelays.length
35638
35979
  }
35639
35980
  });
35640
35981
  return this.alarmConfig();
@@ -35690,6 +36031,40 @@ var NotificationCenter = class NotificationCenter {
35690
36031
  name: this.deviceNameFromDirectory()
35691
36032
  });
35692
36033
  }
36034
+ /**
36035
+ * The opening states of the asked ids, for the alarm editors' sensor table.
36036
+ *
36037
+ * A separate read from `getAlarmConfig` on purpose: that one is polled every
36038
+ * 2–15 s and the household set needs a whole-fleet bindings pass, while the
36039
+ * client already holds the ids. Cost: one `loadRuntimeState` RPC per id, 8 in
36040
+ * flight (`readAlarmOpenings`); the client sends at most `NC_ALARM_SENSORS_MAX`
36041
+ * ids per call and polls every 15 s, every 5 s only while a countdown or the
36042
+ * siren runs. No log line per call (it is polled): one WARN/INFO per change of
36043
+ * wholesale read health (`NcAlarmSensorsReadLog`); a per-id read failure keeps
36044
+ * `readAlarmOpenings`' own debug line, and shows as `unknown` (D49).
36045
+ *
36046
+ * An id with no persisted device (removed) or whose provider never wrote an
36047
+ * opening slice reads `not-opening`, exactly as the arm check sees it — NOT
36048
+ * "verified not an opening". The client shows "Removed" from its fleet read.
36049
+ */
36050
+ async alarmSensors(input) {
36051
+ const read = this.deps.readDeviceOpenings;
36052
+ if (read === void 0) throw new Error("the opening reader is not wired on this node");
36053
+ const ids = alarmSensorReadIds(input.deviceIds);
36054
+ const openings = await read(ids);
36055
+ const line = this.alarmSensorsReadLog.next(ids.length, openings.size);
36056
+ if (line !== null) {
36057
+ const panelId = this.alarmPanel?.deviceId;
36058
+ this.logger[line.level](line.message, {
36059
+ ...panelId === void 0 ? {} : { tags: { deviceId: panelId } },
36060
+ meta: line.meta
36061
+ });
36062
+ }
36063
+ return {
36064
+ sensors: ids.map((id) => alarmSensorStateOf(id, openings.get(id), this.deviceDirectory.get(id)?.online ?? null)),
36065
+ readAt: Date.now()
36066
+ };
36067
+ }
35693
36068
  /** Settings + the DERIVED coverage + the live panel. Read together — see the cap doc. */
35694
36069
  async alarmConfig() {
35695
36070
  const panel = this.alarmPanel;
@@ -92239,8 +92614,8 @@ function buildAudioLabels(input) {
92239
92614
  * Mapped sensor/control caps that a CAMERA must NOT contribute for ITSELF.
92240
92615
  *
92241
92616
  * A sensor cap bound on a camera makes the camera both the source and the
92242
- * subject — a doorbell wrapper on camera 615, a native Reolink doorbell, a
92243
- * camera-side button. Those have no other route into the per-camera event
92617
+ * subject — a composed doorbell on camera 615 (D671), a native Reolink
92618
+ * doorbell, a camera-side button. Those have no other route into the per-camera event
92244
92619
  * substrate, so the camera includes itself.
92245
92620
  *
92246
92621
  * `motion` is the exception, and today the ONLY one: camera motion already
@@ -92509,8 +92884,8 @@ var LinkedCamerasCache = class {
92509
92884
  * THE camera-set derivation for a sensor-cap state change — one function, so
92510
92885
  * every consumer of a device-event attributes it to the same cameras.
92511
92886
  *
92512
- * It is shared on purpose. The self-camera half (a doorbell wrapper bound on
92513
- * camera 615 makes 615 both source and subject) was added to the history-row
92887
+ * It is shared on purpose. The self-camera half (a doorbell bound on camera
92888
+ * 615 makes 615 both source and subject) was added to the history-row
92514
92889
  * ingest on 2026-08-07 and NOT to the synthetic-track projection beside it,
92515
92890
  * which kept resolving `camerasFor` alone. The result: camera 615's real
92516
92891
  * doorbell press on 2026-08-08 06:59Z produced a `SensorEvent` row and no
@@ -92598,8 +92973,8 @@ function isSliceRecord(slice) {
92598
92973
  /**
92599
92974
  * One `DeviceStateChanged` → N history rows, one per camera the event belongs
92600
92975
  * to: the emitting device's LINKED cameras, plus the emitting device ITSELF
92601
- * when it is a camera carrying the sensor cap (a doorbell wrapper bound on
92602
- * camera 615 — see `SELF_CAMERA_EXCLUDED_CAPS` for the one cap that is not
92976
+ * when it is a camera carrying the sensor cap (a doorbell bound on camera
92977
+ * 615 — see `SELF_CAMERA_EXCLUDED_CAPS` for the one cap that is not
92603
92978
  * self-ingested). The EVENT_KIND_BY_CAP gate exits first so non-sensor cap
92604
92979
  * churn costs one map lookup. Returns the number of rows inserted (0 when
92605
92980
  * unmapped, or when no camera takes a row for the device — a non-camera
@@ -93691,9 +94066,10 @@ var MotionEventSnapshotCapturer = class {
93691
94066
  *
93692
94067
  * **What this fixes.** The projection used to resolve its cameras with
93693
94068
  * `camerasFor` alone — LINKED cameras only — while the history-row ingest
93694
- * beside it had gained the self-camera half on 2026-08-07. A doorbell wrapper
93695
- * is bound on the CAMERA (the virtual-doorbell builtin writes the `doorbell`
93696
- * slice onto camera 615 itself), so `camerasFor(615)` answered `[]` and the
94069
+ * beside it had gained the self-camera half on 2026-08-07. A doorbell can be
94070
+ * served on the CAMERA itself (a composed doorbell, D671, writes the `doorbell`
94071
+ * slice onto camera 615; Reolink registers it at camera level), so
94072
+ * `camerasFor(615)` answered `[]` and the
93697
94073
  * projection returned on a `debug` line nobody runs at. Camera 615's real
93698
94074
  * press at 2026-08-08 06:59Z therefore produced a `SensorEvent` row, a HomeKit
93699
94075
  * ring, an Alexa post and a matched notification rule — and no track. Both
@@ -93841,16 +94217,17 @@ async function projectSensorMarkers(deps, data, timestamp) {
93841
94217
  * ## Why the predicate is imported and not written here
93842
94218
  *
93843
94219
  * `evaluateSensorEdge` (`@camstack/types`) is already THE edge predicate: the
93844
- * virtual doorbell feeds it (`builtins/doorbell/trigger-engine.ts`) and so does
93845
- * the recorder's sensor trigger (`addon-pipeline` `recorder/sensor-trigger.ts`).
94220
+ * recorder's sensor trigger feeds it (`addon-pipeline` `recorder/sensor-trigger.ts`)
94221
+ * and so does the orchestrator's camera-sensors bridge
94222
+ * (`addon-pipeline-orchestrator` `camera-sensors/source.ts`).
93846
94223
  * Post-analysis was the third consumer of the same bus event and the only one
93847
94224
  * that did not. A second predicate here would diverge on the first cap added —
93848
94225
  * the exact failure [D62](../../../../../docs/decisions/adr-0062.md) names.
93849
94226
  *
93850
94227
  * ## Where it deliberately differs from the other two
93851
94228
  *
93852
- * The doorbell and the recorder want the RISING edge only. A timeline marker
93853
- * is not a doorbell: a door CLOSING is an event an operator wants to see, and
94229
+ * The recorder and the camera-sensors bridge want the RISING edge only. A
94230
+ * timeline marker is not a doorbell: a door CLOSING is an event an operator wants to see, and
93854
94231
  * the taxonomy has one `contact` kind for both directions. So this gate accepts
93855
94232
  * `edge: 'rising'` **and** the `falling-edge` verdict, and rejects only the
93856
94233
  * verdicts that mean "nothing moved" — which is precisely the 30.