@camstack/addon-post-analysis 1.2.322 → 1.2.324
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{clip-model-registry-BFbP9U7z.js → clip-model-registry-BFLeI0dr.js} +5683 -5032
- package/dist/{clip-model-registry-CwK2SLQD.mjs → clip-model-registry-CshjlVTF.mjs} +5450 -4835
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +2 -2
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Dmt5eh93.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BiD7k7AM.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CBG_lEGV.mjs +26 -0
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Bnb-yu_h.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-DtKO_7UT.mjs → hostInit-D0L1yv82.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +745 -137
- package/dist/pipeline-analytics/index.mjs +745 -137
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-mzYK6V9S.mjs +0 -26
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CuOcvcK7.mjs +0 -26
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as NC_ALARM_DELAY_MAX_SEC, $n as
|
|
1
|
+
import { $ as NC_ALARM_DELAY_MAX_SEC, $n as gifFromMp4Args, $r as DeviceType, $t as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, A as EMBEDDING_REQUIREMENTS_FAILED, An as audioMetricsCapability, Ar as ruleDeviceCompatCapQueries, At as NcRulePatchSchema, B as ListTrackRelationsInputSchema, Bn as describeSystemEventFamilyProblem, Br as systemEventDeliveryProblems, Bt as NcSystemEventKindSchema, C as DEFAULT_EVENT_COLOR, Ci as string, Cn as alarmDeviceSecureOnArm, Cr as readTimelapseGeneratedAt, Ct as NcAlarmSettingsPatchSchema, D as DeclaredDevices, Di as unknown, Dn as applyNonBlockingEdit, Dr as resolveModeDelays, Dt as NcRuleActionSchema, E as DETECTION_PIPELINE_CAP_NAME, Ei as union, En as applyModeDelaysEdits, Er as resolveLocationMode, Et as NcLegacyRuleActionSequenceSchema, F as EXPORT_DENSE_MAX_RANGES, Fn as cosineSimilarity$1, Fr as sensorRaisedOf, Ft as NcSharedSequenceSchema, G as MAX_BIRTH_LATENCY_PROXY_MS, Gr as templateCarriesSummary, Gt as OPS_LOG_DEFAULT_LIMIT, H as LockStateSchema, Hr as systemEventFilterApplies, Ht as NcTemplatePreviewInputSchema, I as FIRST_LEVEL_MACRO_CLASSES, In as createAlarmProfile, Ir as servedTemplateVars, It as NcSnoozeInputSchema, J as MOTION_CLOSE_AFTER_MS, Jn as eventKindLabel, Jr as zoneAnalyticsCapability, Jt as RECORDING_EXPORT_MAX_READ_BYTES, K as MAX_TRACK_PURITIES, Kn as encodeVectorBase64, Kr as templateVarsFor, Kt as OpsLogEntrySchema, L as FULL_IMAGE_BBOX, Ln as deleteAlarmProfile, Lr as storageOccupancyCapability, Lt as NcSnoozeSchema, M as EVENT_DENSITY_BATCH_MAX, Mn as buildEventKindDescriptor, Mr as ruleKindOf, Mt as NcRuleTargetSchema, N as EVENT_KIND_BY_CAP, Nn as cloneAlarmSettings, Nr as sceneMonitorCapability, Nt as NcRuleUpdateInputSchema, O as DetectionSourceSchema, Oi as EventCategory, On as applySecureOnArmEdit, Ot as NcRuleActionsSchema, P as EVENT_OWNER_TYPES, Pn as consumableLowPercentOf, Pr as secureActuationFor, Pt as NcScheduleSchema, Q as NC_ALARM_DELAY_FIELDS, Qn as gifDeriveSpeed, Qr as isSourceCap, Qt as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, R as FailureCounters, Rn as deriveRecordingMode, Rr as subKindsOf, Rt as NcSnoozeSuppressedSchema, S as CoverStateSchema, Si as strictObject, Sn as addonWidgetsSourceCapability, Sr as readMotionTriggeredBy, St as NcAlarmSensorsSchema, T as DETECTION_MACRO_CLASSES, Ti as tuple, Tn as applyBlockingEdit, Tr as resolveClipSearchMinScore, Tt as NcConditionsSchema, U as MAX_ARCHIVED_DEBUG_NOTES, Un as embeddingRefusalOf, Ur as systemEventSummaryScheduleOf, Ut as NcTemplatePreviewSchema, V as ListTrackRelationsResultSchema, Vr as systemEventFamilyProblems, Vt as NcTaxonomySchema, W as MAX_BIRTH_DECISION_RECORDS, Wr as systemEventSummarySplitOf, Wt as NcTemplateVarDescriptorSchema, X as MotionTriggeredBySchema, Xn as faceGalleryCapability, Xr as canonicalHash, Xt as RebuildObjectEmbeddingsRefusalSchema, Y as MediaFileKindEnum, Yn as evictionPolicyOfLocation, Yr as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, Yt as RECORDING_SOURCE_CAMSTACK_ADDON, Z as NC_ALARM_DEFAULT_PROFILE_ID, Zn as failureContributionCapability, Zr as evaluateSensorEdge, Zt as RetrainStatusSchema, _ as BaseDevice, _i as looseObject, _n as TrackRelationKindSchema, _r as occupancyScope, _t as NC_TANK_SYSTEM_EVENT_KINDS, a as CLIP_EMBEDDING_STEP_ID, ai as defineCustomActions, an as SCENE_DIVERGED, ar as isEventOwnerType, at as NC_CLIP_PRE_ROLL_DEFAULT_SEC, b as ClearTrackRelationInputSchema, bi as partialRecord, bn as TrackSourceSchema, br as plateGalleryCapability, bt as NcAlarmConfigSchema, c as resolveClusterModelPin, ci as nodePin, cn as SetTrackPurityInputSchema, cr as isRetiredClearKey, ct as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, d as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, di as ZodObject, dn as TIMELAPSE_DENSE_FLOOR_SEC, dr as isSystemEventSummaryRule, dt as NC_GROUPING_DEFAULT_SEC, ei as errMsg, en as SCENE_DEFAULT_ANCHOR_THRESHOLD, er as isAlarmControllableCap, et as NC_ALARM_SYSTEM_EVENT_KINDS, f as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, fi as _enum, fn as TRACK_RELATIONS_DEFAULT_LIMIT, fr as isVectorIndexDimConflict, ft as NC_GROUPING_MAX_SEC, g as BIRTH_DECISION_DEFAULT_LIMIT, gi as literal, gn as TrackPuritySchema, gr as notificationRulesCapability, gt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, h as ArchivedDebugNoteSchema, hi as discriminatedUnion, hn as TimelapseRuleSchema, hr as memoizePinnedFfmpeg, ht as NC_SNOOZE_MAX_MINUTES, ii as customAction, in as SCENE_DEFAULT_UNCOVERED_POLICY, ir as isEventKindLabelKey, it as NC_CLIP_POST_ROLL_DEFAULT_SEC, j as EMBEDDING_REQUIREMENTS_UNAVAILABLE, jn as audioModeOf, jr as ruleGroupingApplies, jt as NcRuleSchema, k as EMBEDDING_MODEL_FAILED, kn as assertTimelapseCadences, kr as ruleBelongsToAlarm$1, kt as NcRuleInputSchema, l as APPEARANCE_BIN_COUNT, ln as SetTrackPurityResultSchema, lr as isScheduleActive, lt as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, m as AlarmArmModeSchema, mi as boolean, mn as TimelapseRulePatchSchema, mr as mayWriteToLocation, mt as NC_SENSOR_TRANSITION_KINDS, ni as CamProfileSchema, nn as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, nr as isDefaultAlarmProfileId, nt as NC_AUDIO_CONFIRM_HITS_DEFAULT, o as FACE_EMBEDDING_STEP_ID, oi as hydrateSchema, on as SCENE_RESET_RECAPTURES, or as isKnownTemplateVar, ot as NC_CLIP_RETENTION_MAX_SEC, p as AUDIO_MACRO_LABELS, pi as array, pn as TimelapseRuleInputSchema, pr as kebabToCamel, pt as NC_GROUPING_MIN_SEC, q as MAX_TRACK_RELATIONS, qn as ensureAlarmProfiles, qr as vectorDimFromBase64, ri as createEvent, rn as SCENE_DEFAULT_QUIET_SECONDS, rr as isDetectionMacroClass, rt as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, s as readClusterModelPinStrict, si as isDeviceScopedCap, sn as SceneMonitorSchema, sr as isMediaOwnerType, st as NC_CONDITION_CATALOG, t as BUILTIN_CLIP_MODELS, ti as BaseAddon, tn as SCENE_DEFAULT_CHECK_INTERVAL_SEC, tr as isAudioClassName, tt as NC_ALARM_TRIGGERED_MAX_SEC, u as APPEARANCE_BLOCKS, ui as sleep$1, un as SetTrackRelationInputSchema, ur as isSystemDelivery, ut as NC_DEFAULT_SNOOZE_MINUTES, v as BirthDecisionRecordSchema, vi as number, vn as TrackRelationSchema, vt as NC_TAXONOMY, w as DEFAULT_TIMELAPSE_PREVIEW_TEXT, wi as templateLiteral, wn as alarmPanelCapability, wr as renameAlarmProfile, wt as NcConditionDescriptorSchema, x as ClearTrackRelationResultSchema, xi as record, xn as activateAlarmProfile, xr as readDeviceStateFrom, xt as NcAlarmSensorsInputSchema, y as COCO_TO_MACRO, yi as object, yn as TrackSideSchema, yr as pipelineAnalyticsCapability, yt as NC_TEMPLATE_VARS, z as LabelAttributionSchema, zn as describeSystemEventDeliveryProblem, zr as syncActiveAlarmProfileSettings, zt as NcStateSummarySectionIdSchema } from "../clip-model-registry-CshjlVTF.mjs";
|
|
2
2
|
import { t as __exportAll } from "../embedding-encoder/index.mjs";
|
|
3
3
|
import * as fs$1 from "node:fs";
|
|
4
4
|
import { createWriteStream, promises } from "node:fs";
|
|
@@ -477,26 +477,37 @@ function buildArmAnnouncement(coverage, deviceName, bypassed = []) {
|
|
|
477
477
|
};
|
|
478
478
|
}
|
|
479
479
|
/**
|
|
480
|
-
* "Attivi:
|
|
480
|
+
* "Attivi: Porta retro, Cancello — Bypassati: Finestra veranda."
|
|
481
481
|
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
* count pluralises on what IS armed, like every other arm variant.
|
|
482
|
+
* Both halves are named: the bypassed sensors are why the operator pressed
|
|
483
|
+
* Arm anyway, and the remaining ones are what is still promised. An
|
|
484
|
+
* all-devices mode has no finite list to name — it uses a dedicated key.
|
|
486
485
|
*/
|
|
487
486
|
function armedWithBypass(coverage, bypassed, deviceName) {
|
|
488
487
|
const skipped = new Set(coverage.skippedDevices.map((s) => s.deviceId));
|
|
489
488
|
const excluded = new Set(bypassed);
|
|
490
|
-
const
|
|
489
|
+
const bypassedNames = bypassed.map(deviceName).join(", ");
|
|
490
|
+
if (coverage.allDevices) return {
|
|
491
|
+
bodyKey: "alarm.arm.body.bypassedAll",
|
|
492
|
+
textParams: {
|
|
493
|
+
rules: `${coverage.ruleCount}`,
|
|
494
|
+
mode: coverage.mode,
|
|
495
|
+
bypassedNames
|
|
496
|
+
},
|
|
497
|
+
textCount: coverage.ruleCount
|
|
498
|
+
};
|
|
499
|
+
const armedIds = coverage.deviceIds.filter((id) => !skipped.has(id) && !excluded.has(id));
|
|
500
|
+
const names = armedIds.map(deviceName).join(", ");
|
|
491
501
|
return {
|
|
492
502
|
bodyKey: "alarm.arm.body.bypassed",
|
|
493
503
|
textParams: {
|
|
494
504
|
rules: `${coverage.ruleCount}`,
|
|
495
505
|
mode: coverage.mode,
|
|
496
|
-
devices: `${
|
|
497
|
-
|
|
506
|
+
devices: `${armedIds.length}`,
|
|
507
|
+
names,
|
|
508
|
+
bypassedNames
|
|
498
509
|
},
|
|
499
|
-
textCount:
|
|
510
|
+
textCount: armedIds.length
|
|
500
511
|
};
|
|
501
512
|
}
|
|
502
513
|
/**
|
|
@@ -938,7 +949,7 @@ function grantMeta(grant) {
|
|
|
938
949
|
...common,
|
|
939
950
|
ruleId: grant.ruleId,
|
|
940
951
|
rule: grant.ruleName,
|
|
941
|
-
|
|
952
|
+
sequenceId: grant.sequenceId
|
|
942
953
|
};
|
|
943
954
|
case "snooze": return {
|
|
944
955
|
...common,
|
|
@@ -1091,10 +1102,11 @@ var NcActionPlane = class {
|
|
|
1091
1102
|
* Anything short of fully secure is open: a cover stopped half-way, a lock that
|
|
1092
1103
|
* jammed, a cover still closing when the arm is decided, a switch that is on.
|
|
1093
1104
|
*
|
|
1094
|
-
* PRECEDENCE, when one device carries more than one opening slice:
|
|
1095
|
-
*
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
1105
|
+
* PRECEDENCE, when one device carries more than one opening slice: any OPEN
|
|
1106
|
+
* reading wins over a secure one (a reed that says open beats a motor that
|
|
1107
|
+
* still says `closed`). Among opens, a controllable cap (`cover` / `lock-control`
|
|
1108
|
+
* / `switch`) wins so secure-on-arm knows what to actuate; among secures the
|
|
1109
|
+
* contact wins as the physical "closed" when the motor has not moved yet.
|
|
1098
1110
|
*/
|
|
1099
1111
|
/** The one state word, per cap, that counts as secure. Everything else is open. */
|
|
1100
1112
|
var COVER_SECURE = "closed";
|
|
@@ -1207,6 +1219,11 @@ function readSwitch(runtime) {
|
|
|
1207
1219
|
}
|
|
1208
1220
|
/** D630's table over one device's runtime slices — see the header for the three answers. */
|
|
1209
1221
|
function alarmOpeningFrom(runtime) {
|
|
1222
|
+
let openControllable = null;
|
|
1223
|
+
let openAny = null;
|
|
1224
|
+
let secureContact = null;
|
|
1225
|
+
let secureControllable = null;
|
|
1226
|
+
let secureAny = null;
|
|
1210
1227
|
let firstUnknown = null;
|
|
1211
1228
|
for (const read of [
|
|
1212
1229
|
readContact(runtime),
|
|
@@ -1214,10 +1231,21 @@ function alarmOpeningFrom(runtime) {
|
|
|
1214
1231
|
readLock(runtime),
|
|
1215
1232
|
readSwitch(runtime)
|
|
1216
1233
|
]) {
|
|
1217
|
-
if (read.kind === "
|
|
1218
|
-
|
|
1234
|
+
if (read.kind === "unknown") {
|
|
1235
|
+
if (firstUnknown === null) firstUnknown = read;
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
if (read.kind !== "reading") continue;
|
|
1239
|
+
if (read.reading === "open") {
|
|
1240
|
+
if (isAlarmControllableCap(read.cap) && openControllable === null) openControllable = read;
|
|
1241
|
+
if (openAny === null) openAny = read;
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
if (read.cap === "contact" && secureContact === null) secureContact = read;
|
|
1245
|
+
if (isAlarmControllableCap(read.cap) && secureControllable === null) secureControllable = read;
|
|
1246
|
+
if (secureAny === null) secureAny = read;
|
|
1219
1247
|
}
|
|
1220
|
-
return firstUnknown ?? NOT_OPENING;
|
|
1248
|
+
return openControllable ?? openAny ?? secureContact ?? secureControllable ?? secureAny ?? firstUnknown ?? NOT_OPENING;
|
|
1221
1249
|
}
|
|
1222
1250
|
/**
|
|
1223
1251
|
* The sensor-event kind (`CAP_TO_KIND` in `@camstack/types`) → the cap whose
|
|
@@ -2743,12 +2771,6 @@ var NcAlarmPanelDevice = class extends BaseDevice {
|
|
|
2743
2771
|
error: String(err)
|
|
2744
2772
|
};
|
|
2745
2773
|
}
|
|
2746
|
-
const toSecure = openings.open.filter((d) => alarmDeviceSecureOnArm({
|
|
2747
|
-
controllable: d.controllable,
|
|
2748
|
-
deviceId: d.deviceId,
|
|
2749
|
-
secureOnArmOff: this.secureOnArmOffFor(mode)
|
|
2750
|
-
}));
|
|
2751
|
-
if (toSecure.length > 0) this.armGuard?.secureOnArm(toSecure);
|
|
2752
2774
|
const verdict = decideArm({
|
|
2753
2775
|
mode,
|
|
2754
2776
|
open: openings.open,
|
|
@@ -2756,6 +2778,14 @@ var NcAlarmPanelDevice = class extends BaseDevice {
|
|
|
2756
2778
|
blocking: this.blockingFor(mode),
|
|
2757
2779
|
...acknowledged !== void 0 ? { acknowledged } : {}
|
|
2758
2780
|
});
|
|
2781
|
+
if (verdict.kind === "arm") {
|
|
2782
|
+
const toSecure = openings.open.filter((d) => alarmDeviceSecureOnArm({
|
|
2783
|
+
controllable: d.controllable,
|
|
2784
|
+
deviceId: d.deviceId,
|
|
2785
|
+
secureOnArmOff: this.secureOnArmOffFor(mode)
|
|
2786
|
+
}));
|
|
2787
|
+
if (toSecure.length > 0) this.armGuard?.secureOnArm(toSecure);
|
|
2788
|
+
}
|
|
2759
2789
|
return {
|
|
2760
2790
|
kind: "read",
|
|
2761
2791
|
openings,
|
|
@@ -2964,6 +2994,7 @@ var NcAlarmPanelDevice = class extends BaseDevice {
|
|
|
2964
2994
|
status(now = Date.now()) {
|
|
2965
2995
|
return {
|
|
2966
2996
|
state: stateAt(this.machine, now),
|
|
2997
|
+
targetMode: armModeOf(this.machine.target, NC_ALARM_MODES),
|
|
2967
2998
|
availableModes: [...NC_ALARM_MODES],
|
|
2968
2999
|
requiresCode: false,
|
|
2969
3000
|
lastChangedAt: this.machine.changedAt
|
|
@@ -5060,6 +5091,27 @@ function planDenseRanges(input) {
|
|
|
5060
5091
|
}
|
|
5061
5092
|
//#endregion
|
|
5062
5093
|
//#region src/notification-center/text-compose.ts
|
|
5094
|
+
/**
|
|
5095
|
+
* The COMPOSITE variables — phrases assembled from catalog fragments before
|
|
5096
|
+
* the one and only render pass.
|
|
5097
|
+
*
|
|
5098
|
+
* A notification body is not a sentence with slots. "person (John) on Front
|
|
5099
|
+
* door in gate" contains three sub-phrases whose SHAPE differs per language
|
|
5100
|
+
* and whose presence is conditional: the label parentheses, the zone clause,
|
|
5101
|
+
* the plural noun. Expressing that with `{{class}}{{label}}{{zones}}` and
|
|
5102
|
+
* three scalars pushes the grammar into the template, where a translator
|
|
5103
|
+
* cannot fix it. So each becomes ONE variable — `{{subject}}`, `{{inZones}}`,
|
|
5104
|
+
* `{{op}}`, `{{detectionSummary}}` — composed here from its own keys and handed
|
|
5105
|
+
* to {@link renderTemplate} as a finished string.
|
|
5106
|
+
*
|
|
5107
|
+
* **Fragments see the SCALARS only.** Never the composites. That is what makes
|
|
5108
|
+
* the depth exactly one and not "one so far": a fragment key edited to say
|
|
5109
|
+
* `{{inZones}}` renders empty rather than starting a second round, and
|
|
5110
|
+
* `renderTemplate` never re-scans what it inserted, so a composite's own value
|
|
5111
|
+
* is printed verbatim whatever it contains.
|
|
5112
|
+
*
|
|
5113
|
+
* Pure: a catalog in, a string out. No clock, no store, no I/O.
|
|
5114
|
+
*/
|
|
5063
5115
|
/** One normalisation, used on both sides of every class lookup — a pipeline
|
|
5064
5116
|
* that emitted `Person` must find the key written `class.person`. */
|
|
5065
5117
|
function normaliseClass(className) {
|
|
@@ -5068,24 +5120,35 @@ function normaliseClass(className) {
|
|
|
5068
5120
|
/**
|
|
5069
5121
|
* The noun for a class, in the count's plural category.
|
|
5070
5122
|
*
|
|
5071
|
-
*
|
|
5072
|
-
*
|
|
5073
|
-
*
|
|
5074
|
-
* detection.
|
|
5123
|
+
* Lookup order: the NC text catalog (`class.*`, plural-aware) → the shared
|
|
5124
|
+
* {@link eventKindLabel} map (viewer filters / EVENT_TAXONOMY) → `class.other`
|
|
5125
|
+
* with the RAW wire id. A custom model class must still appear rather than
|
|
5126
|
+
* vanish; a missing translation is a smaller defect than a missing detection.
|
|
5075
5127
|
*/
|
|
5076
5128
|
function ncClassNoun(texts, className, count) {
|
|
5077
|
-
const
|
|
5129
|
+
const normalised = normaliseClass(className);
|
|
5130
|
+
const key = `class.${normalised}`;
|
|
5078
5131
|
if (texts.has(key)) return texts.text({
|
|
5079
5132
|
key,
|
|
5080
5133
|
count,
|
|
5081
5134
|
vars: { name: className }
|
|
5082
5135
|
});
|
|
5136
|
+
if (isEventKindLabelKey(normalised)) return eventKindLabel(normalised, texts.language);
|
|
5083
5137
|
return texts.text({
|
|
5084
5138
|
key: "class.other",
|
|
5085
5139
|
count,
|
|
5086
5140
|
vars: { name: className }
|
|
5087
5141
|
});
|
|
5088
5142
|
}
|
|
5143
|
+
/**
|
|
5144
|
+
* The parenthetical half of a labelled subject: a recognised person or plate
|
|
5145
|
+
* is printed verbatim; a classification (species, COCO sub, …) goes through
|
|
5146
|
+
* the shared event-kind map so "animal (cat)" becomes "animale (Gatto)".
|
|
5147
|
+
*/
|
|
5148
|
+
function ncSubjectLabelText(texts, label, labelKind) {
|
|
5149
|
+
if (labelKind === "identity" || labelKind === "plate") return label;
|
|
5150
|
+
return eventKindLabel(label, texts.language);
|
|
5151
|
+
}
|
|
5089
5152
|
/** `person (John)` / `persona (John)` — the thing the notification is about. */
|
|
5090
5153
|
function ncSubjectText(texts, input) {
|
|
5091
5154
|
const classNoun = ncClassNoun(texts, input.className, 1);
|
|
@@ -5097,7 +5160,7 @@ function ncSubjectText(texts, input) {
|
|
|
5097
5160
|
key: subjectLabelKey(input.labelKind),
|
|
5098
5161
|
vars: {
|
|
5099
5162
|
classNoun,
|
|
5100
|
-
label: input.label
|
|
5163
|
+
label: ncSubjectLabelText(texts, input.label, input.labelKind)
|
|
5101
5164
|
}
|
|
5102
5165
|
});
|
|
5103
5166
|
}
|
|
@@ -8667,31 +8730,48 @@ function replaceConditionReferences(monitor, condition, reference) {
|
|
|
8667
8730
|
/** The cap whose presence in a sequence makes the rule the alarm's trigger. */
|
|
8668
8731
|
var ALARM_PANEL_CAP$1 = "alarm-panel";
|
|
8669
8732
|
var ALARM_KINDS = new Set(NC_ALARM_SYSTEM_EVENT_KINDS);
|
|
8670
|
-
function
|
|
8671
|
-
|
|
8733
|
+
function stepActuatesAlarm(step) {
|
|
8734
|
+
return step.kind === "cap" && step.cap === ALARM_PANEL_CAP$1;
|
|
8735
|
+
}
|
|
8736
|
+
function bindingActuatesAlarm(binding, sequenceActionsOf) {
|
|
8737
|
+
if (binding.kind === "do") return stepActuatesAlarm(binding.action);
|
|
8738
|
+
const steps = sequenceActionsOf?.(binding.sequenceId);
|
|
8739
|
+
if (steps === void 0) return false;
|
|
8740
|
+
return steps.some(stepActuatesAlarm);
|
|
8741
|
+
}
|
|
8742
|
+
function actionsActuateAlarm(rule, sequenceActionsOf) {
|
|
8743
|
+
const actions = rule.actions;
|
|
8744
|
+
if (actions === void 0) return false;
|
|
8745
|
+
const hooks = [
|
|
8746
|
+
actions.onActivation,
|
|
8747
|
+
actions.onDeactivation,
|
|
8748
|
+
actions.onTrigger,
|
|
8749
|
+
actions.onTriggerOff
|
|
8750
|
+
];
|
|
8751
|
+
for (const bindings of hooks) for (const binding of bindings ?? []) if (bindingActuatesAlarm(binding, sequenceActionsOf)) return true;
|
|
8752
|
+
for (const button of actions.buttons ?? []) {
|
|
8753
|
+
const steps = sequenceActionsOf?.(button.sequenceId);
|
|
8754
|
+
if (steps !== void 0 && steps.some(stepActuatesAlarm)) return true;
|
|
8755
|
+
}
|
|
8672
8756
|
return false;
|
|
8673
8757
|
}
|
|
8674
8758
|
/**
|
|
8675
8759
|
* May a snooze window silence this rule?
|
|
8676
8760
|
*
|
|
8677
|
-
* `true` = never. Deliberately answered from the rule alone
|
|
8678
|
-
* on the delivery path, where a fallible
|
|
8679
|
-
* DESTROY the exemption (D49) — and the
|
|
8680
|
-
* silencing an alarm.
|
|
8761
|
+
* `true` = never. Deliberately answered from the rule alone (plus an optional
|
|
8762
|
+
* sequence lookup): the gate reads it on the delivery path, where a fallible
|
|
8763
|
+
* lookup would be a read that can DESTROY the exemption (D49) — and the
|
|
8764
|
+
* failure direction of this one is silencing an alarm. A missing sequence
|
|
8765
|
+
* lookup means sequence-ref bindings cannot prove exemption; Do bindings and
|
|
8766
|
+
* system-event kinds still can.
|
|
8681
8767
|
*/
|
|
8682
|
-
function isSnoozeExempt(rule) {
|
|
8768
|
+
function isSnoozeExempt(rule, sequenceActionsOf) {
|
|
8683
8769
|
const kinds = rule.conditions.systemEvent?.kinds ?? [];
|
|
8684
8770
|
for (const kind of kinds) if (ALARM_KINDS.has(kind)) return true;
|
|
8685
|
-
return
|
|
8771
|
+
return actionsActuateAlarm(rule, sequenceActionsOf);
|
|
8686
8772
|
}
|
|
8687
8773
|
//#endregion
|
|
8688
8774
|
//#region src/notification-center/action-buttons.ts
|
|
8689
|
-
/** Sequence names the rule actually declares. A button may only name one. */
|
|
8690
|
-
function declaredSequences(actions) {
|
|
8691
|
-
const names = /* @__PURE__ */ new Set();
|
|
8692
|
-
for (const sequence of actions.onTrigger ?? []) names.add(sequence.name);
|
|
8693
|
-
return names;
|
|
8694
|
-
}
|
|
8695
8775
|
/**
|
|
8696
8776
|
* Build the notification's buttons.
|
|
8697
8777
|
*
|
|
@@ -8704,18 +8784,18 @@ function buildActionButtons(input) {
|
|
|
8704
8784
|
if (actions === void 0) return [];
|
|
8705
8785
|
const buttons = actions.buttons ?? [];
|
|
8706
8786
|
if (buttons.length === 0) return [];
|
|
8707
|
-
const known =
|
|
8787
|
+
const known = input.knownSequenceIds;
|
|
8708
8788
|
const seen = /* @__PURE__ */ new Set();
|
|
8709
8789
|
const out = [];
|
|
8710
8790
|
for (const button of buttons) {
|
|
8711
|
-
if (!known.has(button.
|
|
8791
|
+
if (!known.has(button.sequenceId)) continue;
|
|
8712
8792
|
if (seen.has(button.id)) continue;
|
|
8713
8793
|
seen.add(button.id);
|
|
8714
8794
|
out.push({
|
|
8715
8795
|
id: button.id,
|
|
8716
8796
|
label: button.label,
|
|
8717
8797
|
url: input.mintUrl({
|
|
8718
|
-
|
|
8798
|
+
sequenceId: button.sequenceId,
|
|
8719
8799
|
actionId: button.id
|
|
8720
8800
|
}),
|
|
8721
8801
|
...button.icon !== void 0 ? { icon: button.icon } : {},
|
|
@@ -8725,18 +8805,17 @@ function buildActionButtons(input) {
|
|
|
8725
8805
|
return out;
|
|
8726
8806
|
}
|
|
8727
8807
|
/**
|
|
8728
|
-
* Which of a rule's buttons name a sequence
|
|
8808
|
+
* Which of a rule's buttons name a sequence that is not known.
|
|
8729
8809
|
*
|
|
8730
8810
|
* Exported so the caller can LOG the drop. A button silently missing from a
|
|
8731
8811
|
* notification is the exact shape of failure this repo keeps paying for — the
|
|
8732
8812
|
* operator authored it, it never appeared, and nothing said why.
|
|
8733
8813
|
*/
|
|
8734
|
-
function unresolvableButtons(actions) {
|
|
8814
|
+
function unresolvableButtons(actions, knownSequenceIds) {
|
|
8735
8815
|
if (actions === void 0) return [];
|
|
8736
8816
|
const buttons = actions.buttons ?? [];
|
|
8737
8817
|
if (buttons.length === 0) return [];
|
|
8738
|
-
|
|
8739
|
-
return buttons.filter((b) => !known.has(b.sequence)).map((b) => `${b.id}→${b.sequence}`);
|
|
8818
|
+
return buttons.filter((b) => !knownSequenceIds.has(b.sequenceId)).map((b) => `${b.id}→${b.sequenceId}`);
|
|
8740
8819
|
}
|
|
8741
8820
|
/** How many snooze choices one notification may carry. The schema caps the
|
|
8742
8821
|
* authored list at 4; this is the same bound applied to a stored rule that
|
|
@@ -8787,7 +8866,7 @@ function snoozeChoices(options) {
|
|
|
8787
8866
|
* attention than the notification did.
|
|
8788
8867
|
*/
|
|
8789
8868
|
function buildSnoozeButtons(input) {
|
|
8790
|
-
if (isSnoozeExempt(input.rule)) return [];
|
|
8869
|
+
if (isSnoozeExempt(input.rule, input.sequenceActionsOf)) return [];
|
|
8791
8870
|
return snoozeChoices(input.rule.snoozeOptions).map((minutes) => {
|
|
8792
8871
|
const actionId = snoozeButtonId(minutes);
|
|
8793
8872
|
return {
|
|
@@ -11076,10 +11155,11 @@ function matchesIdentityScope(subject, scope) {
|
|
|
11076
11155
|
* Derived, never a stored flag: both rule editors prune condition keys to the
|
|
11077
11156
|
* catalog on save, so a new key would be dropped by the first edit.
|
|
11078
11157
|
*/
|
|
11079
|
-
function ruleBelongsToAlarm(rule) {
|
|
11158
|
+
function ruleBelongsToAlarm(rule, sequenceActionsOf) {
|
|
11080
11159
|
return ruleBelongsToAlarm$1({
|
|
11081
11160
|
conditions: rule.conditions,
|
|
11082
|
-
...rule.actions
|
|
11161
|
+
...rule.actions !== void 0 ? { actions: rule.actions } : {},
|
|
11162
|
+
...sequenceActionsOf !== void 0 ? { sequenceActionsOf } : {}
|
|
11083
11163
|
});
|
|
11084
11164
|
}
|
|
11085
11165
|
/**
|
|
@@ -12854,7 +12934,7 @@ function incomingFromAlarmTransition(input) {
|
|
|
12854
12934
|
const kind = transition.kind;
|
|
12855
12935
|
const keys = alarmTextKeys(transition, input.body);
|
|
12856
12936
|
const sensor = transition.sensor;
|
|
12857
|
-
const alarmSource = kind === "alarm-triggered" ? source : void 0;
|
|
12937
|
+
const alarmSource = (kind === "alarm-triggered" || kind === "alarm-pending") && source !== void 0 ? source : void 0;
|
|
12858
12938
|
const facts = {
|
|
12859
12939
|
eventId: `alarm:${kind}:${String(transition.at)}${sensor !== void 0 ? `:${String(sensor.deviceId)}` : ""}${transition.refusalId !== void 0 ? `:${transition.refusalId}` : ""}`,
|
|
12860
12940
|
timestamp: transition.at,
|
|
@@ -12967,7 +13047,7 @@ function deriveTankBand(reading) {
|
|
|
12967
13047
|
case null: return null;
|
|
12968
13048
|
}
|
|
12969
13049
|
}
|
|
12970
|
-
function isRecord$
|
|
13050
|
+
function isRecord$9(value) {
|
|
12971
13051
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12972
13052
|
}
|
|
12973
13053
|
function tankStatusOf(value) {
|
|
@@ -12983,11 +13063,11 @@ function vacuumTanksOf(data) {
|
|
|
12983
13063
|
if (data["capName"] !== "vacuum-control") return null;
|
|
12984
13064
|
const deviceId = data["deviceId"];
|
|
12985
13065
|
const slice = data["slice"];
|
|
12986
|
-
if (typeof deviceId !== "number" || !isRecord$
|
|
13066
|
+
if (typeof deviceId !== "number" || !isRecord$9(slice)) return null;
|
|
12987
13067
|
const tanks = [];
|
|
12988
13068
|
for (const key of NC_TANK_KEYS) {
|
|
12989
13069
|
const raw = slice[key];
|
|
12990
|
-
if (!isRecord$
|
|
13070
|
+
if (!isRecord$9(raw)) continue;
|
|
12991
13071
|
const level = raw["level"];
|
|
12992
13072
|
tanks.push({
|
|
12993
13073
|
key,
|
|
@@ -14508,7 +14588,7 @@ var en_default = {
|
|
|
14508
14588
|
"snooze.scope.anyRule": "A rule",
|
|
14509
14589
|
"snooze.scope.rule": "“{{rule}}”",
|
|
14510
14590
|
"alarm.arm.body.allDevices": "Active: all devices",
|
|
14511
|
-
"alarm.arm.body.allDevicesExcept": "Active: all devices except {{
|
|
14591
|
+
"alarm.arm.body.allDevicesExcept": "Active: all devices except {{names}}",
|
|
14512
14592
|
"alarm.arm.body.noDevices": "Active: no device restriction",
|
|
14513
14593
|
"alarm.arm.body.devices": "Active: {{names}}",
|
|
14514
14594
|
"alarm.arm.body.devicesSkipped": "Active: {{names}}\nExcluded: {{skippedNames}}",
|
|
@@ -14518,7 +14598,8 @@ var en_default = {
|
|
|
14518
14598
|
"alarm.arming.body.uncovered": "Active: none — no rule uses this mode",
|
|
14519
14599
|
"alarm.arming.body.allSkipped": "Active: none\nExcluded: {{names}}",
|
|
14520
14600
|
"alarm.arming.body.skipped": "Excluded: {{names}}",
|
|
14521
|
-
"alarm.arm.body.bypassed": "Active: {{
|
|
14601
|
+
"alarm.arm.body.bypassed": "Active: {{names}}\nBypassed: {{bypassedNames}}",
|
|
14602
|
+
"alarm.arm.body.bypassedAll": "Active: all devices\nBypassed: {{bypassedNames}}",
|
|
14522
14603
|
"alarm.arming.body.bypassed": "Bypassed: {{bypassedNames}}",
|
|
14523
14604
|
"alarm.armRefused.body": "Open: {{names}}",
|
|
14524
14605
|
"alarm.armRefused.body.hint": "Open: {{names}}\nTap “Arm anyway” to arm with them excluded.",
|
|
@@ -14835,7 +14916,7 @@ var it_default = {
|
|
|
14835
14916
|
"snooze.scope.anyRule": "Una regola",
|
|
14836
14917
|
"snooze.scope.rule": "“{{rule}}”",
|
|
14837
14918
|
"alarm.arm.body.allDevices": "Attivi: tutti i dispositivi",
|
|
14838
|
-
"alarm.arm.body.allDevicesExcept": "Attivi: tutti i dispositivi tranne {{
|
|
14919
|
+
"alarm.arm.body.allDevicesExcept": "Attivi: tutti i dispositivi tranne {{names}}",
|
|
14839
14920
|
"alarm.arm.body.noDevices": "Attivi: nessuna restrizione",
|
|
14840
14921
|
"alarm.arm.body.devices": "Attivi: {{names}}",
|
|
14841
14922
|
"alarm.arm.body.devicesSkipped": "Attivi: {{names}}\nEsclusi: {{skippedNames}}",
|
|
@@ -14845,7 +14926,8 @@ var it_default = {
|
|
|
14845
14926
|
"alarm.arming.body.uncovered": "Attivi: nessuno — nessuna regola usa questa modalità",
|
|
14846
14927
|
"alarm.arming.body.allSkipped": "Attivi: nessuno\nEsclusi: {{names}}",
|
|
14847
14928
|
"alarm.arming.body.skipped": "Esclusi: {{names}}",
|
|
14848
|
-
"alarm.arm.body.bypassed": "Attivi: {{
|
|
14929
|
+
"alarm.arm.body.bypassed": "Attivi: {{names}}\nBypassati: {{bypassedNames}}",
|
|
14930
|
+
"alarm.arm.body.bypassedAll": "Attivi: tutti i dispositivi\nBypassati: {{bypassedNames}}",
|
|
14849
14931
|
"alarm.arming.body.bypassed": "Bypassati: {{bypassedNames}}",
|
|
14850
14932
|
"alarm.armRefused.body": "Aperti: {{names}}",
|
|
14851
14933
|
"alarm.armRefused.body.hint": "Aperti: {{names}}\nTocca “Arma comunque” per armare escludendoli.",
|
|
@@ -20310,6 +20392,350 @@ var NcDeviceMuteStore = class {
|
|
|
20310
20392
|
}
|
|
20311
20393
|
};
|
|
20312
20394
|
//#endregion
|
|
20395
|
+
//#region src/notification-center/migrate-legacy-actions.ts
|
|
20396
|
+
function isRecord$8(value) {
|
|
20397
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20398
|
+
}
|
|
20399
|
+
/** A legacy inline sequence row — detected by `name` + `actions` + `enabled`. */
|
|
20400
|
+
function asLegacySequence(value) {
|
|
20401
|
+
const parsed = NcLegacyRuleActionSequenceSchema.safeParse(value);
|
|
20402
|
+
return parsed.success ? parsed.data : null;
|
|
20403
|
+
}
|
|
20404
|
+
/**
|
|
20405
|
+
* True when `onTrigger` still looks like the pre-shared-sequence array of
|
|
20406
|
+
* named sequences (not an array of `{ kind: 'sequence' | 'do', … }` bindings).
|
|
20407
|
+
*/
|
|
20408
|
+
function onTriggerLooksLegacy(onTrigger) {
|
|
20409
|
+
if (!Array.isArray(onTrigger) || onTrigger.length === 0) return false;
|
|
20410
|
+
const first = onTrigger[0];
|
|
20411
|
+
if (!isRecord$8(first)) return false;
|
|
20412
|
+
if (first["kind"] === "sequence" || first["kind"] === "do") return false;
|
|
20413
|
+
return asLegacySequence(first) !== null;
|
|
20414
|
+
}
|
|
20415
|
+
function legacyButtonSequenceName(button) {
|
|
20416
|
+
if (!isRecord$8(button)) return null;
|
|
20417
|
+
const name = button["sequence"];
|
|
20418
|
+
return typeof name === "string" && name.length > 0 ? name : null;
|
|
20419
|
+
}
|
|
20420
|
+
/**
|
|
20421
|
+
* Rewrite a raw `actions` block. Returns the (possibly unchanged) new-shape
|
|
20422
|
+
* actions and whether anything was materialised into the sequence store.
|
|
20423
|
+
*/
|
|
20424
|
+
async function migrateLegacyRuleActions(actions, createSequence) {
|
|
20425
|
+
if (actions === void 0 || actions === null) return {
|
|
20426
|
+
actions: void 0,
|
|
20427
|
+
migrated: false
|
|
20428
|
+
};
|
|
20429
|
+
if (!isRecord$8(actions)) return {
|
|
20430
|
+
actions: void 0,
|
|
20431
|
+
migrated: false
|
|
20432
|
+
};
|
|
20433
|
+
if (!onTriggerLooksLegacy(actions["onTrigger"]) && actions["sequence"] === void 0) {
|
|
20434
|
+
const buttons = actions["buttons"];
|
|
20435
|
+
if (!(Array.isArray(buttons) && buttons.some((b) => legacyButtonSequenceName(b) !== null))) {
|
|
20436
|
+
const parsed = NcRuleActionsSchema.safeParse(actions);
|
|
20437
|
+
if (parsed.success) return {
|
|
20438
|
+
actions: parsed.data,
|
|
20439
|
+
migrated: false
|
|
20440
|
+
};
|
|
20441
|
+
return {
|
|
20442
|
+
actions: void 0,
|
|
20443
|
+
migrated: false
|
|
20444
|
+
};
|
|
20445
|
+
}
|
|
20446
|
+
}
|
|
20447
|
+
const legacySequences = [];
|
|
20448
|
+
const onTriggerRaw = actions["onTrigger"];
|
|
20449
|
+
if (Array.isArray(onTriggerRaw)) for (const entry of onTriggerRaw) {
|
|
20450
|
+
const seq = asLegacySequence(entry);
|
|
20451
|
+
if (seq !== null) legacySequences.push(seq);
|
|
20452
|
+
}
|
|
20453
|
+
const nameToId = /* @__PURE__ */ new Map();
|
|
20454
|
+
for (const sequence of legacySequences) {
|
|
20455
|
+
const key = sequence.name.trim();
|
|
20456
|
+
if (key === "" || nameToId.has(key)) continue;
|
|
20457
|
+
const created = await createSequence({
|
|
20458
|
+
name: sequence.name,
|
|
20459
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
20460
|
+
actions: [...sequence.actions]
|
|
20461
|
+
});
|
|
20462
|
+
nameToId.set(key, created.id);
|
|
20463
|
+
}
|
|
20464
|
+
const onTriggerBindings = [];
|
|
20465
|
+
for (const sequence of legacySequences) {
|
|
20466
|
+
if (!sequence.enabled) continue;
|
|
20467
|
+
const id = nameToId.get(sequence.name.trim());
|
|
20468
|
+
if (id === void 0) continue;
|
|
20469
|
+
onTriggerBindings.push({
|
|
20470
|
+
kind: "sequence",
|
|
20471
|
+
sequenceId: id
|
|
20472
|
+
});
|
|
20473
|
+
}
|
|
20474
|
+
const buttonsRaw = actions["buttons"];
|
|
20475
|
+
const buttons = [];
|
|
20476
|
+
if (Array.isArray(buttonsRaw)) for (const button of buttonsRaw) {
|
|
20477
|
+
if (!isRecord$8(button)) continue;
|
|
20478
|
+
const id = typeof button["id"] === "string" ? button["id"] : "";
|
|
20479
|
+
const label = typeof button["label"] === "string" ? button["label"] : "";
|
|
20480
|
+
if (id === "" || label === "") continue;
|
|
20481
|
+
const existingId = typeof button["sequenceId"] === "string" && button["sequenceId"].length > 0 ? button["sequenceId"] : void 0;
|
|
20482
|
+
const legacyName = legacyButtonSequenceName(button);
|
|
20483
|
+
const sequenceId = existingId ?? (legacyName !== null ? nameToId.get(legacyName.trim()) : void 0);
|
|
20484
|
+
if (sequenceId === void 0) continue;
|
|
20485
|
+
buttons.push({
|
|
20486
|
+
id,
|
|
20487
|
+
label,
|
|
20488
|
+
sequenceId,
|
|
20489
|
+
...typeof button["icon"] === "string" ? { icon: button["icon"] } : {},
|
|
20490
|
+
...typeof button["destructive"] === "boolean" ? { destructive: button["destructive"] } : {}
|
|
20491
|
+
});
|
|
20492
|
+
}
|
|
20493
|
+
const next = {
|
|
20494
|
+
...Array.isArray(actions["onActivation"]) ? { onActivation: actions["onActivation"] } : {},
|
|
20495
|
+
...Array.isArray(actions["onDeactivation"]) ? { onDeactivation: actions["onDeactivation"] } : {},
|
|
20496
|
+
...onTriggerBindings.length > 0 ? { onTrigger: onTriggerBindings } : {},
|
|
20497
|
+
...Array.isArray(actions["onTriggerOff"]) ? { onTriggerOff: actions["onTriggerOff"] } : {},
|
|
20498
|
+
...buttons.length > 0 ? { buttons } : {}
|
|
20499
|
+
};
|
|
20500
|
+
const parsed = NcRuleActionsSchema.safeParse(next);
|
|
20501
|
+
if (!parsed.success) return {
|
|
20502
|
+
actions: void 0,
|
|
20503
|
+
migrated: true
|
|
20504
|
+
};
|
|
20505
|
+
return {
|
|
20506
|
+
actions: (parsed.data.onActivation?.length ?? 0) === 0 && (parsed.data.onDeactivation?.length ?? 0) === 0 && (parsed.data.onTrigger?.length ?? 0) === 0 && (parsed.data.onTriggerOff?.length ?? 0) === 0 && (parsed.data.buttons?.length ?? 0) === 0 ? void 0 : parsed.data,
|
|
20507
|
+
migrated: true
|
|
20508
|
+
};
|
|
20509
|
+
}
|
|
20510
|
+
//#endregion
|
|
20511
|
+
//#region src/notification-center/sequence-store.ts
|
|
20512
|
+
/**
|
|
20513
|
+
* Shared action sequences for the notification centre.
|
|
20514
|
+
*
|
|
20515
|
+
* A sequence is a named, ordered run of steps (`wait` | `cap`) with its own
|
|
20516
|
+
* throttle. Rules and notification buttons bind to it by id — never by copying
|
|
20517
|
+
* the steps inline — so two rules that open the same gate share one budget and
|
|
20518
|
+
* one authoring surface.
|
|
20519
|
+
*
|
|
20520
|
+
* Mirrors {@link NcDeviceMuteStore}: declare the collection, mirror into RAM,
|
|
20521
|
+
* write-through so a failed persist never leaves a phantom sequence the next
|
|
20522
|
+
* boot will not know.
|
|
20523
|
+
*/
|
|
20524
|
+
/**
|
|
20525
|
+
* @durable class=config owner=notification-center
|
|
20526
|
+
* write="an admin creates/edits/deletes a shared sequence via
|
|
20527
|
+
* `notificationRules.createSequence` / `updateSequence` / `deleteSequence`,
|
|
20528
|
+
* or the one-shot boot migration that materialises legacy inline sequences"
|
|
20529
|
+
* retention="none — a row goes only when an admin deletes it. Bounded by the
|
|
20530
|
+
* sequence set a human is willing to author."
|
|
20531
|
+
*/
|
|
20532
|
+
var NC_SEQUENCES_COLLECTION = "notification-center:sequences";
|
|
20533
|
+
var NC_SEQUENCES_COLUMNS = [
|
|
20534
|
+
{
|
|
20535
|
+
name: "id",
|
|
20536
|
+
type: "TEXT",
|
|
20537
|
+
primaryKey: true,
|
|
20538
|
+
notNull: true
|
|
20539
|
+
},
|
|
20540
|
+
{
|
|
20541
|
+
name: "name",
|
|
20542
|
+
type: "TEXT",
|
|
20543
|
+
notNull: true
|
|
20544
|
+
},
|
|
20545
|
+
{
|
|
20546
|
+
name: "minDelaySec",
|
|
20547
|
+
type: "INTEGER"
|
|
20548
|
+
},
|
|
20549
|
+
{
|
|
20550
|
+
name: "actions",
|
|
20551
|
+
type: "JSON",
|
|
20552
|
+
notNull: true
|
|
20553
|
+
},
|
|
20554
|
+
{
|
|
20555
|
+
name: "createdAt",
|
|
20556
|
+
type: "INTEGER",
|
|
20557
|
+
notNull: true
|
|
20558
|
+
},
|
|
20559
|
+
{
|
|
20560
|
+
name: "updatedAt",
|
|
20561
|
+
type: "INTEGER",
|
|
20562
|
+
notNull: true
|
|
20563
|
+
}
|
|
20564
|
+
];
|
|
20565
|
+
function isRecord$7(value) {
|
|
20566
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20567
|
+
}
|
|
20568
|
+
function parseActions(raw) {
|
|
20569
|
+
if (!Array.isArray(raw) || raw.length === 0) return null;
|
|
20570
|
+
const out = [];
|
|
20571
|
+
for (const entry of raw) {
|
|
20572
|
+
const parsed = NcRuleActionSchema.safeParse(entry);
|
|
20573
|
+
if (!parsed.success) return null;
|
|
20574
|
+
out.push(parsed.data);
|
|
20575
|
+
}
|
|
20576
|
+
return out;
|
|
20577
|
+
}
|
|
20578
|
+
function rowToSequence(data) {
|
|
20579
|
+
const id = data["id"];
|
|
20580
|
+
const name = data["name"];
|
|
20581
|
+
if (typeof id !== "string" || id.length === 0) return null;
|
|
20582
|
+
if (typeof name !== "string" || name.length === 0) return null;
|
|
20583
|
+
const actions = parseActions(data["actions"]);
|
|
20584
|
+
if (actions === null) return null;
|
|
20585
|
+
const minDelayRaw = data["minDelaySec"];
|
|
20586
|
+
const minDelaySec = typeof minDelayRaw === "number" && Number.isInteger(minDelayRaw) ? minDelayRaw : void 0;
|
|
20587
|
+
const candidate = {
|
|
20588
|
+
id,
|
|
20589
|
+
name,
|
|
20590
|
+
...minDelaySec !== void 0 ? { minDelaySec } : {},
|
|
20591
|
+
actions: [...actions]
|
|
20592
|
+
};
|
|
20593
|
+
const parsed = NcSharedSequenceSchema.safeParse(candidate);
|
|
20594
|
+
return parsed.success ? parsed.data : null;
|
|
20595
|
+
}
|
|
20596
|
+
var NcSequenceStore = class {
|
|
20597
|
+
byId = /* @__PURE__ */ new Map();
|
|
20598
|
+
store;
|
|
20599
|
+
logger;
|
|
20600
|
+
now;
|
|
20601
|
+
constructor(deps) {
|
|
20602
|
+
this.store = deps.store;
|
|
20603
|
+
this.logger = deps.logger;
|
|
20604
|
+
this.now = deps.now ?? (() => Date.now());
|
|
20605
|
+
}
|
|
20606
|
+
static async declare(store) {
|
|
20607
|
+
await store.declareCollection.mutate({
|
|
20608
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20609
|
+
columns: [...NC_SEQUENCES_COLUMNS]
|
|
20610
|
+
});
|
|
20611
|
+
}
|
|
20612
|
+
/**
|
|
20613
|
+
* (Re)hydrate. A failed load KEEPS what is already in memory — dropping every
|
|
20614
|
+
* sequence on a transient store error would leave every button and binding
|
|
20615
|
+
* pointing at nothing until the next successful load.
|
|
20616
|
+
*/
|
|
20617
|
+
async load() {
|
|
20618
|
+
try {
|
|
20619
|
+
const rows = await this.store.query.query({
|
|
20620
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20621
|
+
filter: { limit: 1e4 }
|
|
20622
|
+
});
|
|
20623
|
+
const next = /* @__PURE__ */ new Map();
|
|
20624
|
+
let skipped = 0;
|
|
20625
|
+
for (const row of rows) {
|
|
20626
|
+
if (!isRecord$7(row.data)) {
|
|
20627
|
+
skipped += 1;
|
|
20628
|
+
continue;
|
|
20629
|
+
}
|
|
20630
|
+
const sequence = rowToSequence(row.data);
|
|
20631
|
+
if (sequence === null) {
|
|
20632
|
+
skipped += 1;
|
|
20633
|
+
continue;
|
|
20634
|
+
}
|
|
20635
|
+
next.set(sequence.id, sequence);
|
|
20636
|
+
}
|
|
20637
|
+
this.byId.clear();
|
|
20638
|
+
for (const [id, sequence] of next) this.byId.set(id, sequence);
|
|
20639
|
+
if (skipped > 0) this.logger.warn("sequence rows skipped as malformed — bindings to them will miss", { meta: { skipped } });
|
|
20640
|
+
} catch (err) {
|
|
20641
|
+
this.logger.warn("sequence load failed — keeping the sequences already in memory", { meta: {
|
|
20642
|
+
error: String(err),
|
|
20643
|
+
sequences: this.byId.size
|
|
20644
|
+
} });
|
|
20645
|
+
}
|
|
20646
|
+
}
|
|
20647
|
+
list() {
|
|
20648
|
+
return [...this.byId.values()].toSorted((a, b) => a.name.localeCompare(b.name));
|
|
20649
|
+
}
|
|
20650
|
+
get(sequenceId) {
|
|
20651
|
+
return this.byId.get(sequenceId) ?? null;
|
|
20652
|
+
}
|
|
20653
|
+
/** Hot-path existence check for button minting. */
|
|
20654
|
+
has(sequenceId) {
|
|
20655
|
+
return this.byId.has(sequenceId);
|
|
20656
|
+
}
|
|
20657
|
+
knownIds() {
|
|
20658
|
+
return new Set(this.byId.keys());
|
|
20659
|
+
}
|
|
20660
|
+
async create(input) {
|
|
20661
|
+
const now = this.now();
|
|
20662
|
+
const sequence = {
|
|
20663
|
+
id: randomUUID(),
|
|
20664
|
+
name: input.name,
|
|
20665
|
+
...input.minDelaySec !== void 0 ? { minDelaySec: input.minDelaySec } : {},
|
|
20666
|
+
actions: [...input.actions]
|
|
20667
|
+
};
|
|
20668
|
+
await this.persist(sequence, now, now);
|
|
20669
|
+
this.byId.set(sequence.id, sequence);
|
|
20670
|
+
return sequence;
|
|
20671
|
+
}
|
|
20672
|
+
/**
|
|
20673
|
+
* Create with a CALLER-chosen id — used by the legacy migration so a rewrite
|
|
20674
|
+
* can name the sequence before rewriting the rule that points at it.
|
|
20675
|
+
*/
|
|
20676
|
+
async createWithId(sequence) {
|
|
20677
|
+
const now = this.now();
|
|
20678
|
+
const stored = {
|
|
20679
|
+
id: sequence.id,
|
|
20680
|
+
name: sequence.name,
|
|
20681
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
20682
|
+
actions: [...sequence.actions]
|
|
20683
|
+
};
|
|
20684
|
+
await this.persist(stored, now, now);
|
|
20685
|
+
this.byId.set(stored.id, stored);
|
|
20686
|
+
return stored;
|
|
20687
|
+
}
|
|
20688
|
+
async update(sequenceId, input) {
|
|
20689
|
+
if (this.byId.get(sequenceId) === void 0) throw new Error(`sequence ${sequenceId} does not exist`);
|
|
20690
|
+
const now = this.now();
|
|
20691
|
+
const sequence = {
|
|
20692
|
+
id: sequenceId,
|
|
20693
|
+
name: input.name,
|
|
20694
|
+
...input.minDelaySec !== void 0 ? { minDelaySec: input.minDelaySec } : {},
|
|
20695
|
+
actions: [...input.actions]
|
|
20696
|
+
};
|
|
20697
|
+
const createdAt = await this.readCreatedAt(sequenceId, now);
|
|
20698
|
+
await this.persist(sequence, createdAt, now);
|
|
20699
|
+
this.byId.set(sequenceId, sequence);
|
|
20700
|
+
return sequence;
|
|
20701
|
+
}
|
|
20702
|
+
async delete(sequenceId) {
|
|
20703
|
+
await this.store.delete.mutate({
|
|
20704
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20705
|
+
key: sequenceId
|
|
20706
|
+
});
|
|
20707
|
+
this.byId.delete(sequenceId);
|
|
20708
|
+
}
|
|
20709
|
+
async readCreatedAt(sequenceId, fallback) {
|
|
20710
|
+
try {
|
|
20711
|
+
const raw = (await this.store.query.query({
|
|
20712
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20713
|
+
filter: {
|
|
20714
|
+
where: { id: sequenceId },
|
|
20715
|
+
limit: 1
|
|
20716
|
+
}
|
|
20717
|
+
}))[0]?.data["createdAt"];
|
|
20718
|
+
return typeof raw === "number" && Number.isFinite(raw) ? raw : fallback;
|
|
20719
|
+
} catch {
|
|
20720
|
+
return fallback;
|
|
20721
|
+
}
|
|
20722
|
+
}
|
|
20723
|
+
async persist(sequence, createdAt, updatedAt) {
|
|
20724
|
+
await this.store.set.mutate({
|
|
20725
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20726
|
+
key: sequence.id,
|
|
20727
|
+
value: {
|
|
20728
|
+
id: sequence.id,
|
|
20729
|
+
name: sequence.name,
|
|
20730
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
20731
|
+
actions: sequence.actions,
|
|
20732
|
+
createdAt,
|
|
20733
|
+
updatedAt
|
|
20734
|
+
}
|
|
20735
|
+
});
|
|
20736
|
+
}
|
|
20737
|
+
};
|
|
20738
|
+
//#endregion
|
|
20313
20739
|
//#region src/notification-center/device-prune.ts
|
|
20314
20740
|
function judgeList(list, isDead) {
|
|
20315
20741
|
const dead = list.filter(isDead);
|
|
@@ -20423,58 +20849,49 @@ function pruneRuleConditions(conditions, isDead, notes) {
|
|
|
20423
20849
|
if (sceneDevice !== void 0 && isDead(sceneDevice)) notes.kept("conditions.scene", [String(sceneDevice)]);
|
|
20424
20850
|
return changed ? next : null;
|
|
20425
20851
|
}
|
|
20426
|
-
/** A
|
|
20427
|
-
|
|
20428
|
-
|
|
20429
|
-
|
|
20430
|
-
|
|
20431
|
-
|
|
20432
|
-
return false;
|
|
20433
|
-
});
|
|
20434
|
-
if (dead.length === 0) return null;
|
|
20435
|
-
const anyDeviceStep = kept.some((step) => step.kind === "cap");
|
|
20436
|
-
const [first, ...rest] = kept;
|
|
20437
|
-
if (!anyDeviceStep || first === void 0) return {
|
|
20438
|
-
next: "removed",
|
|
20439
|
-
dead
|
|
20440
|
-
};
|
|
20441
|
-
return {
|
|
20442
|
-
next: {
|
|
20443
|
-
...sequence,
|
|
20444
|
-
actions: [first, ...rest]
|
|
20445
|
-
},
|
|
20446
|
-
dead
|
|
20852
|
+
/** A Do binding whose device is dead is dropped; sequence refs are left alone
|
|
20853
|
+
* (shared sequences are pruned on their own store, not per-rule). */
|
|
20854
|
+
function pruneHookBindings(bindings, field, isDead, notes) {
|
|
20855
|
+
if (bindings === void 0) return {
|
|
20856
|
+
next: void 0,
|
|
20857
|
+
changed: false
|
|
20447
20858
|
};
|
|
20448
|
-
|
|
20449
|
-
/** The actions after a device prune — `null` when no step moved. */
|
|
20450
|
-
function pruneRuleActions(actions, isDead, notes) {
|
|
20451
|
-
const sequences = actions.onTrigger;
|
|
20452
|
-
if (sequences === void 0) return null;
|
|
20859
|
+
const kept = [];
|
|
20453
20860
|
let changed = false;
|
|
20454
|
-
const
|
|
20455
|
-
|
|
20456
|
-
|
|
20457
|
-
const pruned = pruneSequence(sequence, isDead);
|
|
20458
|
-
if (pruned === null) {
|
|
20459
|
-
nextSequences.push(sequence);
|
|
20861
|
+
for (const binding of bindings) {
|
|
20862
|
+
if (binding.kind === "sequence") {
|
|
20863
|
+
kept.push(binding);
|
|
20460
20864
|
continue;
|
|
20461
20865
|
}
|
|
20462
|
-
|
|
20463
|
-
|
|
20464
|
-
|
|
20465
|
-
|
|
20466
|
-
notes.removed(field, deviceSubjects(pruned.dead), "sequence-removed");
|
|
20467
|
-
} else {
|
|
20468
|
-
nextSequences.push(pruned.next);
|
|
20469
|
-
notes.removed(field, deviceSubjects(pruned.dead));
|
|
20866
|
+
if (binding.action.kind === "cap" && isDead(binding.action.deviceId)) {
|
|
20867
|
+
changed = true;
|
|
20868
|
+
notes.removed(field, deviceSubjects([binding.action.deviceId]), "binding-removed");
|
|
20869
|
+
continue;
|
|
20470
20870
|
}
|
|
20871
|
+
kept.push(binding);
|
|
20471
20872
|
}
|
|
20472
|
-
if (!changed) return
|
|
20473
|
-
|
|
20873
|
+
if (!changed) return {
|
|
20874
|
+
next: bindings,
|
|
20875
|
+
changed: false
|
|
20876
|
+
};
|
|
20877
|
+
return {
|
|
20878
|
+
next: kept.length > 0 ? kept : void 0,
|
|
20879
|
+
changed: true
|
|
20880
|
+
};
|
|
20881
|
+
}
|
|
20882
|
+
/** The actions after a device prune — `null` when no binding moved. */
|
|
20883
|
+
function pruneRuleActions(actions, isDead, notes) {
|
|
20884
|
+
const onActivation = pruneHookBindings(actions.onActivation, "actions.onActivation", isDead, notes);
|
|
20885
|
+
const onDeactivation = pruneHookBindings(actions.onDeactivation, "actions.onDeactivation", isDead, notes);
|
|
20886
|
+
const onTrigger = pruneHookBindings(actions.onTrigger, "actions.onTrigger", isDead, notes);
|
|
20887
|
+
const onTriggerOff = pruneHookBindings(actions.onTriggerOff, "actions.onTriggerOff", isDead, notes);
|
|
20888
|
+
if (!onActivation.changed && !onDeactivation.changed && !onTrigger.changed && !onTriggerOff.changed) return null;
|
|
20474
20889
|
return {
|
|
20475
|
-
...
|
|
20476
|
-
|
|
20477
|
-
...
|
|
20890
|
+
...onActivation.next !== void 0 ? { onActivation: [...onActivation.next] } : {},
|
|
20891
|
+
...onDeactivation.next !== void 0 ? { onDeactivation: [...onDeactivation.next] } : {},
|
|
20892
|
+
...onTrigger.next !== void 0 ? { onTrigger: [...onTrigger.next] } : {},
|
|
20893
|
+
...onTriggerOff.next !== void 0 ? { onTriggerOff: [...onTriggerOff.next] } : {},
|
|
20894
|
+
...actions.buttons !== void 0 ? { buttons: actions.buttons } : {}
|
|
20478
20895
|
};
|
|
20479
20896
|
}
|
|
20480
20897
|
function planRuleDevices(rule, isDead, guard) {
|
|
@@ -24709,6 +25126,10 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
|
|
|
24709
25126
|
return {
|
|
24710
25127
|
camera: deviceName,
|
|
24711
25128
|
class: detection?.className ?? (systemEvent !== void 0 ? "" : subject.className),
|
|
25129
|
+
classLabel: (() => {
|
|
25130
|
+
const wire = detection?.className ?? (systemEvent !== void 0 ? "" : subject.className);
|
|
25131
|
+
return wire.length === 0 ? "" : eventKindLabel(wire, texts.language);
|
|
25132
|
+
})(),
|
|
24712
25133
|
label: detection?.label ?? subject.label ?? "",
|
|
24713
25134
|
zones: zoneLabels.join(", "),
|
|
24714
25135
|
subject: detection?.className !== void 0 ? ncSubjectText(texts, {
|
|
@@ -28850,9 +29271,32 @@ function lastRunRow(data) {
|
|
|
28850
29271
|
}
|
|
28851
29272
|
//#endregion
|
|
28852
29273
|
//#region src/notification-center/rule-actions.ts
|
|
29274
|
+
function wrapDo(action) {
|
|
29275
|
+
const name = action.kind === "wait" ? `wait ${String(action.seconds)}s` : `${action.cap}.${action.method}`;
|
|
29276
|
+
return {
|
|
29277
|
+
id: `do:${name}`,
|
|
29278
|
+
name,
|
|
29279
|
+
enabled: true,
|
|
29280
|
+
actions: [action],
|
|
29281
|
+
throttle: false
|
|
29282
|
+
};
|
|
29283
|
+
}
|
|
29284
|
+
function resolveBinding(binding, lookup) {
|
|
29285
|
+
if (binding.kind === "do") return wrapDo(binding.action);
|
|
29286
|
+
const sequence = lookup(binding.sequenceId);
|
|
29287
|
+
if (sequence === null) return { missing: binding.sequenceId };
|
|
29288
|
+
return {
|
|
29289
|
+
id: sequence.id,
|
|
29290
|
+
name: sequence.name,
|
|
29291
|
+
enabled: true,
|
|
29292
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
29293
|
+
actions: sequence.actions,
|
|
29294
|
+
throttle: true
|
|
29295
|
+
};
|
|
29296
|
+
}
|
|
28853
29297
|
var NcRuleActionRunner = class {
|
|
28854
29298
|
deps;
|
|
28855
|
-
/** Last run per
|
|
29299
|
+
/** Last run per `sequenceId` — the throttle's memory (shared across rules). */
|
|
28856
29300
|
lastRunAt = /* @__PURE__ */ new Map();
|
|
28857
29301
|
now;
|
|
28858
29302
|
sleep;
|
|
@@ -28862,11 +29306,49 @@ var NcRuleActionRunner = class {
|
|
|
28862
29306
|
this.sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
28863
29307
|
}
|
|
28864
29308
|
/**
|
|
28865
|
-
*
|
|
29309
|
+
* Resolve bindings via the sequence store and run each, in order.
|
|
28866
29310
|
*
|
|
28867
29311
|
* Never throws: an actuation that fails must not cost the NOTIFICATION about
|
|
28868
|
-
* the very same event.
|
|
28869
|
-
|
|
29312
|
+
* the very same event.
|
|
29313
|
+
*/
|
|
29314
|
+
async runBindings(input) {
|
|
29315
|
+
const sequences = [];
|
|
29316
|
+
const missing = [];
|
|
29317
|
+
for (const binding of input.bindings) {
|
|
29318
|
+
const resolved = resolveBinding(binding, input.lookup);
|
|
29319
|
+
if ("missing" in resolved) {
|
|
29320
|
+
this.deps.logger.warn("rule action binding skipped — sequence missing", {
|
|
29321
|
+
tags: { deviceId: input.deviceId },
|
|
29322
|
+
meta: {
|
|
29323
|
+
ruleId: input.ruleId,
|
|
29324
|
+
sequenceId: resolved.missing
|
|
29325
|
+
}
|
|
29326
|
+
});
|
|
29327
|
+
missing.push({
|
|
29328
|
+
id: resolved.missing,
|
|
29329
|
+
name: resolved.missing,
|
|
29330
|
+
ran: false,
|
|
29331
|
+
stepsRun: 0,
|
|
29332
|
+
skippedReason: "missing"
|
|
29333
|
+
});
|
|
29334
|
+
continue;
|
|
29335
|
+
}
|
|
29336
|
+
sequences.push(resolved);
|
|
29337
|
+
}
|
|
29338
|
+
const ran = await this.run({
|
|
29339
|
+
ruleId: input.ruleId,
|
|
29340
|
+
ruleName: input.ruleName,
|
|
29341
|
+
deviceId: input.deviceId,
|
|
29342
|
+
sequences,
|
|
29343
|
+
...input.origin !== void 0 ? { origin: input.origin } : {}
|
|
29344
|
+
});
|
|
29345
|
+
return [...missing, ...ran];
|
|
29346
|
+
}
|
|
29347
|
+
/**
|
|
29348
|
+
* Run every enabled sequence, in order.
|
|
29349
|
+
*
|
|
29350
|
+
* Prefer {@link runBindings} at call sites that hold bindings. This remains
|
|
29351
|
+
* for button grants (force-enabled shared sequence) and tests.
|
|
28870
29352
|
*/
|
|
28871
29353
|
async run(input) {
|
|
28872
29354
|
const out = [];
|
|
@@ -28875,38 +29357,41 @@ var NcRuleActionRunner = class {
|
|
|
28875
29357
|
}
|
|
28876
29358
|
async runOne(input, sequence) {
|
|
28877
29359
|
if (!sequence.enabled) return {
|
|
29360
|
+
id: sequence.id,
|
|
28878
29361
|
name: sequence.name,
|
|
28879
29362
|
ran: false,
|
|
28880
29363
|
stepsRun: 0,
|
|
28881
29364
|
skippedReason: "disabled"
|
|
28882
29365
|
};
|
|
28883
29366
|
if (sequence.actions.length === 0) return {
|
|
29367
|
+
id: sequence.id,
|
|
28884
29368
|
name: sequence.name,
|
|
28885
29369
|
ran: false,
|
|
28886
29370
|
stepsRun: 0,
|
|
28887
29371
|
skippedReason: "empty"
|
|
28888
29372
|
};
|
|
28889
|
-
const key = `${input.ruleId}:${sequence.name}`;
|
|
28890
29373
|
const now = this.now();
|
|
28891
|
-
const minDelayMs = (sequence.minDelaySec ?? 0) * 1e3;
|
|
28892
|
-
const last = this.lastRunAt.get(
|
|
29374
|
+
const minDelayMs = sequence.throttle ? (sequence.minDelaySec ?? 0) * 1e3 : 0;
|
|
29375
|
+
const last = this.lastRunAt.get(sequence.id);
|
|
28893
29376
|
if (minDelayMs > 0 && last !== void 0 && now - last < minDelayMs) {
|
|
28894
29377
|
this.deps.logger.debug("rule action sequence throttled", {
|
|
28895
29378
|
tags: { deviceId: input.deviceId },
|
|
28896
29379
|
meta: {
|
|
28897
29380
|
ruleId: input.ruleId,
|
|
29381
|
+
sequenceId: sequence.id,
|
|
28898
29382
|
sequence: sequence.name,
|
|
28899
29383
|
sinceLastMs: now - last
|
|
28900
29384
|
}
|
|
28901
29385
|
});
|
|
28902
29386
|
return {
|
|
29387
|
+
id: sequence.id,
|
|
28903
29388
|
name: sequence.name,
|
|
28904
29389
|
ran: false,
|
|
28905
29390
|
stepsRun: 0,
|
|
28906
29391
|
skippedReason: "throttled"
|
|
28907
29392
|
};
|
|
28908
29393
|
}
|
|
28909
|
-
this.lastRunAt.set(
|
|
29394
|
+
if (sequence.throttle) this.lastRunAt.set(sequence.id, now);
|
|
28910
29395
|
let stepsRun = 0;
|
|
28911
29396
|
for (const [index, action] of sequence.actions.entries()) try {
|
|
28912
29397
|
await this.step(action, input.origin);
|
|
@@ -28917,6 +29402,7 @@ var NcRuleActionRunner = class {
|
|
|
28917
29402
|
meta: {
|
|
28918
29403
|
ruleId: input.ruleId,
|
|
28919
29404
|
rule: input.ruleName,
|
|
29405
|
+
sequenceId: sequence.id,
|
|
28920
29406
|
sequence: sequence.name,
|
|
28921
29407
|
step: index,
|
|
28922
29408
|
action: action.kind === "cap" ? `${action.cap}.${action.method}` : "wait",
|
|
@@ -28924,6 +29410,7 @@ var NcRuleActionRunner = class {
|
|
|
28924
29410
|
}
|
|
28925
29411
|
});
|
|
28926
29412
|
return {
|
|
29413
|
+
id: sequence.id,
|
|
28927
29414
|
name: sequence.name,
|
|
28928
29415
|
ran: true,
|
|
28929
29416
|
stepsRun,
|
|
@@ -28935,11 +29422,13 @@ var NcRuleActionRunner = class {
|
|
|
28935
29422
|
meta: {
|
|
28936
29423
|
ruleId: input.ruleId,
|
|
28937
29424
|
rule: input.ruleName,
|
|
29425
|
+
sequenceId: sequence.id,
|
|
28938
29426
|
sequence: sequence.name,
|
|
28939
29427
|
steps: stepsRun
|
|
28940
29428
|
}
|
|
28941
29429
|
});
|
|
28942
29430
|
return {
|
|
29431
|
+
id: sequence.id,
|
|
28943
29432
|
name: sequence.name,
|
|
28944
29433
|
ran: true,
|
|
28945
29434
|
stepsRun
|
|
@@ -28960,6 +29449,17 @@ var NcRuleActionRunner = class {
|
|
|
28960
29449
|
});
|
|
28961
29450
|
}
|
|
28962
29451
|
};
|
|
29452
|
+
/** Force-enable a shared sequence for a button tap (always runs, still throttled). */
|
|
29453
|
+
function asForcedSequence(sequence) {
|
|
29454
|
+
return {
|
|
29455
|
+
id: sequence.id,
|
|
29456
|
+
name: sequence.name,
|
|
29457
|
+
enabled: true,
|
|
29458
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
29459
|
+
actions: sequence.actions,
|
|
29460
|
+
throttle: true
|
|
29461
|
+
};
|
|
29462
|
+
}
|
|
28963
29463
|
//#endregion
|
|
28964
29464
|
//#region src/notification-center/rule-ai-analyst.ts
|
|
28965
29465
|
/**
|
|
@@ -30806,7 +31306,7 @@ function snoozeVerdict(input, deps) {
|
|
|
30806
31306
|
});
|
|
30807
31307
|
if (active === null) return { kind: "clear" };
|
|
30808
31308
|
const rule = deps.ruleFor(input.ruleId);
|
|
30809
|
-
if (rule !== null && isSnoozeExempt(rule)) return {
|
|
31309
|
+
if (rule !== null && isSnoozeExempt(rule, deps.sequenceActionsOf)) return {
|
|
30810
31310
|
kind: "exempt",
|
|
30811
31311
|
snooze: active
|
|
30812
31312
|
};
|
|
@@ -36607,13 +37107,16 @@ function frozenOccupancy(rule, occ) {
|
|
|
36607
37107
|
function fireRefFor(rule, subject, kind) {
|
|
36608
37108
|
const hasEventMedia = kind === "object-event" || kind === "package-event";
|
|
36609
37109
|
const className = subject.classNames[0];
|
|
37110
|
+
const linkedOwners = subject.trackId === void 0 && subject.linkedStills !== void 0 ? linkedMediaOwners(subject.linkedStills, linkedCameraMode(rule.media)) : void 0;
|
|
37111
|
+
const trackId = subject.trackId ?? linkedOwners?.trackId;
|
|
36610
37112
|
return {
|
|
36611
37113
|
ruleId: rule.id,
|
|
36612
37114
|
ruleName: rule.name,
|
|
36613
37115
|
deviceId: subject.deviceId,
|
|
36614
37116
|
...subject.sourceDeviceId !== void 0 ? { sourceDeviceId: subject.sourceDeviceId } : {},
|
|
36615
37117
|
recordId: subject.recordId,
|
|
36616
|
-
...
|
|
37118
|
+
...trackId !== void 0 ? { trackId } : {},
|
|
37119
|
+
...linkedOwners !== void 0 && linkedOwners.extraStills.length > 0 ? { extraStills: linkedOwners.extraStills.map((x) => ({ ...x })) } : {},
|
|
36617
37120
|
...hasEventMedia ? { eventId: subject.recordId } : {},
|
|
36618
37121
|
timestamp: subject.timestamp,
|
|
36619
37122
|
...className !== void 0 ? { className } : {},
|
|
@@ -36918,6 +37421,8 @@ var NotificationCenter = class {
|
|
|
36918
37421
|
snoozes;
|
|
36919
37422
|
/** The permanent per-camera notification off switch — see `device-mute-store.ts`. */
|
|
36920
37423
|
deviceMutes;
|
|
37424
|
+
/** Shared action sequences rules and buttons bind to by id. */
|
|
37425
|
+
sequences;
|
|
36921
37426
|
/** The durable episode rows behind the sustained-state alerts. */
|
|
36922
37427
|
alertStore;
|
|
36923
37428
|
/** Battery band → notification, and the reminder sweep. */
|
|
@@ -37156,7 +37661,8 @@ var NotificationCenter = class {
|
|
|
37156
37661
|
const drafts = [];
|
|
37157
37662
|
const sequenceButtons = this.actionRunner === null || stored === null ? [] : buildActionButtons({
|
|
37158
37663
|
actions: stored.actions,
|
|
37159
|
-
|
|
37664
|
+
knownSequenceIds: this.sequences.knownIds(),
|
|
37665
|
+
mintUrl: ({ sequenceId, actionId }) => {
|
|
37160
37666
|
const id = randomUUID();
|
|
37161
37667
|
drafts.push({
|
|
37162
37668
|
id,
|
|
@@ -37166,7 +37672,7 @@ var NotificationCenter = class {
|
|
|
37166
37672
|
id,
|
|
37167
37673
|
ruleId: input.ruleId,
|
|
37168
37674
|
ruleName: input.ruleName,
|
|
37169
|
-
|
|
37675
|
+
sequenceId,
|
|
37170
37676
|
deviceId: input.deviceId,
|
|
37171
37677
|
actionId,
|
|
37172
37678
|
targetId: input.targetId,
|
|
@@ -37178,6 +37684,7 @@ var NotificationCenter = class {
|
|
|
37178
37684
|
});
|
|
37179
37685
|
const snoozeButtons = input.ownerUserId === void 0 || stored === null ? [] : buildSnoozeButtons({
|
|
37180
37686
|
rule: stored,
|
|
37687
|
+
sequenceActionsOf: this.sequenceActionsOf,
|
|
37181
37688
|
label: (minutes) => this.snoozeButtonLabel(minutes),
|
|
37182
37689
|
mintUrl: ({ actionId, minutes }) => {
|
|
37183
37690
|
const id = randomUUID();
|
|
@@ -37418,7 +37925,7 @@ var NotificationCenter = class {
|
|
|
37418
37925
|
*/
|
|
37419
37926
|
async applyGrantedSnooze(grant) {
|
|
37420
37927
|
const rule = this.rules.get(grant.ruleId);
|
|
37421
|
-
if (rule !== null && isSnoozeExempt(rule)) throw new Error(`"${grant.ruleName}" is an alarm rule and cannot be snoozed`);
|
|
37928
|
+
if (rule !== null && isSnoozeExempt(rule, this.sequenceActionsOf)) throw new Error(`"${grant.ruleName}" is an alarm rule and cannot be snoozed`);
|
|
37422
37929
|
const snooze = await this.snoozes.create({
|
|
37423
37930
|
userId: grant.userId,
|
|
37424
37931
|
scope: "device",
|
|
@@ -37717,16 +38224,13 @@ var NotificationCenter = class {
|
|
|
37717
38224
|
if (this.actionRunner === null) throw new Error("no actuator is wired on this node");
|
|
37718
38225
|
const rule = this.rules.get(grant.ruleId);
|
|
37719
38226
|
if (rule === null) throw new Error(`rule ${grant.ruleId} no longer exists`);
|
|
37720
|
-
const sequence =
|
|
37721
|
-
if (sequence ===
|
|
38227
|
+
const sequence = this.sequences.get(grant.sequenceId);
|
|
38228
|
+
if (sequence === null) throw new Error(`sequence "${grant.sequenceId}" no longer exists`);
|
|
37722
38229
|
const [outcome] = await this.actionRunner.run({
|
|
37723
38230
|
ruleId: rule.id,
|
|
37724
38231
|
ruleName: rule.name,
|
|
37725
38232
|
deviceId: grant.deviceId,
|
|
37726
|
-
sequences: [
|
|
37727
|
-
...sequence,
|
|
37728
|
-
enabled: true
|
|
37729
|
-
}]
|
|
38233
|
+
sequences: [asForcedSequence(sequence)]
|
|
37730
38234
|
});
|
|
37731
38235
|
if (outcome === void 0) throw new Error("the sequence did not run");
|
|
37732
38236
|
if (outcome.failedAt !== void 0) throw new Error(`step ${String(outcome.failedAt)} of "${sequence.name}" failed`);
|
|
@@ -37845,6 +38349,11 @@ var NotificationCenter = class {
|
|
|
37845
38349
|
logger: this.logger.child("device-mute"),
|
|
37846
38350
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
37847
38351
|
});
|
|
38352
|
+
this.sequences = new NcSequenceStore({
|
|
38353
|
+
store: deps.store,
|
|
38354
|
+
logger: this.logger.child("sequences"),
|
|
38355
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
38356
|
+
});
|
|
37848
38357
|
this.alertStore = new NcAlertStore({
|
|
37849
38358
|
store: deps.store,
|
|
37850
38359
|
logger: this.logger.child("device-alerts")
|
|
@@ -38111,6 +38620,7 @@ var NotificationCenter = class {
|
|
|
38111
38620
|
await NcStateSummaryService.declare(store);
|
|
38112
38621
|
await NcSnoozeStore.declare(store);
|
|
38113
38622
|
await NcDeviceMuteStore.declare(store);
|
|
38623
|
+
await NcSequenceStore.declare(store);
|
|
38114
38624
|
await TimelapseStore.declare(store);
|
|
38115
38625
|
await NcSummaryStore.declare(store);
|
|
38116
38626
|
await NcCalendarEventsStore.declare(store);
|
|
@@ -38172,6 +38682,8 @@ var NotificationCenter = class {
|
|
|
38172
38682
|
sinceStartMs: now - t0
|
|
38173
38683
|
} });
|
|
38174
38684
|
};
|
|
38685
|
+
await timed("sequences", () => this.sequences.load());
|
|
38686
|
+
await timed("migrateLegacyActions", () => this.migrateLegacyRuleActions());
|
|
38175
38687
|
await Promise.all([
|
|
38176
38688
|
timed("texts", () => this.reloadTexts()),
|
|
38177
38689
|
timed("rules", () => this.rules.load()),
|
|
@@ -39471,7 +39983,32 @@ var NotificationCenter = class {
|
|
|
39471
39983
|
return { success: true };
|
|
39472
39984
|
},
|
|
39473
39985
|
setRuleEnabled: async ({ ruleId, enabled }) => {
|
|
39474
|
-
await this.rules.setEnabled(ruleId, enabled);
|
|
39986
|
+
const updated = await this.rules.setEnabled(ruleId, enabled);
|
|
39987
|
+
const bindings = enabled ? updated.actions?.onActivation : updated.actions?.onDeactivation;
|
|
39988
|
+
if (bindings !== void 0 && bindings.length > 0 && this.actionRunner !== null) this.actionRunner.runBindings({
|
|
39989
|
+
ruleId: updated.id,
|
|
39990
|
+
ruleName: updated.name,
|
|
39991
|
+
deviceId: this.deviceIdForRuleActions(updated),
|
|
39992
|
+
bindings,
|
|
39993
|
+
lookup: (id) => this.sequences.get(id)
|
|
39994
|
+
});
|
|
39995
|
+
return { success: true };
|
|
39996
|
+
},
|
|
39997
|
+
listSequences: async () => ({ sequences: [...this.sequences.list()] }),
|
|
39998
|
+
getSequence: async ({ sequenceId }) => ({ sequence: this.sequences.get(sequenceId) }),
|
|
39999
|
+
createSequence: async ({ sequence }) => {
|
|
40000
|
+
const created = await this.sequences.create(sequence);
|
|
40001
|
+
this.logger.info("notification sequence created", { meta: { sequenceId: created.id } });
|
|
40002
|
+
return { sequence: created };
|
|
40003
|
+
},
|
|
40004
|
+
updateSequence: async ({ sequenceId, sequence }) => {
|
|
40005
|
+
const updated = await this.sequences.update(sequenceId, sequence);
|
|
40006
|
+
this.logger.info("notification sequence updated", { meta: { sequenceId } });
|
|
40007
|
+
return { sequence: updated };
|
|
40008
|
+
},
|
|
40009
|
+
deleteSequence: async ({ sequenceId }) => {
|
|
40010
|
+
await this.sequences.delete(sequenceId);
|
|
40011
|
+
this.logger.info("notification sequence deleted", { meta: { sequenceId } });
|
|
39475
40012
|
return { success: true };
|
|
39476
40013
|
},
|
|
39477
40014
|
testRule: async ({ rule, lookbackMinutes }) => ({ results: [...await this.dryRun(rule, lookbackMinutes)] }),
|
|
@@ -40205,10 +40742,11 @@ var NotificationCenter = class {
|
|
|
40205
40742
|
const alarmSource = subject.systemEvent?.alarmSource;
|
|
40206
40743
|
const sourceRule = alarmSource !== void 0 ? this.rules.get(alarmSource.ruleId) : null;
|
|
40207
40744
|
const media = sourceRule !== null ? mediaPolicyFields(sourceRule, void 0) : { media: "none" };
|
|
40745
|
+
const mediaDeviceId = alarmSource?.deviceId ?? panel.deviceId;
|
|
40208
40746
|
const entries = targets.map((targetId) => ({
|
|
40209
40747
|
ruleId: NC_ALARM_ANNOUNCE_RULE_ID,
|
|
40210
40748
|
targetId,
|
|
40211
|
-
deviceId:
|
|
40749
|
+
deviceId: mediaDeviceId,
|
|
40212
40750
|
recordKind: "system-event",
|
|
40213
40751
|
recordId: subject.recordId,
|
|
40214
40752
|
payload: {
|
|
@@ -40217,8 +40755,9 @@ var NotificationCenter = class {
|
|
|
40217
40755
|
priority: 2,
|
|
40218
40756
|
...media,
|
|
40219
40757
|
subject: {
|
|
40220
|
-
deviceId:
|
|
40758
|
+
deviceId: mediaDeviceId,
|
|
40221
40759
|
...alarmSource?.trackId !== void 0 ? { trackId: alarmSource.trackId } : {},
|
|
40760
|
+
...alarmSource?.extraStills !== void 0 && alarmSource.extraStills.length > 0 ? { extraStills: alarmSource.extraStills.map((x) => ({ ...x })) } : {},
|
|
40222
40761
|
...alarmSource?.eventId !== void 0 ? { eventId: alarmSource.eventId } : {},
|
|
40223
40762
|
className: "alarm",
|
|
40224
40763
|
zones: [],
|
|
@@ -40968,7 +41507,7 @@ var NotificationCenter = class {
|
|
|
40968
41507
|
} : {}
|
|
40969
41508
|
}
|
|
40970
41509
|
});
|
|
40971
|
-
const unresolvable = unresolvableButtons(rule.actions);
|
|
41510
|
+
const unresolvable = unresolvableButtons(rule.actions, this.sequences.knownIds());
|
|
40972
41511
|
if (unresolvable.length > 0) this.logger.warn("rule buttons reference sequences that do not exist — not sent", {
|
|
40973
41512
|
tags: { deviceId: subject.deviceId },
|
|
40974
41513
|
meta: {
|
|
@@ -41095,20 +41634,85 @@ var NotificationCenter = class {
|
|
|
41095
41634
|
return this.groupStore.list(query);
|
|
41096
41635
|
}
|
|
41097
41636
|
/**
|
|
41098
|
-
*
|
|
41637
|
+
* Materialise legacy inline `onTrigger` sequences into the shared store and
|
|
41638
|
+
* rewrite each rule's actions to bindings + `buttons.sequenceId`. Runs on RAW
|
|
41639
|
+
* rows before {@link NcRuleStore.load} so Zod never skips them.
|
|
41640
|
+
*/
|
|
41641
|
+
async migrateLegacyRuleActions() {
|
|
41642
|
+
let rows;
|
|
41643
|
+
try {
|
|
41644
|
+
rows = await this.deps.store.query.query({
|
|
41645
|
+
collection: NC_RULES_COLLECTION,
|
|
41646
|
+
filter: { limit: 1e4 }
|
|
41647
|
+
});
|
|
41648
|
+
} catch (err) {
|
|
41649
|
+
this.logger.warn("legacy actions migration skipped — could not read rules", { meta: { error: String(err) } });
|
|
41650
|
+
return;
|
|
41651
|
+
}
|
|
41652
|
+
let migrated = 0;
|
|
41653
|
+
for (const row of rows) {
|
|
41654
|
+
const ruleBlob = row.data["rule"];
|
|
41655
|
+
if (typeof ruleBlob !== "object" || ruleBlob === null || Array.isArray(ruleBlob)) continue;
|
|
41656
|
+
const rule = { ...ruleBlob };
|
|
41657
|
+
const ruleId = typeof rule["id"] === "string" ? rule["id"] : void 0;
|
|
41658
|
+
if (ruleId === void 0) continue;
|
|
41659
|
+
const result = await migrateLegacyRuleActions(rule["actions"], async (input) => {
|
|
41660
|
+
return { id: (await this.sequences.create(input)).id };
|
|
41661
|
+
});
|
|
41662
|
+
if (!result.migrated) continue;
|
|
41663
|
+
if (result.actions !== void 0) rule["actions"] = result.actions;
|
|
41664
|
+
else delete rule["actions"];
|
|
41665
|
+
const { id: _rowId, ...columns } = row.data;
|
|
41666
|
+
await this.deps.store.set.mutate({
|
|
41667
|
+
collection: NC_RULES_COLLECTION,
|
|
41668
|
+
key: ruleId,
|
|
41669
|
+
value: {
|
|
41670
|
+
...columns,
|
|
41671
|
+
rule
|
|
41672
|
+
}
|
|
41673
|
+
});
|
|
41674
|
+
migrated += 1;
|
|
41675
|
+
}
|
|
41676
|
+
if (migrated > 0) this.logger.info("migrated legacy inline rule actions into shared sequences", { meta: {
|
|
41677
|
+
rules: migrated,
|
|
41678
|
+
sequences: this.sequences.list().length
|
|
41679
|
+
} });
|
|
41680
|
+
}
|
|
41681
|
+
/**
|
|
41682
|
+
* True when this match is a clear / leave edge — the subject for
|
|
41683
|
+
* `onTriggerOff`. Raise / enter / ordinary matches use `onTrigger`.
|
|
41684
|
+
*/
|
|
41685
|
+
isTriggerOffSubject(subject) {
|
|
41686
|
+
if (subject.sensorRaised === false) return true;
|
|
41687
|
+
if (subject.occupancy !== void 0 && subject.occupancy.occupied === false) return true;
|
|
41688
|
+
return false;
|
|
41689
|
+
}
|
|
41690
|
+
/** Device id for activation/deactivation log tags — first scoped device, else 0. */
|
|
41691
|
+
deviceIdForRuleActions(rule) {
|
|
41692
|
+
const devices = rule.conditions.devices;
|
|
41693
|
+
if (devices !== void 0 && devices.length > 0) {
|
|
41694
|
+
const first = devices[0];
|
|
41695
|
+
if (first !== void 0) return first;
|
|
41696
|
+
}
|
|
41697
|
+
return 0;
|
|
41698
|
+
}
|
|
41699
|
+
sequenceActionsOf = (sequenceId) => this.sequences.get(sequenceId)?.actions;
|
|
41700
|
+
/**
|
|
41701
|
+
* Run this rule's onTrigger / onTriggerOff bindings for this match.
|
|
41099
41702
|
*
|
|
41100
41703
|
* Called BEFORE the cooldown gate — see the call site for why. Everything
|
|
41101
41704
|
* here is fire-and-forget and nothing may throw into the evaluation.
|
|
41102
41705
|
*/
|
|
41103
41706
|
runRuleActions(rule, subject, kind, controls) {
|
|
41104
|
-
const sequences = rule.actions?.onTrigger;
|
|
41105
|
-
if (sequences === void 0 || sequences.length === 0) return;
|
|
41106
41707
|
if (this.actionRunner === null || controls.runActions === false) return;
|
|
41107
|
-
this.
|
|
41708
|
+
const bindings = this.isTriggerOffSubject(subject) ? rule.actions?.onTriggerOff : rule.actions?.onTrigger;
|
|
41709
|
+
if (bindings === void 0 || bindings.length === 0) return;
|
|
41710
|
+
this.actionRunner.runBindings({
|
|
41108
41711
|
ruleId: rule.id,
|
|
41109
41712
|
ruleName: rule.name,
|
|
41110
41713
|
deviceId: subject.deviceId,
|
|
41111
|
-
|
|
41714
|
+
bindings,
|
|
41715
|
+
lookup: (id) => this.sequences.get(id),
|
|
41112
41716
|
origin: fireRefFor(rule, subject, kind)
|
|
41113
41717
|
});
|
|
41114
41718
|
}
|
|
@@ -41238,7 +41842,10 @@ var NotificationCenter = class {
|
|
|
41238
41842
|
...target.params !== void 0 ? { params: target.params } : {},
|
|
41239
41843
|
subject: {
|
|
41240
41844
|
deviceId: subject.deviceId,
|
|
41241
|
-
...alarmSource?.trackId !== void 0 ? {
|
|
41845
|
+
...alarmSource?.trackId !== void 0 ? {
|
|
41846
|
+
trackId: alarmSource.trackId,
|
|
41847
|
+
...alarmSource.extraStills !== void 0 && alarmSource.extraStills.length > 0 ? { extraStills: alarmSource.extraStills.map((x) => ({ ...x })) } : {}
|
|
41848
|
+
} : linkedOwners !== void 0 ? {
|
|
41242
41849
|
trackId: linkedOwners.trackId,
|
|
41243
41850
|
...linkedOwners.extraStills.length > 0 ? { extraStills: linkedOwners.extraStills.map((x) => ({ ...x })) } : {},
|
|
41244
41851
|
analysisTiles: linkedOwners.analysisTiles.map((x) => ({ ...x }))
|
|
@@ -42012,7 +42619,8 @@ var NotificationCenter = class {
|
|
|
42012
42619
|
snoozeDecision(input) {
|
|
42013
42620
|
const verdict = snoozeVerdict(input, {
|
|
42014
42621
|
activeFor: (query) => this.snoozes.activeFor(query),
|
|
42015
|
-
ruleFor: (ruleId) => this.rules.get(ruleId)
|
|
42622
|
+
ruleFor: (ruleId) => this.rules.get(ruleId),
|
|
42623
|
+
sequenceActionsOf: this.sequenceActionsOf
|
|
42016
42624
|
});
|
|
42017
42625
|
if (verdict.kind === "exempt") this.logger.info("snooze does not apply — alarm rules are never silenced", {
|
|
42018
42626
|
tags: { deviceId: input.deviceId },
|