@camstack/addon-post-analysis 1.2.319 → 1.2.320

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 (15) hide show
  1. package/dist/{clip-model-registry-DSqobJfx.mjs → clip-model-registry-C-IEAIQQ.mjs} +223 -46
  2. package/dist/{clip-model-registry-BjYpZuuI.js → clip-model-registry-C_aKxHpy.js} +252 -45
  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-CKHzDA_B.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-D553qWb_.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-7e_3hsiC.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-C1V_UEyk.mjs +26 -0
  9. package/dist/pipeline-analytics/{hostInit-DDDjJDA6.mjs → hostInit-ByDyL2zi.mjs} +2 -2
  10. package/dist/pipeline-analytics/index.js +287 -66
  11. package/dist/pipeline-analytics/index.mjs +287 -66
  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-Bz37odzX.mjs +0 -26
  15. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B_KS96RA.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-BjYpZuuI.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-C_aKxHpy.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");
@@ -604,7 +604,19 @@ var ARM_ANYWAY_TTL_MS = 5 * 6e4;
604
604
  var NcAlarmOpenDeviceSchema = require_clip_model_registry.object({
605
605
  deviceId: require_clip_model_registry.number().int(),
606
606
  name: require_clip_model_registry.string(),
607
- state: require_clip_model_registry.string()
607
+ state: require_clip_model_registry.string(),
608
+ /**
609
+ * Cover / lock / switch — default false so an older bridge answer still
610
+ * parses as a contact-style opening.
611
+ */
612
+ controllable: require_clip_model_registry.boolean().default(false),
613
+ /** Opening cap that produced the reading; used to actuate secure-on-arm. */
614
+ cap: require_clip_model_registry._enum([
615
+ "contact",
616
+ "cover",
617
+ "lock-control",
618
+ "switch"
619
+ ]).optional()
608
620
  }).readonly();
609
621
  /** Why an Arm anyway was not honoured — see `NcAlarmPanelDevice.armAnyway`. */
