@camstack/addon-post-analysis 1.2.25 → 1.2.26
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-Bi1fiPc0.js → dist-CP-FTAl5.js} +83 -5
- package/dist/{dist-DsCSHZ9N.mjs → dist-O227Je-Q.mjs} +78 -6
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-DFKBSXUx.js → node-BQUEteve.js} +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-QDqT3XHa.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-aG07ehD0.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-DvTbKmnz.mjs → hostInit-66T4FwIA.mjs} +1 -1
- package/dist/pipeline-analytics/index.js +87 -15
- package/dist/pipeline-analytics/index.mjs +86 -14
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -8414,6 +8414,60 @@ function buildNcTaxonomy() {
|
|
|
8414
8414
|
/** The frozen NC taxonomy, derived once from the taxonomy dictionary. */
|
|
8415
8415
|
var NC_TAXONOMY = Object.freeze(buildNcTaxonomy());
|
|
8416
8416
|
/**
|
|
8417
|
+
* Most specific first. Extending this list is how a new device kind becomes
|
|
8418
|
+
* gateable; nothing else needs to change.
|
|
8419
|
+
*/
|
|
8420
|
+
var DEVICE_STATE_READERS = [
|
|
8421
|
+
{
|
|
8422
|
+
cap: "alarm-panel",
|
|
8423
|
+
field: "state"
|
|
8424
|
+
},
|
|
8425
|
+
{
|
|
8426
|
+
cap: "cover",
|
|
8427
|
+
field: "state"
|
|
8428
|
+
},
|
|
8429
|
+
{
|
|
8430
|
+
cap: "presence",
|
|
8431
|
+
field: "state"
|
|
8432
|
+
},
|
|
8433
|
+
{
|
|
8434
|
+
cap: "lock",
|
|
8435
|
+
field: "locked",
|
|
8436
|
+
booleanWords: ["locked", "unlocked"]
|
|
8437
|
+
},
|
|
8438
|
+
{
|
|
8439
|
+
cap: "contact",
|
|
8440
|
+
field: "entryOpen",
|
|
8441
|
+
booleanWords: ["open", "closed"]
|
|
8442
|
+
},
|
|
8443
|
+
{
|
|
8444
|
+
cap: "switch",
|
|
8445
|
+
field: "on",
|
|
8446
|
+
booleanWords: ["on", "off"]
|
|
8447
|
+
},
|
|
8448
|
+
{
|
|
8449
|
+
cap: "binary",
|
|
8450
|
+
field: "on",
|
|
8451
|
+
booleanWords: ["on", "off"]
|
|
8452
|
+
}
|
|
8453
|
+
];
|
|
8454
|
+
/**
|
|
8455
|
+
* Collapse a device's full runtime state to the one string a rule compares
|
|
8456
|
+
* against, or `undefined` when nothing in the table applies.
|
|
8457
|
+
*
|
|
8458
|
+
* `undefined` is the safe answer everywhere: the gate treats it as "does not
|
|
8459
|
+
* match", so a device whose kind we cannot read simply never arms a rule.
|
|
8460
|
+
*/
|
|
8461
|
+
function readDeviceStateFrom(runtimeState) {
|
|
8462
|
+
for (const reader of DEVICE_STATE_READERS) {
|
|
8463
|
+
const slice = runtimeState[reader.cap];
|
|
8464
|
+
if (slice === null || typeof slice !== "object") continue;
|
|
8465
|
+
const value = slice[reader.field];
|
|
8466
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
8467
|
+
if (typeof value === "boolean" && reader.booleanWords !== void 0) return value ? reader.booleanWords[0] : reader.booleanWords[1];
|
|
8468
|
+
}
|
|
8469
|
+
}
|
|
8470
|
+
/**
|
|
8417
8471
|
* Error types for the safe expression engine. Two distinct classes so callers
|
|
8418
8472
|
* can tell a compile-time (grammar) failure from a runtime (evaluation)
|
|
8419
8473
|
* failure — both are non-fatal to the host: read paths degrade to "skip link".
|
|
@@ -9236,11 +9290,13 @@ var NcZoneConditionSchema = object({
|
|
|
9236
9290
|
/** Quantifier over `ids` — at least one / every one visited. */
|
|
9237
9291
|
match: _enum(["any", "all"]).default("any")
|
|
9238
9292
|
});
|
|
9239
|
-
/**
|
|
9240
|
-
* The P1 condition set — a flat AND of groups; absent group = pass;
|
|
9241
|
-
* membership lists are OR within the list (spec §2.3).
|
|
9242
|
-
*/
|
|
9243
9293
|
var NcConditionsSchema = object({
|
|
9294
|
+
/** Gate on ANOTHER device's current state (the alarm armed, a switch on). */
|
|
9295
|
+
deviceState: object({
|
|
9296
|
+
deviceId: number().int(),
|
|
9297
|
+
/** Any of these matches. */
|
|
9298
|
+
states: array(string().min(1)).min(1)
|
|
9299
|
+
}).optional(),
|
|
9244
9300
|
/** Device scope — absent = all devices. */
|
|
9245
9301
|
devices: array(number()).optional(),
|
|
9246
9302
|
/** Detector class names (any overlap with the record's class set). */
|
|
@@ -9605,7 +9661,8 @@ var NcConditionDescriptorSchema = object({
|
|
|
9605
9661
|
"packagePhase",
|
|
9606
9662
|
"crossingSelect",
|
|
9607
9663
|
"polygonDraw",
|
|
9608
|
-
"occupancy"
|
|
9664
|
+
"occupancy",
|
|
9665
|
+
"deviceState"
|
|
9609
9666
|
]),
|
|
9610
9667
|
operator: _enum([
|
|
9611
9668
|
"in",
|
|
@@ -9864,6 +9921,21 @@ var NC_CONDITION_CATALOG = [
|
|
|
9864
9921
|
phase: "P1",
|
|
9865
9922
|
description: "pipeline / onboard / sensor; a record with no stamped source counts as pipeline."
|
|
9866
9923
|
},
|
|
9924
|
+
{
|
|
9925
|
+
id: "deviceState",
|
|
9926
|
+
group: "scope",
|
|
9927
|
+
label: "Device state",
|
|
9928
|
+
valueType: "deviceState",
|
|
9929
|
+
operator: "in",
|
|
9930
|
+
appliesTo: [
|
|
9931
|
+
"immediate",
|
|
9932
|
+
"track-end",
|
|
9933
|
+
"device-event",
|
|
9934
|
+
"package-event"
|
|
9935
|
+
],
|
|
9936
|
+
phase: "P2",
|
|
9937
|
+
description: "Only fire while another device is in one of the chosen states — the alarm armed, a switch on, a contact closed. A state that cannot be read does NOT fire."
|
|
9938
|
+
},
|
|
9867
9939
|
{
|
|
9868
9940
|
id: "sensorKinds",
|
|
9869
9941
|
group: "device",
|
|
@@ -28021,6 +28093,12 @@ Object.defineProperty(exports, "plateGalleryCapability", {
|
|
|
28021
28093
|
return plateGalleryCapability;
|
|
28022
28094
|
}
|
|
28023
28095
|
});
|
|
28096
|
+
Object.defineProperty(exports, "readDeviceStateFrom", {
|
|
28097
|
+
enumerable: true,
|
|
28098
|
+
get: function() {
|
|
28099
|
+
return readDeviceStateFrom;
|
|
28100
|
+
}
|
|
28101
|
+
});
|
|
28024
28102
|
Object.defineProperty(exports, "string", {
|
|
28025
28103
|
enumerable: true,
|
|
28026
28104
|
get: function() {
|
|
@@ -8392,6 +8392,60 @@ function buildNcTaxonomy() {
|
|
|
8392
8392
|
/** The frozen NC taxonomy, derived once from the taxonomy dictionary. */
|
|
8393
8393
|
var NC_TAXONOMY = Object.freeze(buildNcTaxonomy());
|
|
8394
8394
|
/**
|
|
8395
|
+
* Most specific first. Extending this list is how a new device kind becomes
|
|
8396
|
+
* gateable; nothing else needs to change.
|
|
8397
|
+
*/
|
|
8398
|
+
var DEVICE_STATE_READERS = [
|
|
8399
|
+
{
|
|
8400
|
+
cap: "alarm-panel",
|
|
8401
|
+
field: "state"
|
|
8402
|
+
},
|
|
8403
|
+
{
|
|
8404
|
+
cap: "cover",
|
|
8405
|
+
field: "state"
|
|
8406
|
+
},
|
|
8407
|
+
{
|
|
8408
|
+
cap: "presence",
|
|
8409
|
+
field: "state"
|
|
8410
|
+
},
|
|
8411
|
+
{
|
|
8412
|
+
cap: "lock",
|
|
8413
|
+
field: "locked",
|
|
8414
|
+
booleanWords: ["locked", "unlocked"]
|
|
8415
|
+
},
|
|
8416
|
+
{
|
|
8417
|
+
cap: "contact",
|
|
8418
|
+
field: "entryOpen",
|
|
8419
|
+
booleanWords: ["open", "closed"]
|
|
8420
|
+
},
|
|
8421
|
+
{
|
|
8422
|
+
cap: "switch",
|
|
8423
|
+
field: "on",
|
|
8424
|
+
booleanWords: ["on", "off"]
|
|
8425
|
+
},
|
|
8426
|
+
{
|
|
8427
|
+
cap: "binary",
|
|
8428
|
+
field: "on",
|
|
8429
|
+
booleanWords: ["on", "off"]
|
|
8430
|
+
}
|
|
8431
|
+
];
|
|
8432
|
+
/**
|
|
8433
|
+
* Collapse a device's full runtime state to the one string a rule compares
|
|
8434
|
+
* against, or `undefined` when nothing in the table applies.
|
|
8435
|
+
*
|
|
8436
|
+
* `undefined` is the safe answer everywhere: the gate treats it as "does not
|
|
8437
|
+
* match", so a device whose kind we cannot read simply never arms a rule.
|
|
8438
|
+
*/
|
|
8439
|
+
function readDeviceStateFrom(runtimeState) {
|
|
8440
|
+
for (const reader of DEVICE_STATE_READERS) {
|
|
8441
|
+
const slice = runtimeState[reader.cap];
|
|
8442
|
+
if (slice === null || typeof slice !== "object") continue;
|
|
8443
|
+
const value = slice[reader.field];
|
|
8444
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
8445
|
+
if (typeof value === "boolean" && reader.booleanWords !== void 0) return value ? reader.booleanWords[0] : reader.booleanWords[1];
|
|
8446
|
+
}
|
|
8447
|
+
}
|
|
8448
|
+
/**
|
|
8395
8449
|
* Error types for the safe expression engine. Two distinct classes so callers
|
|
8396
8450
|
* can tell a compile-time (grammar) failure from a runtime (evaluation)
|
|
8397
8451
|
* failure — both are non-fatal to the host: read paths degrade to "skip link".
|
|
@@ -9214,11 +9268,13 @@ var NcZoneConditionSchema = object({
|
|
|
9214
9268
|
/** Quantifier over `ids` — at least one / every one visited. */
|
|
9215
9269
|
match: _enum(["any", "all"]).default("any")
|
|
9216
9270
|
});
|
|
9217
|
-
/**
|
|
9218
|
-
* The P1 condition set — a flat AND of groups; absent group = pass;
|
|
9219
|
-
* membership lists are OR within the list (spec §2.3).
|
|
9220
|
-
*/
|
|
9221
9271
|
var NcConditionsSchema = object({
|
|
9272
|
+
/** Gate on ANOTHER device's current state (the alarm armed, a switch on). */
|
|
9273
|
+
deviceState: object({
|
|
9274
|
+
deviceId: number().int(),
|
|
9275
|
+
/** Any of these matches. */
|
|
9276
|
+
states: array(string().min(1)).min(1)
|
|
9277
|
+
}).optional(),
|
|
9222
9278
|
/** Device scope — absent = all devices. */
|
|
9223
9279
|
devices: array(number()).optional(),
|
|
9224
9280
|
/** Detector class names (any overlap with the record's class set). */
|
|
@@ -9583,7 +9639,8 @@ var NcConditionDescriptorSchema = object({
|
|
|
9583
9639
|
"packagePhase",
|
|
9584
9640
|
"crossingSelect",
|
|
9585
9641
|
"polygonDraw",
|
|
9586
|
-
"occupancy"
|
|
9642
|
+
"occupancy",
|
|
9643
|
+
"deviceState"
|
|
9587
9644
|
]),
|
|
9588
9645
|
operator: _enum([
|
|
9589
9646
|
"in",
|
|
@@ -9842,6 +9899,21 @@ var NC_CONDITION_CATALOG = [
|
|
|
9842
9899
|
phase: "P1",
|
|
9843
9900
|
description: "pipeline / onboard / sensor; a record with no stamped source counts as pipeline."
|
|
9844
9901
|
},
|
|
9902
|
+
{
|
|
9903
|
+
id: "deviceState",
|
|
9904
|
+
group: "scope",
|
|
9905
|
+
label: "Device state",
|
|
9906
|
+
valueType: "deviceState",
|
|
9907
|
+
operator: "in",
|
|
9908
|
+
appliesTo: [
|
|
9909
|
+
"immediate",
|
|
9910
|
+
"track-end",
|
|
9911
|
+
"device-event",
|
|
9912
|
+
"package-event"
|
|
9913
|
+
],
|
|
9914
|
+
phase: "P2",
|
|
9915
|
+
description: "Only fire while another device is in one of the chosen states — the alarm armed, a switch on, a contact closed. A state that cannot be read does NOT fire."
|
|
9916
|
+
},
|
|
9845
9917
|
{
|
|
9846
9918
|
id: "sensorKinds",
|
|
9847
9919
|
group: "device",
|
|
@@ -27741,4 +27813,4 @@ Object.freeze({
|
|
|
27741
27813
|
"smtp-provider": "email"
|
|
27742
27814
|
});
|
|
27743
27815
|
//#endregion
|
|
27744
|
-
export {
|
|
27816
|
+
export { readDeviceStateFrom as A, _enum as B, defineCustomActions as C, notificationRulesCapability as D, hfModelUrl as E, BaseAddon as F, object as G, boolean as H, DeviceType as I, string as K, createEvent as L, videoclipsCapability as M, zoneAnalyticsCapability as N, pipelineAnalyticsCapability as O, errMsg as P, hydrateSchema as R, customAction as S, faceGalleryCapability as T, literal as U, array as V, number as W, TimelapseRuleSchema as _, NC_CONDITION_CATALOG as a, buildEventKindDescriptor as b, NcRuleInputSchema as c, NcSnoozeInputSchema as d, NcSnoozeSchema as f, TimelapseRuleInputSchema as g, OpsLogEntrySchema as h, MACRO_LABELS as i, subKindsOf as j, plateGalleryCapability as k, NcRulePatchSchema as l, NcTaxonomySchema as m, EVENT_KIND_BY_CAP as n, NC_TAXONOMY as o, NcSnoozeSuppressedSchema as p, EventCategory as q, EVENT_PAD_MS as r, NcConditionDescriptorSchema as s, DEFAULT_EVENT_COLOR as t, NcRuleSchema as u, addonWidgetsSourceCapability as v, embeddingEncoderCapability as w, cosineSimilarity as x, audioMetricsCapability as y, nodePin as z };
|
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-CP-FTAl5.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
let node_fs$1 = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
node_fs = require_dist.__toESM(node_fs);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { E as hfModelUrl,
|
|
1
|
+
import { E as hfModelUrl, F as BaseAddon, w as embeddingEncoderCapability } from "../dist-O227Je-Q.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import "node:fs";
|
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-CP-FTAl5.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
let node_path = require("node:path");
|
|
8
8
|
node_path = require_dist.__toESM(node_path);
|
|
@@ -4758,17 +4758,7 @@ function referencesAudioClass(classes) {
|
|
|
4758
4758
|
if (classes === void 0) return false;
|
|
4759
4759
|
return classes.some((c) => c.trim().toLowerCase().startsWith("audio-"));
|
|
4760
4760
|
}
|
|
4761
|
-
|
|
4762
|
-
* Evaluate one rule against one subject. The rule's `delivery` must match
|
|
4763
|
-
* the subject kind (`immediate` ↔ `object-event`, `track-end` ↔
|
|
4764
|
-
* `track-end`) — a mismatch fails with `'delivery'`. Schedule is evaluated
|
|
4765
|
-
* at the SUBJECT timestamp (the persist moment), so boot-time reconcile of
|
|
4766
|
-
* missed records applies the schedule the record lived under.
|
|
4767
|
-
*
|
|
4768
|
-
* Throttle/cooldown is deliberately NOT part of this function — cooldown
|
|
4769
|
-
* is stateful; see {@link cooldownKey} / {@link isCoolingDown}.
|
|
4770
|
-
*/
|
|
4771
|
-
function evaluateRule(rule, subject) {
|
|
4761
|
+
function evaluateRule(rule, subject, deviceState) {
|
|
4772
4762
|
if (subject.kind === "occupancy-event") {
|
|
4773
4763
|
if (rule.delivery !== "device-event") return fail("delivery");
|
|
4774
4764
|
const occ = rule.conditions.occupancy;
|
|
@@ -4783,6 +4773,11 @@ function evaluateRule(rule, subject) {
|
|
|
4783
4773
|
if (rule.conditions.occupancy !== void 0) return fail("occupancy");
|
|
4784
4774
|
}
|
|
4785
4775
|
const c = rule.conditions;
|
|
4776
|
+
if (c.deviceState !== void 0) {
|
|
4777
|
+
const current = deviceState?.(c.deviceState.deviceId);
|
|
4778
|
+
if (current === void 0) return fail("deviceState");
|
|
4779
|
+
if (!toLowerSet(c.deviceState.states).has(current.trim().toLowerCase())) return fail("deviceState");
|
|
4780
|
+
}
|
|
4786
4781
|
if (c.devices !== void 0 && c.devices.length > 0 && !c.devices.includes(subject.deviceId)) return fail("devices");
|
|
4787
4782
|
if (c.source !== void 0 && c.source !== "any") {
|
|
4788
4783
|
if ((subject.source ?? "pipeline") !== c.source) return fail("source");
|
|
@@ -6612,6 +6607,53 @@ var NcRuleStore = class {
|
|
|
6612
6607
|
});
|
|
6613
6608
|
}
|
|
6614
6609
|
};
|
|
6610
|
+
var NcDeviceStateCache = class {
|
|
6611
|
+
deps;
|
|
6612
|
+
states = /* @__PURE__ */ new Map();
|
|
6613
|
+
lastOkAt = 0;
|
|
6614
|
+
ids = [];
|
|
6615
|
+
now;
|
|
6616
|
+
constructor(deps) {
|
|
6617
|
+
this.deps = deps;
|
|
6618
|
+
this.now = deps.now ?? (() => Date.now());
|
|
6619
|
+
}
|
|
6620
|
+
/**
|
|
6621
|
+
* The device ids worth reading — recomputed from the rule set, so a cluster
|
|
6622
|
+
* with no gated rule does no work at all.
|
|
6623
|
+
*/
|
|
6624
|
+
setWatched(ids) {
|
|
6625
|
+
this.ids = [...new Set(ids)].sort((a, b) => a - b);
|
|
6626
|
+
}
|
|
6627
|
+
watched() {
|
|
6628
|
+
return this.ids;
|
|
6629
|
+
}
|
|
6630
|
+
/** Hot-path read. No I/O, no await — this is called per event. */
|
|
6631
|
+
get(deviceId) {
|
|
6632
|
+
if (this.now() - this.lastOkAt > 3e5) return void 0;
|
|
6633
|
+
return this.states.get(deviceId);
|
|
6634
|
+
}
|
|
6635
|
+
/**
|
|
6636
|
+
* Re-read every watched state. Never throws: a gate that could fail the
|
|
6637
|
+
* evaluation tick would take the notification pipeline with it.
|
|
6638
|
+
*/
|
|
6639
|
+
async refresh() {
|
|
6640
|
+
if (this.ids.length === 0) {
|
|
6641
|
+
this.states = /* @__PURE__ */ new Map();
|
|
6642
|
+
return;
|
|
6643
|
+
}
|
|
6644
|
+
try {
|
|
6645
|
+
const next = await this.deps.readStates(this.ids);
|
|
6646
|
+
this.states = new Map(next);
|
|
6647
|
+
this.lastOkAt = this.now();
|
|
6648
|
+
for (const id of this.ids) if (!next.has(id)) this.deps.logger.info("device-state gate: no state for a watched device", { tags: { deviceId: id } });
|
|
6649
|
+
} catch (err) {
|
|
6650
|
+
this.deps.logger.warn("device-state refresh failed — keeping the previous states until they age out", { meta: {
|
|
6651
|
+
watched: this.ids.length,
|
|
6652
|
+
error: String(err)
|
|
6653
|
+
} });
|
|
6654
|
+
}
|
|
6655
|
+
}
|
|
6656
|
+
};
|
|
6615
6657
|
//#endregion
|
|
6616
6658
|
//#region src/notification-center/snooze-digest.ts
|
|
6617
6659
|
/** `14:03` in the node's local zone — a digest is read by a person, at home. */
|
|
@@ -7445,6 +7487,9 @@ var NotificationCenter = class NotificationCenter {
|
|
|
7445
7487
|
/** Snooze windows + what they hid. Mirrored into RAM; the dispatcher's gate
|
|
7446
7488
|
* reads it with no I/O, the drain tick refreshes it. */
|
|
7447
7489
|
snoozes;
|
|
7490
|
+
/** States the `deviceState` rule gate reads. Refreshed on the reload tick;
|
|
7491
|
+
* read with no I/O on the persist hook. */
|
|
7492
|
+
deviceStates;
|
|
7448
7493
|
/** True when ≥1 enabled `device-event` rule declares an occupancy condition —
|
|
7449
7494
|
* the watcher is idle (zero per-frame cost) otherwise. */
|
|
7450
7495
|
occupancyEnabled = false;
|
|
@@ -7477,6 +7522,11 @@ var NotificationCenter = class NotificationCenter {
|
|
|
7477
7522
|
store: deps.store,
|
|
7478
7523
|
logger: this.logger.child("occupancy")
|
|
7479
7524
|
});
|
|
7525
|
+
this.deviceStates = new NcDeviceStateCache({
|
|
7526
|
+
logger: this.logger.child("device-state"),
|
|
7527
|
+
readStates: deps.readDeviceStates ?? (async () => /* @__PURE__ */ new Map()),
|
|
7528
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
7529
|
+
});
|
|
7480
7530
|
this.snoozes = new NcSnoozeStore({
|
|
7481
7531
|
store: deps.store,
|
|
7482
7532
|
logger: this.logger.child("snooze"),
|
|
@@ -7531,6 +7581,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
7531
7581
|
await this.snoozes.load();
|
|
7532
7582
|
this.refreshOccupancyWatch();
|
|
7533
7583
|
if (!opts.evaluation) return;
|
|
7584
|
+
await this.deviceStates.refresh();
|
|
7534
7585
|
this.evaluationActive = true;
|
|
7535
7586
|
await this.outbox.load();
|
|
7536
7587
|
await this.seedCooldowns();
|
|
@@ -7802,7 +7853,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
7802
7853
|
let anyMatched = false;
|
|
7803
7854
|
const rejections = [];
|
|
7804
7855
|
for (const rule of candidates) {
|
|
7805
|
-
const evaluation = evaluateRule(rule, subject);
|
|
7856
|
+
const evaluation = evaluateRule(rule, subject, (id) => this.deviceStates.get(id));
|
|
7806
7857
|
if (!evaluation.matched) {
|
|
7807
7858
|
rejections.push(`${rule.name}:${evaluation.failedCondition ?? "unknown"}`);
|
|
7808
7859
|
this.logger.debug("rule did not match", {
|
|
@@ -8003,6 +8054,13 @@ var NotificationCenter = class NotificationCenter {
|
|
|
8003
8054
|
}
|
|
8004
8055
|
this.occupancyWatcher.setWatchedKeys(specs);
|
|
8005
8056
|
this.occupancyEnabled = specs.length > 0;
|
|
8057
|
+
const gated = [];
|
|
8058
|
+
for (const rule of this.rules.list()) {
|
|
8059
|
+
if (!rule.enabled) continue;
|
|
8060
|
+
const ds = rule.conditions.deviceState;
|
|
8061
|
+
if (ds !== void 0) gated.push(ds.deviceId);
|
|
8062
|
+
}
|
|
8063
|
+
this.deviceStates.setWatched(gated);
|
|
8006
8064
|
}
|
|
8007
8065
|
/** Boot reseed of confirmed occupancy edge-state (durability, constraint 4) —
|
|
8008
8066
|
* hydrate the watcher from the store, then prune orphaned durable rows to the
|
|
@@ -8019,6 +8077,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
8019
8077
|
await this.rules.load();
|
|
8020
8078
|
this.refreshOccupancyWatch();
|
|
8021
8079
|
if (!this.evaluationActive) return;
|
|
8080
|
+
await this.deviceStates.refresh();
|
|
8022
8081
|
await this.occupancyStore.pruneExcept(this.activeOccupancyKeys());
|
|
8023
8082
|
}
|
|
8024
8083
|
/** The full-key set the watcher currently tracks (post prune) — the bound for
|
|
@@ -8246,7 +8305,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
8246
8305
|
const results = [];
|
|
8247
8306
|
for (const subject of subjects.sort((a, b) => b.timestamp - a.timestamp)) {
|
|
8248
8307
|
if (results.length >= TEST_RULE_MAX_RESULTS) break;
|
|
8249
|
-
const evaluation = evaluateRule(rule, subject);
|
|
8308
|
+
const evaluation = evaluateRule(rule, subject, (id) => this.deviceStates.get(id));
|
|
8250
8309
|
results.push({
|
|
8251
8310
|
recordId: subject.recordId,
|
|
8252
8311
|
recordKind: subject.kind === "track-end" ? "track" : subject.kind === "audio-event" ? "object-event" : subject.kind === "occupancy-event" ? "device-event" : subject.kind,
|
|
@@ -21841,7 +21900,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
21841
21900
|
let storage = this.ctx.kernel.storage;
|
|
21842
21901
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
21843
21902
|
if (mediaRoot) {
|
|
21844
|
-
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-
|
|
21903
|
+
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-BQUEteve.js"));
|
|
21845
21904
|
storage = new FilesystemStorageProvider(mediaRoot, { eventMedia: mediaRoot });
|
|
21846
21905
|
logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
|
|
21847
21906
|
}
|
|
@@ -22198,6 +22257,19 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
22198
22257
|
addonId: device.addonId
|
|
22199
22258
|
};
|
|
22200
22259
|
},
|
|
22260
|
+
readDeviceStates: async (ids) => {
|
|
22261
|
+
const out = /* @__PURE__ */ new Map();
|
|
22262
|
+
for (const id of ids) try {
|
|
22263
|
+
const state = require_dist.readDeviceStateFrom(await api.deviceManager.loadRuntimeState.query({ deviceId: id }));
|
|
22264
|
+
if (state !== void 0) out.set(id, state);
|
|
22265
|
+
} catch (err) {
|
|
22266
|
+
this.ctx.logger.debug("device-state read failed", {
|
|
22267
|
+
tags: { deviceId: id },
|
|
22268
|
+
meta: { error: String(err) }
|
|
22269
|
+
});
|
|
22270
|
+
}
|
|
22271
|
+
return out;
|
|
22272
|
+
},
|
|
22201
22273
|
dispatcher: {
|
|
22202
22274
|
getZoneNames: async (deviceId) => {
|
|
22203
22275
|
return (await api.zones.listZones.query({ deviceId })).map((z) => ({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as readDeviceStateFrom, B as _enum, C as defineCustomActions, D as notificationRulesCapability, F as BaseAddon, G as object, H as boolean, I as DeviceType, K as string, L as createEvent, M as videoclipsCapability, N as zoneAnalyticsCapability, O as pipelineAnalyticsCapability, P as errMsg, R as hydrateSchema, S as customAction, T as faceGalleryCapability, U as literal, V as array, W as number, _ as TimelapseRuleSchema, a as NC_CONDITION_CATALOG, b as buildEventKindDescriptor, c as NcRuleInputSchema, d as NcSnoozeInputSchema, f as NcSnoozeSchema, g as TimelapseRuleInputSchema, h as OpsLogEntrySchema, i as MACRO_LABELS, j as subKindsOf, k as plateGalleryCapability, l as NcRulePatchSchema, m as NcTaxonomySchema, n as EVENT_KIND_BY_CAP, o as NC_TAXONOMY, p as NcSnoozeSuppressedSchema, q as EventCategory, r as EVENT_PAD_MS, s as NcConditionDescriptorSchema, t as DEFAULT_EVENT_COLOR, u as NcRuleSchema, v as addonWidgetsSourceCapability, x as cosineSimilarity, y as audioMetricsCapability, z as nodePin } from "../dist-O227Je-Q.mjs";
|
|
2
2
|
import { promises } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -4752,17 +4752,7 @@ function referencesAudioClass(classes) {
|
|
|
4752
4752
|
if (classes === void 0) return false;
|
|
4753
4753
|
return classes.some((c) => c.trim().toLowerCase().startsWith("audio-"));
|
|
4754
4754
|
}
|
|
4755
|
-
|
|
4756
|
-
* Evaluate one rule against one subject. The rule's `delivery` must match
|
|
4757
|
-
* the subject kind (`immediate` ↔ `object-event`, `track-end` ↔
|
|
4758
|
-
* `track-end`) — a mismatch fails with `'delivery'`. Schedule is evaluated
|
|
4759
|
-
* at the SUBJECT timestamp (the persist moment), so boot-time reconcile of
|
|
4760
|
-
* missed records applies the schedule the record lived under.
|
|
4761
|
-
*
|
|
4762
|
-
* Throttle/cooldown is deliberately NOT part of this function — cooldown
|
|
4763
|
-
* is stateful; see {@link cooldownKey} / {@link isCoolingDown}.
|
|
4764
|
-
*/
|
|
4765
|
-
function evaluateRule(rule, subject) {
|
|
4755
|
+
function evaluateRule(rule, subject, deviceState) {
|
|
4766
4756
|
if (subject.kind === "occupancy-event") {
|
|
4767
4757
|
if (rule.delivery !== "device-event") return fail("delivery");
|
|
4768
4758
|
const occ = rule.conditions.occupancy;
|
|
@@ -4777,6 +4767,11 @@ function evaluateRule(rule, subject) {
|
|
|
4777
4767
|
if (rule.conditions.occupancy !== void 0) return fail("occupancy");
|
|
4778
4768
|
}
|
|
4779
4769
|
const c = rule.conditions;
|
|
4770
|
+
if (c.deviceState !== void 0) {
|
|
4771
|
+
const current = deviceState?.(c.deviceState.deviceId);
|
|
4772
|
+
if (current === void 0) return fail("deviceState");
|
|
4773
|
+
if (!toLowerSet(c.deviceState.states).has(current.trim().toLowerCase())) return fail("deviceState");
|
|
4774
|
+
}
|
|
4780
4775
|
if (c.devices !== void 0 && c.devices.length > 0 && !c.devices.includes(subject.deviceId)) return fail("devices");
|
|
4781
4776
|
if (c.source !== void 0 && c.source !== "any") {
|
|
4782
4777
|
if ((subject.source ?? "pipeline") !== c.source) return fail("source");
|
|
@@ -6606,6 +6601,53 @@ var NcRuleStore = class {
|
|
|
6606
6601
|
});
|
|
6607
6602
|
}
|
|
6608
6603
|
};
|
|
6604
|
+
var NcDeviceStateCache = class {
|
|
6605
|
+
deps;
|
|
6606
|
+
states = /* @__PURE__ */ new Map();
|
|
6607
|
+
lastOkAt = 0;
|
|
6608
|
+
ids = [];
|
|
6609
|
+
now;
|
|
6610
|
+
constructor(deps) {
|
|
6611
|
+
this.deps = deps;
|
|
6612
|
+
this.now = deps.now ?? (() => Date.now());
|
|
6613
|
+
}
|
|
6614
|
+
/**
|
|
6615
|
+
* The device ids worth reading — recomputed from the rule set, so a cluster
|
|
6616
|
+
* with no gated rule does no work at all.
|
|
6617
|
+
*/
|
|
6618
|
+
setWatched(ids) {
|
|
6619
|
+
this.ids = [...new Set(ids)].sort((a, b) => a - b);
|
|
6620
|
+
}
|
|
6621
|
+
watched() {
|
|
6622
|
+
return this.ids;
|
|
6623
|
+
}
|
|
6624
|
+
/** Hot-path read. No I/O, no await — this is called per event. */
|
|
6625
|
+
get(deviceId) {
|
|
6626
|
+
if (this.now() - this.lastOkAt > 3e5) return void 0;
|
|
6627
|
+
return this.states.get(deviceId);
|
|
6628
|
+
}
|
|
6629
|
+
/**
|
|
6630
|
+
* Re-read every watched state. Never throws: a gate that could fail the
|
|
6631
|
+
* evaluation tick would take the notification pipeline with it.
|
|
6632
|
+
*/
|
|
6633
|
+
async refresh() {
|
|
6634
|
+
if (this.ids.length === 0) {
|
|
6635
|
+
this.states = /* @__PURE__ */ new Map();
|
|
6636
|
+
return;
|
|
6637
|
+
}
|
|
6638
|
+
try {
|
|
6639
|
+
const next = await this.deps.readStates(this.ids);
|
|
6640
|
+
this.states = new Map(next);
|
|
6641
|
+
this.lastOkAt = this.now();
|
|
6642
|
+
for (const id of this.ids) if (!next.has(id)) this.deps.logger.info("device-state gate: no state for a watched device", { tags: { deviceId: id } });
|
|
6643
|
+
} catch (err) {
|
|
6644
|
+
this.deps.logger.warn("device-state refresh failed — keeping the previous states until they age out", { meta: {
|
|
6645
|
+
watched: this.ids.length,
|
|
6646
|
+
error: String(err)
|
|
6647
|
+
} });
|
|
6648
|
+
}
|
|
6649
|
+
}
|
|
6650
|
+
};
|
|
6609
6651
|
//#endregion
|
|
6610
6652
|
//#region src/notification-center/snooze-digest.ts
|
|
6611
6653
|
/** `14:03` in the node's local zone — a digest is read by a person, at home. */
|
|
@@ -7439,6 +7481,9 @@ var NotificationCenter = class NotificationCenter {
|
|
|
7439
7481
|
/** Snooze windows + what they hid. Mirrored into RAM; the dispatcher's gate
|
|
7440
7482
|
* reads it with no I/O, the drain tick refreshes it. */
|
|
7441
7483
|
snoozes;
|
|
7484
|
+
/** States the `deviceState` rule gate reads. Refreshed on the reload tick;
|
|
7485
|
+
* read with no I/O on the persist hook. */
|
|
7486
|
+
deviceStates;
|
|
7442
7487
|
/** True when ≥1 enabled `device-event` rule declares an occupancy condition —
|
|
7443
7488
|
* the watcher is idle (zero per-frame cost) otherwise. */
|
|
7444
7489
|
occupancyEnabled = false;
|
|
@@ -7471,6 +7516,11 @@ var NotificationCenter = class NotificationCenter {
|
|
|
7471
7516
|
store: deps.store,
|
|
7472
7517
|
logger: this.logger.child("occupancy")
|
|
7473
7518
|
});
|
|
7519
|
+
this.deviceStates = new NcDeviceStateCache({
|
|
7520
|
+
logger: this.logger.child("device-state"),
|
|
7521
|
+
readStates: deps.readDeviceStates ?? (async () => /* @__PURE__ */ new Map()),
|
|
7522
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
7523
|
+
});
|
|
7474
7524
|
this.snoozes = new NcSnoozeStore({
|
|
7475
7525
|
store: deps.store,
|
|
7476
7526
|
logger: this.logger.child("snooze"),
|
|
@@ -7525,6 +7575,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
7525
7575
|
await this.snoozes.load();
|
|
7526
7576
|
this.refreshOccupancyWatch();
|
|
7527
7577
|
if (!opts.evaluation) return;
|
|
7578
|
+
await this.deviceStates.refresh();
|
|
7528
7579
|
this.evaluationActive = true;
|
|
7529
7580
|
await this.outbox.load();
|
|
7530
7581
|
await this.seedCooldowns();
|
|
@@ -7796,7 +7847,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
7796
7847
|
let anyMatched = false;
|
|
7797
7848
|
const rejections = [];
|
|
7798
7849
|
for (const rule of candidates) {
|
|
7799
|
-
const evaluation = evaluateRule(rule, subject);
|
|
7850
|
+
const evaluation = evaluateRule(rule, subject, (id) => this.deviceStates.get(id));
|
|
7800
7851
|
if (!evaluation.matched) {
|
|
7801
7852
|
rejections.push(`${rule.name}:${evaluation.failedCondition ?? "unknown"}`);
|
|
7802
7853
|
this.logger.debug("rule did not match", {
|
|
@@ -7997,6 +8048,13 @@ var NotificationCenter = class NotificationCenter {
|
|
|
7997
8048
|
}
|
|
7998
8049
|
this.occupancyWatcher.setWatchedKeys(specs);
|
|
7999
8050
|
this.occupancyEnabled = specs.length > 0;
|
|
8051
|
+
const gated = [];
|
|
8052
|
+
for (const rule of this.rules.list()) {
|
|
8053
|
+
if (!rule.enabled) continue;
|
|
8054
|
+
const ds = rule.conditions.deviceState;
|
|
8055
|
+
if (ds !== void 0) gated.push(ds.deviceId);
|
|
8056
|
+
}
|
|
8057
|
+
this.deviceStates.setWatched(gated);
|
|
8000
8058
|
}
|
|
8001
8059
|
/** Boot reseed of confirmed occupancy edge-state (durability, constraint 4) —
|
|
8002
8060
|
* hydrate the watcher from the store, then prune orphaned durable rows to the
|
|
@@ -8013,6 +8071,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
8013
8071
|
await this.rules.load();
|
|
8014
8072
|
this.refreshOccupancyWatch();
|
|
8015
8073
|
if (!this.evaluationActive) return;
|
|
8074
|
+
await this.deviceStates.refresh();
|
|
8016
8075
|
await this.occupancyStore.pruneExcept(this.activeOccupancyKeys());
|
|
8017
8076
|
}
|
|
8018
8077
|
/** The full-key set the watcher currently tracks (post prune) — the bound for
|
|
@@ -8240,7 +8299,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
8240
8299
|
const results = [];
|
|
8241
8300
|
for (const subject of subjects.sort((a, b) => b.timestamp - a.timestamp)) {
|
|
8242
8301
|
if (results.length >= TEST_RULE_MAX_RESULTS) break;
|
|
8243
|
-
const evaluation = evaluateRule(rule, subject);
|
|
8302
|
+
const evaluation = evaluateRule(rule, subject, (id) => this.deviceStates.get(id));
|
|
8244
8303
|
results.push({
|
|
8245
8304
|
recordId: subject.recordId,
|
|
8246
8305
|
recordKind: subject.kind === "track-end" ? "track" : subject.kind === "audio-event" ? "object-event" : subject.kind === "occupancy-event" ? "device-event" : subject.kind,
|
|
@@ -22192,6 +22251,19 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
22192
22251
|
addonId: device.addonId
|
|
22193
22252
|
};
|
|
22194
22253
|
},
|
|
22254
|
+
readDeviceStates: async (ids) => {
|
|
22255
|
+
const out = /* @__PURE__ */ new Map();
|
|
22256
|
+
for (const id of ids) try {
|
|
22257
|
+
const state = readDeviceStateFrom(await api.deviceManager.loadRuntimeState.query({ deviceId: id }));
|
|
22258
|
+
if (state !== void 0) out.set(id, state);
|
|
22259
|
+
} catch (err) {
|
|
22260
|
+
this.ctx.logger.debug("device-state read failed", {
|
|
22261
|
+
tags: { deviceId: id },
|
|
22262
|
+
meta: { error: String(err) }
|
|
22263
|
+
});
|
|
22264
|
+
}
|
|
22265
|
+
return out;
|
|
22266
|
+
},
|
|
22195
22267
|
dispatcher: {
|
|
22196
22268
|
getZoneNames: async (deviceId) => {
|
|
22197
22269
|
return (await api.zones.listZones.query({ deviceId })).map((z) => ({
|
|
@@ -30,7 +30,7 @@ async function d(e) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
async function f() {
|
|
33
|
-
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-
|
|
33
|
+
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-aG07ehD0.mjs")).catch((e) => {
|
|
34
34
|
throw l = void 0, e;
|
|
35
35
|
}), l;
|
|
36
36
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-post-analysis",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.26",
|
|
4
4
|
"description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|