@camstack/addon-post-analysis 1.2.280 → 1.2.282
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{dist-Z6AO1MsY.mjs → dist-C3zTKShG.mjs} +552 -25
- package/dist/{dist-VeiAavYR.js → dist-D0zzdvSd.js} +599 -24
- 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-C_C1P6Bk.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-B9WQ8rry.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DT-cQNv-.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-B62fofgN.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DWPDYuJS.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-B5__n3rE.mjs → hostInit-lUAYfFf-.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +592 -26
- package/dist/pipeline-analytics/index.mjs +592 -26
- 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-DBnj0lb3.mjs +0 -26
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $t as systemEventFamilyProblems, A as NC_SENSOR_TRANSITION_KINDS, At as isEventOwnerType, B as NcRuleSchema, C as MAX_BIRTH_DECISION_RECORDS, Cn as partialRecord, Ct as encodeVectorBase64, D as NC_ALARM_SYSTEM_EVENT_KINDS, Dn as EventCategory, Dt as failureContributionCapability, E as MediaFileKindEnum, En as unknown, Et as faceGalleryCapability, F as NcAlarmSettingsPatchSchema, Ft as isSystemDelivery, G as NcSnoozeSchema, Gt as readTimelapseGeneratedAt, H as NcRuleUpdateInputSchema, Ht as pipelineAnalyticsCapability, I as NcConditionDescriptorSchema, It as kebabToCamel, J as NcTaxonomySchema, Jt as ruleBelongsToAlarm$1, K as NcSnoozeSuppressedSchema, Kt as resolveLocationMode, L as NcConditionsSchema, Lt as mayWriteToLocation, M as NC_TAXONOMY, Mt as isMediaOwnerType, N as NC_TEMPLATE_VARS, Nt as isScheduleActive, O as NC_CONDITION_CATALOG, P as NcAlarmConfigSchema, Pt as isSourceCap, Q as OpsLogEntrySchema, Qt as subKindsOf, R as NcRuleInputSchema, Rt as notificationRulesCapability, S as MAX_ARCHIVED_DEBUG_NOTES, Sn as object, T as MOTION_CLOSE_AFTER_MS, Tn as string, Tt as evictionPolicyOfLocation, U as NcScheduleSchema, Ut as plateGalleryCapability, V as NcRuleTargetSchema, Vt as pickClusterStepModels, W as NcSnoozeInputSchema, Wt as readDeviceStateFrom, X as NcTemplatePreviewSchema, Xt as sensorRaisedOf, Y as NcTemplatePreviewInputSchema, Yt as sceneMonitorCapability, Z as NcTemplateVarDescriptorSchema, Zt as storageOccupancyCapability, _ as FIRST_LEVEL_MACRO_CLASSES, _n as array, _t as audioModeOf, a as BirthDecisionRecordSchema, an as canonicalHash, at as SCENE_DEFAULT_UNCOVERED_POLICY, b as LabelAttributionSchema, bn as literal, bt as deriveRecordingMode, c as CoverStateSchema, cn as DeviceType, ct as TIMELAPSE_DENSE_FLOOR_SEC, dn as defineCustomActions, dt as TimelapseRuleSchema, en as systemEventFilterApplies, et as RECORDING_EXPORT_MAX_READ_BYTES, f as DETECTION_MACRO_CLASSES, fn as hydrateSchema, ft as TrackSourceSchema, g as EVENT_OWNER_TYPES, gn as _enum, gt as audioMetricsCapability, h as EVENT_KIND_BY_CAP, hn as sleep$1, ht as assertTimelapseCadences, i as BaseDevice, in as errMsg$1, it as SCENE_DEFAULT_ANCHOR_THRESHOLD, jt as isKnownTemplateVar, k as NC_DEFAULT_SNOOZE_MINUTES, kt as isDetectionMacroClass, l as DEFAULT_EVENT_COLOR, ln as createEvent, lt as TimelapseRuleInputSchema, m as DeclaredDevices, mn as nodePin, mt as alarmPanelCapability, n as AlarmArmModeSchema, nn as vectorDimFromBase64, nt as RetrainStatusSchema, o as CLUSTER_MODEL_SCOPED_STEPS, on as BaseAddon, ot as SCENE_DIVERGED, pn as isDeviceScopedCap, pt as addonWidgetsSourceCapability, q as NcSystemEventKindSchema, r as ArchivedDebugNoteSchema, rn as zoneAnalyticsCapability, s as COCO_TO_MACRO, sn as CamProfileSchema, st as SceneMonitorSchema, t as AUDIO_MACRO_LABELS, tn as templateVarsFor, tt as RECORDING_SOURCE_CAMSTACK_ADDON, un as customAction, ut as TimelapseRulePatchSchema, v as FULL_IMAGE_BBOX, vn as boolean, vt as buildEventKindDescriptor, wn as record, wt as evaluateSensorEdge, x as LockStateSchema, xn as number, xt as describeSystemEventFamilyProblem, y as FailureCounters, yn as discriminatedUnion, yt as cosineSimilarity$1, z as NcRulePatchSchema, zt as occupancyScope } from "../dist-C3zTKShG.mjs";
|
|
2
2
|
import { t as __exportAll } from "../embedding-encoder/index.mjs";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { promises } from "node:fs";
|
|
@@ -281,6 +281,7 @@ function coverageFor(rules, panelDeviceId, mode, skipProbe) {
|
|
|
281
281
|
const ids = /* @__PURE__ */ new Set();
|
|
282
282
|
let ruleCount = 0;
|
|
283
283
|
let allDevices = false;
|
|
284
|
+
let except = null;
|
|
284
285
|
for (const rule of rules) {
|
|
285
286
|
if (!rule.enabled) continue;
|
|
286
287
|
const gate = rule.conditions.deviceState;
|
|
@@ -288,21 +289,35 @@ function coverageFor(rules, panelDeviceId, mode, skipProbe) {
|
|
|
288
289
|
if (!gate.states.includes(wanted)) continue;
|
|
289
290
|
ruleCount += 1;
|
|
290
291
|
const scope = rule.conditions.devices;
|
|
292
|
+
const excluded = rule.conditions.devicesExclude ?? [];
|
|
291
293
|
if (scope === void 0 || scope.length === 0) {
|
|
292
294
|
allDevices = true;
|
|
295
|
+
except = narrowExcept(except, excluded);
|
|
293
296
|
continue;
|
|
294
297
|
}
|
|
295
|
-
for (const id of scope) ids.add(id);
|
|
298
|
+
for (const id of scope) if (!excluded.includes(id)) ids.add(id);
|
|
296
299
|
}
|
|
297
300
|
const deviceIds = [...ids].toSorted((a, b) => a - b);
|
|
301
|
+
const allDevicesExcept = except === null || except === "none" ? [] : [...except].filter((id) => !ids.has(id)).toSorted((a, b) => a - b);
|
|
298
302
|
return {
|
|
299
303
|
mode,
|
|
300
304
|
ruleCount,
|
|
301
305
|
allDevices,
|
|
306
|
+
allDevicesExcept,
|
|
302
307
|
deviceIds,
|
|
303
308
|
skippedDevices: skippedFor(deviceIds, skipProbe)
|
|
304
309
|
};
|
|
305
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* Fold one UNSCOPED covering rule into the except-set: with no exclusion it
|
|
313
|
+
* covers everything (`'none'`, final); otherwise only what every unscoped rule
|
|
314
|
+
* excludes stays uncovered — the intersection.
|
|
315
|
+
*/
|
|
316
|
+
function narrowExcept(current, excluded) {
|
|
317
|
+
if (current === "none" || excluded.length === 0) return "none";
|
|
318
|
+
if (current === null) return new Set(excluded);
|
|
319
|
+
return new Set(excluded.filter((id) => current.has(id)));
|
|
320
|
+
}
|
|
306
321
|
function skippedFor(deviceIds, skipProbe) {
|
|
307
322
|
if (skipProbe === void 0) return [];
|
|
308
323
|
const out = [];
|
|
@@ -376,6 +391,19 @@ function buildArmAnnouncement(coverage, deviceName, bypassed = []) {
|
|
|
376
391
|
textCount: names.length
|
|
377
392
|
};
|
|
378
393
|
}
|
|
394
|
+
if (coverage.allDevices && coverage.allDevicesExcept.length > 0) {
|
|
395
|
+
const names = coverage.allDevicesExcept.map(deviceName);
|
|
396
|
+
return {
|
|
397
|
+
bodyKey: "alarm.arm.body.allDevicesExcept",
|
|
398
|
+
textParams: {
|
|
399
|
+
rules,
|
|
400
|
+
mode,
|
|
401
|
+
count: `${names.length}`,
|
|
402
|
+
names: names.join(", ")
|
|
403
|
+
},
|
|
404
|
+
textCount: names.length
|
|
405
|
+
};
|
|
406
|
+
}
|
|
379
407
|
if (coverage.allDevices) return {
|
|
380
408
|
bodyKey: "alarm.arm.body.allDevices",
|
|
381
409
|
textParams: {
|
|
@@ -594,7 +622,8 @@ function alarmCoveredDevices(rules, panelDeviceId, mode) {
|
|
|
594
622
|
if (!gate.states.includes(wanted)) continue;
|
|
595
623
|
const scope = rule.conditions.devices;
|
|
596
624
|
if (scope === void 0 || scope.length === 0) continue;
|
|
597
|
-
|
|
625
|
+
const excluded = rule.conditions.devicesExclude ?? [];
|
|
626
|
+
for (const id of scope) if (!excluded.includes(id)) ids.add(id);
|
|
598
627
|
}
|
|
599
628
|
return [...ids].toSorted((a, b) => a - b);
|
|
600
629
|
}
|
|
@@ -7788,10 +7817,10 @@ function replaceConditionReferences(monitor, condition, reference) {
|
|
|
7788
7817
|
//#endregion
|
|
7789
7818
|
//#region src/notification-center/snooze-exempt.ts
|
|
7790
7819
|
/** The cap whose presence in a sequence makes the rule the alarm's trigger. */
|
|
7791
|
-
var ALARM_PANEL_CAP$
|
|
7820
|
+
var ALARM_PANEL_CAP$1 = "alarm-panel";
|
|
7792
7821
|
var ALARM_KINDS = new Set(NC_ALARM_SYSTEM_EVENT_KINDS);
|
|
7793
7822
|
function actuatesAlarmPanel(sequences) {
|
|
7794
|
-
for (const sequence of sequences) for (const step of sequence.actions) if (step.kind === "cap" && step.cap === ALARM_PANEL_CAP$
|
|
7823
|
+
for (const sequence of sequences) for (const step of sequence.actions) if (step.kind === "cap" && step.cap === ALARM_PANEL_CAP$1) return true;
|
|
7795
7824
|
return false;
|
|
7796
7825
|
}
|
|
7797
7826
|
/**
|
|
@@ -9556,6 +9585,7 @@ function isLinkedSetSubject(subject) {
|
|
|
9556
9585
|
function subjectFromSensorEvent(ev, markerTrackId, linked) {
|
|
9557
9586
|
const eventType = readSensorEventType(ev.value);
|
|
9558
9587
|
const sensorEdge = sensorEdgeOf(ev.kind, ev.value);
|
|
9588
|
+
const sensorRaised = sensorRaisedOf(ev.kind, ev.value);
|
|
9559
9589
|
return {
|
|
9560
9590
|
kind: "device-event",
|
|
9561
9591
|
recordId: String(ev.id),
|
|
@@ -9569,6 +9599,7 @@ function subjectFromSensorEvent(ev, markerTrackId, linked) {
|
|
|
9569
9599
|
sensorKind: ev.kind,
|
|
9570
9600
|
...eventType !== void 0 ? { eventType } : {},
|
|
9571
9601
|
...sensorEdge !== void 0 ? { sensorEdge } : {},
|
|
9602
|
+
...sensorRaised !== void 0 ? { sensorRaised } : {},
|
|
9572
9603
|
...linked !== void 0 ? {
|
|
9573
9604
|
linkedSensorId: ev.sourceDeviceId,
|
|
9574
9605
|
linkedCameraIds: [...linked.linkedCameraIds],
|
|
@@ -9796,6 +9827,7 @@ function presentConditionIds(c) {
|
|
|
9796
9827
|
const ids = [];
|
|
9797
9828
|
if (c.systemEvent !== void 0) ids.push("systemEvent");
|
|
9798
9829
|
if (c.devices !== void 0 && c.devices.length > 0) ids.push("devices");
|
|
9830
|
+
if (c.devicesExclude !== void 0 && c.devicesExclude.length > 0) ids.push("devicesExclude");
|
|
9799
9831
|
if (c.source !== void 0 && c.source !== "any") ids.push("source");
|
|
9800
9832
|
if (c.classes !== void 0 && c.classes.length > 0) ids.push("classes");
|
|
9801
9833
|
if (c.classesExclude !== void 0 && c.classesExclude.length > 0) ids.push("classesExclude");
|
|
@@ -9812,6 +9844,7 @@ function presentConditionIds(c) {
|
|
|
9812
9844
|
if (c.plates !== void 0) ids.push("plates");
|
|
9813
9845
|
if (c.sensorKinds !== void 0 && c.sensorKinds.length > 0) ids.push("sensorKinds");
|
|
9814
9846
|
if (c.eventTypeTokens !== void 0 && c.eventTypeTokens.length > 0) ids.push("eventTypeTokens");
|
|
9847
|
+
if (c.sensorTransition !== void 0) ids.push("sensorTransition");
|
|
9815
9848
|
if (c.packagePhase !== void 0 && c.packagePhase !== "both") ids.push("packagePhase");
|
|
9816
9849
|
if (c.customZones !== void 0 && c.customZones.length > 0) ids.push("customZones");
|
|
9817
9850
|
if (c.occupancy !== void 0) ids.push("occupancy");
|
|
@@ -9906,12 +9939,23 @@ function zonesVisitedBy(subject) {
|
|
|
9906
9939
|
* before the subject exists (the linked-camera capture gate). Coarse in one
|
|
9907
9940
|
* direction only: `true` may still fail later; `false` never would have fired.
|
|
9908
9941
|
*/
|
|
9909
|
-
function deviceEventRuleMayFireOnKind(rule, sensorKind) {
|
|
9942
|
+
function deviceEventRuleMayFireOnKind(rule, sensorKind, sensorRaised) {
|
|
9910
9943
|
const c = rule.conditions;
|
|
9911
9944
|
if (c.occupancy !== void 0) return false;
|
|
9945
|
+
if (sensorTransitionRefuses(c.sensorTransition, sensorRaised)) return false;
|
|
9912
9946
|
if (c.sensorKinds === void 0 || c.sensorKinds.length === 0) return true;
|
|
9913
9947
|
return toLowerSet(c.sensorKinds).has(sensorKind.trim().toLowerCase());
|
|
9914
9948
|
}
|
|
9949
|
+
/**
|
|
9950
|
+
* Does a rule's `sensorTransition` refuse this edge? Only a KNOWN edge can be
|
|
9951
|
+
* refused: `sensorRaised === undefined` (a kind with no raise, an unreadable
|
|
9952
|
+
* slice) always passes — a flood rule never goes silent on a bad read (D640).
|
|
9953
|
+
* The ONE predicate {@link evaluateRule} and the capture gate both ask.
|
|
9954
|
+
*/
|
|
9955
|
+
function sensorTransitionRefuses(transition, sensorRaised) {
|
|
9956
|
+
if (transition === void 0 || sensorRaised === void 0) return false;
|
|
9957
|
+
return sensorRaised !== (transition === "raised");
|
|
9958
|
+
}
|
|
9915
9959
|
function toLowerSet(values) {
|
|
9916
9960
|
return new Set(values.map((v) => v.trim().toLowerCase()));
|
|
9917
9961
|
}
|
|
@@ -9948,9 +9992,6 @@ function matchesIdentityScope(subject, scope) {
|
|
|
9948
9992
|
function ruleAwaitsRecognition(rule) {
|
|
9949
9993
|
return rule.conditions.identities !== void 0 || rule.conditions.plates !== void 0;
|
|
9950
9994
|
}
|
|
9951
|
-
var ARMED_WORD_PREFIX = "armed_";
|
|
9952
|
-
var ALARM_PANEL_CAP$1 = "alarm-panel";
|
|
9953
|
-
var ALARM_PANEL_TRIGGER_METHOD = "trigger";
|
|
9954
9995
|
/**
|
|
9955
9996
|
* A rule "belongs to the alarm" when its `deviceState` gate names ANY device's
|
|
9956
9997
|
* `armed_*` word — CamStack's own panel or any other, e.g. an HA-mirrored
|
|
@@ -9968,8 +10009,10 @@ var ALARM_PANEL_TRIGGER_METHOD = "trigger";
|
|
|
9968
10009
|
* catalog on save, so a new key would be dropped by the first edit.
|
|
9969
10010
|
*/
|
|
9970
10011
|
function ruleBelongsToAlarm(rule) {
|
|
9971
|
-
|
|
9972
|
-
|
|
10012
|
+
return ruleBelongsToAlarm$1({
|
|
10013
|
+
conditions: rule.conditions,
|
|
10014
|
+
...rule.actions?.onTrigger !== void 0 ? { onTrigger: rule.actions.onTrigger } : {}
|
|
10015
|
+
});
|
|
9973
10016
|
}
|
|
9974
10017
|
/**
|
|
9975
10018
|
* The edge an alarm rule refuses: a close, or a halt/fault (a cover stopped
|
|
@@ -10145,6 +10188,7 @@ function evaluateRule(rule, subject, deviceState, zoneOwners, sceneStates) {
|
|
|
10145
10188
|
if (!sceneConditionSatisfied(c.scene, observation)) return fail("scene");
|
|
10146
10189
|
}
|
|
10147
10190
|
if (c.devices !== void 0 && c.devices.length > 0 && !matchesDeviceScope(c.devices, subject)) return fail("devices");
|
|
10191
|
+
if (c.devicesExclude !== void 0 && deviceScopeExcludes(c.devicesExclude, subject)) return fail("devicesExclude");
|
|
10148
10192
|
if (c.source !== void 0 && c.source !== "any") {
|
|
10149
10193
|
if ((subject.source ?? "pipeline") !== c.source) return fail("source");
|
|
10150
10194
|
}
|
|
@@ -10221,6 +10265,7 @@ function evaluateRule(rule, subject, deviceState, zoneOwners, sceneStates) {
|
|
|
10221
10265
|
if (c.eventTypeTokens !== void 0 && c.eventTypeTokens.length > 0) {
|
|
10222
10266
|
if (subject.eventType === void 0 || !toLowerSet(c.eventTypeTokens).has(subject.eventType.trim().toLowerCase())) return fail("eventTypeTokens");
|
|
10223
10267
|
}
|
|
10268
|
+
if (sensorTransitionRefuses(c.sensorTransition, subject.sensorRaised)) return fail("sensorTransition");
|
|
10224
10269
|
if (c.packagePhase !== void 0 && c.packagePhase !== "both") {
|
|
10225
10270
|
if (subject.packagePhase !== c.packagePhase) return fail("packagePhase");
|
|
10226
10271
|
}
|
|
@@ -10326,6 +10371,27 @@ function matchesDeviceScope(scope, subject) {
|
|
|
10326
10371
|
if (scope.includes(subject.deviceId)) return true;
|
|
10327
10372
|
return subject.sourceDeviceId !== void 0 && scope.includes(subject.sourceDeviceId);
|
|
10328
10373
|
}
|
|
10374
|
+
/**
|
|
10375
|
+
* Does a rule's `devicesExclude` (D641) exclude this subject?
|
|
10376
|
+
*
|
|
10377
|
+
* The exact complement of {@link matchesDeviceScope}, row by row. A per-camera
|
|
10378
|
+
* row is excluded by its camera OR by the sensor that fired, the two ids an
|
|
10379
|
+
* include matches on. A linked set stands for the rows it replaced (D630 §6):
|
|
10380
|
+
* it is excluded when its SENSOR is, or when EVERY camera that took a row is —
|
|
10381
|
+
* one excluded row camera leaves the others firing, as their own rows would
|
|
10382
|
+
* have. A camera that only LINKS the sensor excludes nothing, for the reason it
|
|
10383
|
+
* includes nothing: ADR 0632 widens the media, never which rules fire.
|
|
10384
|
+
*/
|
|
10385
|
+
function deviceScopeExcludes(excluded, subject) {
|
|
10386
|
+
if (excluded.length === 0) return false;
|
|
10387
|
+
if (subject.linkedSensorId !== void 0) {
|
|
10388
|
+
if (excluded.includes(subject.linkedSensorId)) return true;
|
|
10389
|
+
const rows = subject.rowCameraIds ?? [];
|
|
10390
|
+
return rows.length > 0 && rows.every((cameraId) => excluded.includes(cameraId));
|
|
10391
|
+
}
|
|
10392
|
+
if (excluded.includes(subject.deviceId)) return true;
|
|
10393
|
+
return subject.sourceDeviceId !== void 0 && excluded.includes(subject.sourceDeviceId);
|
|
10394
|
+
}
|
|
10329
10395
|
function cooldownKey(rule, subject) {
|
|
10330
10396
|
const first = subject.classNames[0];
|
|
10331
10397
|
const classKey = keysPerClass(rule, subject) && first !== void 0 ? `:c:${first}` : "";
|
|
@@ -12453,6 +12519,7 @@ var en_default = {
|
|
|
12453
12519
|
"snooze.scope.anyRule": "A rule",
|
|
12454
12520
|
"snooze.scope.rule": "“{{rule}}”",
|
|
12455
12521
|
"alarm.arm.body.allDevices": "Active: all devices",
|
|
12522
|
+
"alarm.arm.body.allDevicesExcept": "Active: all devices except {{count}}: {{names}}",
|
|
12456
12523
|
"alarm.arm.body.noDevices": "Active: no device restriction",
|
|
12457
12524
|
"alarm.arm.body.devices": "Active: {{names}}",
|
|
12458
12525
|
"alarm.arm.body.devicesSkipped": "Active: {{names}}\nExcluded: {{skippedNames}}",
|
|
@@ -12741,6 +12808,7 @@ var it_default = {
|
|
|
12741
12808
|
"snooze.scope.anyRule": "Una regola",
|
|
12742
12809
|
"snooze.scope.rule": "“{{rule}}”",
|
|
12743
12810
|
"alarm.arm.body.allDevices": "Attivi: tutti i dispositivi",
|
|
12811
|
+
"alarm.arm.body.allDevicesExcept": "Attivi: tutti i dispositivi tranne {{count}}: {{names}}",
|
|
12744
12812
|
"alarm.arm.body.noDevices": "Attivi: nessuna restrizione",
|
|
12745
12813
|
"alarm.arm.body.devices": "Attivi: {{names}}",
|
|
12746
12814
|
"alarm.arm.body.devicesSkipped": "Attivi: {{names}}\nEsclusi: {{skippedNames}}",
|
|
@@ -13143,6 +13211,245 @@ function pickTrackNarrationFrames(rows, max) {
|
|
|
13143
13211
|
return picked;
|
|
13144
13212
|
}
|
|
13145
13213
|
//#endregion
|
|
13214
|
+
//#region src/notification-center/rule-family-guard.ts
|
|
13215
|
+
/**
|
|
13216
|
+
* The hub's half of D637: a NEW or UPDATED system-event rule answers ONE family
|
|
13217
|
+
* (device events, system, alarm) and carries only that family's filters.
|
|
13218
|
+
*
|
|
13219
|
+
* THE authority for the invariant. The cap-provider route
|
|
13220
|
+
* (`notificationRules.createRule/updateRule`, index.ts) and the rule bridge
|
|
13221
|
+
* (`nc.createRule/updateRule`, nc-actions.ts) both call it through
|
|
13222
|
+
* `guardRuleFamily` (create) and `guardPatchFamily` (update), so the two save
|
|
13223
|
+
* paths cannot drift. This is the same
|
|
13224
|
+
* arrangement as `assertRuleHasAddressee`.
|
|
13225
|
+
*
|
|
13226
|
+
* Not in Zod: a cross-field refine on `NcConditionsSchema` would also reject the
|
|
13227
|
+
* STORED mixed rules at load (NcRuleSchema.parse in the store), and those must
|
|
13228
|
+
* keep firing until the operator recreates them. The engine is untouched, and
|
|
13229
|
+
* so is the device-prune runner: removing a deleted device id from a stored
|
|
13230
|
+
* rule is not authoring.
|
|
13231
|
+
*/
|
|
13232
|
+
var NcRuleFamilyRefusal = class extends Error {
|
|
13233
|
+
problems;
|
|
13234
|
+
/** The rule that was judged: the input on a create, the MERGED rule on an update. */
|
|
13235
|
+
rule;
|
|
13236
|
+
constructor(problems, rule) {
|
|
13237
|
+
super(`system-event rule refused: ${problems.map(describeSystemEventFamilyProblem).join("; ")}`);
|
|
13238
|
+
this.name = "NcRuleFamilyRefusal";
|
|
13239
|
+
this.problems = problems;
|
|
13240
|
+
this.rule = rule;
|
|
13241
|
+
}
|
|
13242
|
+
};
|
|
13243
|
+
function assertRuleFamily(rule) {
|
|
13244
|
+
if (!isSystemDelivery(rule.delivery)) return;
|
|
13245
|
+
const problems = systemEventFamilyProblems(rule.conditions.systemEvent);
|
|
13246
|
+
if (problems.length > 0) throw new NcRuleFamilyRefusal(problems, rule);
|
|
13247
|
+
}
|
|
13248
|
+
var byValue = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
13249
|
+
var byNumber = (a, b) => a - b;
|
|
13250
|
+
/**
|
|
13251
|
+
* A system-event condition with every list sorted. The engine reads `kinds` and
|
|
13252
|
+
* the id-list filters as SETS (membership only), so an editor that re-sorts
|
|
13253
|
+
* them — the admin's `toggleSystemEventKind` puts kinds in option order — has
|
|
13254
|
+
* not changed the rule. Copies; the input is never touched.
|
|
13255
|
+
*/
|
|
13256
|
+
function sortedSystemEvent(condition) {
|
|
13257
|
+
const { kinds, deviceIds, excludeDeviceIds, deviceTypes, nodeIds, packageNames } = condition;
|
|
13258
|
+
return {
|
|
13259
|
+
...condition,
|
|
13260
|
+
kinds: kinds.toSorted(byValue),
|
|
13261
|
+
...deviceIds !== void 0 ? { deviceIds: deviceIds.toSorted(byNumber) } : {},
|
|
13262
|
+
...excludeDeviceIds !== void 0 ? { excludeDeviceIds: excludeDeviceIds.toSorted(byNumber) } : {},
|
|
13263
|
+
...deviceTypes !== void 0 ? { deviceTypes: deviceTypes.toSorted(byValue) } : {},
|
|
13264
|
+
...nodeIds !== void 0 ? { nodeIds: nodeIds.toSorted(byValue) } : {},
|
|
13265
|
+
...packageNames !== void 0 ? { packageNames: packageNames.toSorted(byValue) } : {}
|
|
13266
|
+
};
|
|
13267
|
+
}
|
|
13268
|
+
/** The canonical form two conditions are compared in. */
|
|
13269
|
+
function conditionsFingerprint(conditions) {
|
|
13270
|
+
const { systemEvent } = conditions;
|
|
13271
|
+
return canonicalHash(systemEvent === void 0 ? conditions : {
|
|
13272
|
+
...conditions,
|
|
13273
|
+
systemEvent: sortedSystemEvent(systemEvent)
|
|
13274
|
+
});
|
|
13275
|
+
}
|
|
13276
|
+
/**
|
|
13277
|
+
* Do two conditions say the same thing? Both go through `NcConditionsSchema`
|
|
13278
|
+
* (the normalisation the store applies on every write) and are compared
|
|
13279
|
+
* canonically: key order is irrelevant, an `undefined` key equals an absent
|
|
13280
|
+
* one, and the system-event `kinds` and id-list filters are compared as SETS.
|
|
13281
|
+
* Every other array keeps its order. A side that does not parse is treated as
|
|
13282
|
+
* a change, so it is judged rather than waved through.
|
|
13283
|
+
*/
|
|
13284
|
+
function sameConditions(a, b) {
|
|
13285
|
+
const left = NcConditionsSchema.safeParse(a);
|
|
13286
|
+
const right = NcConditionsSchema.safeParse(b);
|
|
13287
|
+
if (!left.success || !right.success) return false;
|
|
13288
|
+
return conditionsFingerprint(left.data) === conditionsFingerprint(right.data);
|
|
13289
|
+
}
|
|
13290
|
+
/**
|
|
13291
|
+
* The rule a patch WOULD produce, as far as the family is concerned — merged
|
|
13292
|
+
* the way the store merges it: `delivery` and `conditions` replace whole.
|
|
13293
|
+
*
|
|
13294
|
+
* `null` when the patch CHANGES neither. Both editors send the whole rule on
|
|
13295
|
+
* every save, so "the patch carries `conditions`" is not "the patch changes
|
|
13296
|
+
* them": an unchanged full-form save of a stored mixed rule (a rename, a
|
|
13297
|
+
* retarget) must pass, and only a save that alters the delivery or the
|
|
13298
|
+
* conditions is judged.
|
|
13299
|
+
*/
|
|
13300
|
+
function mergedFamilySubject(existing, patch) {
|
|
13301
|
+
const deliveryChanged = patch.delivery !== void 0 && patch.delivery !== existing.delivery;
|
|
13302
|
+
const conditionsChanged = patch.conditions !== void 0 && !sameConditions(patch.conditions, existing.conditions);
|
|
13303
|
+
if (!deliveryChanged && !conditionsChanged) return null;
|
|
13304
|
+
return {
|
|
13305
|
+
delivery: patch.delivery ?? existing.delivery,
|
|
13306
|
+
conditions: patch.conditions ?? existing.conditions
|
|
13307
|
+
};
|
|
13308
|
+
}
|
|
13309
|
+
/**
|
|
13310
|
+
* On update, the MERGED rule is judged. A patch that changes neither `delivery`
|
|
13311
|
+
* nor `conditions` (absent, or equal to what is stored) leaves the kinds as
|
|
13312
|
+
* they are, so a stored mixed rule can still be switched off, renamed or
|
|
13313
|
+
* retargeted before the operator recreates it.
|
|
13314
|
+
*/
|
|
13315
|
+
function assertPatchKeepsFamily(existing, patch) {
|
|
13316
|
+
if (existing === null) return;
|
|
13317
|
+
const merged = mergedFamilySubject(existing, patch);
|
|
13318
|
+
if (merged !== null) assertRuleFamily(merged);
|
|
13319
|
+
}
|
|
13320
|
+
/**
|
|
13321
|
+
* Run a family check and LOG a refusal: who tried to save, the rule that was
|
|
13322
|
+
* judged (its delivery and kinds) and every problem in words (the families,
|
|
13323
|
+
* the offending kind or filter). Then rethrow. A refused save is dropped work,
|
|
13324
|
+
* and dropped work is never silent. Any other error passes through unlogged.
|
|
13325
|
+
*/
|
|
13326
|
+
function logRefusal$2(check, ctx) {
|
|
13327
|
+
try {
|
|
13328
|
+
check();
|
|
13329
|
+
} catch (err) {
|
|
13330
|
+
if (err instanceof NcRuleFamilyRefusal) ctx.logger.warn("nc rule refused: outside one family", { meta: {
|
|
13331
|
+
...ctx.ruleId !== void 0 ? { ruleId: ctx.ruleId } : {},
|
|
13332
|
+
by: ctx.by,
|
|
13333
|
+
delivery: err.rule.delivery,
|
|
13334
|
+
kinds: err.rule.conditions.systemEvent?.kinds ?? [],
|
|
13335
|
+
problems: err.problems.map(describeSystemEventFamilyProblem)
|
|
13336
|
+
} });
|
|
13337
|
+
throw err;
|
|
13338
|
+
}
|
|
13339
|
+
}
|
|
13340
|
+
/** {@link assertRuleFamily}, logged. The create path of both routes. */
|
|
13341
|
+
function guardRuleFamily(rule, ctx) {
|
|
13342
|
+
logRefusal$2(() => assertRuleFamily(rule), ctx);
|
|
13343
|
+
}
|
|
13344
|
+
/** {@link assertPatchKeepsFamily}, logged. The update path of both routes. */
|
|
13345
|
+
function guardPatchFamily(existing, patch, ctx) {
|
|
13346
|
+
logRefusal$2(() => assertPatchKeepsFamily(existing, patch), ctx);
|
|
13347
|
+
}
|
|
13348
|
+
//#endregion
|
|
13349
|
+
//#region src/notification-center/device-scope-guard.ts
|
|
13350
|
+
/**
|
|
13351
|
+
* The deliveries whose evaluation reads the device scope — the `devices`
|
|
13352
|
+
* descriptor's `appliesTo`, restated as the one delivery that lacks it.
|
|
13353
|
+
*/
|
|
13354
|
+
var NO_DEVICE_SCOPE_DELIVERY = "system-event";
|
|
13355
|
+
var NcDevicesExcludeRefusal = class extends Error {
|
|
13356
|
+
delivery;
|
|
13357
|
+
devicesExclude;
|
|
13358
|
+
constructor(delivery, devicesExclude) {
|
|
13359
|
+
super(`rule refused: devicesExclude does not apply to a '${delivery}' rule — a system rule excludes devices with systemEvent.excludeDeviceIds`);
|
|
13360
|
+
this.name = "NcDevicesExcludeRefusal";
|
|
13361
|
+
this.delivery = delivery;
|
|
13362
|
+
this.devicesExclude = devicesExclude;
|
|
13363
|
+
}
|
|
13364
|
+
};
|
|
13365
|
+
function assertDevicesExcludeDelivery(rule) {
|
|
13366
|
+
const excluded = rule.conditions.devicesExclude;
|
|
13367
|
+
if (excluded === void 0 || rule.delivery !== NO_DEVICE_SCOPE_DELIVERY) return;
|
|
13368
|
+
throw new NcDevicesExcludeRefusal(rule.delivery, excluded);
|
|
13369
|
+
}
|
|
13370
|
+
/** Run the check; a refusal is dropped work, so it is logged before it is rethrown. */
|
|
13371
|
+
function logRefusal$1(check, ctx) {
|
|
13372
|
+
try {
|
|
13373
|
+
check();
|
|
13374
|
+
} catch (err) {
|
|
13375
|
+
if (err instanceof NcDevicesExcludeRefusal) warnRefusal$1(ctx.logger, err, ctx);
|
|
13376
|
+
throw err;
|
|
13377
|
+
}
|
|
13378
|
+
}
|
|
13379
|
+
function warnRefusal$1(logger, err, ctx) {
|
|
13380
|
+
logger.warn("nc rule refused: devicesExclude on a system rule", { meta: {
|
|
13381
|
+
...ctx.ruleId !== void 0 ? { ruleId: ctx.ruleId } : {},
|
|
13382
|
+
by: ctx.by,
|
|
13383
|
+
delivery: err.delivery,
|
|
13384
|
+
devicesExclude: [...err.devicesExclude]
|
|
13385
|
+
} });
|
|
13386
|
+
}
|
|
13387
|
+
/** The create path of both routes. */
|
|
13388
|
+
function guardRuleDevicesExclude(rule, ctx) {
|
|
13389
|
+
logRefusal$1(() => assertDevicesExcludeDelivery(rule), ctx);
|
|
13390
|
+
}
|
|
13391
|
+
/**
|
|
13392
|
+
* The update path of both routes: the MERGED rule is judged, and only when the
|
|
13393
|
+
* patch changes `delivery` or `conditions`.
|
|
13394
|
+
*/
|
|
13395
|
+
function guardPatchDevicesExclude(existing, patch, ctx) {
|
|
13396
|
+
if (existing === null) return;
|
|
13397
|
+
const merged = mergedFamilySubject(existing, patch);
|
|
13398
|
+
if (merged === null) return;
|
|
13399
|
+
logRefusal$1(() => assertDevicesExcludeDelivery(merged), ctx);
|
|
13400
|
+
}
|
|
13401
|
+
//#endregion
|
|
13402
|
+
//#region src/notification-center/sensor-transition-guard.ts
|
|
13403
|
+
/** The one delivery whose subject carries a sensor edge. */
|
|
13404
|
+
var SENSOR_TRANSITION_DELIVERY = "device-event";
|
|
13405
|
+
var NcSensorTransitionRefusal = class extends Error {
|
|
13406
|
+
delivery;
|
|
13407
|
+
sensorTransition;
|
|
13408
|
+
constructor(delivery, sensorTransition) {
|
|
13409
|
+
super(`rule refused: sensorTransition '${sensorTransition}' applies only to a device-event rule, not '${delivery}'`);
|
|
13410
|
+
this.name = "NcSensorTransitionRefusal";
|
|
13411
|
+
this.delivery = delivery;
|
|
13412
|
+
this.sensorTransition = sensorTransition;
|
|
13413
|
+
}
|
|
13414
|
+
};
|
|
13415
|
+
function assertSensorTransitionDelivery(rule) {
|
|
13416
|
+
const transition = rule.conditions.sensorTransition;
|
|
13417
|
+
if (transition === void 0 || rule.delivery === SENSOR_TRANSITION_DELIVERY) return;
|
|
13418
|
+
throw new NcSensorTransitionRefusal(rule.delivery, transition);
|
|
13419
|
+
}
|
|
13420
|
+
/** Run the check; a refusal is dropped work, so it is logged before it is rethrown. */
|
|
13421
|
+
function logRefusal(check, ctx) {
|
|
13422
|
+
try {
|
|
13423
|
+
check();
|
|
13424
|
+
} catch (err) {
|
|
13425
|
+
if (err instanceof NcSensorTransitionRefusal) warnRefusal(ctx.logger, err, ctx);
|
|
13426
|
+
throw err;
|
|
13427
|
+
}
|
|
13428
|
+
}
|
|
13429
|
+
function warnRefusal(logger, err, ctx) {
|
|
13430
|
+
logger.warn("nc rule refused: sensorTransition off a device-event rule", { meta: {
|
|
13431
|
+
...ctx.ruleId !== void 0 ? { ruleId: ctx.ruleId } : {},
|
|
13432
|
+
by: ctx.by,
|
|
13433
|
+
delivery: err.delivery,
|
|
13434
|
+
sensorTransition: err.sensorTransition
|
|
13435
|
+
} });
|
|
13436
|
+
}
|
|
13437
|
+
/** The create path of both routes. */
|
|
13438
|
+
function guardRuleSensorTransition(rule, ctx) {
|
|
13439
|
+
logRefusal(() => assertSensorTransitionDelivery(rule), ctx);
|
|
13440
|
+
}
|
|
13441
|
+
/**
|
|
13442
|
+
* The update path of both routes: the MERGED rule is judged, and only when the
|
|
13443
|
+
* patch changes `delivery` or `conditions` — an unchanged full-form save
|
|
13444
|
+
* (a rename, a retarget) is not re-judged.
|
|
13445
|
+
*/
|
|
13446
|
+
function guardPatchSensorTransition(existing, patch, ctx) {
|
|
13447
|
+
if (existing === null) return;
|
|
13448
|
+
const merged = mergedFamilySubject(existing, patch);
|
|
13449
|
+
if (merged === null) return;
|
|
13450
|
+
logRefusal(() => assertSensorTransitionDelivery(merged), ctx);
|
|
13451
|
+
}
|
|
13452
|
+
//#endregion
|
|
13146
13453
|
//#region src/notification-center/occupancy-watcher.ts
|
|
13147
13454
|
/** Sentinel key segments for the "no zone" (whole-frame) and "no class" scopes. */
|
|
13148
13455
|
var FRAME_SCOPE = "@frame";
|
|
@@ -13911,6 +14218,19 @@ function labelFor(conditions) {
|
|
|
13911
14218
|
return conditions.identities?.[0] ?? conditions.labelEquals?.[0] ?? conditions.plates?.values?.[0] ?? void 0;
|
|
13912
14219
|
}
|
|
13913
14220
|
/**
|
|
14221
|
+
* The edge a planned sensor event carries (D640): RAISED by default — the
|
|
14222
|
+
* alert, which is what an operator pressing Test on a flood rule expects to
|
|
14223
|
+
* see — and cleared only for a rule that asks for the clear. Only for a kind
|
|
14224
|
+
* that HAS a raise: a doorbell press carries none in production, and a test
|
|
14225
|
+
* subject claiming one would be a subject no real record could be.
|
|
14226
|
+
*/
|
|
14227
|
+
function sensorEdgeFor(trigger, c) {
|
|
14228
|
+
const kind = c.sensorKinds?.[0];
|
|
14229
|
+
if (trigger !== "device-event" || kind === void 0) return {};
|
|
14230
|
+
if (!NC_SENSOR_TRANSITION_KINDS.includes(kind.trim().toLowerCase())) return {};
|
|
14231
|
+
return { sensorRaised: c.sensorTransition !== "cleared" };
|
|
14232
|
+
}
|
|
14233
|
+
/**
|
|
13914
14234
|
* Plan the injection that fires this rule.
|
|
13915
14235
|
*
|
|
13916
14236
|
* Every condition the engine reads off the SUBJECT is satisfied here; the two
|
|
@@ -13932,6 +14252,10 @@ function planRuleTest(input) {
|
|
|
13932
14252
|
note: "This rule names no camera and none was supplied, so the event is attributed to no device and carries no picture. Scope the rule to a camera, or pick one, to see the notification it would really send."
|
|
13933
14253
|
});
|
|
13934
14254
|
const deviceId = scopedDeviceId ?? NC_SYSTEM_DEVICE_ID;
|
|
14255
|
+
if (trigger !== "system-event" && (c.devicesExclude ?? []).includes(deviceId)) notes.push({
|
|
14256
|
+
conditionId: "devicesExclude",
|
|
14257
|
+
note: "The camera this test uses is one the rule excludes, so the evaluation will refuse the event — exactly as it refuses every real one there. Pick another camera to see the notification."
|
|
14258
|
+
});
|
|
13935
14259
|
const scheduledAt = scheduleActiveAt(rule.schedule, now);
|
|
13936
14260
|
if (scheduledAt === null) notes.push({
|
|
13937
14261
|
conditionId: "schedule",
|
|
@@ -13981,6 +14305,7 @@ function planRuleTest(input) {
|
|
|
13981
14305
|
} } : {},
|
|
13982
14306
|
...c.sensorKinds?.[0] !== void 0 ? { sensorKind: c.sensorKinds[0] } : {},
|
|
13983
14307
|
...c.eventTypeTokens?.[0] !== void 0 ? { eventType: c.eventTypeTokens[0] } : {},
|
|
14308
|
+
...sensorEdgeFor(trigger, c),
|
|
13984
14309
|
...trigger === "package-event" ? { packagePhase: c.packagePhase === "picked-up" ? "picked-up" : "delivered" } : {},
|
|
13985
14310
|
...c.source !== void 0 && c.source !== "any" ? { source: c.source } : {},
|
|
13986
14311
|
...bbox !== void 0 ? { bbox } : {},
|
|
@@ -14499,6 +14824,10 @@ var NcTestEventInputSchema = object({
|
|
|
14499
14824
|
/** DEVICE-EVENT: the raw device event-type token, when the source is an
|
|
14500
14825
|
* event-emitter slice. */
|
|
14501
14826
|
eventType: string().optional(),
|
|
14827
|
+
/** DEVICE-EVENT: which edge a boolean sensor moved on — true raised, false
|
|
14828
|
+
* cleared (D640). Absent = the slice could not say, which production
|
|
14829
|
+
* answers for a doorbell press or an unreadable read. */
|
|
14830
|
+
sensorRaised: boolean().optional(),
|
|
14502
14831
|
/** PACKAGE-EVENT: which lifecycle phase. */
|
|
14503
14832
|
packagePhase: _enum(["delivered", "picked-up"]).optional(),
|
|
14504
14833
|
/** Detection provenance. Absent = `pipeline` (what a record with no stamped
|
|
@@ -14717,6 +15046,7 @@ function buildSyntheticEvent(input, recordId, now) {
|
|
|
14717
15046
|
...input.labelConfidence !== void 0 ? { labelConfidence: input.labelConfidence } : {},
|
|
14718
15047
|
...input.sensorKind !== void 0 ? { sensorKind: input.sensorKind } : {},
|
|
14719
15048
|
...input.eventType !== void 0 ? { eventType: input.eventType } : {},
|
|
15049
|
+
...input.sensorRaised !== void 0 ? { sensorRaised: input.sensorRaised } : {},
|
|
14720
15050
|
...input.packagePhase !== void 0 ? { packagePhase: input.packagePhase } : {},
|
|
14721
15051
|
...input.bbox !== void 0 ? { bbox: input.bbox } : {},
|
|
14722
15052
|
...input.crossing !== void 0 ? { crossing: input.crossing } : {},
|
|
@@ -15843,6 +16173,18 @@ function makeNcActionHandlers(deps) {
|
|
|
15843
16173
|
const parsed = NcRuleInputSchema.parse(input.rule);
|
|
15844
16174
|
assertAdminOnlyFields(parsed, [], c);
|
|
15845
16175
|
assertRuleHasAddressee(parsed.targets, parsed.targetUsers);
|
|
16176
|
+
guardRuleFamily(parsed, {
|
|
16177
|
+
logger: deps.logger,
|
|
16178
|
+
by: c.userId
|
|
16179
|
+
});
|
|
16180
|
+
guardRuleSensorTransition(parsed, {
|
|
16181
|
+
logger: deps.logger,
|
|
16182
|
+
by: c.userId
|
|
16183
|
+
});
|
|
16184
|
+
guardRuleDevicesExclude(parsed, {
|
|
16185
|
+
logger: deps.logger,
|
|
16186
|
+
by: c.userId
|
|
16187
|
+
});
|
|
15846
16188
|
await assertTargetsOwned(parsed.targets.map((t) => t.targetId), c);
|
|
15847
16189
|
const rule = await deps.ruleStore.create({
|
|
15848
16190
|
...parsed,
|
|
@@ -15861,6 +16203,21 @@ function makeNcActionHandlers(deps) {
|
|
|
15861
16203
|
const clear = input.clear ?? [];
|
|
15862
16204
|
assertAdminOnlyFields(patch, clear, c, existing);
|
|
15863
16205
|
assertPatchKeepsAddressee(existing, patch, clear);
|
|
16206
|
+
guardPatchFamily(existing, patch, {
|
|
16207
|
+
logger: deps.logger,
|
|
16208
|
+
by: c.userId,
|
|
16209
|
+
ruleId: input.ruleId
|
|
16210
|
+
});
|
|
16211
|
+
guardPatchSensorTransition(existing, patch, {
|
|
16212
|
+
logger: deps.logger,
|
|
16213
|
+
by: c.userId,
|
|
16214
|
+
ruleId: input.ruleId
|
|
16215
|
+
});
|
|
16216
|
+
guardPatchDevicesExclude(existing, patch, {
|
|
16217
|
+
logger: deps.logger,
|
|
16218
|
+
by: c.userId,
|
|
16219
|
+
ruleId: input.ruleId
|
|
16220
|
+
});
|
|
15864
16221
|
if (patch.targets !== void 0) await assertTargetsOwned(patch.targets.map((t) => t.targetId), c);
|
|
15865
16222
|
const { ownerUserId: _owner, disabledTargetIds: _optOut, ...safe } = patch;
|
|
15866
16223
|
const rule = await deps.ruleStore.update(input.ruleId, safe, clear);
|
|
@@ -19004,6 +19361,19 @@ function pruneRuleConditions(conditions, isDead, notes) {
|
|
|
19004
19361
|
notes.removed("conditions.devices", deviceSubjects(verdict.dead));
|
|
19005
19362
|
}
|
|
19006
19363
|
}
|
|
19364
|
+
const excluded = conditions.devicesExclude;
|
|
19365
|
+
if (excluded !== void 0) {
|
|
19366
|
+
const verdict = judgeList(excluded, isDead);
|
|
19367
|
+
if (!verdict.untouched) {
|
|
19368
|
+
const { devicesExclude: _dead, ...rest } = next;
|
|
19369
|
+
next = verdict.last ? rest : {
|
|
19370
|
+
...rest,
|
|
19371
|
+
devicesExclude: [...verdict.next]
|
|
19372
|
+
};
|
|
19373
|
+
changed = true;
|
|
19374
|
+
notes.removed("conditions.devicesExclude", deviceSubjects(verdict.dead));
|
|
19375
|
+
}
|
|
19376
|
+
}
|
|
19007
19377
|
const systemEvent = conditions.systemEvent;
|
|
19008
19378
|
if (systemEvent?.deviceIds !== void 0) {
|
|
19009
19379
|
const verdict = judgeList(systemEvent.deviceIds, isDead);
|
|
@@ -19184,6 +19554,7 @@ function referencedDeviceIds(snapshot) {
|
|
|
19184
19554
|
for (const rule of snapshot.rules) {
|
|
19185
19555
|
const c = rule.conditions;
|
|
19186
19556
|
add(c.devices);
|
|
19557
|
+
add(c.devicesExclude);
|
|
19187
19558
|
add(c.systemEvent?.deviceIds);
|
|
19188
19559
|
if (c.deviceState !== void 0) ids.add(c.deviceState.deviceId);
|
|
19189
19560
|
if (c.scene?.deviceId !== void 0) ids.add(c.scene.deviceId);
|
|
@@ -22037,6 +22408,50 @@ var NcOutbox = class {
|
|
|
22037
22408
|
return [];
|
|
22038
22409
|
}
|
|
22039
22410
|
}
|
|
22411
|
+
/**
|
|
22412
|
+
* The rows `getRuleLastRuns` reduces — ONE statement: fire time inside
|
|
22413
|
+
* `[sinceMs, ∞)`, newest-first on `idx_nc_outbox_created`, projected to the
|
|
22414
|
+
* four columns the reduction reads (never the `payload` JSON), at most
|
|
22415
|
+
* `limit` rows. `sinceMs: null` = no lower bound (the operator keeps the
|
|
22416
|
+
* outbox forever); `limit` still bounds the read.
|
|
22417
|
+
*
|
|
22418
|
+
* Unlike {@link queryHistory} this THROWS on a store error: its caller
|
|
22419
|
+
* answers "which rules did not run", and an empty page from a failed read
|
|
22420
|
+
* would answer "none of them did" (D393).
|
|
22421
|
+
*/
|
|
22422
|
+
async queryLastRunRows(sinceMs, limit) {
|
|
22423
|
+
const rows = await this.store.query.query({
|
|
22424
|
+
collection: NC_OUTBOX_COLLECTION,
|
|
22425
|
+
filter: {
|
|
22426
|
+
...sinceMs !== null ? { whereBetween: { createdAt: [sinceMs, Number.MAX_SAFE_INTEGER] } } : {},
|
|
22427
|
+
orderBy: {
|
|
22428
|
+
field: "createdAt",
|
|
22429
|
+
direction: "desc"
|
|
22430
|
+
},
|
|
22431
|
+
limit
|
|
22432
|
+
},
|
|
22433
|
+
columns: [...NC_LAST_RUN_COLUMNS]
|
|
22434
|
+
});
|
|
22435
|
+
const out = [];
|
|
22436
|
+
let malformed = 0;
|
|
22437
|
+
let oldestReadAt = null;
|
|
22438
|
+
for (const row of rows) {
|
|
22439
|
+
const at = Number(row.data["createdAt"]);
|
|
22440
|
+
if (Number.isFinite(at) && (oldestReadAt === null || at < oldestReadAt)) oldestReadAt = at;
|
|
22441
|
+
const parsed = lastRunRow(row.data);
|
|
22442
|
+
if (parsed === null) malformed += 1;
|
|
22443
|
+
else out.push(parsed);
|
|
22444
|
+
}
|
|
22445
|
+
if (malformed > 0) this.logger.warn("last-run read skipped malformed outbox rows", { meta: {
|
|
22446
|
+
malformed,
|
|
22447
|
+
read: rows.length
|
|
22448
|
+
} });
|
|
22449
|
+
return {
|
|
22450
|
+
rows: out,
|
|
22451
|
+
truncated: rows.length >= limit,
|
|
22452
|
+
oldestReadAt
|
|
22453
|
+
};
|
|
22454
|
+
}
|
|
22040
22455
|
async getWatermark() {
|
|
22041
22456
|
try {
|
|
22042
22457
|
const row = await this.store.get.query({
|
|
@@ -22324,6 +22739,27 @@ function rowToEntry$1(id, data) {
|
|
|
22324
22739
|
payload
|
|
22325
22740
|
};
|
|
22326
22741
|
}
|
|
22742
|
+
/** The columns {@link NcOutbox.queryLastRunRows} projects. */
|
|
22743
|
+
var NC_LAST_RUN_COLUMNS = [
|
|
22744
|
+
"ruleId",
|
|
22745
|
+
"createdAt",
|
|
22746
|
+
"status",
|
|
22747
|
+
"deviceId"
|
|
22748
|
+
];
|
|
22749
|
+
/** A projected row back to a {@link NcLastRunRow} — a guard, never a cast. */
|
|
22750
|
+
function lastRunRow(data) {
|
|
22751
|
+
const ruleId = data["ruleId"];
|
|
22752
|
+
const createdAt = Number(data["createdAt"]);
|
|
22753
|
+
const deviceId = Number(data["deviceId"]);
|
|
22754
|
+
const status = data["status"];
|
|
22755
|
+
if (typeof ruleId !== "string" || !Number.isFinite(createdAt) || !Number.isFinite(deviceId) || status !== "pending" && status !== "sent" && status !== "dead") return null;
|
|
22756
|
+
return {
|
|
22757
|
+
ruleId,
|
|
22758
|
+
createdAt,
|
|
22759
|
+
status,
|
|
22760
|
+
deviceId
|
|
22761
|
+
};
|
|
22762
|
+
}
|
|
22327
22763
|
//#endregion
|
|
22328
22764
|
//#region src/notification-center/rule-actions.ts
|
|
22329
22765
|
var NcRuleActionRunner = class {
|
|
@@ -23015,6 +23451,58 @@ var NcRuleAiStore = class {
|
|
|
23015
23451
|
}
|
|
23016
23452
|
};
|
|
23017
23453
|
//#endregion
|
|
23454
|
+
//#region src/notification-center/rule-last-runs.ts
|
|
23455
|
+
/**
|
|
23456
|
+
* Row bound of the one read. Projected rows are four scalars (~100 B), so the
|
|
23457
|
+
* bound costs ~2 MB at worst. The live outbox volume was NOT measured when
|
|
23458
|
+
* this was chosen. It exists so a runaway rule cannot turn an admin page load
|
|
23459
|
+
* into a table walk — and when it bites, `horizonMs` says so.
|
|
23460
|
+
*/
|
|
23461
|
+
var NC_RULE_LAST_RUNS_SCAN_LIMIT = 2e4;
|
|
23462
|
+
/** Least successful first: a fire that reached some targets and not others
|
|
23463
|
+
* did not "deliver". */
|
|
23464
|
+
var STATUS_SEVERITY = {
|
|
23465
|
+
sent: 0,
|
|
23466
|
+
pending: 1,
|
|
23467
|
+
dead: 2
|
|
23468
|
+
};
|
|
23469
|
+
function worse(a, b) {
|
|
23470
|
+
return STATUS_SEVERITY[b] > STATUS_SEVERITY[a] ? b : a;
|
|
23471
|
+
}
|
|
23472
|
+
/**
|
|
23473
|
+
* Reduce newest-first rows to one run per rule, in one pass.
|
|
23474
|
+
*
|
|
23475
|
+
* Rows sharing the newest `createdAt` of a rule are one fire to several
|
|
23476
|
+
* targets; they merge into the least successful status. `read.rows` MUST be
|
|
23477
|
+
* ordered newest-first — that is what the outbox read asks the store for.
|
|
23478
|
+
*/
|
|
23479
|
+
function summariseRuleLastRuns(read, scan) {
|
|
23480
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
23481
|
+
for (const row of read.rows) {
|
|
23482
|
+
const seen = byRule.get(row.ruleId);
|
|
23483
|
+
if (seen === void 0) byRule.set(row.ruleId, {
|
|
23484
|
+
ruleId: row.ruleId,
|
|
23485
|
+
lastAt: row.createdAt,
|
|
23486
|
+
status: row.status,
|
|
23487
|
+
deviceId: row.deviceId
|
|
23488
|
+
});
|
|
23489
|
+
else if (seen.lastAt === row.createdAt && worse(seen.status, row.status) !== seen.status) byRule.set(row.ruleId, {
|
|
23490
|
+
...seen,
|
|
23491
|
+
status: row.status,
|
|
23492
|
+
deviceId: row.deviceId
|
|
23493
|
+
});
|
|
23494
|
+
}
|
|
23495
|
+
return {
|
|
23496
|
+
runs: [...byRule.values()],
|
|
23497
|
+
horizonMs: horizonOf(read, scan)
|
|
23498
|
+
};
|
|
23499
|
+
}
|
|
23500
|
+
/** How far back the answer really looked — see the module doc. */
|
|
23501
|
+
function horizonOf(read, scan) {
|
|
23502
|
+
if (read.truncated && read.oldestReadAt !== null) return Math.max(0, scan.now - read.oldestReadAt);
|
|
23503
|
+
return scan.windowMs;
|
|
23504
|
+
}
|
|
23505
|
+
//#endregion
|
|
23018
23506
|
//#region src/notification-center/durable-reread.ts
|
|
23019
23507
|
function isRecord$3(value) {
|
|
23020
23508
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -27979,9 +28467,14 @@ function outboxEntryToHistory(texts, entry) {
|
|
|
27979
28467
|
function renderAudioSpec(spec) {
|
|
27980
28468
|
return spec.mode === "label" ? `label:${spec.labels.join("+")}` : `level:${String(spec.dbThreshold)}dBFS|h${String(spec.hitPercent)}|s${String(spec.samplingSeconds)}`;
|
|
27981
28469
|
}
|
|
27982
|
-
/**
|
|
27983
|
-
|
|
27984
|
-
|
|
28470
|
+
/**
|
|
28471
|
+
* The camera scope in one token — `@all` is a real answer, not a missing one,
|
|
28472
|
+
* and `@all-except:<ids>` is a different one (D641): an excluding rule logged
|
|
28473
|
+
* as `@all` would claim the camera it never watches.
|
|
28474
|
+
*/
|
|
28475
|
+
function renderAudioScope(devices, excluded) {
|
|
28476
|
+
if (devices.length === 0) return excluded.length === 0 ? "@all" : `@all-except:${excluded.join("+")}`;
|
|
28477
|
+
return devices.filter((id) => !excluded.includes(id)).join("+");
|
|
27985
28478
|
}
|
|
27986
28479
|
var NotificationCenter = class NotificationCenter {
|
|
27987
28480
|
logger;
|
|
@@ -29723,7 +30216,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
29723
30216
|
* scope the evaluation will match on (`matchesDeviceScope`).
|
|
29724
30217
|
*/
|
|
29725
30218
|
beginLinkedStills(request) {
|
|
29726
|
-
const { sensorId, sensorKind, cameraIds, atMs, rowCameraIds } = request;
|
|
30219
|
+
const { sensorId, sensorKind, cameraIds, atMs, rowCameraIds, sensorRaised } = request;
|
|
29727
30220
|
const shelf = this.linkedStills;
|
|
29728
30221
|
if (shelf === null || !this.evaluationActive || cameraIds.length < 2) return void 0;
|
|
29729
30222
|
if (this.deviceMutes.isMuted(sensorId)) return void 0;
|
|
@@ -29733,7 +30226,13 @@ var NotificationCenter = class NotificationCenter {
|
|
|
29733
30226
|
const involved = [sensorId, ...rowShown];
|
|
29734
30227
|
if (!this.rules.listEnabled("device-event").some((rule) => {
|
|
29735
30228
|
if (rule.media.attach === "none") return false;
|
|
29736
|
-
if (!deviceEventRuleMayFireOnKind(rule, sensorKind)) return false;
|
|
30229
|
+
if (!deviceEventRuleMayFireOnKind(rule, sensorKind, sensorRaised)) return false;
|
|
30230
|
+
const excluded = rule.conditions.devicesExclude;
|
|
30231
|
+
if (excluded !== void 0 && deviceScopeExcludes(excluded, {
|
|
30232
|
+
deviceId: sensorId,
|
|
30233
|
+
linkedSensorId: sensorId,
|
|
30234
|
+
rowCameraIds: rowShown
|
|
30235
|
+
})) return false;
|
|
29737
30236
|
const devices = rule.conditions.devices;
|
|
29738
30237
|
return devices === void 0 || devices.length === 0 || devices.some((id) => involved.includes(id));
|
|
29739
30238
|
})) return void 0;
|
|
@@ -29855,6 +30354,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
29855
30354
|
if (!(mode === "condition" ? rule.conditions.audio !== void 0 : rule.conditions.audio === void 0 && referencesAudioClass(rule.conditions.classes))) continue;
|
|
29856
30355
|
const devices = rule.conditions.devices;
|
|
29857
30356
|
if (devices !== void 0 && devices.length > 0 && !devices.includes(deviceId)) continue;
|
|
30357
|
+
const excluded = rule.conditions.devicesExclude;
|
|
30358
|
+
if (excluded !== void 0 && deviceScopeExcludes(excluded, { deviceId })) continue;
|
|
29858
30359
|
return shelf.capture(deviceId, atMs, "audio");
|
|
29859
30360
|
}
|
|
29860
30361
|
}
|
|
@@ -29957,6 +30458,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
29957
30458
|
if (occ === void 0 || !matchesOccupancy(occ, subject)) continue;
|
|
29958
30459
|
const devices = rule.conditions.devices;
|
|
29959
30460
|
if (devices !== void 0 && devices.length > 0 && !devices.includes(edge.deviceId)) continue;
|
|
30461
|
+
const excluded = rule.conditions.devicesExclude;
|
|
30462
|
+
if (excluded !== void 0 && deviceScopeExcludes(excluded, { deviceId: edge.deviceId })) continue;
|
|
29960
30463
|
return shelf.capture(edge.deviceId, edge.timestamp, "occupancy");
|
|
29961
30464
|
}
|
|
29962
30465
|
}
|
|
@@ -30088,6 +30591,18 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30088
30591
|
getRule: async ({ ruleId }) => ({ rule: this.rules.get(ruleId) }),
|
|
30089
30592
|
createRule: async ({ rule, caller }) => {
|
|
30090
30593
|
assertRuleHasAddressee(rule.targets, rule.targetUsers);
|
|
30594
|
+
guardRuleFamily(rule, {
|
|
30595
|
+
logger: this.logger,
|
|
30596
|
+
by: caller.userId
|
|
30597
|
+
});
|
|
30598
|
+
guardRuleSensorTransition(rule, {
|
|
30599
|
+
logger: this.logger,
|
|
30600
|
+
by: caller.userId
|
|
30601
|
+
});
|
|
30602
|
+
guardRuleDevicesExclude(rule, {
|
|
30603
|
+
logger: this.logger,
|
|
30604
|
+
by: caller.userId
|
|
30605
|
+
});
|
|
30091
30606
|
await this.validateTargetRefs(rule.targets.map((t) => t.targetId));
|
|
30092
30607
|
const created = await this.rules.create(rule, caller.userId);
|
|
30093
30608
|
this.logger.info("notification rule created", { meta: {
|
|
@@ -30099,7 +30614,23 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30099
30614
|
},
|
|
30100
30615
|
updateRule: async ({ ruleId, patch, clear, caller }) => {
|
|
30101
30616
|
if (patch.targets !== void 0) await this.validateTargetRefs(patch.targets.map((t) => t.targetId));
|
|
30102
|
-
|
|
30617
|
+
const existing = this.rules.get(ruleId);
|
|
30618
|
+
assertPatchKeepsAddressee(existing, patch, clear ?? []);
|
|
30619
|
+
guardPatchFamily(existing, patch, {
|
|
30620
|
+
logger: this.logger,
|
|
30621
|
+
by: caller.userId,
|
|
30622
|
+
ruleId
|
|
30623
|
+
});
|
|
30624
|
+
guardPatchSensorTransition(existing, patch, {
|
|
30625
|
+
logger: this.logger,
|
|
30626
|
+
by: caller.userId,
|
|
30627
|
+
ruleId
|
|
30628
|
+
});
|
|
30629
|
+
guardPatchDevicesExclude(existing, patch, {
|
|
30630
|
+
logger: this.logger,
|
|
30631
|
+
by: caller.userId,
|
|
30632
|
+
ruleId
|
|
30633
|
+
});
|
|
30103
30634
|
const { disabledTargetIds: _optOut, ...safePatch } = patch;
|
|
30104
30635
|
const updated = await this.rules.update(ruleId, safePatch, clear ?? []);
|
|
30105
30636
|
this.logger.info("notification rule updated", { meta: {
|
|
@@ -30141,6 +30672,14 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30141
30672
|
}
|
|
30142
30673
|
return { entries: mapped };
|
|
30143
30674
|
},
|
|
30675
|
+
getRuleLastRuns: async () => {
|
|
30676
|
+
const now = this.now();
|
|
30677
|
+
const windowMs = (await this.deps.pruneWindows()).outboxMs;
|
|
30678
|
+
return summariseRuleLastRuns(await this.outbox.queryLastRunRows(windowMs !== null ? now - windowMs : null, NC_RULE_LAST_RUNS_SCAN_LIMIT), {
|
|
30679
|
+
now,
|
|
30680
|
+
windowMs
|
|
30681
|
+
});
|
|
30682
|
+
},
|
|
30144
30683
|
resolveArtifactUrl: async ({ artifactId }) => ({ url: await this.deps.resolveArtifactUrl?.(artifactId) ?? null }),
|
|
30145
30684
|
listDeviceMutes: async () => ({ mutedDeviceIds: this.deviceMutes.listMuted() }),
|
|
30146
30685
|
setDeviceMuted: async ({ deviceId, muted }) => {
|
|
@@ -30472,6 +31011,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30472
31011
|
coverage: this.coverageFor(panel).map((c) => ({
|
|
30473
31012
|
...c,
|
|
30474
31013
|
deviceIds: [...c.deviceIds],
|
|
31014
|
+
allDevicesExcept: [...c.allDevicesExcept],
|
|
30475
31015
|
skippedDevices: c.skippedDevices.map((s) => ({ ...s }))
|
|
30476
31016
|
}))
|
|
30477
31017
|
};
|
|
@@ -30981,6 +31521,9 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30981
31521
|
failed: evaluation.failedCondition,
|
|
30982
31522
|
classes: subject.classNames,
|
|
30983
31523
|
...subject.sensorEdge !== void 0 ? { sensorEdge: subject.sensorEdge } : {},
|
|
31524
|
+
...subject.sensorRaised !== void 0 ? { sensorRaised: subject.sensorRaised } : {},
|
|
31525
|
+
...subject.sourceDeviceId !== void 0 ? { sourceDeviceId: subject.sourceDeviceId } : {},
|
|
31526
|
+
...subject.linkedSensorId !== void 0 ? { linkedSensorId: subject.linkedSensorId } : {},
|
|
30984
31527
|
eventId: subject.recordId,
|
|
30985
31528
|
...subject.trackId !== void 0 ? { trackId: subject.trackId } : {}
|
|
30986
31529
|
}
|
|
@@ -31090,7 +31633,11 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31090
31633
|
eventId: subject.recordId,
|
|
31091
31634
|
...subject.trackId !== void 0 ? { trackId: subject.trackId } : {},
|
|
31092
31635
|
...subject.confidence !== void 0 ? { confidence: subject.confidence } : {},
|
|
31093
|
-
...subject.zones.length > 0 ? { zones: subject.zones } : {}
|
|
31636
|
+
...subject.zones.length > 0 ? { zones: subject.zones } : {},
|
|
31637
|
+
...rule.conditions.sensorTransition !== void 0 ? {
|
|
31638
|
+
sensorTransition: rule.conditions.sensorTransition,
|
|
31639
|
+
sensorRaised: subject.sensorRaised ?? null
|
|
31640
|
+
} : {}
|
|
31094
31641
|
}
|
|
31095
31642
|
});
|
|
31096
31643
|
const unresolvable = unresolvableButtons(rule.actions);
|
|
@@ -31697,7 +32244,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31697
32244
|
ruleId: rule.id,
|
|
31698
32245
|
ruleName: rule.name,
|
|
31699
32246
|
spec,
|
|
31700
|
-
devices: rule.conditions.devices ?? []
|
|
32247
|
+
devices: rule.conditions.devices ?? [],
|
|
32248
|
+
excluded: rule.conditions.devicesExclude ?? []
|
|
31701
32249
|
});
|
|
31702
32250
|
}
|
|
31703
32251
|
this.audioWatcher.setWatchedSpecs(audioSpecs);
|
|
@@ -31789,10 +32337,12 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31789
32337
|
* for at all"; the per-camera line answers "why is 617 not notifying" — and
|
|
31790
32338
|
* that question is ALWAYS asked per camera, so the line carries
|
|
31791
32339
|
* `tags.deviceId` and a rule scoped to no camera is reported as watching
|
|
31792
|
-
* every one rather than silently omitted
|
|
32340
|
+
* every one rather than silently omitted — every one EXCEPT its
|
|
32341
|
+
* `devicesExclude` cameras (D641), each of which gets its own NOT-watching
|
|
32342
|
+
* line on that camera.
|
|
31793
32343
|
*/
|
|
31794
32344
|
reportAudioWatch(entries) {
|
|
31795
|
-
const rendered = entries.map((e) => `${e.ruleId}=${renderAudioSpec(e.spec)}@${renderAudioScope(e.devices)}`).toSorted().join(",");
|
|
32345
|
+
const rendered = entries.map((e) => `${e.ruleId}=${renderAudioSpec(e.spec)}@${renderAudioScope(e.devices, e.excluded)}`).toSorted().join(",");
|
|
31796
32346
|
if (rendered === this.lastAudioWatchReport) return;
|
|
31797
32347
|
this.lastAudioWatchReport = rendered;
|
|
31798
32348
|
this.logger.info("audio watched specs", { meta: {
|
|
@@ -31806,14 +32356,28 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31806
32356
|
mode: entry.spec.mode,
|
|
31807
32357
|
spec: renderAudioSpec(entry.spec)
|
|
31808
32358
|
};
|
|
32359
|
+
for (const deviceId of entry.excluded) this.logger.info("audio rule NOT watching camera", {
|
|
32360
|
+
tags: { deviceId },
|
|
32361
|
+
meta: {
|
|
32362
|
+
...meta,
|
|
32363
|
+
reason: "devicesExclude"
|
|
32364
|
+
}
|
|
32365
|
+
});
|
|
31809
32366
|
if (entry.devices.length === 0) {
|
|
31810
|
-
this.logger.info("audio rule watching every camera", { meta });
|
|
32367
|
+
if (entry.excluded.length === 0) this.logger.info("audio rule watching every camera", { meta });
|
|
32368
|
+
else this.logger.info("audio rule watching every camera except the excluded", { meta: {
|
|
32369
|
+
...meta,
|
|
32370
|
+
excluded: [...entry.excluded]
|
|
32371
|
+
} });
|
|
31811
32372
|
continue;
|
|
31812
32373
|
}
|
|
31813
|
-
for (const deviceId of entry.devices)
|
|
31814
|
-
|
|
31815
|
-
|
|
31816
|
-
|
|
32374
|
+
for (const deviceId of entry.devices) {
|
|
32375
|
+
if (entry.excluded.includes(deviceId)) continue;
|
|
32376
|
+
this.logger.info("audio rule watching camera", {
|
|
32377
|
+
tags: { deviceId },
|
|
32378
|
+
meta
|
|
32379
|
+
});
|
|
32380
|
+
}
|
|
31817
32381
|
}
|
|
31818
32382
|
}
|
|
31819
32383
|
/** Boot reseed of confirmed occupancy edge-state (durability, constraint 4) —
|
|
@@ -89201,10 +89765,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
89201
89765
|
startLinkedStills(data, timestamp, cameras) {
|
|
89202
89766
|
const sensorKind = EVENT_KIND_BY_CAP[data.capName]?.kind;
|
|
89203
89767
|
if (sensorKind === void 0) return void 0;
|
|
89768
|
+
const sensorRaised = sensorRaisedOf(sensorKind, sensorSliceRecord(data.slice));
|
|
89204
89769
|
try {
|
|
89205
89770
|
return this.notificationCenter?.beginLinkedStills({
|
|
89206
89771
|
sensorId: data.deviceId,
|
|
89207
89772
|
sensorKind,
|
|
89773
|
+
...sensorRaised !== void 0 ? { sensorRaised } : {},
|
|
89208
89774
|
cameraIds: cameras.mediaCameraIds,
|
|
89209
89775
|
rowCameraIds: cameras.rowCameraIds,
|
|
89210
89776
|
atMs: timestamp
|