@camstack/addon-post-analysis 1.2.26 → 1.2.28
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-CP-FTAl5.js → dist-DY_XHH2r.js} +3794 -811
- package/dist/{dist-O227Je-Q.mjs → dist-DvGwl3d2.mjs} +3739 -792
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-BQUEteve.js → node-DSR4j3NF.js} +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-aG07ehD0.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-B7qyOMi9.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-66T4FwIA.mjs → hostInit-CXz29iwU.mjs} +1 -1
- package/dist/pipeline-analytics/index.js +506 -3
- package/dist/pipeline-analytics/index.mjs +505 -2
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -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-DY_XHH2r.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
|
|
1
|
+
import { E as embeddingEncoderCapability, O as hfModelUrl, R as BaseAddon } from "../dist-DvGwl3d2.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-DY_XHH2r.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);
|
|
@@ -347,6 +347,324 @@ function mimeFromExtension(file) {
|
|
|
347
347
|
return "application/octet-stream";
|
|
348
348
|
}
|
|
349
349
|
//#endregion
|
|
350
|
+
//#region src/notification-center/alarm/alarm-state-machine.ts
|
|
351
|
+
var DISARMED = {
|
|
352
|
+
target: "disarmed",
|
|
353
|
+
fired: false,
|
|
354
|
+
changedAt: 0
|
|
355
|
+
};
|
|
356
|
+
function armedState(mode) {
|
|
357
|
+
return `armed_${mode}`;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* The state to report at `now`.
|
|
361
|
+
*
|
|
362
|
+
* A comparison, not a job: nothing has to run for the exit delay to end. A
|
|
363
|
+
* caller that stops ticking stops NOTIFYING about the change, but the panel's
|
|
364
|
+
* answer is still correct the moment anyone asks.
|
|
365
|
+
*/
|
|
366
|
+
function stateAt(m, now) {
|
|
367
|
+
if (m.fired) return "triggered";
|
|
368
|
+
if (m.triggerAt !== void 0) return now >= m.triggerAt ? "triggered" : "pending";
|
|
369
|
+
if (m.armedAt !== void 0) return now >= m.armedAt ? m.target : "arming";
|
|
370
|
+
return m.target;
|
|
371
|
+
}
|
|
372
|
+
/** Whether the panel is armed enough for a trigger to matter. */
|
|
373
|
+
function isArmed(m, now) {
|
|
374
|
+
const s = stateAt(m, now);
|
|
375
|
+
return s.startsWith("armed_") || s === "pending" || s === "triggered";
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Arm into `mode`. An exit delay of 0 arms immediately — some modes (`home`)
|
|
379
|
+
* legitimately have none, because nobody is leaving.
|
|
380
|
+
*/
|
|
381
|
+
function arm(mode, delays, now) {
|
|
382
|
+
const target = armedState(mode);
|
|
383
|
+
if (delays.exitDelaySec <= 0) return {
|
|
384
|
+
target,
|
|
385
|
+
fired: false,
|
|
386
|
+
changedAt: now
|
|
387
|
+
};
|
|
388
|
+
return {
|
|
389
|
+
target,
|
|
390
|
+
armedAt: now + delays.exitDelaySec * 1e3,
|
|
391
|
+
fired: false,
|
|
392
|
+
changedAt: now
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Disarm, from ANY state — including mid-arming and mid-pending.
|
|
397
|
+
*
|
|
398
|
+
* Cancelling a pending entry is the single most-used transition in a real
|
|
399
|
+
* alarm (walking in and typing the code), and it must CANCEL rather than
|
|
400
|
+
* queue: a disarm that let the pending timer still fire would set the alarm off
|
|
401
|
+
* behind the operator.
|
|
402
|
+
*/
|
|
403
|
+
function disarm(now) {
|
|
404
|
+
return {
|
|
405
|
+
...DISARMED,
|
|
406
|
+
changedAt: now
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Something matched while armed.
|
|
411
|
+
*
|
|
412
|
+
* Starts the entry delay, or fires at once when there is none. A trigger while
|
|
413
|
+
* DISARMED is ignored — the panel is the gate, and a rule that fires while
|
|
414
|
+
* disarmed is a notification, not a break-in.
|
|
415
|
+
*
|
|
416
|
+
* A second trigger during `pending` does NOT restart the countdown: an
|
|
417
|
+
* intruder walking past two sensors would otherwise postpone the alarm.
|
|
418
|
+
*/
|
|
419
|
+
function trigger(m, delays, now) {
|
|
420
|
+
if (!isArmed(m, now)) return m;
|
|
421
|
+
if (m.fired || m.triggerAt !== void 0) return m;
|
|
422
|
+
if (delays.entryDelaySec <= 0) return {
|
|
423
|
+
...m,
|
|
424
|
+
fired: true,
|
|
425
|
+
changedAt: now
|
|
426
|
+
};
|
|
427
|
+
return {
|
|
428
|
+
...m,
|
|
429
|
+
triggerAt: now + delays.entryDelaySec * 1e3,
|
|
430
|
+
changedAt: now
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Settle the machine so a stored value never depends on when it is read.
|
|
435
|
+
*
|
|
436
|
+
* `stateAt` already answers correctly from an unsettled value; this is what a
|
|
437
|
+
* caller persists after a tick, so `fired` becomes true in the record rather
|
|
438
|
+
* than only in the reading of it.
|
|
439
|
+
*/
|
|
440
|
+
function settle(m, now) {
|
|
441
|
+
if (m.triggerAt !== void 0 && now >= m.triggerAt) return {
|
|
442
|
+
target: m.target,
|
|
443
|
+
fired: true,
|
|
444
|
+
changedAt: m.triggerAt
|
|
445
|
+
};
|
|
446
|
+
if (m.armedAt !== void 0 && now >= m.armedAt) return {
|
|
447
|
+
target: m.target,
|
|
448
|
+
fired: m.fired,
|
|
449
|
+
changedAt: m.armedAt
|
|
450
|
+
};
|
|
451
|
+
return m;
|
|
452
|
+
}
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region src/notification-center/alarm/alarm-panel-device.ts
|
|
455
|
+
/**
|
|
456
|
+
* The Notification Center's own alarm panel.
|
|
457
|
+
*
|
|
458
|
+
* A REAL device, not a special case: it carries `alarm-panel` natively, so the
|
|
459
|
+
* `deviceState` rule gate reads it through `loadRuntimeState` exactly like a
|
|
460
|
+
* switch or a contact, and both exporters see it through their ordinary
|
|
461
|
+
* device mapping. Nothing had to learn about alarms.
|
|
462
|
+
*
|
|
463
|
+
* It needed no new addon and no base-class change either — the wiring already
|
|
464
|
+
* existed and was simply never used this way: `ctx.kernel.devices.create()` is
|
|
465
|
+
* on EVERY addon's kernel services, not only a `BaseDeviceProvider`'s, and
|
|
466
|
+
* `integrations.create` gives it a fixed home to hang from.
|
|
467
|
+
*
|
|
468
|
+
* The lifecycle lives in {@link ../alarm-state-machine.js} as pure functions.
|
|
469
|
+
* This class is the thin part: persist the machine, publish the derived state,
|
|
470
|
+
* and re-publish when a delay elapses.
|
|
471
|
+
*/
|
|
472
|
+
/** Every arm mode a camstack-owned panel offers. */
|
|
473
|
+
var MODES = [
|
|
474
|
+
"home",
|
|
475
|
+
"away",
|
|
476
|
+
"night"
|
|
477
|
+
];
|
|
478
|
+
/**
|
|
479
|
+
* Defaults an operator can change per panel. `home` has no exit delay on
|
|
480
|
+
* purpose — nobody is leaving — but the shared value is applied to every mode
|
|
481
|
+
* until per-mode delays are asked for.
|
|
482
|
+
*/
|
|
483
|
+
var DEFAULT_DELAYS = {
|
|
484
|
+
exitDelaySec: 30,
|
|
485
|
+
entryDelaySec: 20
|
|
486
|
+
};
|
|
487
|
+
/**
|
|
488
|
+
* How often the panel re-publishes while a delay is running.
|
|
489
|
+
*
|
|
490
|
+
* The state is computed from an INSTANT, so this tick only decides how quickly
|
|
491
|
+
* the change is ANNOUNCED — a missed tick can never leave the panel stuck in
|
|
492
|
+
* `arming`. One second is what makes a countdown look like a countdown.
|
|
493
|
+
*/
|
|
494
|
+
var TICK_MS = 1e3;
|
|
495
|
+
/**
|
|
496
|
+
* The panel's persisted config: the delays an operator tunes, and the machine
|
|
497
|
+
* itself. The machine is persisted because the alternative is an alarm that
|
|
498
|
+
* disarms itself on every deploy.
|
|
499
|
+
*/
|
|
500
|
+
var ncAlarmPanelSchema = require_dist.object({
|
|
501
|
+
exitDelaySec: require_dist.number().int().min(0).max(600).default(DEFAULT_DELAYS.exitDelaySec),
|
|
502
|
+
entryDelaySec: require_dist.number().int().min(0).max(600).default(DEFAULT_DELAYS.entryDelaySec),
|
|
503
|
+
machine: require_dist.record(require_dist.string(), require_dist.unknown()).optional()
|
|
504
|
+
});
|
|
505
|
+
var NcAlarmPanelDevice = class extends require_dist.BaseDevice {
|
|
506
|
+
features = [];
|
|
507
|
+
machine = DISARMED;
|
|
508
|
+
delays = DEFAULT_DELAYS;
|
|
509
|
+
ticker = null;
|
|
510
|
+
constructor(ctx) {
|
|
511
|
+
super(ctx, ncAlarmPanelSchema, { type: ctx.deviceMeta.type });
|
|
512
|
+
this.delays = {
|
|
513
|
+
exitDelaySec: this.config.get("exitDelaySec"),
|
|
514
|
+
entryDelaySec: this.config.get("entryDelaySec")
|
|
515
|
+
};
|
|
516
|
+
this.restore();
|
|
517
|
+
this.registerAlarmCap();
|
|
518
|
+
this.publish();
|
|
519
|
+
}
|
|
520
|
+
/** Fire a trigger at the panel. Called by the notification center when a rule
|
|
521
|
+
* that arms the alarm matches. Ignored unless the panel is armed. */
|
|
522
|
+
onRuleTriggered(now = Date.now()) {
|
|
523
|
+
const next = trigger(this.machine, this.delays, now);
|
|
524
|
+
if (next === this.machine) return;
|
|
525
|
+
this.machine = next;
|
|
526
|
+
this.publish(now);
|
|
527
|
+
}
|
|
528
|
+
/** Current lifecycle word — what the `deviceState` gate compares against. */
|
|
529
|
+
currentState(now = Date.now()) {
|
|
530
|
+
return stateAt(this.machine, now);
|
|
531
|
+
}
|
|
532
|
+
/** Stop the countdown when the device goes away. The state itself is
|
|
533
|
+
* persisted, so nothing is lost — only the announcements stop. */
|
|
534
|
+
stopCountdown() {
|
|
535
|
+
this.stopTicking();
|
|
536
|
+
}
|
|
537
|
+
registerAlarmCap() {
|
|
538
|
+
this.ctx.registerNativeCap(require_dist.alarmPanelCapability, {
|
|
539
|
+
getStatus: async () => this.status(),
|
|
540
|
+
arm: async ({ mode }) => {
|
|
541
|
+
this.machine = arm(mode, this.delays, Date.now());
|
|
542
|
+
this.publish();
|
|
543
|
+
},
|
|
544
|
+
disarm: async () => {
|
|
545
|
+
this.machine = disarm(Date.now());
|
|
546
|
+
this.publish();
|
|
547
|
+
},
|
|
548
|
+
trigger: async () => {
|
|
549
|
+
this.machine = {
|
|
550
|
+
...this.machine,
|
|
551
|
+
fired: true,
|
|
552
|
+
changedAt: Date.now()
|
|
553
|
+
};
|
|
554
|
+
this.publish();
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
status(now = Date.now()) {
|
|
559
|
+
return {
|
|
560
|
+
state: stateAt(this.machine, now),
|
|
561
|
+
availableModes: [...MODES],
|
|
562
|
+
requiresCode: false,
|
|
563
|
+
lastChangedAt: this.machine.changedAt
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Write the slice and keep ticking while a delay is running.
|
|
568
|
+
*
|
|
569
|
+
* `settle` is applied first so the PERSISTED machine does not depend on when
|
|
570
|
+
* it is next read — a hub restarted mid-`pending` comes back already fired,
|
|
571
|
+
* rather than fired only once somebody looks.
|
|
572
|
+
*/
|
|
573
|
+
publish(now = Date.now()) {
|
|
574
|
+
this.machine = settle(this.machine, now);
|
|
575
|
+
this.runtimeState.setCapState("alarm-panel", this.status(now));
|
|
576
|
+
this.persist();
|
|
577
|
+
if (this.machine.armedAt !== void 0 || this.machine.triggerAt !== void 0) this.startTicking();
|
|
578
|
+
else this.stopTicking();
|
|
579
|
+
}
|
|
580
|
+
startTicking() {
|
|
581
|
+
if (this.ticker !== null) return;
|
|
582
|
+
this.ticker = setInterval(() => this.publish(), TICK_MS);
|
|
583
|
+
this.ticker.unref?.();
|
|
584
|
+
}
|
|
585
|
+
stopTicking() {
|
|
586
|
+
if (this.ticker === null) return;
|
|
587
|
+
clearInterval(this.ticker);
|
|
588
|
+
this.ticker = null;
|
|
589
|
+
}
|
|
590
|
+
/** The machine rides the device's own config blob — it is small, and it must
|
|
591
|
+
* survive a restart or the alarm disarms itself on every deploy. */
|
|
592
|
+
persist() {
|
|
593
|
+
this.config.set("machine", { ...this.machine });
|
|
594
|
+
}
|
|
595
|
+
restore() {
|
|
596
|
+
const raw = this.config.get("machine");
|
|
597
|
+
if (raw === null || typeof raw !== "object") return;
|
|
598
|
+
const m = raw;
|
|
599
|
+
if (typeof m.target !== "string" || typeof m.changedAt !== "number") return;
|
|
600
|
+
this.machine = {
|
|
601
|
+
target: m.target,
|
|
602
|
+
...typeof m.armedAt === "number" ? { armedAt: m.armedAt } : {},
|
|
603
|
+
...typeof m.triggerAt === "number" ? { triggerAt: m.triggerAt } : {},
|
|
604
|
+
fired: m.fired === true,
|
|
605
|
+
changedAt: m.changedAt
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
//#endregion
|
|
610
|
+
//#region src/notification-center/alarm/ensure-alarm-panel.ts
|
|
611
|
+
/**
|
|
612
|
+
* Bring the Notification Center's alarm panel into existence, once.
|
|
613
|
+
*
|
|
614
|
+
* Two idempotent steps — a fixed integration to hang from, and one device under
|
|
615
|
+
* it. Both are `get-or-create`, because this runs on every boot and an operator
|
|
616
|
+
* must never end up with a second panel after a restart.
|
|
617
|
+
*
|
|
618
|
+
* The integration is FIXED: it exists because the addon says so, not because an
|
|
619
|
+
* operator added it, and there is no add/delete flow for it. That is the whole
|
|
620
|
+
* meaning of "a system integration" here — the same `integrations` row every
|
|
621
|
+
* provider uses, created by its owner instead of by a form.
|
|
622
|
+
*/
|
|
623
|
+
/** Stable id of the one panel. Never derived from a name — a rename must not
|
|
624
|
+
* mint a second device. */
|
|
625
|
+
var NC_ALARM_STABLE_ID = "nc-alarm-panel";
|
|
626
|
+
var NC_ALARM_DEVICE_NAME = "Alarm";
|
|
627
|
+
/**
|
|
628
|
+
* Idempotent. Returns the ids either way so the caller can hold onto the panel.
|
|
629
|
+
*
|
|
630
|
+
* Failures are the CALLER's to handle: a hub whose alarm could not be created
|
|
631
|
+
* should say so loudly and keep notifying, not fail to boot — but that decision
|
|
632
|
+
* does not belong in here.
|
|
633
|
+
*/
|
|
634
|
+
async function ensureAlarmPanel(deps) {
|
|
635
|
+
const integration = await deps.getIntegration(deps.addonId) ?? await deps.createIntegration({
|
|
636
|
+
addonId: deps.addonId,
|
|
637
|
+
name: deps.integrationName,
|
|
638
|
+
info: { fixed: true }
|
|
639
|
+
});
|
|
640
|
+
const existingDevice = await deps.findDeviceByStableId(NC_ALARM_STABLE_ID);
|
|
641
|
+
if (existingDevice !== null) return {
|
|
642
|
+
integrationId: integration.id,
|
|
643
|
+
deviceId: existingDevice.id,
|
|
644
|
+
created: false
|
|
645
|
+
};
|
|
646
|
+
const device = await deps.createDevice({
|
|
647
|
+
stableId: NC_ALARM_STABLE_ID,
|
|
648
|
+
integrationId: integration.id,
|
|
649
|
+
name: NC_ALARM_DEVICE_NAME
|
|
650
|
+
});
|
|
651
|
+
deps.logger.info("alarm panel created", {
|
|
652
|
+
tags: { deviceId: device.id },
|
|
653
|
+
meta: {
|
|
654
|
+
integrationId: integration.id,
|
|
655
|
+
stableId: NC_ALARM_STABLE_ID
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
return {
|
|
659
|
+
integrationId: integration.id,
|
|
660
|
+
deviceId: device.id,
|
|
661
|
+
created: true
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
/** The device class + type the caller passes to `kernel.devices.create`. */
|
|
665
|
+
var NC_ALARM_DEVICE_CLASS = NcAlarmPanelDevice;
|
|
666
|
+
var NC_ALARM_DEVICE_TYPE = require_dist.DeviceType.AlarmPanel;
|
|
667
|
+
//#endregion
|
|
350
668
|
//#region src/pipeline-analytics/videoclips-provider.ts
|
|
351
669
|
var SOURCE = "analytics";
|
|
352
670
|
function clipIdFor(eventId, startMs, endMs) {
|
|
@@ -6406,6 +6724,117 @@ function rowToEntry$1(id, data) {
|
|
|
6406
6724
|
};
|
|
6407
6725
|
}
|
|
6408
6726
|
//#endregion
|
|
6727
|
+
//#region src/notification-center/rule-actions.ts
|
|
6728
|
+
var NcRuleActionRunner = class {
|
|
6729
|
+
deps;
|
|
6730
|
+
/** Last run per `${ruleId}:${sequenceName}` — the throttle's memory. */
|
|
6731
|
+
lastRunAt = /* @__PURE__ */ new Map();
|
|
6732
|
+
now;
|
|
6733
|
+
sleep;
|
|
6734
|
+
constructor(deps) {
|
|
6735
|
+
this.deps = deps;
|
|
6736
|
+
this.now = deps.now ?? (() => Date.now());
|
|
6737
|
+
this.sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
6738
|
+
}
|
|
6739
|
+
/**
|
|
6740
|
+
* Run every enabled sequence, in order.
|
|
6741
|
+
*
|
|
6742
|
+
* Never throws: an actuation that fails must not cost the NOTIFICATION about
|
|
6743
|
+
* the very same event. The operator finds out from the log and from the
|
|
6744
|
+
* device not having moved — not from silence on both.
|
|
6745
|
+
*/
|
|
6746
|
+
async run(input) {
|
|
6747
|
+
const out = [];
|
|
6748
|
+
for (const sequence of input.sequences) out.push(await this.runOne(input, sequence));
|
|
6749
|
+
return out;
|
|
6750
|
+
}
|
|
6751
|
+
async runOne(input, sequence) {
|
|
6752
|
+
if (!sequence.enabled) return {
|
|
6753
|
+
name: sequence.name,
|
|
6754
|
+
ran: false,
|
|
6755
|
+
stepsRun: 0,
|
|
6756
|
+
skippedReason: "disabled"
|
|
6757
|
+
};
|
|
6758
|
+
if (sequence.actions.length === 0) return {
|
|
6759
|
+
name: sequence.name,
|
|
6760
|
+
ran: false,
|
|
6761
|
+
stepsRun: 0,
|
|
6762
|
+
skippedReason: "empty"
|
|
6763
|
+
};
|
|
6764
|
+
const key = `${input.ruleId}:${sequence.name}`;
|
|
6765
|
+
const now = this.now();
|
|
6766
|
+
const minDelayMs = (sequence.minDelaySec ?? 0) * 1e3;
|
|
6767
|
+
const last = this.lastRunAt.get(key);
|
|
6768
|
+
if (minDelayMs > 0 && last !== void 0 && now - last < minDelayMs) {
|
|
6769
|
+
this.deps.logger.debug("rule action sequence throttled", {
|
|
6770
|
+
tags: { deviceId: input.deviceId },
|
|
6771
|
+
meta: {
|
|
6772
|
+
ruleId: input.ruleId,
|
|
6773
|
+
sequence: sequence.name,
|
|
6774
|
+
sinceLastMs: now - last
|
|
6775
|
+
}
|
|
6776
|
+
});
|
|
6777
|
+
return {
|
|
6778
|
+
name: sequence.name,
|
|
6779
|
+
ran: false,
|
|
6780
|
+
stepsRun: 0,
|
|
6781
|
+
skippedReason: "throttled"
|
|
6782
|
+
};
|
|
6783
|
+
}
|
|
6784
|
+
this.lastRunAt.set(key, now);
|
|
6785
|
+
let stepsRun = 0;
|
|
6786
|
+
for (const [index, action] of sequence.actions.entries()) try {
|
|
6787
|
+
await this.step(action);
|
|
6788
|
+
stepsRun += 1;
|
|
6789
|
+
} catch (err) {
|
|
6790
|
+
this.deps.logger.warn("rule action failed — stopping the sequence", {
|
|
6791
|
+
tags: { deviceId: input.deviceId },
|
|
6792
|
+
meta: {
|
|
6793
|
+
ruleId: input.ruleId,
|
|
6794
|
+
rule: input.ruleName,
|
|
6795
|
+
sequence: sequence.name,
|
|
6796
|
+
step: index,
|
|
6797
|
+
action: action.kind === "cap" ? `${action.cap}.${action.method}` : "wait",
|
|
6798
|
+
error: String(err)
|
|
6799
|
+
}
|
|
6800
|
+
});
|
|
6801
|
+
return {
|
|
6802
|
+
name: sequence.name,
|
|
6803
|
+
ran: true,
|
|
6804
|
+
stepsRun,
|
|
6805
|
+
failedAt: index
|
|
6806
|
+
};
|
|
6807
|
+
}
|
|
6808
|
+
this.deps.logger.info("rule actions ran", {
|
|
6809
|
+
tags: { deviceId: input.deviceId },
|
|
6810
|
+
meta: {
|
|
6811
|
+
ruleId: input.ruleId,
|
|
6812
|
+
rule: input.ruleName,
|
|
6813
|
+
sequence: sequence.name,
|
|
6814
|
+
steps: stepsRun
|
|
6815
|
+
}
|
|
6816
|
+
});
|
|
6817
|
+
return {
|
|
6818
|
+
name: sequence.name,
|
|
6819
|
+
ran: true,
|
|
6820
|
+
stepsRun
|
|
6821
|
+
};
|
|
6822
|
+
}
|
|
6823
|
+
async step(action) {
|
|
6824
|
+
if (action.kind === "wait") {
|
|
6825
|
+
await this.sleep(action.seconds * 1e3);
|
|
6826
|
+
return;
|
|
6827
|
+
}
|
|
6828
|
+
if (!this.deps.isDeviceScoped(action.cap)) throw new Error(`rule action refused: "${action.cap}" is not a device capability — only devices can be actuated`);
|
|
6829
|
+
await this.deps.actuate({
|
|
6830
|
+
deviceId: action.deviceId,
|
|
6831
|
+
cap: action.cap,
|
|
6832
|
+
method: action.method,
|
|
6833
|
+
args: action.args ?? {}
|
|
6834
|
+
});
|
|
6835
|
+
}
|
|
6836
|
+
};
|
|
6837
|
+
//#endregion
|
|
6409
6838
|
//#region src/notification-center/rule-store.ts
|
|
6410
6839
|
var NC_RULES_COLLECTION = "notification-center:rules";
|
|
6411
6840
|
var NC_RULES_COLUMNS = [
|
|
@@ -7490,6 +7919,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
7490
7919
|
/** States the `deviceState` rule gate reads. Refreshed on the reload tick;
|
|
7491
7920
|
* read with no I/O on the persist hook. */
|
|
7492
7921
|
deviceStates;
|
|
7922
|
+
/** Runs a matched rule's action sequences. Null when no actuator is wired. */
|
|
7923
|
+
actionRunner;
|
|
7493
7924
|
/** True when ≥1 enabled `device-event` rule declares an occupancy condition —
|
|
7494
7925
|
* the watcher is idle (zero per-frame cost) otherwise. */
|
|
7495
7926
|
occupancyEnabled = false;
|
|
@@ -7522,6 +7953,12 @@ var NotificationCenter = class NotificationCenter {
|
|
|
7522
7953
|
store: deps.store,
|
|
7523
7954
|
logger: this.logger.child("occupancy")
|
|
7524
7955
|
});
|
|
7956
|
+
this.actionRunner = deps.actuate !== void 0 && deps.isCapDeviceScoped !== void 0 ? new NcRuleActionRunner({
|
|
7957
|
+
logger: this.logger.child("actions"),
|
|
7958
|
+
actuate: deps.actuate,
|
|
7959
|
+
isDeviceScoped: deps.isCapDeviceScoped,
|
|
7960
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
7961
|
+
}) : null;
|
|
7525
7962
|
this.deviceStates = new NcDeviceStateCache({
|
|
7526
7963
|
logger: this.logger.child("device-state"),
|
|
7527
7964
|
readStates: deps.readDeviceStates ?? (async () => /* @__PURE__ */ new Map()),
|
|
@@ -7897,6 +8334,13 @@ var NotificationCenter = class NotificationCenter {
|
|
|
7897
8334
|
...subject.zones.length > 0 ? { zones: subject.zones } : {}
|
|
7898
8335
|
}
|
|
7899
8336
|
});
|
|
8337
|
+
const sequences = rule.actions?.onTrigger;
|
|
8338
|
+
if (sequences !== void 0 && sequences.length > 0 && this.actionRunner !== null) this.actionRunner.run({
|
|
8339
|
+
ruleId: rule.id,
|
|
8340
|
+
ruleName: rule.name,
|
|
8341
|
+
deviceId: subject.deviceId,
|
|
8342
|
+
sequences
|
|
8343
|
+
});
|
|
7900
8344
|
const userTargets = await this.resolveUserTargets(rule, subject.deviceId);
|
|
7901
8345
|
if (await this.outbox.enqueue(this.buildEntries(rule, subject, kind, evaluation.matchedOn, userTargets)) > 0) this.lastFiredAt.set(key, now);
|
|
7902
8346
|
anyMatched = true;
|
|
@@ -8302,10 +8746,16 @@ var NotificationCenter = class NotificationCenter {
|
|
|
8302
8746
|
const tracks = await this.deps.listRecentTracks(since, TEST_RULE_MAX_RESULTS);
|
|
8303
8747
|
for (const track of tracks) subjects.push(subjectFromTrack(track));
|
|
8304
8748
|
}
|
|
8749
|
+
let lookup = (id) => this.deviceStates.get(id);
|
|
8750
|
+
const gated = rule.conditions.deviceState;
|
|
8751
|
+
if (gated !== void 0 && this.deps.readDeviceStates !== void 0) try {
|
|
8752
|
+
const fresh = await this.deps.readDeviceStates([gated.deviceId]);
|
|
8753
|
+
lookup = (id) => fresh.get(id) ?? this.deviceStates.get(id);
|
|
8754
|
+
} catch {}
|
|
8305
8755
|
const results = [];
|
|
8306
8756
|
for (const subject of subjects.sort((a, b) => b.timestamp - a.timestamp)) {
|
|
8307
8757
|
if (results.length >= TEST_RULE_MAX_RESULTS) break;
|
|
8308
|
-
const evaluation = evaluateRule(rule, subject,
|
|
8758
|
+
const evaluation = evaluateRule(rule, subject, lookup);
|
|
8309
8759
|
results.push({
|
|
8310
8760
|
recordId: subject.recordId,
|
|
8311
8761
|
recordKind: subject.kind === "track-end" ? "track" : subject.kind === "audio-event" ? "object-event" : subject.kind === "occupancy-event" ? "device-event" : subject.kind,
|
|
@@ -21263,6 +21713,11 @@ async function resolveDefaultEventMedia(deps) {
|
|
|
21263
21713
|
}
|
|
21264
21714
|
//#endregion
|
|
21265
21715
|
//#region src/pipeline-analytics/index.ts
|
|
21716
|
+
/** This addon's manifest id — the owner of the fixed Notification Center
|
|
21717
|
+
* integration and of the alarm device under it. */
|
|
21718
|
+
var PIPELINE_ANALYTICS_ADDON_ID = "pipeline-analytics";
|
|
21719
|
+
/** Operator-visible name of the fixed integration. */
|
|
21720
|
+
var NC_INTEGRATION_NAME = "Notification Center";
|
|
21266
21721
|
/**
|
|
21267
21722
|
* Pipeline Analytics addon — subscribes to `PipelineInferenceResult` +
|
|
21268
21723
|
* `PipelineAudioInferenceResult` on the bus, refines raw detections
|
|
@@ -21789,6 +22244,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
21789
22244
|
const api = this.ctx.api;
|
|
21790
22245
|
if (!api) throw new Error("pipeline-analytics requires ctx.api (device-manager + settings-store)");
|
|
21791
22246
|
await this.declareCollections(api);
|
|
22247
|
+
await this.ensureAlarmPanel(api);
|
|
21792
22248
|
const logger = this.ctx.logger;
|
|
21793
22249
|
const storage = await this.resolveMediaStorage(logger);
|
|
21794
22250
|
const stores = await this.buildStores(api, logger, storage);
|
|
@@ -21876,6 +22332,40 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
21876
22332
|
}
|
|
21877
22333
|
/** Declare typed collections up-front so the first insert doesn't race with
|
|
21878
22334
|
* a CREATE TABLE. Idempotent. */
|
|
22335
|
+
/**
|
|
22336
|
+
* Bring the Notification Center's alarm panel into existence, once.
|
|
22337
|
+
*
|
|
22338
|
+
* Both steps are get-or-create, because this runs on every boot and an
|
|
22339
|
+
* operator must never end up with a second panel after a restart. The
|
|
22340
|
+
* integration is FIXED: it exists because this addon says so, not because
|
|
22341
|
+
* anybody added it, and there is no add/delete flow for it.
|
|
22342
|
+
*/
|
|
22343
|
+
async ensureAlarmPanel(api) {
|
|
22344
|
+
const devices = this.ctx.kernel.devices;
|
|
22345
|
+
try {
|
|
22346
|
+
const result = await ensureAlarmPanel({
|
|
22347
|
+
logger: this.ctx.logger.child("alarm"),
|
|
22348
|
+
addonId: PIPELINE_ANALYTICS_ADDON_ID,
|
|
22349
|
+
integrationName: NC_INTEGRATION_NAME,
|
|
22350
|
+
getIntegration: async (addonId) => api.integrations.getByAddonId.query({ addonId }),
|
|
22351
|
+
createIntegration: async (input) => api.integrations.create.mutate(input),
|
|
22352
|
+
findDeviceByStableId: async (stableId) => {
|
|
22353
|
+
const found = (await api.deviceManager.listAll.query({ addonId: PIPELINE_ANALYTICS_ADDON_ID })).find((d) => d.stableId === stableId);
|
|
22354
|
+
return found === void 0 ? null : { id: found.id };
|
|
22355
|
+
},
|
|
22356
|
+
createDevice: async ({ stableId, integrationId, name }) => {
|
|
22357
|
+
return { id: (await devices.create(stableId, NC_ALARM_DEVICE_CLASS, {}, null, {
|
|
22358
|
+
type: NC_ALARM_DEVICE_TYPE,
|
|
22359
|
+
name,
|
|
22360
|
+
integrationId
|
|
22361
|
+
})).id };
|
|
22362
|
+
}
|
|
22363
|
+
});
|
|
22364
|
+
if (result.created) this.ctx.logger.info("notification-center alarm panel ready", { tags: { deviceId: result.deviceId } });
|
|
22365
|
+
} catch (err) {
|
|
22366
|
+
this.ctx.logger.warn("alarm panel could not be ensured — rules still notify", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
22367
|
+
}
|
|
22368
|
+
}
|
|
21879
22369
|
async declareCollections(api) {
|
|
21880
22370
|
await TrackStore.declare(api.settingsStore);
|
|
21881
22371
|
await MediaStore.declare(api.settingsStore);
|
|
@@ -21900,7 +22390,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
21900
22390
|
let storage = this.ctx.kernel.storage;
|
|
21901
22391
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
21902
22392
|
if (mediaRoot) {
|
|
21903
|
-
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-
|
|
22393
|
+
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-DSR4j3NF.js"));
|
|
21904
22394
|
storage = new FilesystemStorageProvider(mediaRoot, { eventMedia: mediaRoot });
|
|
21905
22395
|
logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
|
|
21906
22396
|
}
|
|
@@ -22257,6 +22747,19 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
22257
22747
|
addonId: device.addonId
|
|
22258
22748
|
};
|
|
22259
22749
|
},
|
|
22750
|
+
actuate: async ({ deviceId, cap, method, args }) => {
|
|
22751
|
+
const router = api[require_dist.kebabToCamel(cap)];
|
|
22752
|
+
if (router === null || typeof router !== "object") throw new Error(`rule action: no router for capability "${cap}"`);
|
|
22753
|
+
const proc = router[method];
|
|
22754
|
+
const call = proc !== null && typeof proc === "object" ? proc : null;
|
|
22755
|
+
const fn = call?.mutate ?? call?.query;
|
|
22756
|
+
if (typeof fn !== "function") throw new Error(`rule action: "${cap}.${method}" is not a callable cap method`);
|
|
22757
|
+
await fn({
|
|
22758
|
+
deviceId,
|
|
22759
|
+
...args
|
|
22760
|
+
});
|
|
22761
|
+
},
|
|
22762
|
+
isCapDeviceScoped: require_dist.isDeviceScopedCap,
|
|
22260
22763
|
readDeviceStates: async (ids) => {
|
|
22261
22764
|
const out = /* @__PURE__ */ new Map();
|
|
22262
22765
|
for (const id of ids) try {
|