@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.
- package/dist/{clip-model-registry-CrBn6Qvt.mjs → clip-model-registry-DPqDIZrG.mjs} +409 -200
- package/dist/{clip-model-registry-B3M4ZI17.js → clip-model-registry-X457TLXn.js} +456 -199
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +2 -2
- 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
- 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
- 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
- package/dist/pipeline-analytics/{hostInit-Dj0aaMhv.mjs → hostInit-Da_2XVg_.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +411 -34
- package/dist/pipeline-analytics/index.mjs +411 -34
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- 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-
|
|
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
|
-
*
|
|
1402
|
-
*
|
|
1403
|
-
*
|
|
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.
|
|
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
|
|
1939
|
-
*
|
|
1940
|
-
*
|
|
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 (
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
92513
|
-
*
|
|
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
|
|
92602
|
-
*
|
|
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
|
|
93695
|
-
*
|
|
93696
|
-
* slice onto camera 615
|
|
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
|
-
*
|
|
93845
|
-
* the
|
|
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
|
|
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.
|