610
622
  var NcAlarmArmAnywayRejectionSchema = require_clip_model_registry._enum([
@@ -669,23 +681,22 @@ function alarmCoveredDevices(rules, panelDeviceId, mode) {
669
681
  /**
670
682
  * Refuse or arm — the whole decision, as one partition of the open devices.
671
683
  *
672
- * A device may be open at arm time for exactly two reasons: the mode TOLERATES
673
- * it (its non-blocking list), or the operator was shown it in a refusal and
674
- * pressed that refusal's Arm anyway (`acknowledged`). Anything else open
675
- * refuses — including a device that opened AFTER the refusal the operator is
676
- * confirming: they were never shown it, and excluding it would arm the alarm
677
- * around an opening nobody agreed to.
684
+ * A device may be open at arm time when:
685
+ * - the mode TOLERATES it (`nonBlocking` for contacts),
686
+ * - it is controllable and NOT on the mode's `blocking` list (default),
687
+ * - or the operator confirmed it with Arm anyway (`acknowledged`).
678
688
  *
679
- * There is no memory here. A plain arm pressed twice is the same question
680
- * twice and gets the same answer; the only way past a refusal is the explicit
681
- * Arm anyway, whose lifecycle (one-shot, stale, expired) the panel owns.
689
+ * Anything else open refuses — including a device that opened AFTER the refusal
690
+ * the operator is confirming.
682
691
  *
683
692
  * Excluded is EVERY open device — tolerated and acknowledged alike — and the
684
693
  * exclusion is two-phase (`alarm-exclusions.ts`): each one is watched again
685
694
  * once it has closed and opened.
686
695
  */
687
696
  function decideArm(input) {
697
+ const forceBlock = new Set(input.blocking ?? []);
688
698
  const tolerated = new Set([...input.nonBlocking, ...input.acknowledged ?? []]);
699
+ for (const d of input.open) if (d.controllable && !forceBlock.has(d.deviceId)) tolerated.add(d.deviceId);
689
700
  const blocking = input.open.filter((d) => !tolerated.has(d.deviceId));
690
701
  if (blocking.length > 0) return {
691
702
  kind: "refused",
@@ -1073,39 +1084,24 @@ var NcActionPlane = class {
1073
1084
  * and never open):
1074
1085
  *
1075
1086
  * - `not-opening` — no opening slice at all: a camera named by an intrusion
1076
- * rule, a switch. It takes no part in the arm decision.
1087
+ * rule. It takes no part in the arm decision.
1077
1088
  * - `unknown` — an opening cap's slice IS there, but its field is missing or
1078
1089
  * is not a word that cap defines, or the slice is a provider's PLACEHOLDER
1079
- * (`lastChangedAt === 0`: written by a constructor before anybody read the
1080
- * device — an HA lock's `unlocked`, an HA cover's `closed`, a binary
1081
- * sensor's `entryOpen: false`). It carries the cap and the raw value (for a
1082
- * placeholder, the whole slice) so the caller's log line can say what it
1083
- * saw. It never blocks and is never armed as secure on a guess — and a
1084
- * placeholder is never taken for a state, so neither the reconcile tick,
1085
- * the arm check nor an exclusion lift acts on one.
1086
- * - `reading` — open or secure, with the state word a person will read.
1087
- * ANY non-zero `lastChangedAt` is a reading, however old — including `1`,
1088
- * the stamp a provider writes on a value it REPLAYED after a respawn with
1089
- * no upstream change time (Homematic `HM_SEEDED_CHANGED_AT`): the value is
1090
- * known, only its change time is not. The transition gate
1091
- * (`sensor-transition-gate.ts`, "A placeholder slice is not a state") reads
1092
- * the same stamp as "not a fresh event"; `0` alone means "not read yet".
1090
+ * (`lastChangedAt === 0`). It never blocks and is never armed as secure on a
1091
+ * guess.
1092
+ * - `reading` — open or secure, with the state word a person will read and the
1093
+ * cap that produced it (so secure-on-arm knows what to actuate).
1093
1094
  *
1094
1095
  * A read that FAILED never reaches {@link alarmOpeningFrom}: the caller leaves
1095
1096
  * that id out of its answer (see {@link readAlarmOpenings}).
1096
1097
  *
1097
1098
  * Anything short of fully secure is open: a cover stopped half-way, a lock that
1098
- * jammed, a cover still closing when the arm is decided.
1099
+ * jammed, a cover still closing when the arm is decided, a switch that is on.
1099
1100
  *
1100
1101
  * PRECEDENCE, when one device carries more than one opening slice: `contact`
1101
- * first, then `cover`, then `lock-control`. The contact is the physical truth
1102
- * about the opening — a reed switch on the frame says the window is open even
1103
- * when the motor's last reported word says `closed`. A slice that cannot be
1104
- * read falls through to the next; only when none of them reads is the device
1105
- * `unknown`, naming the first unreadable cap.
1106
- *
1107
- * The vocabulary of each word is its cap's own schema (`CoverStateSchema`,
1108
- * `LockStateSchema`) — a word the cap does not define is not guessed at.
1102
+ * first, then `cover`, then `lock-control`, then `switch`. The contact is the
1103
+ * physical truth about the opening — a reed switch on the frame says the window
1104
+ * is open even when the motor's last reported word says `closed`.
1109
1105
  */
1110
1106
  /** The one state word, per cap, that counts as secure. Everything else is open. */
1111
1107
  var COVER_SECURE = "closed";
@@ -1124,11 +1120,12 @@ function fieldOf(slice, field) {
1124
1120
  function isPlaceholder(slice) {
1125
1121
  return fieldOf(slice, "lastChangedAt") === 0;
1126
1122
  }
1127
- function reading(word, secure) {
1123
+ function reading(word, secure, cap) {
1128
1124
  return {
1129
1125
  kind: "reading",
1130
1126
  reading: word === secure ? "secure" : "open",
1131
- state: word
1127
+ state: word,
1128
+ cap
1132
1129
  };
1133
1130
  }
1134
1131
  function readContact(runtime) {
@@ -1148,11 +1145,13 @@ function readContact(runtime) {
1148
1145
  return raw ? {
1149
1146
  kind: "reading",
1150
1147
  reading: "open",
1151
- state: "open"
1148
+ state: "open",
1149
+ cap: "contact"
1152
1150
  } : {
1153
1151
  kind: "reading",
1154
1152
  reading: "secure",
1155
- state: "closed"
1153
+ state: "closed",
1154
+ cap: "contact"
1156
1155
  };
1157
1156
  }
1158
1157
  function readCover(runtime) {
@@ -1165,7 +1164,7 @@ function readCover(runtime) {
1165
1164
  };
1166
1165
  const raw = fieldOf(slice, "state");
1167
1166
  const parsed = require_clip_model_registry.CoverStateSchema.safeParse(raw);
1168
- return parsed.success ? reading(parsed.data, COVER_SECURE) : {
1167
+ return parsed.success ? reading(parsed.data, COVER_SECURE, "cover") : {
1169
1168
  kind: "unknown",
1170
1169
  cap: "cover",
1171
1170
  raw
@@ -1181,19 +1180,46 @@ function readLock(runtime) {
1181
1180
  };
1182
1181
  const raw = fieldOf(slice, "state");
1183
1182
  const parsed = require_clip_model_registry.LockStateSchema.safeParse(raw);
1184
- return parsed.success ? reading(parsed.data, LOCK_SECURE) : {
1183
+ return parsed.success ? reading(parsed.data, LOCK_SECURE, "lock-control") : {
1185
1184
  kind: "unknown",
1186
1185
  cap: "lock-control",
1187
1186
  raw
1188
1187
  };
1189
1188
  }
1189
+ function readSwitch(runtime) {
1190
+ const slice = sliceOf(runtime, "switch");
1191
+ if (slice === void 0) return { kind: "absent" };
1192
+ if (isPlaceholder(slice)) return {
1193
+ kind: "unknown",
1194
+ cap: "switch",
1195
+ raw: slice
1196
+ };
1197
+ const raw = fieldOf(slice, "on");
1198
+ if (typeof raw !== "boolean") return {
1199
+ kind: "unknown",
1200
+ cap: "switch",
1201
+ raw
1202
+ };
1203
+ return raw ? {
1204
+ kind: "reading",
1205
+ reading: "open",
1206
+ state: "on",
1207
+ cap: "switch"
1208
+ } : {
1209
+ kind: "reading",
1210
+ reading: "secure",
1211
+ state: "off",
1212
+ cap: "switch"
1213
+ };
1214
+ }
1190
1215
  /** D630's table over one device's runtime slices — see the header for the three answers. */
1191
1216
  function alarmOpeningFrom(runtime) {
1192
1217
  let firstUnknown = null;
1193
1218
  for (const read of [
1194
1219
  readContact(runtime),
1195
1220
  readCover(runtime),
1196
- readLock(runtime)
1221
+ readLock(runtime),
1222
+ readSwitch(runtime)
1197
1223
  ]) {
1198
1224
  if (read.kind === "reading") return read;
1199
1225
  if (read.kind === "unknown" && firstUnknown === null) firstUnknown = read;
@@ -1208,13 +1234,14 @@ function alarmOpeningFrom(runtime) {
1208
1234
  var OPENING_CAP_BY_EVENT_KIND = {
1209
1235
  contact: "contact",
1210
1236
  cover: "cover",
1211
- lock: "lock-control"
1237
+ lock: "lock-control",
1238
+ switch: "switch"
1212
1239
  };
1213
1240
  /**
1214
1241
  * The same table, read off one sensor-event row: its `value` IS the slice of
1215
1242
  * the cap its `kind` names, so the event path and the reconcile read answer in
1216
1243
  * one vocabulary — a cover is secure only when `closed`, a lock only when
1217
- * `locked`, whichever path saw it.
1244
+ * `locked`, a switch only when off, whichever path saw it.
1218
1245
  */
1219
1246
  function alarmOpeningFromSensorEvent(kind, value) {
1220
1247
  const cap = OPENING_CAP_BY_EVENT_KIND[kind];
@@ -1235,7 +1262,9 @@ function classifyCoveredOpening(opening) {
1235
1262
  if (opening.kind === "not-opening" || opening.reading !== "open") return { kind: "skip" };
1236
1263
  return {
1237
1264
  kind: "open",
1238
- state: opening.state
1265
+ state: opening.state,
1266
+ controllable: require_clip_model_registry.isAlarmControllableCap(opening.cap),
1267
+ cap: opening.cap
1239
1268
  };
1240
1269
  }
1241
1270
  /**
@@ -1892,6 +1921,22 @@ var ncAlarmPanelSchema = require_clip_model_registry.object({
1892
1921
  deviceIds: require_clip_model_registry.array(require_clip_model_registry.number().int())
1893
1922
  })).default([]),
1894
1923
  /**
1924
+ * Controllable openings that DO block when open (opt-in). Loose + guarded like
1925
+ * {@link nonBlocking}.
1926
+ */
1927
+ blocking: require_clip_model_registry.array(require_clip_model_registry.object({
1928
+ mode: require_clip_model_registry.string(),
1929
+ deviceIds: require_clip_model_registry.array(require_clip_model_registry.number().int())
1930
+ })).default([]),
1931
+ /**
1932
+ * Controllable openings that must NOT secure on arm (opt-out). Loose + guarded
1933
+ * like {@link nonBlocking}.
1934
+ */
1935
+ secureOnArmOff: require_clip_model_registry.array(require_clip_model_registry.object({
1936
+ mode: require_clip_model_registry.string(),
1937
+ deviceIds: require_clip_model_registry.array(require_clip_model_registry.number().int())
1938
+ })).default([]),
1939
+ /**
1895
1940
  * Per arm mode, the delays that override the global ones (D697). Loose on purpose — `mode` is
1896
1941
  * a string and the numbers are unbounded here — and read back through
1897
1942
  * {@link modeDelaysFromPersisted}, never a cast: an entry this release cannot trust is dropped
@@ -1910,7 +1955,7 @@ var ncAlarmPanelSchema = require_clip_model_registry.object({
1910
1955
  * per mode kept — the same "at most one list per mode" the settings schema
1911
1956
  * enforces at the boundary. A type guard (`safeParse`), never a cast.
1912
1957
  */
1913
- function nonBlockingFromPersisted(raw) {
1958
+ function modeDeviceListFromPersisted(raw) {
1914
1959
  const seen = /* @__PURE__ */ new Set();
1915
1960
  const lists = [];
1916
1961
  for (const entry of raw) {
@@ -2015,7 +2060,9 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2015
2060
  ...this.readDelays(),
2016
2061
  announceArm: this.config.get("announceArm"),
2017
2062
  announceTargets: [...this.config.get("announceTargets")],
2018
- nonBlocking: nonBlockingFromPersisted(this.config.get("nonBlocking")),
2063
+ nonBlocking: modeDeviceListFromPersisted(this.config.get("nonBlocking")),
2064
+ blocking: modeDeviceListFromPersisted(this.config.get("blocking")),
2065
+ secureOnArmOff: modeDeviceListFromPersisted(this.config.get("secureOnArmOff")),
2019
2066
  modeDelays: modeDelaysFromPersisted(this.config.get("modeDelays")).entries
2020
2067
  };
2021
2068
  }
@@ -2042,30 +2089,73 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2042
2089
  async applySettings(patch) {
2043
2090
  const edits = patch.modeDelays ?? [];
2044
2091
  const membership = patch.nonBlockingEdit;
2092
+ const blockingEdit = patch.blockingEdit;
2093
+ const secureEdit = patch.secureOnArmEdit;
2045
2094
  for (const edit of edits) this.refuseUnofferedMode(edit.mode);
2046
2095
  if (membership !== void 0) {
2047
2096
  this.refuseUnofferedMode(membership.mode, membership.deviceId);
2048
- const verdict = require_clip_model_registry.applyNonBlockingEdit(patch.nonBlocking !== void 0 ? patch.nonBlocking : nonBlockingFromPersisted(this.config.get("nonBlocking")), membership);
2097
+ const verdict = require_clip_model_registry.applyNonBlockingEdit(patch.nonBlocking !== void 0 ? patch.nonBlocking : modeDeviceListFromPersisted(this.config.get("nonBlocking")), membership);
2049
2098
  if (verdict.kind === "refused") {
2050
2099
  this.ctx.logger.info("alarm settings refused", {
2051
2100
  tags: { deviceId: membership.deviceId },
2052
2101
  meta: {
2053
2102
  panelId: this.id,
2054
2103
  reason: "list-full",
2055
- mode: verdict.mode
2104
+ mode: verdict.mode,
2105
+ list: "nonBlocking"
2056
2106
  }
2057
2107
  });
2058
2108
  throw new Error(`the ${verdict.mode} non-blocking list already holds ${verdict.max} devices — remove one first`);
2059
2109
  }
2060
2110
  }
2111
+ if (blockingEdit !== void 0) {
2112
+ this.refuseUnofferedMode(blockingEdit.mode, blockingEdit.deviceId);
2113
+ const verdict = require_clip_model_registry.applyBlockingEdit(patch.blocking !== void 0 ? patch.blocking : modeDeviceListFromPersisted(this.config.get("blocking")), blockingEdit);
2114
+ if (verdict.kind === "refused") {
2115
+ this.ctx.logger.info("alarm settings refused", {
2116
+ tags: { deviceId: blockingEdit.deviceId },
2117
+ meta: {
2118
+ panelId: this.id,
2119
+ reason: "list-full",
2120
+ mode: verdict.mode,
2121
+ list: "blocking"
2122
+ }
2123
+ });
2124
+ throw new Error(`the ${verdict.mode} blocking list already holds ${verdict.max} devices — remove one first`);
2125
+ }
2126
+ }
2127
+ if (secureEdit !== void 0) {
2128
+ this.refuseUnofferedMode(secureEdit.mode, secureEdit.deviceId);
2129
+ const verdict = require_clip_model_registry.applySecureOnArmEdit(patch.secureOnArmOff !== void 0 ? patch.secureOnArmOff : modeDeviceListFromPersisted(this.config.get("secureOnArmOff")), secureEdit);
2130
+ if (verdict.kind === "refused") {
2131
+ this.ctx.logger.info("alarm settings refused", {
2132
+ tags: { deviceId: secureEdit.deviceId },
2133
+ meta: {
2134
+ panelId: this.id,
2135
+ reason: "list-full",
2136
+ mode: verdict.mode,
2137
+ list: "secureOnArmOff"
2138
+ }
2139
+ });
2140
+ throw new Error(`the ${verdict.mode} secure-on-arm-off list already holds ${verdict.max} devices — remove one first`);
2141
+ }
2142
+ }
2061
2143
  const listWrites = [];
2062
2144
  if (patch.nonBlocking !== void 0) listWrites.push(this.config.set("nonBlocking", patch.nonBlocking.map((l) => ({
2063
2145
  mode: l.mode,
2064
2146
  deviceIds: [...l.deviceIds]
2065
2147
  }))));
2148
+ if (patch.blocking !== void 0) listWrites.push(this.config.set("blocking", patch.blocking.map((l) => ({
2149
+ mode: l.mode,
2150
+ deviceIds: [...l.deviceIds]
2151
+ }))));
2152
+ if (patch.secureOnArmOff !== void 0) listWrites.push(this.config.set("secureOnArmOff", patch.secureOnArmOff.map((l) => ({
2153
+ mode: l.mode,
2154
+ deviceIds: [...l.deviceIds]
2155
+ }))));
2066
2156
  let membershipChanged = null;
2067
2157
  if (membership !== void 0) {
2068
- const result = require_clip_model_registry.applyNonBlockingEdit(nonBlockingFromPersisted(this.config.get("nonBlocking")), membership);
2158
+ const result = require_clip_model_registry.applyNonBlockingEdit(modeDeviceListFromPersisted(this.config.get("nonBlocking")), membership);
2069
2159
  if (result.kind === "applied") {
2070
2160
  membershipChanged = result.changed;
2071
2161
  if (result.changed) listWrites.push(this.config.set("nonBlocking", result.lists.map((l) => ({
@@ -2074,6 +2164,26 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2074
2164
  }))));
2075
2165
  }
2076
2166
  }
2167
+ if (blockingEdit !== void 0) {
2168
+ const result = require_clip_model_registry.applyBlockingEdit(modeDeviceListFromPersisted(this.config.get("blocking")), blockingEdit);
2169
+ if (result.kind === "applied" && result.changed) {
2170
+ membershipChanged = true;
2171
+ listWrites.push(this.config.set("blocking", result.lists.map((l) => ({
2172
+ mode: l.mode,
2173
+ deviceIds: [...l.deviceIds]
2174
+ }))));
2175
+ }
2176
+ }
2177
+ if (secureEdit !== void 0) {
2178
+ const result = require_clip_model_registry.applySecureOnArmEdit(modeDeviceListFromPersisted(this.config.get("secureOnArmOff")), secureEdit);
2179
+ if (result.kind === "applied" && result.changed) {
2180
+ membershipChanged = true;
2181
+ listWrites.push(this.config.set("secureOnArmOff", result.lists.map((l) => ({
2182
+ mode: l.mode,
2183
+ deviceIds: [...l.deviceIds]
2184
+ }))));
2185
+ }
2186
+ }
2077
2187
  const persistedDelays = modeDelaysFromPersisted(this.config.get("modeDelays"));
2078
2188
  const beforeDelays = persistedDelays.entries;
2079
2189
  const afterDelays = edits.length > 0 ? require_clip_model_registry.applyModeDelaysEdits(beforeDelays, edits) : beforeDelays;
@@ -2212,7 +2322,7 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2212
2322
  if (read.verdict.kind === "refused") return this.refuse(mode, read.verdict.blocking, now);
2213
2323
  return this.settleArm(mode, read.verdict.excluded.map((device) => ({
2214
2324
  device,
2215
- why: "non-blocking"
2325
+ why: this.exclusionWhyFor(mode, device)
2216
2326
  })), now);
2217
2327
  }
2218
2328
  /**
@@ -2261,12 +2371,14 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2261
2371
  return outcome;
2262
2372
  }
2263
2373
  const tolerated = this.nonBlockingFor(held.mode);
2374
+ const forceBlock = this.blockingFor(held.mode);
2264
2375
  const unknown = new Set(read.openings.unknown);
2265
2376
  if (read.verdict.kind === "refused") {
2266
2377
  const plain = decideArm({
2267
2378
  mode: held.mode,
2268
2379
  open: read.openings.open,
2269
- nonBlocking: tolerated
2380
+ nonBlocking: tolerated,
2381
+ blocking: forceBlock
2270
2382
  });
2271
2383
  const blockingNow = plain.kind === "refused" ? plain.blocking : read.verdict.blocking;
2272
2384
  const shownIds = new Set(blockingNow.map((d) => d.deviceId));
@@ -2396,10 +2508,17 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2396
2508
  error: String(err)
2397
2509
  };
2398
2510
  }
2511
+ const toSecure = openings.open.filter((d) => require_clip_model_registry.alarmDeviceSecureOnArm({
2512
+ controllable: d.controllable,
2513
+ deviceId: d.deviceId,
2514
+ secureOnArmOff: this.secureOnArmOffFor(mode)
2515
+ }));
2516
+ if (toSecure.length > 0) this.armGuard?.secureOnArm(toSecure);
2399
2517
  const verdict = decideArm({
2400
2518
  mode,
2401
2519
  open: openings.open,
2402
2520
  nonBlocking: this.nonBlockingFor(mode),
2521
+ blocking: this.blockingFor(mode),
2403
2522
  ...acknowledged !== void 0 ? { acknowledged } : {}
2404
2523
  });
2405
2524
  return {
@@ -2424,7 +2543,28 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2424
2543
  }
2425
2544
  /** This mode's non-blocking ids, from the persisted lists. */
2426
2545
  nonBlockingFor(mode) {
2427
- return nonBlockingFromPersisted(this.config.get("nonBlocking")).find((l) => l.mode === mode)?.deviceIds ?? [];
2546
+ return this.modeListIds("nonBlocking", mode);
2547
+ }
2548
+ /** Controllable openings that force-block when open. */
2549
+ blockingFor(mode) {
2550
+ return this.modeListIds("blocking", mode);
2551
+ }
2552
+ /** Controllable openings that opt out of secure-on-arm. */
2553
+ secureOnArmOffFor(mode) {
2554
+ return this.modeListIds("secureOnArmOff", mode);
2555
+ }
2556
+ modeListIds(key, mode) {
2557
+ return modeDeviceListFromPersisted(this.config.get(key)).find((l) => l.mode === mode)?.deviceIds ?? [];
2558
+ }
2559
+ /** Why an open device was armed around on a plain (non–Arm-anyway) arm. */
2560
+ exclusionWhyFor(mode, device) {
2561
+ if (this.nonBlockingFor(mode).includes(device.deviceId)) return "non-blocking";
2562
+ if (require_clip_model_registry.alarmDeviceSecureOnArm({
2563
+ controllable: device.controllable,
2564
+ deviceId: device.deviceId,
2565
+ secureOnArmOff: this.secureOnArmOffFor(mode)
2566
+ })) return "secure-on-arm";
2567
+ return "non-blocking";
2428
2568
  }
2429
2569
  /**
2430
2570
  * Arm, with these exclusions.
@@ -8618,6 +8758,7 @@ function sameWord(a, b) {
8618
8758
  * therefore moves the table with the arm.
8619
8759
  */
8620
8760
  function alarmSensorStateOf(deviceId, opening, online) {
8761
+ const controllable = opening?.kind === "reading" ? require_clip_model_registry.isAlarmControllableCap(opening.cap) : false;
8621
8762
  const verdict = classifyCoveredOpening(opening);
8622
8763
  switch (verdict.kind) {
8623
8764
  case "unknown": return {
@@ -8625,27 +8766,31 @@ function alarmSensorStateOf(deviceId, opening, online) {
8625
8766
  reading: "unknown",
8626
8767
  state: null,
8627
8768
  unknownWhy: verdict.why,
8628
- online
8769
+ online,
8770
+ controllable
8629
8771
  };
8630
8772
  case "open": return {
8631
8773
  deviceId,
8632
8774
  reading: "open",
8633
8775
  state: verdict.state,
8634
8776
  unknownWhy: null,
8635
- online
8777
+ online,
8778
+ controllable: verdict.controllable
8636
8779
  };
8637
8780
  case "skip": return opening?.kind === "reading" ? {
8638
8781
  deviceId,
8639
8782
  reading: "closed",
8640
8783
  state: opening.state,
8641
8784
  unknownWhy: null,
8642
- online
8785
+ online,
8786
+ controllable
8643
8787
  } : {
8644
8788
  deviceId,
8645
8789
  reading: "not-opening",
8646
8790
  state: null,
8647
8791
  unknownWhy: null,
8648
- online
8792
+ online,
8793
+ controllable: false
8649
8794
  };
8650
8795
  }
8651
8796
  }
@@ -20009,19 +20154,39 @@ function planSummaryDevices(rule, isDead, guard) {
20009
20154
  notes: notes.notes
20010
20155
  };
20011
20156
  }
20157
+ function stripDeadFromLists(lists, isDead) {
20158
+ const dead = lists.flatMap((list) => list.deviceIds.filter(isDead));
20159
+ if (dead.length === 0) return {
20160
+ next: lists,
20161
+ dead
20162
+ };
20163
+ return {
20164
+ dead,
20165
+ next: lists.map((list) => ({
20166
+ ...list,
20167
+ deviceIds: list.deviceIds.filter((id) => !isDead(id))
20168
+ }))
20169
+ };
20170
+ }
20012
20171
  function planAlarmDevices(alarm, isDead) {
20013
20172
  if (alarm === null) return null;
20014
- const nonBlockingDead = alarm.nonBlocking.flatMap((list) => list.deviceIds.filter(isDead));
20015
- const touched = nonBlockingDead.length > 0;
20173
+ const nonBlocking = stripDeadFromLists(alarm.nonBlocking, isDead);
20174
+ const blocking = stripDeadFromLists(alarm.blocking, isDead);
20175
+ const secureOnArmOff = stripDeadFromLists(alarm.secureOnArmOff, isDead);
20016
20176
  const forgetExcluded = alarm.excluded.filter(isDead);
20017
- if (!touched && forgetExcluded.length === 0) return null;
20177
+ const deadIds = [
20178
+ ...nonBlocking.dead,
20179
+ ...blocking.dead,
20180
+ ...secureOnArmOff.dead,
20181
+ ...forgetExcluded
20182
+ ];
20183
+ if (deadIds.length === 0) return null;
20018
20184
  return {
20019
- dropped: deviceSubjects([...new Set([...nonBlockingDead, ...forgetExcluded])]),
20185
+ dropped: deviceSubjects([...new Set(deadIds)]),
20020
20186
  droppedKind: "device",
20021
- ...touched ? { nonBlocking: alarm.nonBlocking.map((list) => ({
20022
- ...list,
20023
- deviceIds: list.deviceIds.filter((id) => !isDead(id))
20024
- })) } : {},
20187
+ ...nonBlocking.dead.length > 0 ? { nonBlocking: nonBlocking.next } : {},
20188
+ ...blocking.dead.length > 0 ? { blocking: blocking.next } : {},
20189
+ ...secureOnArmOff.dead.length > 0 ? { secureOnArmOff: secureOnArmOff.next } : {},
20025
20190
  forgetExcluded
20026
20191
  };
20027
20192
  }
@@ -20063,6 +20228,8 @@ function referencedDeviceIds(snapshot) {
20063
20228
  for (const rule of snapshot.timelapse) add(rule.deviceIds);
20064
20229
  for (const rule of snapshot.summary) add(rule.deviceIds);
20065
20230
  for (const list of snapshot.alarm?.nonBlocking ?? []) add(list.deviceIds);
20231
+ for (const list of snapshot.alarm?.blocking ?? []) add(list.deviceIds);
20232
+ for (const list of snapshot.alarm?.secureOnArmOff ?? []) add(list.deviceIds);
20066
20233
  add(snapshot.alarm?.excluded);
20067
20234
  add(snapshot.mutedDeviceIds);
20068
20235
  return ids;
@@ -20407,6 +20574,8 @@ var NcDeletedSubjectPruner = class {
20407
20574
  summary: this.writable("summary", this.deps.summary.list()),
20408
20575
  alarm: panel !== null && settings !== void 0 ? {
20409
20576
  nonBlocking: settings.nonBlocking,
20577
+ blocking: settings.blocking,
20578
+ secureOnArmOff: settings.secureOnArmOff,
20410
20579
  announceTargets: settings.announceTargets,
20411
20580
  excluded: panel.excludedDevices()
20412
20581
  } : null,
@@ -20563,6 +20732,8 @@ var NcDeletedSubjectPruner = class {
20563
20732
  ...NO_SNAPSHOT,
20564
20733
  alarm: {
20565
20734
  nonBlocking: settings.nonBlocking,
20735
+ blocking: settings.blocking,
20736
+ secureOnArmOff: settings.secureOnArmOff,
20566
20737
  announceTargets: settings.announceTargets,
20567
20738
  excluded: panel.excludedDevices()
20568
20739
  }
@@ -20571,6 +20742,8 @@ var NcDeletedSubjectPruner = class {
20571
20742
  try {
20572
20743
  const patch = {
20573
20744
  ...edit.nonBlocking !== void 0 ? { nonBlocking: edit.nonBlocking } : {},
20745
+ ...edit.blocking !== void 0 ? { blocking: edit.blocking } : {},
20746
+ ...edit.secureOnArmOff !== void 0 ? { secureOnArmOff: edit.secureOnArmOff } : {},
20574
20747
  ...edit.announceTargets !== void 0 ? { announceTargets: [...edit.announceTargets] } : {}
20575
20748
  };
20576
20749
  if (Object.keys(patch).length > 0) await panel.applySettings(patch);
@@ -35573,6 +35746,8 @@ var NC_ALARM_SETTINGS_FALLBACK = {
35573
35746
  ...DEFAULT_DELAYS,
35574
35747
  announceTargets: [],
35575
35748
  nonBlocking: [],
35749
+ blocking: [],
35750
+ secureOnArmOff: [],
35576
35751
  modeDelays: []
35577
35752
  };
35578
35753
  /**
@@ -38577,6 +38752,9 @@ var NotificationCenter = class {
38577
38752
  alarmArmGuard() {
38578
38753
  return {
38579
38754
  openings: (mode) => this.alarmOpenings(mode),
38755
+ secureOnArm: (devices) => {
38756
+ this.secureDevicesOnArm(devices);
38757
+ },
38580
38758
  announceRefusal: (input) => this.announceArmRefused(input),
38581
38759
  announceCheckFailed: (input) => this.announceArmCheckFailed(input)
38582
38760
  };
@@ -38643,7 +38821,9 @@ var NotificationCenter = class {
38643
38821
  if (verdict.kind === "open") open.push({
38644
38822
  deviceId,
38645
38823
  name: name(deviceId),
38646
- state: verdict.state
38824
+ state: verdict.state,
38825
+ controllable: verdict.controllable,
38826
+ cap: verdict.cap
38647
38827
  });
38648
38828
  }
38649
38829
  this.logger.info("alarm arm check", {
@@ -38662,6 +38842,47 @@ var NotificationCenter = class {
38662
38842
  };
38663
38843
  }
38664
38844
  /**
38845
+ * Fire-and-forget secure actuation for controllable openings on arm.
38846
+ * Failures are WARN per device — they never refuse the arm.
38847
+ */
38848
+ async secureDevicesOnArm(devices) {
38849
+ for (const device of devices) {
38850
+ const actuation = device.cap !== void 0 ? require_clip_model_registry.secureActuationFor(device.cap) : null;
38851
+ if (actuation === null) continue;
38852
+ try {
38853
+ await this.actuateWithAttribution({
38854
+ deviceId: device.deviceId,
38855
+ cap: actuation.cap,
38856
+ method: actuation.method,
38857
+ args: {
38858
+ deviceId: device.deviceId,
38859
+ ...actuation.args
38860
+ }
38861
+ });
38862
+ this.logger.info("alarm secure-on-arm actuated", {
38863
+ tags: { deviceId: device.deviceId },
38864
+ meta: {
38865
+ name: device.name,
38866
+ state: device.state,
38867
+ cap: actuation.cap,
38868
+ method: actuation.method
38869
+ }
38870
+ });
38871
+ } catch (err) {
38872
+ this.logger.warn("alarm secure-on-arm actuation failed — arm continues", {
38873
+ tags: { deviceId: device.deviceId },
38874
+ meta: {
38875
+ name: device.name,
38876
+ state: device.state,
38877
+ cap: actuation.cap,
38878
+ method: actuation.method,
38879
+ error: String(err)
38880
+ }
38881
+ });
38882
+ }
38883
+ }
38884
+ }
38885
+ /**
38665
38886
  * The refusal, as an ordinary notification.
38666
38887
  *
38667
38888
  * Same path every alarm announcement takes (D126: the alarm speaks through