@camstack/addon-post-analysis 1.2.34 → 1.2.35
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-7ujnICn7.js → dist-Cmb3B0Ij.js} +203 -96
- package/dist/{dist-Bjhp9nkX.mjs → dist-nh2kuzSC.mjs} +203 -96
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-B4EfMwxa.js → node-BNgGpVnp.js} +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Dh0UMdC9.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DsOwA-XR.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-PQKnzGEk.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-tUf8t2IV.mjs → hostInit-BPatwh25.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +550 -57
- package/dist/pipeline-analytics/index.mjs +549 -56
- 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_ui_mf_2_library__loadShare__.js-DmydN-_W.mjs +0 -26
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as EventCategory, A as notificationRulesCapability, B as createEvent, C as cosineSimilarity, D as faceGalleryCapability, F as videoclipsCapability, G as array, H as isDeviceScopedCap, I as zoneAnalyticsCapability, J as number, K as boolean, L as errMsg, M as plateGalleryCapability, N as readDeviceStateFrom, P as subKindsOf, Q as unknown, R as BaseAddon, S as buildEventKindDescriptor, T as defineCustomActions, U as nodePin, V as hydrateSchema, W as _enum, X as record, Y as object, Z as string, _ as TimelapseRuleInputSchema, a as MACRO_LABELS, b as alarmPanelCapability, c as NcConditionDescriptorSchema, d as NcRuleSchema, f as NcSnoozeInputSchema, g as OpsLogEntrySchema, h as NcTaxonomySchema, i as EVENT_PAD_MS, j as pipelineAnalyticsCapability, k as kebabToCamel, l as NcRuleInputSchema, m as NcSnoozeSuppressedSchema, n as DEFAULT_EVENT_COLOR, o as NC_CONDITION_CATALOG, p as NcSnoozeSchema, q as literal, r as EVENT_KIND_BY_CAP, s as NC_TAXONOMY, t as BaseDevice, u as NcRulePatchSchema, v as TimelapseRuleSchema, w as customAction, x as audioMetricsCapability, y as addonWidgetsSourceCapability, z as DeviceType } from "../dist-
|
|
1
|
+
import { $ as EventCategory, A as notificationRulesCapability, B as createEvent, C as cosineSimilarity, D as faceGalleryCapability, F as videoclipsCapability, G as array, H as isDeviceScopedCap, I as zoneAnalyticsCapability, J as number, K as boolean, L as errMsg, M as plateGalleryCapability, N as readDeviceStateFrom, P as subKindsOf, Q as unknown, R as BaseAddon, S as buildEventKindDescriptor, T as defineCustomActions, U as nodePin, V as hydrateSchema, W as _enum, X as record, Y as object, Z as string, _ as TimelapseRuleInputSchema, a as MACRO_LABELS, b as alarmPanelCapability, c as NcConditionDescriptorSchema, d as NcRuleSchema, f as NcSnoozeInputSchema, g as OpsLogEntrySchema, h as NcTaxonomySchema, i as EVENT_PAD_MS, j as pipelineAnalyticsCapability, k as kebabToCamel, l as NcRuleInputSchema, m as NcSnoozeSuppressedSchema, n as DEFAULT_EVENT_COLOR, o as NC_CONDITION_CATALOG, p as NcSnoozeSchema, q as literal, r as EVENT_KIND_BY_CAP, s as NC_TAXONOMY, t as BaseDevice, u as NcRulePatchSchema, v as TimelapseRuleSchema, w as customAction, x as audioMetricsCapability, y as addonWidgetsSourceCapability, z as DeviceType } from "../dist-nh2kuzSC.mjs";
|
|
2
2
|
import { promises } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -544,7 +544,132 @@ function mimeFromExtension(file) {
|
|
|
544
544
|
return "application/octet-stream";
|
|
545
545
|
}
|
|
546
546
|
//#endregion
|
|
547
|
+
//#region src/notification-center/alarm/alarm-mode-coverage.ts
|
|
548
|
+
/**
|
|
549
|
+
* Whether the panel just BECAME armed, in the sense a person means.
|
|
550
|
+
*
|
|
551
|
+
* Two transitions reach an `armed_*` state and only one of them is an arm:
|
|
552
|
+
*
|
|
553
|
+
* - `arming`/`disarmed` → `armed_away` is somebody arming the alarm. Announce.
|
|
554
|
+
* - `triggered` → `armed_away` is the siren's duration ending and the panel
|
|
555
|
+
* re-arming itself. Announcing it would send "Away armed" in the middle of
|
|
556
|
+
* a break-in, seconds after the alarm notification, saying nothing new.
|
|
557
|
+
*
|
|
558
|
+
* `armed_home` → `armed_away` IS an arm: the operator changed mode, and the
|
|
559
|
+
* set of devices covered just changed with it — which is the whole content of
|
|
560
|
+
* the message.
|
|
561
|
+
*
|
|
562
|
+
* A null `previous` is the FIRST publish and never announces. A hub restarting
|
|
563
|
+
* while armed re-publishes `armed_away`, and nobody armed anything — every
|
|
564
|
+
* deploy would otherwise send one. Handled here rather than by the caller
|
|
565
|
+
* happening to wire the hook late, because "it works because of the order two
|
|
566
|
+
* unrelated things run in" is how this stops working.
|
|
567
|
+
*/
|
|
568
|
+
function shouldAnnounceArm(previous, next) {
|
|
569
|
+
if (previous === null) return false;
|
|
570
|
+
if (!next.startsWith("armed_")) return false;
|
|
571
|
+
if (previous === next) return false;
|
|
572
|
+
if (previous === "triggered") return false;
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
/** The mode inside an `armed_<mode>` state, or null for any other state. */
|
|
576
|
+
function armModeOf(state, modes) {
|
|
577
|
+
for (const mode of modes) if (state === armedStateFor(mode)) return mode;
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
/** The `deviceState` word a rule must gate on for `mode` to cover it. */
|
|
581
|
+
function armedStateFor(mode) {
|
|
582
|
+
return `armed_${mode}`;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* One entry per mode the panel offers, including modes nothing is gated on.
|
|
586
|
+
*
|
|
587
|
+
* Empty modes are KEPT rather than filtered: "Night arms nothing" is the single
|
|
588
|
+
* most useful thing this can tell an operator, and a list that omits it looks
|
|
589
|
+
* identical to a list where night is covered.
|
|
590
|
+
*/
|
|
591
|
+
function alarmModeCoverage(rules, panelDeviceId, modes) {
|
|
592
|
+
return modes.map((mode) => coverageFor(rules, panelDeviceId, mode));
|
|
593
|
+
}
|
|
594
|
+
function coverageFor(rules, panelDeviceId, mode) {
|
|
595
|
+
const wanted = armedStateFor(mode);
|
|
596
|
+
const ids = /* @__PURE__ */ new Set();
|
|
597
|
+
let ruleCount = 0;
|
|
598
|
+
let allDevices = false;
|
|
599
|
+
for (const rule of rules) {
|
|
600
|
+
if (!rule.enabled) continue;
|
|
601
|
+
const gate = rule.conditions.deviceState;
|
|
602
|
+
if (gate === void 0 || gate.deviceId !== panelDeviceId) continue;
|
|
603
|
+
if (!gate.states.includes(wanted)) continue;
|
|
604
|
+
ruleCount += 1;
|
|
605
|
+
const scope = rule.conditions.devices;
|
|
606
|
+
if (scope === void 0 || scope.length === 0) {
|
|
607
|
+
allDevices = true;
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
for (const id of scope) ids.add(id);
|
|
611
|
+
}
|
|
612
|
+
return {
|
|
613
|
+
mode,
|
|
614
|
+
ruleCount,
|
|
615
|
+
allDevices,
|
|
616
|
+
deviceIds: [...ids].toSorted((a, b) => a - b)
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* "Away armed — 3 cameras: Front door, Garage, Gate."
|
|
621
|
+
*
|
|
622
|
+
* Names, not ids: this is read by a person standing at a door. An id that has
|
|
623
|
+
* no name falls back to `Device <id>` rather than being dropped — a silently
|
|
624
|
+
* shorter list would understate what is armed, which is the one error this
|
|
625
|
+
* message must not make.
|
|
626
|
+
*
|
|
627
|
+
* Returns null when the mode covers NOTHING and no rule is gated on it. A
|
|
628
|
+
* notification saying "Night armed" that is followed by nothing happening all
|
|
629
|
+
* night is worse than no notification: it is a false assurance. The caller
|
|
630
|
+
* still logs the arm.
|
|
631
|
+
*/
|
|
632
|
+
function buildArmAnnouncement(coverage, deviceNames) {
|
|
633
|
+
if (coverage.ruleCount === 0) return null;
|
|
634
|
+
const title = `${modeLabel(coverage.mode)} armed`;
|
|
635
|
+
if (coverage.allDevices) return {
|
|
636
|
+
title,
|
|
637
|
+
body: `Every device is armed (${plural$1(coverage.ruleCount, "rule")}).`
|
|
638
|
+
};
|
|
639
|
+
if (coverage.deviceIds.length === 0) return {
|
|
640
|
+
title,
|
|
641
|
+
body: `${plural$1(coverage.ruleCount, "rule")}, no device restriction.`
|
|
642
|
+
};
|
|
643
|
+
const names = coverage.deviceIds.map((id) => deviceNames.get(id) ?? `Device ${id}`);
|
|
644
|
+
return {
|
|
645
|
+
title,
|
|
646
|
+
body: `${plural$1(names.length, "device")} armed: ${names.join(", ")}.`
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
function modeLabel(mode) {
|
|
650
|
+
return mode.charAt(0).toUpperCase() + mode.slice(1).replace(/_/g, " ");
|
|
651
|
+
}
|
|
652
|
+
function plural$1(n, one) {
|
|
653
|
+
return n === 1 ? `1 ${one}` : `${n} ${one}s`;
|
|
654
|
+
}
|
|
655
|
+
//#endregion
|
|
547
656
|
//#region src/notification-center/alarm/alarm-state-machine.ts
|
|
657
|
+
/**
|
|
658
|
+
* Defaults an operator can change per panel. `home` has no exit delay on
|
|
659
|
+
* purpose — nobody is leaving — but the shared value is applied to every mode
|
|
660
|
+
* until per-mode delays are asked for.
|
|
661
|
+
*
|
|
662
|
+
* `triggeredDurationSec: 0` = "sound until somebody disarms it". That is what
|
|
663
|
+
* the panel did before the field existed, so an install that never opens the
|
|
664
|
+
* tab keeps behaving exactly as it did — a default that silently re-armed
|
|
665
|
+
* every existing alarm after N seconds would be a behaviour change nobody
|
|
666
|
+
* asked for.
|
|
667
|
+
*/
|
|
668
|
+
var DEFAULT_DELAYS = {
|
|
669
|
+
exitDelaySec: 30,
|
|
670
|
+
entryDelaySec: 20,
|
|
671
|
+
triggeredDurationSec: 0
|
|
672
|
+
};
|
|
548
673
|
var DISARMED = {
|
|
549
674
|
target: "disarmed",
|
|
550
675
|
fired: false,
|
|
@@ -561,6 +686,7 @@ function armedState(mode) {
|
|
|
561
686
|
* answer is still correct the moment anyone asks.
|
|
562
687
|
*/
|
|
563
688
|
function stateAt(m, now) {
|
|
689
|
+
if (m.clearAt !== void 0 && now >= m.clearAt) return m.target;
|
|
564
690
|
if (m.fired) return "triggered";
|
|
565
691
|
if (m.triggerAt !== void 0) return now >= m.triggerAt ? "triggered" : "pending";
|
|
566
692
|
if (m.armedAt !== void 0) return now >= m.armedAt ? m.target : "arming";
|
|
@@ -614,19 +740,24 @@ function disarm(now) {
|
|
|
614
740
|
* intruder walking past two sensors would otherwise postpone the alarm.
|
|
615
741
|
*/
|
|
616
742
|
function trigger(m, delays, now) {
|
|
743
|
+
const state = stateAt(m, now);
|
|
744
|
+
if (state === "triggered" || state === "pending") return m;
|
|
617
745
|
if (!isArmed(m, now)) return m;
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
...m,
|
|
621
|
-
fired: true,
|
|
622
|
-
changedAt: now
|
|
623
|
-
};
|
|
746
|
+
const base = settle(m, now);
|
|
747
|
+
const firesAt = now + Math.max(0, delays.entryDelaySec) * 1e3;
|
|
624
748
|
return {
|
|
625
|
-
...
|
|
626
|
-
triggerAt:
|
|
749
|
+
...base,
|
|
750
|
+
triggerAt: firesAt,
|
|
751
|
+
...autoClear(delays, firesAt),
|
|
752
|
+
fired: false,
|
|
627
753
|
changedAt: now
|
|
628
754
|
};
|
|
629
755
|
}
|
|
756
|
+
/** `clearAt` iff the operator asked for a bounded siren — see {@link AlarmDelays}. */
|
|
757
|
+
function autoClear(delays, firesAt) {
|
|
758
|
+
if (delays.triggeredDurationSec <= 0) return {};
|
|
759
|
+
return { clearAt: firesAt + delays.triggeredDurationSec * 1e3 };
|
|
760
|
+
}
|
|
630
761
|
/**
|
|
631
762
|
* Settle the machine so a stored value never depends on when it is read.
|
|
632
763
|
*
|
|
@@ -635,10 +766,16 @@ function trigger(m, delays, now) {
|
|
|
635
766
|
* than only in the reading of it.
|
|
636
767
|
*/
|
|
637
768
|
function settle(m, now) {
|
|
769
|
+
if (m.clearAt !== void 0 && now >= m.clearAt) return {
|
|
770
|
+
target: m.target,
|
|
771
|
+
fired: false,
|
|
772
|
+
changedAt: m.clearAt
|
|
773
|
+
};
|
|
638
774
|
if (m.triggerAt !== void 0 && now >= m.triggerAt) return {
|
|
639
775
|
target: m.target,
|
|
640
776
|
fired: true,
|
|
641
|
-
changedAt: m.triggerAt
|
|
777
|
+
changedAt: m.triggerAt,
|
|
778
|
+
...m.clearAt !== void 0 ? { clearAt: m.clearAt } : {}
|
|
642
779
|
};
|
|
643
780
|
if (m.armedAt !== void 0 && now >= m.armedAt) return {
|
|
644
781
|
target: m.target,
|
|
@@ -667,21 +804,12 @@ function settle(m, now) {
|
|
|
667
804
|
* and re-publish when a delay elapses.
|
|
668
805
|
*/
|
|
669
806
|
/** Every arm mode a camstack-owned panel offers. */
|
|
670
|
-
var
|
|
807
|
+
var NC_ALARM_MODES = [
|
|
671
808
|
"home",
|
|
672
809
|
"away",
|
|
673
810
|
"night"
|
|
674
811
|
];
|
|
675
812
|
/**
|
|
676
|
-
* Defaults an operator can change per panel. `home` has no exit delay on
|
|
677
|
-
* purpose — nobody is leaving — but the shared value is applied to every mode
|
|
678
|
-
* until per-mode delays are asked for.
|
|
679
|
-
*/
|
|
680
|
-
var DEFAULT_DELAYS = {
|
|
681
|
-
exitDelaySec: 30,
|
|
682
|
-
entryDelaySec: 20
|
|
683
|
-
};
|
|
684
|
-
/**
|
|
685
813
|
* How often the panel re-publishes while a delay is running.
|
|
686
814
|
*
|
|
687
815
|
* The state is computed from an INSTANT, so this tick only decides how quickly
|
|
@@ -697,30 +825,94 @@ var TICK_MS = 1e3;
|
|
|
697
825
|
var ncAlarmPanelSchema = object({
|
|
698
826
|
exitDelaySec: number().int().min(0).max(600).default(DEFAULT_DELAYS.exitDelaySec),
|
|
699
827
|
entryDelaySec: number().int().min(0).max(600).default(DEFAULT_DELAYS.entryDelaySec),
|
|
828
|
+
triggeredDurationSec: number().int().min(0).max(3600).default(DEFAULT_DELAYS.triggeredDurationSec),
|
|
829
|
+
/** Send a notification when a mode takes effect. Off until asked for. */
|
|
830
|
+
announceArm: boolean().default(false),
|
|
831
|
+
/** Target ids that announcement goes to — see `NcAlarmSettingsSchema`. */
|
|
832
|
+
announceTargets: array(string()).default([]),
|
|
700
833
|
machine: record(string(), unknown()).optional()
|
|
701
834
|
});
|
|
835
|
+
/** `armed_home` and friends — the only targets a machine may settle into. */
|
|
836
|
+
function isArmTarget(value) {
|
|
837
|
+
return value === "disarmed" || typeof value === "string" && value.startsWith("armed_");
|
|
838
|
+
}
|
|
839
|
+
function optionalInstant(value) {
|
|
840
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Rebuild the machine from the persisted blob, or null when there is nothing
|
|
844
|
+
* trustworthy in it.
|
|
845
|
+
*
|
|
846
|
+
* A type GUARD, not a cast: the blob has survived a schema change once already
|
|
847
|
+
* (`clearAt` did not exist), and the failure mode of a cast here is an alarm
|
|
848
|
+
* that reports a state it cannot reach. Anything unrecognised falls back to
|
|
849
|
+
* DISARMED, which is the state an operator will notice.
|
|
850
|
+
*
|
|
851
|
+
* Exported for the test that proves a pre-`clearAt` blob still restores.
|
|
852
|
+
*/
|
|
853
|
+
function machineFromPersisted(raw) {
|
|
854
|
+
if (raw === null || typeof raw !== "object") return null;
|
|
855
|
+
const rec = { ...raw };
|
|
856
|
+
if (!isArmTarget(rec["target"]) || typeof rec["changedAt"] !== "number") return null;
|
|
857
|
+
const armedAt = optionalInstant(rec["armedAt"]);
|
|
858
|
+
const triggerAt = optionalInstant(rec["triggerAt"]);
|
|
859
|
+
const clearAt = optionalInstant(rec["clearAt"]);
|
|
860
|
+
return {
|
|
861
|
+
target: rec["target"],
|
|
862
|
+
...armedAt !== void 0 ? { armedAt } : {},
|
|
863
|
+
...triggerAt !== void 0 ? { triggerAt } : {},
|
|
864
|
+
...clearAt !== void 0 ? { clearAt } : {},
|
|
865
|
+
fired: rec["fired"] === true,
|
|
866
|
+
changedAt: rec["changedAt"]
|
|
867
|
+
};
|
|
868
|
+
}
|
|
702
869
|
var NcAlarmPanelDevice = class extends BaseDevice {
|
|
703
870
|
features = [];
|
|
704
871
|
machine = DISARMED;
|
|
705
872
|
delays = DEFAULT_DELAYS;
|
|
706
873
|
ticker = null;
|
|
874
|
+
onArmed = null;
|
|
875
|
+
/**
|
|
876
|
+
* The state the last publish reported. Null until the first one, so the
|
|
877
|
+
* BOOT publish never announces: a hub restarting while armed would otherwise
|
|
878
|
+
* announce "Away armed" on every deploy.
|
|
879
|
+
*/
|
|
880
|
+
lastPublished = null;
|
|
707
881
|
constructor(ctx) {
|
|
708
882
|
super(ctx, ncAlarmPanelSchema, { type: ctx.deviceMeta.type });
|
|
709
|
-
this.delays =
|
|
710
|
-
exitDelaySec: this.config.get("exitDelaySec"),
|
|
711
|
-
entryDelaySec: this.config.get("entryDelaySec")
|
|
712
|
-
};
|
|
883
|
+
this.delays = this.readDelays();
|
|
713
884
|
this.restore();
|
|
714
885
|
this.registerAlarmCap();
|
|
715
886
|
this.publish();
|
|
716
887
|
}
|
|
717
|
-
/**
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
888
|
+
/** The operator-visible configuration, as the cap serves it. */
|
|
889
|
+
settings() {
|
|
890
|
+
return {
|
|
891
|
+
...this.readDelays(),
|
|
892
|
+
announceArm: this.config.get("announceArm"),
|
|
893
|
+
announceTargets: [...this.config.get("announceTargets")]
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Apply an editor's patch and return what the panel settled on.
|
|
898
|
+
*
|
|
899
|
+
* The new delays take effect on the NEXT transition, never retroactively: a
|
|
900
|
+
* machine already counting down keeps the instants it was given. Re-deriving
|
|
901
|
+
* `armedAt` from a delay the operator changed mid-exit would move the arm
|
|
902
|
+
* instant under somebody who is walking out of the door.
|
|
903
|
+
*/
|
|
904
|
+
async applySettings(patch) {
|
|
905
|
+
if (patch.exitDelaySec !== void 0) await this.config.set("exitDelaySec", patch.exitDelaySec);
|
|
906
|
+
if (patch.entryDelaySec !== void 0) await this.config.set("entryDelaySec", patch.entryDelaySec);
|
|
907
|
+
if (patch.triggeredDurationSec !== void 0) await this.config.set("triggeredDurationSec", patch.triggeredDurationSec);
|
|
908
|
+
if (patch.announceArm !== void 0) await this.config.set("announceArm", patch.announceArm);
|
|
909
|
+
if (patch.announceTargets !== void 0) await this.config.set("announceTargets", [...patch.announceTargets]);
|
|
910
|
+
this.delays = this.readDelays();
|
|
911
|
+
return this.settings();
|
|
912
|
+
}
|
|
913
|
+
/** Wire the arm announcement. See {@link NcAlarmArmedHook}. */
|
|
914
|
+
setArmedHook(hook) {
|
|
915
|
+
this.onArmed = hook;
|
|
724
916
|
}
|
|
725
917
|
/** Current lifecycle word — what the `deviceState` gate compares against. */
|
|
726
918
|
currentState(now = Date.now()) {
|
|
@@ -743,19 +935,24 @@ var NcAlarmPanelDevice = class extends BaseDevice {
|
|
|
743
935
|
this.publish();
|
|
744
936
|
},
|
|
745
937
|
trigger: async () => {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
938
|
+
const now = Date.now();
|
|
939
|
+
const next = trigger(this.machine, this.delays, now);
|
|
940
|
+
if (next === this.machine) {
|
|
941
|
+
this.ctx.logger.info("alarm trigger ignored — the panel is not armed", {
|
|
942
|
+
tags: { deviceId: this.id },
|
|
943
|
+
meta: { state: stateAt(this.machine, now) }
|
|
944
|
+
});
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
this.machine = next;
|
|
948
|
+
this.publish(now);
|
|
752
949
|
}
|
|
753
950
|
});
|
|
754
951
|
}
|
|
755
952
|
status(now = Date.now()) {
|
|
756
953
|
return {
|
|
757
954
|
state: stateAt(this.machine, now),
|
|
758
|
-
availableModes: [...
|
|
955
|
+
availableModes: [...NC_ALARM_MODES],
|
|
759
956
|
requiresCode: false,
|
|
760
957
|
lastChangedAt: this.machine.changedAt
|
|
761
958
|
};
|
|
@@ -769,11 +966,48 @@ var NcAlarmPanelDevice = class extends BaseDevice {
|
|
|
769
966
|
*/
|
|
770
967
|
publish(now = Date.now()) {
|
|
771
968
|
this.machine = settle(this.machine, now);
|
|
969
|
+
const state = stateAt(this.machine, now);
|
|
772
970
|
this.runtimeState.setCapState("alarm-panel", this.status(now));
|
|
773
971
|
this.persist();
|
|
774
|
-
|
|
972
|
+
this.announceIfArmed(state);
|
|
973
|
+
if (this.machine.armedAt !== void 0 || this.machine.triggerAt !== void 0 || this.machine.clearAt !== void 0) this.startTicking();
|
|
775
974
|
else this.stopTicking();
|
|
776
975
|
}
|
|
976
|
+
/**
|
|
977
|
+
* Fire the arm hook on the transition INTO an armed mode.
|
|
978
|
+
*
|
|
979
|
+
* Never throws into the publish path: an announcement that could take the
|
|
980
|
+
* panel's own state update with it would make a notification failure look
|
|
981
|
+
* like an alarm failure.
|
|
982
|
+
*/
|
|
983
|
+
announceIfArmed(state) {
|
|
984
|
+
const previous = this.lastPublished;
|
|
985
|
+
this.lastPublished = state;
|
|
986
|
+
const hook = this.onArmed;
|
|
987
|
+
if (hook === null) return;
|
|
988
|
+
if (!shouldAnnounceArm(previous, state)) return;
|
|
989
|
+
const mode = armModeOf(state, NC_ALARM_MODES);
|
|
990
|
+
if (mode === null) return;
|
|
991
|
+
try {
|
|
992
|
+
hook(mode);
|
|
993
|
+
} catch (err) {
|
|
994
|
+
this.ctx.logger.warn("alarm arm announcement failed", {
|
|
995
|
+
tags: { deviceId: this.id },
|
|
996
|
+
meta: {
|
|
997
|
+
mode,
|
|
998
|
+
error: String(err)
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
/** The three durations, read from the persisted config. */
|
|
1004
|
+
readDelays() {
|
|
1005
|
+
return {
|
|
1006
|
+
exitDelaySec: this.config.get("exitDelaySec"),
|
|
1007
|
+
entryDelaySec: this.config.get("entryDelaySec"),
|
|
1008
|
+
triggeredDurationSec: this.config.get("triggeredDurationSec")
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
777
1011
|
startTicking() {
|
|
778
1012
|
if (this.ticker !== null) return;
|
|
779
1013
|
this.ticker = setInterval(() => this.publish(), TICK_MS);
|
|
@@ -790,17 +1024,9 @@ var NcAlarmPanelDevice = class extends BaseDevice {
|
|
|
790
1024
|
this.config.set("machine", { ...this.machine });
|
|
791
1025
|
}
|
|
792
1026
|
restore() {
|
|
793
|
-
const
|
|
794
|
-
if (
|
|
795
|
-
|
|
796
|
-
if (typeof m.target !== "string" || typeof m.changedAt !== "number") return;
|
|
797
|
-
this.machine = {
|
|
798
|
-
target: m.target,
|
|
799
|
-
...typeof m.armedAt === "number" ? { armedAt: m.armedAt } : {},
|
|
800
|
-
...typeof m.triggerAt === "number" ? { triggerAt: m.triggerAt } : {},
|
|
801
|
-
fired: m.fired === true,
|
|
802
|
-
changedAt: m.changedAt
|
|
803
|
-
};
|
|
1027
|
+
const restored = machineFromPersisted(this.config.get("machine"));
|
|
1028
|
+
if (restored === null) return;
|
|
1029
|
+
this.machine = restored;
|
|
804
1030
|
}
|
|
805
1031
|
};
|
|
806
1032
|
//#endregion
|
|
@@ -5068,14 +5294,21 @@ function readSensorEventType(value) {
|
|
|
5068
5294
|
const eventType = last["eventType"];
|
|
5069
5295
|
return typeof eventType === "string" && eventType.length > 0 ? eventType : void 0;
|
|
5070
5296
|
}
|
|
5071
|
-
/**
|
|
5072
|
-
*
|
|
5297
|
+
/**
|
|
5298
|
+
* Build the subject for a `device-event` evaluation (a persisted SensorEvent —
|
|
5299
|
+
* one row per linked camera; `deviceId` is the CAMERA).
|
|
5300
|
+
*
|
|
5301
|
+
* BOTH ids ride along. The camera is what the notification shows; the sensor is
|
|
5302
|
+
* what the operator named in the rule. Carrying only the camera is what made a
|
|
5303
|
+
* sensor-scoped rule unmatched — see {@link NcRuleSubject.sourceDeviceId}.
|
|
5304
|
+
*/
|
|
5073
5305
|
function subjectFromSensorEvent(ev) {
|
|
5074
5306
|
const eventType = readSensorEventType(ev.value);
|
|
5075
5307
|
return {
|
|
5076
5308
|
kind: "device-event",
|
|
5077
5309
|
recordId: ev.id,
|
|
5078
5310
|
deviceId: ev.deviceId,
|
|
5311
|
+
...ev.sourceDeviceId !== ev.deviceId ? { sourceDeviceId: ev.sourceDeviceId } : {},
|
|
5079
5312
|
timestamp: ev.timestamp,
|
|
5080
5313
|
classNames: [],
|
|
5081
5314
|
zones: [],
|
|
@@ -5307,7 +5540,7 @@ function evaluateRule(rule, subject, deviceState) {
|
|
|
5307
5540
|
if (current === void 0) return fail("deviceState");
|
|
5308
5541
|
if (!toLowerSet(c.deviceState.states).has(current.trim().toLowerCase())) return fail("deviceState");
|
|
5309
5542
|
}
|
|
5310
|
-
if (c.devices !== void 0 && c.devices.length > 0 && !c.devices
|
|
5543
|
+
if (c.devices !== void 0 && c.devices.length > 0 && !matchesDeviceScope(c.devices, subject)) return fail("devices");
|
|
5311
5544
|
if (c.source !== void 0 && c.source !== "any") {
|
|
5312
5545
|
if ((subject.source ?? "pipeline") !== c.source) return fail("source");
|
|
5313
5546
|
}
|
|
@@ -5494,6 +5727,28 @@ function keysPerClass(rule, subject) {
|
|
|
5494
5727
|
return rule.throttle.granularity === "per-class";
|
|
5495
5728
|
}
|
|
5496
5729
|
/** Stable cooldown key per the rule's throttle scope + class granularity. */
|
|
5730
|
+
/**
|
|
5731
|
+
* Does the rule's device scope cover this subject?
|
|
5732
|
+
*
|
|
5733
|
+
* EITHER id matches, and both are needed for the same rule to be authorable
|
|
5734
|
+
* the two ways an operator thinks about it:
|
|
5735
|
+
*
|
|
5736
|
+
* - "when anything happens on the front-door CAMERA" → the camera id, which
|
|
5737
|
+
* is what a sensor row is attributed to;
|
|
5738
|
+
* - "when the front-door CONTACT opens" → the sensor id, which is the device
|
|
5739
|
+
* the operator actually cares about and the one they pick in the Devices
|
|
5740
|
+
* tab. Before this, that rule matched nothing, ever, silently.
|
|
5741
|
+
*
|
|
5742
|
+
* Consequence worth knowing: a sensor linked to N cameras persists N rows, so
|
|
5743
|
+
* a SENSOR-scoped rule is evaluated N times for one door opening. The rule's
|
|
5744
|
+
* own throttle is what collapses that — `scope: 'rule'` gives one
|
|
5745
|
+
* notification, `scope: 'device'` gives one per camera, which is the right
|
|
5746
|
+
* choice when each carries its own picture.
|
|
5747
|
+
*/
|
|
5748
|
+
function matchesDeviceScope(scope, subject) {
|
|
5749
|
+
if (scope.includes(subject.deviceId)) return true;
|
|
5750
|
+
return subject.sourceDeviceId !== void 0 && scope.includes(subject.sourceDeviceId);
|
|
5751
|
+
}
|
|
5497
5752
|
function cooldownKey(rule, subject) {
|
|
5498
5753
|
const first = subject.classNames[0];
|
|
5499
5754
|
const classKey = keysPerClass(rule, subject) && first !== void 0 ? `:c:${first}` : "";
|
|
@@ -5768,6 +6023,82 @@ var NcDispatcher = class {
|
|
|
5768
6023
|
return false;
|
|
5769
6024
|
}
|
|
5770
6025
|
}
|
|
6026
|
+
/**
|
|
6027
|
+
* Send a Notification-Center-composed message to ONE target.
|
|
6028
|
+
*
|
|
6029
|
+
* Never throws and never retries — see {@link NcAnnouncementSendInput}. Every
|
|
6030
|
+
* way of not delivering produces a line: an announcement that silently did
|
|
6031
|
+
* not arrive is indistinguishable from the alarm not having armed, which is
|
|
6032
|
+
* the exact confusion this feature exists to remove.
|
|
6033
|
+
*/
|
|
6034
|
+
async deliverAnnouncement(input) {
|
|
6035
|
+
const tags = input.deviceId !== void 0 ? { deviceId: input.deviceId } : void 0;
|
|
6036
|
+
let target;
|
|
6037
|
+
try {
|
|
6038
|
+
target = await this.resolveTarget(input.targetId);
|
|
6039
|
+
} catch (err) {
|
|
6040
|
+
this.deps.logger.warn("announcement: target catalog unreachable", {
|
|
6041
|
+
...tags !== void 0 ? { tags } : {},
|
|
6042
|
+
meta: {
|
|
6043
|
+
reason: input.reason,
|
|
6044
|
+
targetId: input.targetId,
|
|
6045
|
+
error: String(err)
|
|
6046
|
+
}
|
|
6047
|
+
});
|
|
6048
|
+
return false;
|
|
6049
|
+
}
|
|
6050
|
+
if (target === null || !target.enabled) {
|
|
6051
|
+
this.deps.logger.warn("announcement: target gone or disabled", {
|
|
6052
|
+
...tags !== void 0 ? { tags } : {},
|
|
6053
|
+
meta: {
|
|
6054
|
+
reason: input.reason,
|
|
6055
|
+
targetId: input.targetId
|
|
6056
|
+
}
|
|
6057
|
+
});
|
|
6058
|
+
return false;
|
|
6059
|
+
}
|
|
6060
|
+
try {
|
|
6061
|
+
const result = await this.deps.send({
|
|
6062
|
+
addonId: target.addonId,
|
|
6063
|
+
targetId: target.id,
|
|
6064
|
+
notification: {
|
|
6065
|
+
title: input.title,
|
|
6066
|
+
body: input.body,
|
|
6067
|
+
priority: 2,
|
|
6068
|
+
...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {}
|
|
6069
|
+
}
|
|
6070
|
+
});
|
|
6071
|
+
if (!result.success) {
|
|
6072
|
+
this.deps.logger.warn("announcement send failed", {
|
|
6073
|
+
...tags !== void 0 ? { tags } : {},
|
|
6074
|
+
meta: {
|
|
6075
|
+
reason: input.reason,
|
|
6076
|
+
targetId: target.id,
|
|
6077
|
+
error: result.error
|
|
6078
|
+
}
|
|
6079
|
+
});
|
|
6080
|
+
return false;
|
|
6081
|
+
}
|
|
6082
|
+
this.deps.logger.info("announcement delivered", {
|
|
6083
|
+
...tags !== void 0 ? { tags } : {},
|
|
6084
|
+
meta: {
|
|
6085
|
+
reason: input.reason,
|
|
6086
|
+
target: target.name
|
|
6087
|
+
}
|
|
6088
|
+
});
|
|
6089
|
+
return true;
|
|
6090
|
+
} catch (err) {
|
|
6091
|
+
this.deps.logger.warn("announcement send threw", {
|
|
6092
|
+
...tags !== void 0 ? { tags } : {},
|
|
6093
|
+
meta: {
|
|
6094
|
+
reason: input.reason,
|
|
6095
|
+
targetId: target.id,
|
|
6096
|
+
error: String(err)
|
|
6097
|
+
}
|
|
6098
|
+
});
|
|
6099
|
+
return false;
|
|
6100
|
+
}
|
|
6101
|
+
}
|
|
5771
6102
|
/** Device display names for a set of ids, for the digest's lines. */
|
|
5772
6103
|
async resolveDeviceNames(deviceIds) {
|
|
5773
6104
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -8124,6 +8455,19 @@ var TimelapseStore = class {
|
|
|
8124
8455
|
* CRUD served elsewhere becomes effective within one rule-reload tick.
|
|
8125
8456
|
*/
|
|
8126
8457
|
/**
|
|
8458
|
+
* What `getAlarmConfig` answers on a node with no panel.
|
|
8459
|
+
*
|
|
8460
|
+
* The DEFAULTS, not zeroes: paired with `deviceId: null` this tells a client
|
|
8461
|
+
* "there is no panel here" while still describing the shape it would have.
|
|
8462
|
+
* Zeroes would render as "no entry delay", which is a claim about an alarm
|
|
8463
|
+
* that does not exist.
|
|
8464
|
+
*/
|
|
8465
|
+
var NC_ALARM_SETTINGS_FALLBACK = {
|
|
8466
|
+
...DEFAULT_DELAYS,
|
|
8467
|
+
announceArm: false,
|
|
8468
|
+
announceTargets: []
|
|
8469
|
+
};
|
|
8470
|
+
/**
|
|
8127
8471
|
* How often the "matched NO rule" report may fire per device. Long enough that
|
|
8128
8472
|
* a busy camera prints one line rather than one per event, short enough that a
|
|
8129
8473
|
* rule which has stopped matching is visible within minutes rather than by
|
|
@@ -8249,6 +8593,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
8249
8593
|
/** True when ≥1 enabled `device-event` rule declares an occupancy condition —
|
|
8250
8594
|
* the watcher is idle (zero per-frame cost) otherwise. */
|
|
8251
8595
|
occupancyEnabled = false;
|
|
8596
|
+
/** The panel this node owns, or null. See {@link NcAlarmPanelPort}. */
|
|
8597
|
+
alarmPanel = null;
|
|
8252
8598
|
/** In-memory cooldown map — seeded from persisted outbox rows on start. */
|
|
8253
8599
|
lastFiredAt = /* @__PURE__ */ new Map();
|
|
8254
8600
|
/** Per-device rate limit for the "matched NO rule" report — see `reportNoMatch`. */
|
|
@@ -8679,9 +9025,105 @@ var NotificationCenter = class NotificationCenter {
|
|
|
8679
9025
|
cancelSnooze: async ({ snoozeId, caller }) => {
|
|
8680
9026
|
await this.cancelSnooze(snoozeId, caller);
|
|
8681
9027
|
return { success: true };
|
|
9028
|
+
},
|
|
9029
|
+
getAlarmConfig: async () => this.alarmConfig(),
|
|
9030
|
+
setAlarmConfig: async ({ patch }) => {
|
|
9031
|
+
const panel = this.alarmPanel;
|
|
9032
|
+
if (panel === null) throw new Error("this node has no alarm panel");
|
|
9033
|
+
const settings = await panel.applySettings(patch);
|
|
9034
|
+
this.logger.info("alarm settings changed", {
|
|
9035
|
+
tags: { deviceId: panel.deviceId },
|
|
9036
|
+
meta: {
|
|
9037
|
+
exitDelaySec: settings.exitDelaySec,
|
|
9038
|
+
entryDelaySec: settings.entryDelaySec,
|
|
9039
|
+
triggeredDurationSec: settings.triggeredDurationSec,
|
|
9040
|
+
announceArm: settings.announceArm,
|
|
9041
|
+
announceTargets: settings.announceTargets.length
|
|
9042
|
+
}
|
|
9043
|
+
});
|
|
9044
|
+
return this.alarmConfig();
|
|
8682
9045
|
}
|
|
8683
9046
|
};
|
|
8684
9047
|
}
|
|
9048
|
+
/**
|
|
9049
|
+
* Adopt the panel this node owns. Called once by the addon after
|
|
9050
|
+
* `ensureAlarmPanel`; a node without one never calls it and the cap then
|
|
9051
|
+
* answers `deviceId: null`.
|
|
9052
|
+
*/
|
|
9053
|
+
setAlarmPanel(panel) {
|
|
9054
|
+
this.alarmPanel = panel;
|
|
9055
|
+
}
|
|
9056
|
+
/** Settings + the DERIVED coverage. Read together — see the cap doc. */
|
|
9057
|
+
alarmConfig() {
|
|
9058
|
+
const panel = this.alarmPanel;
|
|
9059
|
+
if (panel === null) return {
|
|
9060
|
+
deviceId: null,
|
|
9061
|
+
settings: NC_ALARM_SETTINGS_FALLBACK,
|
|
9062
|
+
coverage: []
|
|
9063
|
+
};
|
|
9064
|
+
return {
|
|
9065
|
+
deviceId: panel.deviceId,
|
|
9066
|
+
settings: panel.settings(),
|
|
9067
|
+
coverage: this.coverageFor(panel).map((c) => ({
|
|
9068
|
+
...c,
|
|
9069
|
+
deviceIds: [...c.deviceIds]
|
|
9070
|
+
}))
|
|
9071
|
+
};
|
|
9072
|
+
}
|
|
9073
|
+
coverageFor(panel) {
|
|
9074
|
+
return alarmModeCoverage(this.rules.list(), panel.deviceId, panel.availableModes());
|
|
9075
|
+
}
|
|
9076
|
+
/**
|
|
9077
|
+
* Say which devices a mode just armed.
|
|
9078
|
+
*
|
|
9079
|
+
* Called by the panel on the transition INTO an armed mode. Fire-and-forget
|
|
9080
|
+
* and never able to fail the panel: `announceArm` returns void, and every
|
|
9081
|
+
* branch that sends nothing logs why — a mode that arms nothing is the most
|
|
9082
|
+
* important thing this can report, and it is exactly the case where no
|
|
9083
|
+
* message goes out.
|
|
9084
|
+
*/
|
|
9085
|
+
announceArm(mode) {
|
|
9086
|
+
this.deliverArmAnnouncement(mode).catch((err) => {
|
|
9087
|
+
this.logger.warn("alarm arm announcement failed", { meta: {
|
|
9088
|
+
mode,
|
|
9089
|
+
error: String(err)
|
|
9090
|
+
} });
|
|
9091
|
+
});
|
|
9092
|
+
}
|
|
9093
|
+
async deliverArmAnnouncement(mode) {
|
|
9094
|
+
const panel = this.alarmPanel;
|
|
9095
|
+
if (panel === null) return;
|
|
9096
|
+
const settings = panel.settings();
|
|
9097
|
+
const tags = { deviceId: panel.deviceId };
|
|
9098
|
+
if (!settings.announceArm) return;
|
|
9099
|
+
if (settings.announceTargets.length === 0) {
|
|
9100
|
+
this.logger.info("alarm armed but no announcement target is configured", {
|
|
9101
|
+
tags,
|
|
9102
|
+
meta: { mode }
|
|
9103
|
+
});
|
|
9104
|
+
return;
|
|
9105
|
+
}
|
|
9106
|
+
const coverage = this.coverageFor(panel).find((c) => c.mode === mode);
|
|
9107
|
+
if (coverage === void 0) return;
|
|
9108
|
+
const message = buildArmAnnouncement(coverage, await this.dispatcher.resolveDeviceNames(coverage.deviceIds));
|
|
9109
|
+
if (message === null) {
|
|
9110
|
+
this.logger.warn("alarm armed into a mode no enabled rule is gated on", {
|
|
9111
|
+
tags,
|
|
9112
|
+
meta: {
|
|
9113
|
+
mode,
|
|
9114
|
+
state: `armed_${mode}`
|
|
9115
|
+
}
|
|
9116
|
+
});
|
|
9117
|
+
return;
|
|
9118
|
+
}
|
|
9119
|
+
for (const targetId of settings.announceTargets) await this.dispatcher.deliverAnnouncement({
|
|
9120
|
+
reason: "alarm-arm",
|
|
9121
|
+
targetId,
|
|
9122
|
+
title: message.title,
|
|
9123
|
+
body: message.body,
|
|
9124
|
+
deviceId: panel.deviceId
|
|
9125
|
+
});
|
|
9126
|
+
}
|
|
8685
9127
|
/** Append one evaluation to the serialized chain (see {@link evalChain}). */
|
|
8686
9128
|
scheduleEvaluation(subject, kind, logContext) {
|
|
8687
9129
|
this.evalChain = this.evalChain.then(async () => {
|
|
@@ -22660,6 +23102,17 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
22660
23102
|
* `notification-rules` provider (CRUD over the central store) is
|
|
22661
23103
|
* registered from every node. */
|
|
22662
23104
|
notificationCenter = null;
|
|
23105
|
+
/**
|
|
23106
|
+
* The live alarm panel on THIS node, or null.
|
|
23107
|
+
*
|
|
23108
|
+
* Held, where the earlier note said not to. The objection then was a field
|
|
23109
|
+
* nothing reads growing into a second source of truth for which device the
|
|
23110
|
+
* alarm is — and it stands: the panel is still addressed by stable id
|
|
23111
|
+
* everywhere else. This reference exists because two things now READ it, and
|
|
23112
|
+
* neither can go through the device id: the settings tab, which needs the
|
|
23113
|
+
* panel's own config, and the arm announcement, which the panel pushes.
|
|
23114
|
+
*/
|
|
23115
|
+
alarmPanelDevice = null;
|
|
22663
23116
|
/** THE single owner of capture pressure (S3, refactor spec 2026-07-22):
|
|
22664
23117
|
* every native-fetch call site routed so far (the per-frame batch media
|
|
22665
23118
|
* dispatch + `persistKeyFrames`; part B routes the rest) goes through ONE
|
|
@@ -22950,14 +23403,18 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
22950
23403
|
return found === void 0 ? null : { id: found.id };
|
|
22951
23404
|
},
|
|
22952
23405
|
createDevice: async ({ stableId, integrationId, name }) => {
|
|
22953
|
-
|
|
23406
|
+
const device = await devices.create(stableId, NC_ALARM_DEVICE_CLASS, {}, null, {
|
|
22954
23407
|
type: NC_ALARM_DEVICE_TYPE,
|
|
22955
23408
|
name,
|
|
22956
23409
|
integrationId
|
|
22957
|
-
})
|
|
23410
|
+
});
|
|
23411
|
+
this.holdAlarmPanel(device);
|
|
23412
|
+
return { id: device.id };
|
|
22958
23413
|
},
|
|
22959
23414
|
adoptDevice: async ({ stableId }) => {
|
|
22960
|
-
|
|
23415
|
+
const device = await devices.create(stableId, NC_ALARM_DEVICE_CLASS, {}, null, void 0);
|
|
23416
|
+
this.holdAlarmPanel(device);
|
|
23417
|
+
return { id: device.id };
|
|
22961
23418
|
}
|
|
22962
23419
|
});
|
|
22963
23420
|
if (result.created) this.ctx.logger.info("notification-center alarm panel ready", { tags: { deviceId: result.deviceId } });
|
|
@@ -22965,6 +23422,41 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
22965
23422
|
this.ctx.logger.warn("alarm panel could not be ensured — rules still notify", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
22966
23423
|
}
|
|
22967
23424
|
}
|
|
23425
|
+
/**
|
|
23426
|
+
* Keep the constructed panel, if that is what came back.
|
|
23427
|
+
*
|
|
23428
|
+
* An `instanceof` narrowing rather than a cast: `devices.create` promises an
|
|
23429
|
+
* `IDevice`, and the only honest way to know it is the alarm is to ask. A
|
|
23430
|
+
* cast here would compile against any future device class and fail at the
|
|
23431
|
+
* first `settings()` call, in production, with the tab already open.
|
|
23432
|
+
*/
|
|
23433
|
+
holdAlarmPanel(device) {
|
|
23434
|
+
if (!(device instanceof NcAlarmPanelDevice)) {
|
|
23435
|
+
this.ctx.logger.warn("alarm panel is not the expected device class — settings tab disabled");
|
|
23436
|
+
return;
|
|
23437
|
+
}
|
|
23438
|
+
this.alarmPanelDevice = device;
|
|
23439
|
+
}
|
|
23440
|
+
/**
|
|
23441
|
+
* Join the panel and the Notification Center, once both exist.
|
|
23442
|
+
*
|
|
23443
|
+
* They are built in that order (`ensureAlarmPanel` runs before the centre is
|
|
23444
|
+
* constructed) and neither can reach the other on its own, so the join is
|
|
23445
|
+
* here. A node with no panel simply never calls it, and the cap answers
|
|
23446
|
+
* `deviceId: null` — which is what an agent node should say.
|
|
23447
|
+
*/
|
|
23448
|
+
wireAlarmPanel(center) {
|
|
23449
|
+
const panel = this.alarmPanelDevice;
|
|
23450
|
+
if (panel === null) return;
|
|
23451
|
+
center.setAlarmPanel({
|
|
23452
|
+
deviceId: panel.id,
|
|
23453
|
+
settings: () => panel.settings(),
|
|
23454
|
+
applySettings: (patch) => panel.applySettings(patch),
|
|
23455
|
+
availableModes: () => NC_ALARM_MODES
|
|
23456
|
+
});
|
|
23457
|
+
panel.setArmedHook((mode) => center.announceArm(mode));
|
|
23458
|
+
this.ctx.logger.info("alarm panel wired to the notification center", { tags: { deviceId: panel.id } });
|
|
23459
|
+
}
|
|
22968
23460
|
async declareCollections(api) {
|
|
22969
23461
|
await TrackStore.declare(api.settingsStore);
|
|
22970
23462
|
await MediaStore.declare(api.settingsStore);
|
|
@@ -23450,6 +23942,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
23450
23942
|
})).tracks;
|
|
23451
23943
|
}
|
|
23452
23944
|
});
|
|
23945
|
+
this.wireAlarmPanel(this.notificationCenter);
|
|
23453
23946
|
await this.serveNcActionPlane(this.notificationCenter);
|
|
23454
23947
|
await this.notificationCenter.start({ evaluation: this.isPostProcessingNode });
|
|
23455
23948
|
}
|