@camstack/addon-post-analysis 1.2.321 → 1.2.323
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-BCx3qCL5.js → clip-model-registry-8IMzj7jJ.js} +4542 -3562
- package/dist/{clip-model-registry-Dnm9dfy0.mjs → clip-model-registry-BENGs4rw.mjs} +5926 -5036
- 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-CPcZN3mA.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CGo6RmK3.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-Dt-jaGuG.mjs → hostInit-D5UUhDKa.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +941 -108
- package/dist/pipeline-analytics/index.mjs +941 -108
- 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-CGUnHrR_.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-CVH5tT7I.mjs +0 -26
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ 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-BENGs4rw.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";
|
|
@@ -938,7 +938,7 @@ function grantMeta(grant) {
|
|
|
938
938
|
...common,
|
|
939
939
|
ruleId: grant.ruleId,
|
|
940
940
|
rule: grant.ruleName,
|
|
941
|
-
|
|
941
|
+
sequenceId: grant.sequenceId
|
|
942
942
|
};
|
|
943
943
|
case "snooze": return {
|
|
944
944
|
...common,
|
|
@@ -1941,6 +1941,21 @@ var ncAlarmPanelSchema = object({
|
|
|
1941
1941
|
exitDelaySec: number().optional(),
|
|
1942
1942
|
entryDelaySec: number().optional(),
|
|
1943
1943
|
triggeredDurationSec: number().optional()
|
|
1944
|
+
})).default([]),
|
|
1945
|
+
/**
|
|
1946
|
+
* Active alarm configuration profile id. Empty / absent on a blob older than
|
|
1947
|
+
* profiles → seeded to Default on first read via {@link ensureAlarmProfiles}.
|
|
1948
|
+
*/
|
|
1949
|
+
activeProfileId: string().default(NC_ALARM_DEFAULT_PROFILE_ID),
|
|
1950
|
+
/**
|
|
1951
|
+
* Named settings blobs. Loose on purpose — each profile's settings are
|
|
1952
|
+
* re-validated through {@link settingsBlobFromPersisted} so a corrupt entry
|
|
1953
|
+
* drops rather than taking down the panel.
|
|
1954
|
+
*/
|
|
1955
|
+
profiles: array(object({
|
|
1956
|
+
id: string(),
|
|
1957
|
+
name: string(),
|
|
1958
|
+
settings: record(string(), unknown())
|
|
1944
1959
|
})).default([])
|
|
1945
1960
|
});
|
|
1946
1961
|
/**
|
|
@@ -1963,6 +1978,106 @@ function modeDeviceListFromPersisted(raw) {
|
|
|
1963
1978
|
return lists;
|
|
1964
1979
|
}
|
|
1965
1980
|
/**
|
|
1981
|
+
* Rebuild a trusted {@link NcAlarmSettings} from a persisted profile blob.
|
|
1982
|
+
* Unknown / corrupt fields fall back to system-safe empties via the flat-key
|
|
1983
|
+
* readers the panel already uses for the live settings.
|
|
1984
|
+
*/
|
|
1985
|
+
function settingsBlobFromPersisted(raw) {
|
|
1986
|
+
const exit = typeof raw["exitDelaySec"] === "number" ? raw["exitDelaySec"] : DEFAULT_DELAYS.exitDelaySec;
|
|
1987
|
+
const entry = typeof raw["entryDelaySec"] === "number" ? raw["entryDelaySec"] : DEFAULT_DELAYS.entryDelaySec;
|
|
1988
|
+
const triggered = typeof raw["triggeredDurationSec"] === "number" ? raw["triggeredDurationSec"] : DEFAULT_DELAYS.triggeredDurationSec;
|
|
1989
|
+
const announceTargets = Array.isArray(raw["announceTargets"]) ? raw["announceTargets"].filter((t) => typeof t === "string") : [];
|
|
1990
|
+
const lists = (key) => {
|
|
1991
|
+
const v = raw[key];
|
|
1992
|
+
if (!Array.isArray(v)) return [];
|
|
1993
|
+
const rows = [];
|
|
1994
|
+
for (const item of v) {
|
|
1995
|
+
if (typeof item !== "object" || item === null) continue;
|
|
1996
|
+
const rec = item;
|
|
1997
|
+
if (typeof rec["mode"] !== "string" || !Array.isArray(rec["deviceIds"])) continue;
|
|
1998
|
+
rows.push({
|
|
1999
|
+
mode: rec["mode"],
|
|
2000
|
+
deviceIds: rec["deviceIds"].filter((id) => typeof id === "number")
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
return modeDeviceListFromPersisted(rows);
|
|
2004
|
+
};
|
|
2005
|
+
const modeDelaysRaw = Array.isArray(raw["modeDelays"]) ? raw["modeDelays"] : [];
|
|
2006
|
+
const modeDelaysLoose = [];
|
|
2007
|
+
for (const item of modeDelaysRaw) {
|
|
2008
|
+
if (typeof item !== "object" || item === null) continue;
|
|
2009
|
+
const rec = item;
|
|
2010
|
+
if (typeof rec["mode"] !== "string") continue;
|
|
2011
|
+
const entry = { mode: rec["mode"] };
|
|
2012
|
+
if (typeof rec["exitDelaySec"] === "number") entry.exitDelaySec = rec["exitDelaySec"];
|
|
2013
|
+
if (typeof rec["entryDelaySec"] === "number") entry.entryDelaySec = rec["entryDelaySec"];
|
|
2014
|
+
if (typeof rec["triggeredDurationSec"] === "number") entry.triggeredDurationSec = rec["triggeredDurationSec"];
|
|
2015
|
+
modeDelaysLoose.push(entry);
|
|
2016
|
+
}
|
|
2017
|
+
return {
|
|
2018
|
+
exitDelaySec: Math.max(0, Math.min(600, Math.trunc(exit))),
|
|
2019
|
+
entryDelaySec: Math.max(0, Math.min(600, Math.trunc(entry))),
|
|
2020
|
+
triggeredDurationSec: Math.max(0, Math.min(3600, Math.trunc(triggered))),
|
|
2021
|
+
announceTargets,
|
|
2022
|
+
nonBlocking: lists("nonBlocking"),
|
|
2023
|
+
blocking: lists("blocking"),
|
|
2024
|
+
secureOnArmOff: lists("secureOnArmOff"),
|
|
2025
|
+
modeDelays: modeDelaysFromPersisted(modeDelaysLoose).entries
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
function settingsToPersisted(settings) {
|
|
2029
|
+
return {
|
|
2030
|
+
exitDelaySec: settings.exitDelaySec,
|
|
2031
|
+
entryDelaySec: settings.entryDelaySec,
|
|
2032
|
+
triggeredDurationSec: settings.triggeredDurationSec,
|
|
2033
|
+
announceTargets: [...settings.announceTargets],
|
|
2034
|
+
nonBlocking: settings.nonBlocking.map((l) => ({
|
|
2035
|
+
mode: l.mode,
|
|
2036
|
+
deviceIds: [...l.deviceIds]
|
|
2037
|
+
})),
|
|
2038
|
+
blocking: settings.blocking.map((l) => ({
|
|
2039
|
+
mode: l.mode,
|
|
2040
|
+
deviceIds: [...l.deviceIds]
|
|
2041
|
+
})),
|
|
2042
|
+
secureOnArmOff: settings.secureOnArmOff.map((l) => ({
|
|
2043
|
+
mode: l.mode,
|
|
2044
|
+
deviceIds: [...l.deviceIds]
|
|
2045
|
+
})),
|
|
2046
|
+
modeDelays: settings.modeDelays.map((e) => ({ ...e }))
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
function profilesStateFromPersisted(activeId, rows, current) {
|
|
2050
|
+
const profiles = rows.filter((r) => r.id.length > 0 && r.name.trim().length > 0).map((r) => ({
|
|
2051
|
+
id: r.id,
|
|
2052
|
+
name: r.name,
|
|
2053
|
+
settings: settingsBlobFromPersisted(r.settings)
|
|
2054
|
+
}));
|
|
2055
|
+
return ensureAlarmProfiles(profiles.length > 0 ? {
|
|
2056
|
+
activeId,
|
|
2057
|
+
profiles
|
|
2058
|
+
} : null, current);
|
|
2059
|
+
}
|
|
2060
|
+
function profilesToPersisted(state) {
|
|
2061
|
+
return {
|
|
2062
|
+
activeProfileId: state.activeId,
|
|
2063
|
+
profiles: state.profiles.map((p) => ({
|
|
2064
|
+
id: p.id,
|
|
2065
|
+
name: p.name,
|
|
2066
|
+
settings: settingsToPersisted(p.settings)
|
|
2067
|
+
}))
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
function profilesViewOf(state) {
|
|
2071
|
+
return {
|
|
2072
|
+
activeId: state.activeId,
|
|
2073
|
+
items: state.profiles.map((p) => ({
|
|
2074
|
+
id: p.id,
|
|
2075
|
+
name: p.name,
|
|
2076
|
+
isDefault: isDefaultAlarmProfileId(p.id)
|
|
2077
|
+
}))
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
/**
|
|
1966
2081
|
* The log line for a trigger the state machine ignored, per state. `trigger`
|
|
1967
2082
|
* ignores four: disarmed, arming (the exit delay), and pending / triggered
|
|
1968
2083
|
* (already counting down or going off — a second sensor must not restart it).
|
|
@@ -2210,7 +2325,134 @@ var NcAlarmPanelDevice = class extends BaseDevice {
|
|
|
2210
2325
|
changed: membershipChanged
|
|
2211
2326
|
}
|
|
2212
2327
|
});
|
|
2213
|
-
|
|
2328
|
+
const settled = this.settings();
|
|
2329
|
+
await this.persistProfilesState(syncActiveAlarmProfileSettings(this.readProfilesState(), settled));
|
|
2330
|
+
return settled;
|
|
2331
|
+
}
|
|
2332
|
+
/** Named profiles + which one is active — for {@link NcAlarmConfig.profiles}. */
|
|
2333
|
+
profilesView() {
|
|
2334
|
+
return profilesViewOf(this.readProfilesState());
|
|
2335
|
+
}
|
|
2336
|
+
/**
|
|
2337
|
+
* Create a named profile (system defaults, or clone). Does not activate.
|
|
2338
|
+
* `newId` is assigned by the caller (usually `randomUUID`).
|
|
2339
|
+
*/
|
|
2340
|
+
async createProfile(input) {
|
|
2341
|
+
const result = createAlarmProfile(this.readProfilesState(), {
|
|
2342
|
+
name: input.name,
|
|
2343
|
+
id: input.newId,
|
|
2344
|
+
source: input.source
|
|
2345
|
+
});
|
|
2346
|
+
if (result.kind === "refused") throw this.profilesRefuseError(result.reason, result.detail);
|
|
2347
|
+
await this.persistProfilesState(result.state);
|
|
2348
|
+
this.ctx.logger.info("alarm profile created", {
|
|
2349
|
+
tags: { deviceId: this.id },
|
|
2350
|
+
meta: {
|
|
2351
|
+
profileId: input.newId,
|
|
2352
|
+
name: input.name.trim(),
|
|
2353
|
+
source: input.source.kind
|
|
2354
|
+
}
|
|
2355
|
+
});
|
|
2356
|
+
return profilesViewOf(result.state);
|
|
2357
|
+
}
|
|
2358
|
+
async renameProfile(id, name) {
|
|
2359
|
+
const result = renameAlarmProfile(this.readProfilesState(), {
|
|
2360
|
+
id,
|
|
2361
|
+
name
|
|
2362
|
+
});
|
|
2363
|
+
if (result.kind === "refused") throw this.profilesRefuseError(result.reason, result.detail);
|
|
2364
|
+
await this.persistProfilesState(result.state);
|
|
2365
|
+
this.ctx.logger.info("alarm profile renamed", {
|
|
2366
|
+
tags: { deviceId: this.id },
|
|
2367
|
+
meta: {
|
|
2368
|
+
profileId: id,
|
|
2369
|
+
name: name.trim()
|
|
2370
|
+
}
|
|
2371
|
+
});
|
|
2372
|
+
return profilesViewOf(result.state);
|
|
2373
|
+
}
|
|
2374
|
+
/**
|
|
2375
|
+
* Delete a non-default profile. If it was active, Default becomes active and
|
|
2376
|
+
* its settings are applied to the flat keys (arm path).
|
|
2377
|
+
*/
|
|
2378
|
+
async deleteProfile(id) {
|
|
2379
|
+
const before = this.readProfilesState();
|
|
2380
|
+
const result = deleteAlarmProfile(before, id);
|
|
2381
|
+
if (result.kind === "refused") throw this.profilesRefuseError(result.reason, result.detail);
|
|
2382
|
+
const switched = before.activeId === id;
|
|
2383
|
+
await this.persistProfilesState(result.state);
|
|
2384
|
+
if (switched) {
|
|
2385
|
+
const def = result.state.profiles.find((p) => p.id === NC_ALARM_DEFAULT_PROFILE_ID);
|
|
2386
|
+
if (def !== void 0) await this.replaceFlatSettings(def.settings);
|
|
2387
|
+
}
|
|
2388
|
+
this.ctx.logger.info("alarm profile deleted", {
|
|
2389
|
+
tags: { deviceId: this.id },
|
|
2390
|
+
meta: {
|
|
2391
|
+
profileId: id,
|
|
2392
|
+
activatedDefault: switched
|
|
2393
|
+
}
|
|
2394
|
+
});
|
|
2395
|
+
return profilesViewOf(result.state);
|
|
2396
|
+
}
|
|
2397
|
+
/**
|
|
2398
|
+
* Make a profile active and apply its blob onto the flat settings the arm
|
|
2399
|
+
* path reads.
|
|
2400
|
+
*/
|
|
2401
|
+
async activateProfile(id) {
|
|
2402
|
+
const result = activateAlarmProfile(syncActiveAlarmProfileSettings(this.readProfilesState(), this.settings()), id);
|
|
2403
|
+
if (result.kind === "refused") throw this.profilesRefuseError(result.reason, result.detail);
|
|
2404
|
+
const next = result.state.profiles.find((p) => p.id === id);
|
|
2405
|
+
if (next === void 0) throw this.profilesRefuseError("not-found", id);
|
|
2406
|
+
await this.persistProfilesState(result.state);
|
|
2407
|
+
await this.replaceFlatSettings(next.settings);
|
|
2408
|
+
this.ctx.logger.info("alarm profile activated", {
|
|
2409
|
+
tags: { deviceId: this.id },
|
|
2410
|
+
meta: {
|
|
2411
|
+
profileId: id,
|
|
2412
|
+
name: next.name
|
|
2413
|
+
}
|
|
2414
|
+
});
|
|
2415
|
+
return profilesViewOf(result.state);
|
|
2416
|
+
}
|
|
2417
|
+
readProfilesState() {
|
|
2418
|
+
return profilesStateFromPersisted(this.config.get("activeProfileId"), this.config.get("profiles"), this.settings());
|
|
2419
|
+
}
|
|
2420
|
+
async persistProfilesState(state) {
|
|
2421
|
+
const persisted = profilesToPersisted(state);
|
|
2422
|
+
await Promise.all([this.config.set("activeProfileId", persisted.activeProfileId), this.config.set("profiles", persisted.profiles)]);
|
|
2423
|
+
}
|
|
2424
|
+
/** Write a full settings blob onto the flat keys the arm path reads. */
|
|
2425
|
+
async replaceFlatSettings(settings) {
|
|
2426
|
+
const blob = cloneAlarmSettings(settings);
|
|
2427
|
+
await Promise.all([
|
|
2428
|
+
this.config.set("exitDelaySec", blob.exitDelaySec),
|
|
2429
|
+
this.config.set("entryDelaySec", blob.entryDelaySec),
|
|
2430
|
+
this.config.set("triggeredDurationSec", blob.triggeredDurationSec),
|
|
2431
|
+
this.config.set("announceTargets", [...blob.announceTargets]),
|
|
2432
|
+
this.config.set("nonBlocking", blob.nonBlocking.map((l) => ({
|
|
2433
|
+
mode: l.mode,
|
|
2434
|
+
deviceIds: [...l.deviceIds]
|
|
2435
|
+
}))),
|
|
2436
|
+
this.config.set("blocking", blob.blocking.map((l) => ({
|
|
2437
|
+
mode: l.mode,
|
|
2438
|
+
deviceIds: [...l.deviceIds]
|
|
2439
|
+
}))),
|
|
2440
|
+
this.config.set("secureOnArmOff", blob.secureOnArmOff.map((l) => ({
|
|
2441
|
+
mode: l.mode,
|
|
2442
|
+
deviceIds: [...l.deviceIds]
|
|
2443
|
+
}))),
|
|
2444
|
+
this.config.set("modeDelays", blob.modeDelays.map((e) => ({ ...e })))
|
|
2445
|
+
]);
|
|
2446
|
+
}
|
|
2447
|
+
profilesRefuseError(reason, detail) {
|
|
2448
|
+
switch (reason) {
|
|
2449
|
+
case "name-empty": return /* @__PURE__ */ new Error("alarm profile name must not be empty");
|
|
2450
|
+
case "name-taken": return /* @__PURE__ */ new Error(`an alarm profile named '${detail ?? ""}' already exists`);
|
|
2451
|
+
case "cannot-delete-default": return /* @__PURE__ */ new Error("the Default alarm profile cannot be deleted");
|
|
2452
|
+
case "clone-source-missing": return /* @__PURE__ */ new Error(`alarm profile '${detail ?? ""}' was not found to clone`);
|
|
2453
|
+
case "not-found": return /* @__PURE__ */ new Error(`alarm profile '${detail ?? ""}' was not found`);
|
|
2454
|
+
default: return /* @__PURE__ */ new Error(`alarm profile operation refused (${reason})`);
|
|
2455
|
+
}
|
|
2214
2456
|
}
|
|
2215
2457
|
/**
|
|
2216
2458
|
* Refuse a mode this panel does not offer — logged by reason, then thrown. A membership edit
|
|
@@ -2943,6 +3185,11 @@ function ncAlarmPanelPort(panel) {
|
|
|
2943
3185
|
deviceId: panel.id,
|
|
2944
3186
|
settings: () => panel.settings(),
|
|
2945
3187
|
applySettings: (patch) => panel.applySettings(patch),
|
|
3188
|
+
profilesView: () => panel.profilesView(),
|
|
3189
|
+
createProfile: (input) => panel.createProfile(input),
|
|
3190
|
+
renameProfile: (id, name) => panel.renameProfile(id, name),
|
|
3191
|
+
deleteProfile: (id) => panel.deleteProfile(id),
|
|
3192
|
+
activateProfile: (id) => panel.activateProfile(id),
|
|
2946
3193
|
availableModes: () => NC_ALARM_MODES,
|
|
2947
3194
|
noteRuleTrigger: (source) => panel.noteRuleTrigger(source),
|
|
2948
3195
|
excludedDevices: () => panel.excludedDevices(),
|
|
@@ -4813,6 +5060,27 @@ function planDenseRanges(input) {
|
|
|
4813
5060
|
}
|
|
4814
5061
|
//#endregion
|
|
4815
5062
|
//#region src/notification-center/text-compose.ts
|
|
5063
|
+
/**
|
|
5064
|
+
* The COMPOSITE variables — phrases assembled from catalog fragments before
|
|
5065
|
+
* the one and only render pass.
|
|
5066
|
+
*
|
|
5067
|
+
* A notification body is not a sentence with slots. "person (John) on Front
|
|
5068
|
+
* door in gate" contains three sub-phrases whose SHAPE differs per language
|
|
5069
|
+
* and whose presence is conditional: the label parentheses, the zone clause,
|
|
5070
|
+
* the plural noun. Expressing that with `{{class}}{{label}}{{zones}}` and
|
|
5071
|
+
* three scalars pushes the grammar into the template, where a translator
|
|
5072
|
+
* cannot fix it. So each becomes ONE variable — `{{subject}}`, `{{inZones}}`,
|
|
5073
|
+
* `{{op}}`, `{{detectionSummary}}` — composed here from its own keys and handed
|
|
5074
|
+
* to {@link renderTemplate} as a finished string.
|
|
5075
|
+
*
|
|
5076
|
+
* **Fragments see the SCALARS only.** Never the composites. That is what makes
|
|
5077
|
+
* the depth exactly one and not "one so far": a fragment key edited to say
|
|
5078
|
+
* `{{inZones}}` renders empty rather than starting a second round, and
|
|
5079
|
+
* `renderTemplate` never re-scans what it inserted, so a composite's own value
|
|
5080
|
+
* is printed verbatim whatever it contains.
|
|
5081
|
+
*
|
|
5082
|
+
* Pure: a catalog in, a string out. No clock, no store, no I/O.
|
|
5083
|
+
*/
|
|
4816
5084
|
/** One normalisation, used on both sides of every class lookup — a pipeline
|
|
4817
5085
|
* that emitted `Person` must find the key written `class.person`. */
|
|
4818
5086
|
function normaliseClass(className) {
|
|
@@ -4821,24 +5089,35 @@ function normaliseClass(className) {
|
|
|
4821
5089
|
/**
|
|
4822
5090
|
* The noun for a class, in the count's plural category.
|
|
4823
5091
|
*
|
|
4824
|
-
*
|
|
4825
|
-
*
|
|
4826
|
-
*
|
|
4827
|
-
* detection.
|
|
5092
|
+
* Lookup order: the NC text catalog (`class.*`, plural-aware) → the shared
|
|
5093
|
+
* {@link eventKindLabel} map (viewer filters / EVENT_TAXONOMY) → `class.other`
|
|
5094
|
+
* with the RAW wire id. A custom model class must still appear rather than
|
|
5095
|
+
* vanish; a missing translation is a smaller defect than a missing detection.
|
|
4828
5096
|
*/
|
|
4829
5097
|
function ncClassNoun(texts, className, count) {
|
|
4830
|
-
const
|
|
5098
|
+
const normalised = normaliseClass(className);
|
|
5099
|
+
const key = `class.${normalised}`;
|
|
4831
5100
|
if (texts.has(key)) return texts.text({
|
|
4832
5101
|
key,
|
|
4833
5102
|
count,
|
|
4834
5103
|
vars: { name: className }
|
|
4835
5104
|
});
|
|
5105
|
+
if (isEventKindLabelKey(normalised)) return eventKindLabel(normalised, texts.language);
|
|
4836
5106
|
return texts.text({
|
|
4837
5107
|
key: "class.other",
|
|
4838
5108
|
count,
|
|
4839
5109
|
vars: { name: className }
|
|
4840
5110
|
});
|
|
4841
5111
|
}
|
|
5112
|
+
/**
|
|
5113
|
+
* The parenthetical half of a labelled subject: a recognised person or plate
|
|
5114
|
+
* is printed verbatim; a classification (species, COCO sub, …) goes through
|
|
5115
|
+
* the shared event-kind map so "animal (cat)" becomes "animale (Gatto)".
|
|
5116
|
+
*/
|
|
5117
|
+
function ncSubjectLabelText(texts, label, labelKind) {
|
|
5118
|
+
if (labelKind === "identity" || labelKind === "plate") return label;
|
|
5119
|
+
return eventKindLabel(label, texts.language);
|
|
5120
|
+
}
|
|
4842
5121
|
/** `person (John)` / `persona (John)` — the thing the notification is about. */
|
|
4843
5122
|
function ncSubjectText(texts, input) {
|
|
4844
5123
|
const classNoun = ncClassNoun(texts, input.className, 1);
|
|
@@ -4850,7 +5129,7 @@ function ncSubjectText(texts, input) {
|
|
|
4850
5129
|
key: subjectLabelKey(input.labelKind),
|
|
4851
5130
|
vars: {
|
|
4852
5131
|
classNoun,
|
|
4853
|
-
label: input.label
|
|
5132
|
+
label: ncSubjectLabelText(texts, input.label, input.labelKind)
|
|
4854
5133
|
}
|
|
4855
5134
|
});
|
|
4856
5135
|
}
|
|
@@ -8420,31 +8699,48 @@ function replaceConditionReferences(monitor, condition, reference) {
|
|
|
8420
8699
|
/** The cap whose presence in a sequence makes the rule the alarm's trigger. */
|
|
8421
8700
|
var ALARM_PANEL_CAP$1 = "alarm-panel";
|
|
8422
8701
|
var ALARM_KINDS = new Set(NC_ALARM_SYSTEM_EVENT_KINDS);
|
|
8423
|
-
function
|
|
8424
|
-
|
|
8702
|
+
function stepActuatesAlarm(step) {
|
|
8703
|
+
return step.kind === "cap" && step.cap === ALARM_PANEL_CAP$1;
|
|
8704
|
+
}
|
|
8705
|
+
function bindingActuatesAlarm(binding, sequenceActionsOf) {
|
|
8706
|
+
if (binding.kind === "do") return stepActuatesAlarm(binding.action);
|
|
8707
|
+
const steps = sequenceActionsOf?.(binding.sequenceId);
|
|
8708
|
+
if (steps === void 0) return false;
|
|
8709
|
+
return steps.some(stepActuatesAlarm);
|
|
8710
|
+
}
|
|
8711
|
+
function actionsActuateAlarm(rule, sequenceActionsOf) {
|
|
8712
|
+
const actions = rule.actions;
|
|
8713
|
+
if (actions === void 0) return false;
|
|
8714
|
+
const hooks = [
|
|
8715
|
+
actions.onActivation,
|
|
8716
|
+
actions.onDeactivation,
|
|
8717
|
+
actions.onTrigger,
|
|
8718
|
+
actions.onTriggerOff
|
|
8719
|
+
];
|
|
8720
|
+
for (const bindings of hooks) for (const binding of bindings ?? []) if (bindingActuatesAlarm(binding, sequenceActionsOf)) return true;
|
|
8721
|
+
for (const button of actions.buttons ?? []) {
|
|
8722
|
+
const steps = sequenceActionsOf?.(button.sequenceId);
|
|
8723
|
+
if (steps !== void 0 && steps.some(stepActuatesAlarm)) return true;
|
|
8724
|
+
}
|
|
8425
8725
|
return false;
|
|
8426
8726
|
}
|
|
8427
8727
|
/**
|
|
8428
8728
|
* May a snooze window silence this rule?
|
|
8429
8729
|
*
|
|
8430
|
-
* `true` = never. Deliberately answered from the rule alone
|
|
8431
|
-
* on the delivery path, where a fallible
|
|
8432
|
-
* DESTROY the exemption (D49) — and the
|
|
8433
|
-
* silencing an alarm.
|
|
8730
|
+
* `true` = never. Deliberately answered from the rule alone (plus an optional
|
|
8731
|
+
* sequence lookup): the gate reads it on the delivery path, where a fallible
|
|
8732
|
+
* lookup would be a read that can DESTROY the exemption (D49) — and the
|
|
8733
|
+
* failure direction of this one is silencing an alarm. A missing sequence
|
|
8734
|
+
* lookup means sequence-ref bindings cannot prove exemption; Do bindings and
|
|
8735
|
+
* system-event kinds still can.
|
|
8434
8736
|
*/
|
|
8435
|
-
function isSnoozeExempt(rule) {
|
|
8737
|
+
function isSnoozeExempt(rule, sequenceActionsOf) {
|
|
8436
8738
|
const kinds = rule.conditions.systemEvent?.kinds ?? [];
|
|
8437
8739
|
for (const kind of kinds) if (ALARM_KINDS.has(kind)) return true;
|
|
8438
|
-
return
|
|
8740
|
+
return actionsActuateAlarm(rule, sequenceActionsOf);
|
|
8439
8741
|
}
|
|
8440
8742
|
//#endregion
|
|
8441
8743
|
//#region src/notification-center/action-buttons.ts
|
|
8442
|
-
/** Sequence names the rule actually declares. A button may only name one. */
|
|
8443
|
-
function declaredSequences(actions) {
|
|
8444
|
-
const names = /* @__PURE__ */ new Set();
|
|
8445
|
-
for (const sequence of actions.onTrigger ?? []) names.add(sequence.name);
|
|
8446
|
-
return names;
|
|
8447
|
-
}
|
|
8448
8744
|
/**
|
|
8449
8745
|
* Build the notification's buttons.
|
|
8450
8746
|
*
|
|
@@ -8457,18 +8753,18 @@ function buildActionButtons(input) {
|
|
|
8457
8753
|
if (actions === void 0) return [];
|
|
8458
8754
|
const buttons = actions.buttons ?? [];
|
|
8459
8755
|
if (buttons.length === 0) return [];
|
|
8460
|
-
const known =
|
|
8756
|
+
const known = input.knownSequenceIds;
|
|
8461
8757
|
const seen = /* @__PURE__ */ new Set();
|
|
8462
8758
|
const out = [];
|
|
8463
8759
|
for (const button of buttons) {
|
|
8464
|
-
if (!known.has(button.
|
|
8760
|
+
if (!known.has(button.sequenceId)) continue;
|
|
8465
8761
|
if (seen.has(button.id)) continue;
|
|
8466
8762
|
seen.add(button.id);
|
|
8467
8763
|
out.push({
|
|
8468
8764
|
id: button.id,
|
|
8469
8765
|
label: button.label,
|
|
8470
8766
|
url: input.mintUrl({
|
|
8471
|
-
|
|
8767
|
+
sequenceId: button.sequenceId,
|
|
8472
8768
|
actionId: button.id
|
|
8473
8769
|
}),
|
|
8474
8770
|
...button.icon !== void 0 ? { icon: button.icon } : {},
|
|
@@ -8478,18 +8774,17 @@ function buildActionButtons(input) {
|
|
|
8478
8774
|
return out;
|
|
8479
8775
|
}
|
|
8480
8776
|
/**
|
|
8481
|
-
* Which of a rule's buttons name a sequence
|
|
8777
|
+
* Which of a rule's buttons name a sequence that is not known.
|
|
8482
8778
|
*
|
|
8483
8779
|
* Exported so the caller can LOG the drop. A button silently missing from a
|
|
8484
8780
|
* notification is the exact shape of failure this repo keeps paying for — the
|
|
8485
8781
|
* operator authored it, it never appeared, and nothing said why.
|
|
8486
8782
|
*/
|
|
8487
|
-
function unresolvableButtons(actions) {
|
|
8783
|
+
function unresolvableButtons(actions, knownSequenceIds) {
|
|
8488
8784
|
if (actions === void 0) return [];
|
|
8489
8785
|
const buttons = actions.buttons ?? [];
|
|
8490
8786
|
if (buttons.length === 0) return [];
|
|
8491
|
-
|
|
8492
|
-
return buttons.filter((b) => !known.has(b.sequence)).map((b) => `${b.id}→${b.sequence}`);
|
|
8787
|
+
return buttons.filter((b) => !knownSequenceIds.has(b.sequenceId)).map((b) => `${b.id}→${b.sequenceId}`);
|
|
8493
8788
|
}
|
|
8494
8789
|
/** How many snooze choices one notification may carry. The schema caps the
|
|
8495
8790
|
* authored list at 4; this is the same bound applied to a stored rule that
|
|
@@ -8540,7 +8835,7 @@ function snoozeChoices(options) {
|
|
|
8540
8835
|
* attention than the notification did.
|
|
8541
8836
|
*/
|
|
8542
8837
|
function buildSnoozeButtons(input) {
|
|
8543
|
-
if (isSnoozeExempt(input.rule)) return [];
|
|
8838
|
+
if (isSnoozeExempt(input.rule, input.sequenceActionsOf)) return [];
|
|
8544
8839
|
return snoozeChoices(input.rule.snoozeOptions).map((minutes) => {
|
|
8545
8840
|
const actionId = snoozeButtonId(minutes);
|
|
8546
8841
|
return {
|
|
@@ -10829,10 +11124,11 @@ function matchesIdentityScope(subject, scope) {
|
|
|
10829
11124
|
* Derived, never a stored flag: both rule editors prune condition keys to the
|
|
10830
11125
|
* catalog on save, so a new key would be dropped by the first edit.
|
|
10831
11126
|
*/
|
|
10832
|
-
function ruleBelongsToAlarm(rule) {
|
|
11127
|
+
function ruleBelongsToAlarm(rule, sequenceActionsOf) {
|
|
10833
11128
|
return ruleBelongsToAlarm$1({
|
|
10834
11129
|
conditions: rule.conditions,
|
|
10835
|
-
...rule.actions
|
|
11130
|
+
...rule.actions !== void 0 ? { actions: rule.actions } : {},
|
|
11131
|
+
...sequenceActionsOf !== void 0 ? { sequenceActionsOf } : {}
|
|
10836
11132
|
});
|
|
10837
11133
|
}
|
|
10838
11134
|
/**
|
|
@@ -12720,7 +13016,7 @@ function deriveTankBand(reading) {
|
|
|
12720
13016
|
case null: return null;
|
|
12721
13017
|
}
|
|
12722
13018
|
}
|
|
12723
|
-
function isRecord$
|
|
13019
|
+
function isRecord$9(value) {
|
|
12724
13020
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12725
13021
|
}
|
|
12726
13022
|
function tankStatusOf(value) {
|
|
@@ -12736,11 +13032,11 @@ function vacuumTanksOf(data) {
|
|
|
12736
13032
|
if (data["capName"] !== "vacuum-control") return null;
|
|
12737
13033
|
const deviceId = data["deviceId"];
|
|
12738
13034
|
const slice = data["slice"];
|
|
12739
|
-
if (typeof deviceId !== "number" || !isRecord$
|
|
13035
|
+
if (typeof deviceId !== "number" || !isRecord$9(slice)) return null;
|
|
12740
13036
|
const tanks = [];
|
|
12741
13037
|
for (const key of NC_TANK_KEYS) {
|
|
12742
13038
|
const raw = slice[key];
|
|
12743
|
-
if (!isRecord$
|
|
13039
|
+
if (!isRecord$9(raw)) continue;
|
|
12744
13040
|
const level = raw["level"];
|
|
12745
13041
|
tanks.push({
|
|
12746
13042
|
key,
|
|
@@ -20063,6 +20359,350 @@ var NcDeviceMuteStore = class {
|
|
|
20063
20359
|
}
|
|
20064
20360
|
};
|
|
20065
20361
|
//#endregion
|
|
20362
|
+
//#region src/notification-center/migrate-legacy-actions.ts
|
|
20363
|
+
function isRecord$8(value) {
|
|
20364
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20365
|
+
}
|
|
20366
|
+
/** A legacy inline sequence row — detected by `name` + `actions` + `enabled`. */
|
|
20367
|
+
function asLegacySequence(value) {
|
|
20368
|
+
const parsed = NcLegacyRuleActionSequenceSchema.safeParse(value);
|
|
20369
|
+
return parsed.success ? parsed.data : null;
|
|
20370
|
+
}
|
|
20371
|
+
/**
|
|
20372
|
+
* True when `onTrigger` still looks like the pre-shared-sequence array of
|
|
20373
|
+
* named sequences (not an array of `{ kind: 'sequence' | 'do', … }` bindings).
|
|
20374
|
+
*/
|
|
20375
|
+
function onTriggerLooksLegacy(onTrigger) {
|
|
20376
|
+
if (!Array.isArray(onTrigger) || onTrigger.length === 0) return false;
|
|
20377
|
+
const first = onTrigger[0];
|
|
20378
|
+
if (!isRecord$8(first)) return false;
|
|
20379
|
+
if (first["kind"] === "sequence" || first["kind"] === "do") return false;
|
|
20380
|
+
return asLegacySequence(first) !== null;
|
|
20381
|
+
}
|
|
20382
|
+
function legacyButtonSequenceName(button) {
|
|
20383
|
+
if (!isRecord$8(button)) return null;
|
|
20384
|
+
const name = button["sequence"];
|
|
20385
|
+
return typeof name === "string" && name.length > 0 ? name : null;
|
|
20386
|
+
}
|
|
20387
|
+
/**
|
|
20388
|
+
* Rewrite a raw `actions` block. Returns the (possibly unchanged) new-shape
|
|
20389
|
+
* actions and whether anything was materialised into the sequence store.
|
|
20390
|
+
*/
|
|
20391
|
+
async function migrateLegacyRuleActions(actions, createSequence) {
|
|
20392
|
+
if (actions === void 0 || actions === null) return {
|
|
20393
|
+
actions: void 0,
|
|
20394
|
+
migrated: false
|
|
20395
|
+
};
|
|
20396
|
+
if (!isRecord$8(actions)) return {
|
|
20397
|
+
actions: void 0,
|
|
20398
|
+
migrated: false
|
|
20399
|
+
};
|
|
20400
|
+
if (!onTriggerLooksLegacy(actions["onTrigger"]) && actions["sequence"] === void 0) {
|
|
20401
|
+
const buttons = actions["buttons"];
|
|
20402
|
+
if (!(Array.isArray(buttons) && buttons.some((b) => legacyButtonSequenceName(b) !== null))) {
|
|
20403
|
+
const parsed = NcRuleActionsSchema.safeParse(actions);
|
|
20404
|
+
if (parsed.success) return {
|
|
20405
|
+
actions: parsed.data,
|
|
20406
|
+
migrated: false
|
|
20407
|
+
};
|
|
20408
|
+
return {
|
|
20409
|
+
actions: void 0,
|
|
20410
|
+
migrated: false
|
|
20411
|
+
};
|
|
20412
|
+
}
|
|
20413
|
+
}
|
|
20414
|
+
const legacySequences = [];
|
|
20415
|
+
const onTriggerRaw = actions["onTrigger"];
|
|
20416
|
+
if (Array.isArray(onTriggerRaw)) for (const entry of onTriggerRaw) {
|
|
20417
|
+
const seq = asLegacySequence(entry);
|
|
20418
|
+
if (seq !== null) legacySequences.push(seq);
|
|
20419
|
+
}
|
|
20420
|
+
const nameToId = /* @__PURE__ */ new Map();
|
|
20421
|
+
for (const sequence of legacySequences) {
|
|
20422
|
+
const key = sequence.name.trim();
|
|
20423
|
+
if (key === "" || nameToId.has(key)) continue;
|
|
20424
|
+
const created = await createSequence({
|
|
20425
|
+
name: sequence.name,
|
|
20426
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
20427
|
+
actions: [...sequence.actions]
|
|
20428
|
+
});
|
|
20429
|
+
nameToId.set(key, created.id);
|
|
20430
|
+
}
|
|
20431
|
+
const onTriggerBindings = [];
|
|
20432
|
+
for (const sequence of legacySequences) {
|
|
20433
|
+
if (!sequence.enabled) continue;
|
|
20434
|
+
const id = nameToId.get(sequence.name.trim());
|
|
20435
|
+
if (id === void 0) continue;
|
|
20436
|
+
onTriggerBindings.push({
|
|
20437
|
+
kind: "sequence",
|
|
20438
|
+
sequenceId: id
|
|
20439
|
+
});
|
|
20440
|
+
}
|
|
20441
|
+
const buttonsRaw = actions["buttons"];
|
|
20442
|
+
const buttons = [];
|
|
20443
|
+
if (Array.isArray(buttonsRaw)) for (const button of buttonsRaw) {
|
|
20444
|
+
if (!isRecord$8(button)) continue;
|
|
20445
|
+
const id = typeof button["id"] === "string" ? button["id"] : "";
|
|
20446
|
+
const label = typeof button["label"] === "string" ? button["label"] : "";
|
|
20447
|
+
if (id === "" || label === "") continue;
|
|
20448
|
+
const existingId = typeof button["sequenceId"] === "string" && button["sequenceId"].length > 0 ? button["sequenceId"] : void 0;
|
|
20449
|
+
const legacyName = legacyButtonSequenceName(button);
|
|
20450
|
+
const sequenceId = existingId ?? (legacyName !== null ? nameToId.get(legacyName.trim()) : void 0);
|
|
20451
|
+
if (sequenceId === void 0) continue;
|
|
20452
|
+
buttons.push({
|
|
20453
|
+
id,
|
|
20454
|
+
label,
|
|
20455
|
+
sequenceId,
|
|
20456
|
+
...typeof button["icon"] === "string" ? { icon: button["icon"] } : {},
|
|
20457
|
+
...typeof button["destructive"] === "boolean" ? { destructive: button["destructive"] } : {}
|
|
20458
|
+
});
|
|
20459
|
+
}
|
|
20460
|
+
const next = {
|
|
20461
|
+
...Array.isArray(actions["onActivation"]) ? { onActivation: actions["onActivation"] } : {},
|
|
20462
|
+
...Array.isArray(actions["onDeactivation"]) ? { onDeactivation: actions["onDeactivation"] } : {},
|
|
20463
|
+
...onTriggerBindings.length > 0 ? { onTrigger: onTriggerBindings } : {},
|
|
20464
|
+
...Array.isArray(actions["onTriggerOff"]) ? { onTriggerOff: actions["onTriggerOff"] } : {},
|
|
20465
|
+
...buttons.length > 0 ? { buttons } : {}
|
|
20466
|
+
};
|
|
20467
|
+
const parsed = NcRuleActionsSchema.safeParse(next);
|
|
20468
|
+
if (!parsed.success) return {
|
|
20469
|
+
actions: void 0,
|
|
20470
|
+
migrated: true
|
|
20471
|
+
};
|
|
20472
|
+
return {
|
|
20473
|
+
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,
|
|
20474
|
+
migrated: true
|
|
20475
|
+
};
|
|
20476
|
+
}
|
|
20477
|
+
//#endregion
|
|
20478
|
+
//#region src/notification-center/sequence-store.ts
|
|
20479
|
+
/**
|
|
20480
|
+
* Shared action sequences for the notification centre.
|
|
20481
|
+
*
|
|
20482
|
+
* A sequence is a named, ordered run of steps (`wait` | `cap`) with its own
|
|
20483
|
+
* throttle. Rules and notification buttons bind to it by id — never by copying
|
|
20484
|
+
* the steps inline — so two rules that open the same gate share one budget and
|
|
20485
|
+
* one authoring surface.
|
|
20486
|
+
*
|
|
20487
|
+
* Mirrors {@link NcDeviceMuteStore}: declare the collection, mirror into RAM,
|
|
20488
|
+
* write-through so a failed persist never leaves a phantom sequence the next
|
|
20489
|
+
* boot will not know.
|
|
20490
|
+
*/
|
|
20491
|
+
/**
|
|
20492
|
+
* @durable class=config owner=notification-center
|
|
20493
|
+
* write="an admin creates/edits/deletes a shared sequence via
|
|
20494
|
+
* `notificationRules.createSequence` / `updateSequence` / `deleteSequence`,
|
|
20495
|
+
* or the one-shot boot migration that materialises legacy inline sequences"
|
|
20496
|
+
* retention="none — a row goes only when an admin deletes it. Bounded by the
|
|
20497
|
+
* sequence set a human is willing to author."
|
|
20498
|
+
*/
|
|
20499
|
+
var NC_SEQUENCES_COLLECTION = "notification-center:sequences";
|
|
20500
|
+
var NC_SEQUENCES_COLUMNS = [
|
|
20501
|
+
{
|
|
20502
|
+
name: "id",
|
|
20503
|
+
type: "TEXT",
|
|
20504
|
+
primaryKey: true,
|
|
20505
|
+
notNull: true
|
|
20506
|
+
},
|
|
20507
|
+
{
|
|
20508
|
+
name: "name",
|
|
20509
|
+
type: "TEXT",
|
|
20510
|
+
notNull: true
|
|
20511
|
+
},
|
|
20512
|
+
{
|
|
20513
|
+
name: "minDelaySec",
|
|
20514
|
+
type: "INTEGER"
|
|
20515
|
+
},
|
|
20516
|
+
{
|
|
20517
|
+
name: "actions",
|
|
20518
|
+
type: "JSON",
|
|
20519
|
+
notNull: true
|
|
20520
|
+
},
|
|
20521
|
+
{
|
|
20522
|
+
name: "createdAt",
|
|
20523
|
+
type: "INTEGER",
|
|
20524
|
+
notNull: true
|
|
20525
|
+
},
|
|
20526
|
+
{
|
|
20527
|
+
name: "updatedAt",
|
|
20528
|
+
type: "INTEGER",
|
|
20529
|
+
notNull: true
|
|
20530
|
+
}
|
|
20531
|
+
];
|
|
20532
|
+
function isRecord$7(value) {
|
|
20533
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20534
|
+
}
|
|
20535
|
+
function parseActions(raw) {
|
|
20536
|
+
if (!Array.isArray(raw) || raw.length === 0) return null;
|
|
20537
|
+
const out = [];
|
|
20538
|
+
for (const entry of raw) {
|
|
20539
|
+
const parsed = NcRuleActionSchema.safeParse(entry);
|
|
20540
|
+
if (!parsed.success) return null;
|
|
20541
|
+
out.push(parsed.data);
|
|
20542
|
+
}
|
|
20543
|
+
return out;
|
|
20544
|
+
}
|
|
20545
|
+
function rowToSequence(data) {
|
|
20546
|
+
const id = data["id"];
|
|
20547
|
+
const name = data["name"];
|
|
20548
|
+
if (typeof id !== "string" || id.length === 0) return null;
|
|
20549
|
+
if (typeof name !== "string" || name.length === 0) return null;
|
|
20550
|
+
const actions = parseActions(data["actions"]);
|
|
20551
|
+
if (actions === null) return null;
|
|
20552
|
+
const minDelayRaw = data["minDelaySec"];
|
|
20553
|
+
const minDelaySec = typeof minDelayRaw === "number" && Number.isInteger(minDelayRaw) ? minDelayRaw : void 0;
|
|
20554
|
+
const candidate = {
|
|
20555
|
+
id,
|
|
20556
|
+
name,
|
|
20557
|
+
...minDelaySec !== void 0 ? { minDelaySec } : {},
|
|
20558
|
+
actions: [...actions]
|
|
20559
|
+
};
|
|
20560
|
+
const parsed = NcSharedSequenceSchema.safeParse(candidate);
|
|
20561
|
+
return parsed.success ? parsed.data : null;
|
|
20562
|
+
}
|
|
20563
|
+
var NcSequenceStore = class {
|
|
20564
|
+
byId = /* @__PURE__ */ new Map();
|
|
20565
|
+
store;
|
|
20566
|
+
logger;
|
|
20567
|
+
now;
|
|
20568
|
+
constructor(deps) {
|
|
20569
|
+
this.store = deps.store;
|
|
20570
|
+
this.logger = deps.logger;
|
|
20571
|
+
this.now = deps.now ?? (() => Date.now());
|
|
20572
|
+
}
|
|
20573
|
+
static async declare(store) {
|
|
20574
|
+
await store.declareCollection.mutate({
|
|
20575
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20576
|
+
columns: [...NC_SEQUENCES_COLUMNS]
|
|
20577
|
+
});
|
|
20578
|
+
}
|
|
20579
|
+
/**
|
|
20580
|
+
* (Re)hydrate. A failed load KEEPS what is already in memory — dropping every
|
|
20581
|
+
* sequence on a transient store error would leave every button and binding
|
|
20582
|
+
* pointing at nothing until the next successful load.
|
|
20583
|
+
*/
|
|
20584
|
+
async load() {
|
|
20585
|
+
try {
|
|
20586
|
+
const rows = await this.store.query.query({
|
|
20587
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20588
|
+
filter: { limit: 1e4 }
|
|
20589
|
+
});
|
|
20590
|
+
const next = /* @__PURE__ */ new Map();
|
|
20591
|
+
let skipped = 0;
|
|
20592
|
+
for (const row of rows) {
|
|
20593
|
+
if (!isRecord$7(row.data)) {
|
|
20594
|
+
skipped += 1;
|
|
20595
|
+
continue;
|
|
20596
|
+
}
|
|
20597
|
+
const sequence = rowToSequence(row.data);
|
|
20598
|
+
if (sequence === null) {
|
|
20599
|
+
skipped += 1;
|
|
20600
|
+
continue;
|
|
20601
|
+
}
|
|
20602
|
+
next.set(sequence.id, sequence);
|
|
20603
|
+
}
|
|
20604
|
+
this.byId.clear();
|
|
20605
|
+
for (const [id, sequence] of next) this.byId.set(id, sequence);
|
|
20606
|
+
if (skipped > 0) this.logger.warn("sequence rows skipped as malformed — bindings to them will miss", { meta: { skipped } });
|
|
20607
|
+
} catch (err) {
|
|
20608
|
+
this.logger.warn("sequence load failed — keeping the sequences already in memory", { meta: {
|
|
20609
|
+
error: String(err),
|
|
20610
|
+
sequences: this.byId.size
|
|
20611
|
+
} });
|
|
20612
|
+
}
|
|
20613
|
+
}
|
|
20614
|
+
list() {
|
|
20615
|
+
return [...this.byId.values()].toSorted((a, b) => a.name.localeCompare(b.name));
|
|
20616
|
+
}
|
|
20617
|
+
get(sequenceId) {
|
|
20618
|
+
return this.byId.get(sequenceId) ?? null;
|
|
20619
|
+
}
|
|
20620
|
+
/** Hot-path existence check for button minting. */
|
|
20621
|
+
has(sequenceId) {
|
|
20622
|
+
return this.byId.has(sequenceId);
|
|
20623
|
+
}
|
|
20624
|
+
knownIds() {
|
|
20625
|
+
return new Set(this.byId.keys());
|
|
20626
|
+
}
|
|
20627
|
+
async create(input) {
|
|
20628
|
+
const now = this.now();
|
|
20629
|
+
const sequence = {
|
|
20630
|
+
id: randomUUID(),
|
|
20631
|
+
name: input.name,
|
|
20632
|
+
...input.minDelaySec !== void 0 ? { minDelaySec: input.minDelaySec } : {},
|
|
20633
|
+
actions: [...input.actions]
|
|
20634
|
+
};
|
|
20635
|
+
await this.persist(sequence, now, now);
|
|
20636
|
+
this.byId.set(sequence.id, sequence);
|
|
20637
|
+
return sequence;
|
|
20638
|
+
}
|
|
20639
|
+
/**
|
|
20640
|
+
* Create with a CALLER-chosen id — used by the legacy migration so a rewrite
|
|
20641
|
+
* can name the sequence before rewriting the rule that points at it.
|
|
20642
|
+
*/
|
|
20643
|
+
async createWithId(sequence) {
|
|
20644
|
+
const now = this.now();
|
|
20645
|
+
const stored = {
|
|
20646
|
+
id: sequence.id,
|
|
20647
|
+
name: sequence.name,
|
|
20648
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
20649
|
+
actions: [...sequence.actions]
|
|
20650
|
+
};
|
|
20651
|
+
await this.persist(stored, now, now);
|
|
20652
|
+
this.byId.set(stored.id, stored);
|
|
20653
|
+
return stored;
|
|
20654
|
+
}
|
|
20655
|
+
async update(sequenceId, input) {
|
|
20656
|
+
if (this.byId.get(sequenceId) === void 0) throw new Error(`sequence ${sequenceId} does not exist`);
|
|
20657
|
+
const now = this.now();
|
|
20658
|
+
const sequence = {
|
|
20659
|
+
id: sequenceId,
|
|
20660
|
+
name: input.name,
|
|
20661
|
+
...input.minDelaySec !== void 0 ? { minDelaySec: input.minDelaySec } : {},
|
|
20662
|
+
actions: [...input.actions]
|
|
20663
|
+
};
|
|
20664
|
+
const createdAt = await this.readCreatedAt(sequenceId, now);
|
|
20665
|
+
await this.persist(sequence, createdAt, now);
|
|
20666
|
+
this.byId.set(sequenceId, sequence);
|
|
20667
|
+
return sequence;
|
|
20668
|
+
}
|
|
20669
|
+
async delete(sequenceId) {
|
|
20670
|
+
await this.store.delete.mutate({
|
|
20671
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20672
|
+
key: sequenceId
|
|
20673
|
+
});
|
|
20674
|
+
this.byId.delete(sequenceId);
|
|
20675
|
+
}
|
|
20676
|
+
async readCreatedAt(sequenceId, fallback) {
|
|
20677
|
+
try {
|
|
20678
|
+
const raw = (await this.store.query.query({
|
|
20679
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20680
|
+
filter: {
|
|
20681
|
+
where: { id: sequenceId },
|
|
20682
|
+
limit: 1
|
|
20683
|
+
}
|
|
20684
|
+
}))[0]?.data["createdAt"];
|
|
20685
|
+
return typeof raw === "number" && Number.isFinite(raw) ? raw : fallback;
|
|
20686
|
+
} catch {
|
|
20687
|
+
return fallback;
|
|
20688
|
+
}
|
|
20689
|
+
}
|
|
20690
|
+
async persist(sequence, createdAt, updatedAt) {
|
|
20691
|
+
await this.store.set.mutate({
|
|
20692
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20693
|
+
key: sequence.id,
|
|
20694
|
+
value: {
|
|
20695
|
+
id: sequence.id,
|
|
20696
|
+
name: sequence.name,
|
|
20697
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
20698
|
+
actions: sequence.actions,
|
|
20699
|
+
createdAt,
|
|
20700
|
+
updatedAt
|
|
20701
|
+
}
|
|
20702
|
+
});
|
|
20703
|
+
}
|
|
20704
|
+
};
|
|
20705
|
+
//#endregion
|
|
20066
20706
|
//#region src/notification-center/device-prune.ts
|
|
20067
20707
|
function judgeList(list, isDead) {
|
|
20068
20708
|
const dead = list.filter(isDead);
|
|
@@ -20176,58 +20816,49 @@ function pruneRuleConditions(conditions, isDead, notes) {
|
|
|
20176
20816
|
if (sceneDevice !== void 0 && isDead(sceneDevice)) notes.kept("conditions.scene", [String(sceneDevice)]);
|
|
20177
20817
|
return changed ? next : null;
|
|
20178
20818
|
}
|
|
20179
|
-
/** A
|
|
20180
|
-
|
|
20181
|
-
|
|
20182
|
-
|
|
20183
|
-
|
|
20184
|
-
|
|
20185
|
-
return false;
|
|
20186
|
-
});
|
|
20187
|
-
if (dead.length === 0) return null;
|
|
20188
|
-
const anyDeviceStep = kept.some((step) => step.kind === "cap");
|
|
20189
|
-
const [first, ...rest] = kept;
|
|
20190
|
-
if (!anyDeviceStep || first === void 0) return {
|
|
20191
|
-
next: "removed",
|
|
20192
|
-
dead
|
|
20193
|
-
};
|
|
20194
|
-
return {
|
|
20195
|
-
next: {
|
|
20196
|
-
...sequence,
|
|
20197
|
-
actions: [first, ...rest]
|
|
20198
|
-
},
|
|
20199
|
-
dead
|
|
20819
|
+
/** A Do binding whose device is dead is dropped; sequence refs are left alone
|
|
20820
|
+
* (shared sequences are pruned on their own store, not per-rule). */
|
|
20821
|
+
function pruneHookBindings(bindings, field, isDead, notes) {
|
|
20822
|
+
if (bindings === void 0) return {
|
|
20823
|
+
next: void 0,
|
|
20824
|
+
changed: false
|
|
20200
20825
|
};
|
|
20201
|
-
|
|
20202
|
-
/** The actions after a device prune — `null` when no step moved. */
|
|
20203
|
-
function pruneRuleActions(actions, isDead, notes) {
|
|
20204
|
-
const sequences = actions.onTrigger;
|
|
20205
|
-
if (sequences === void 0) return null;
|
|
20826
|
+
const kept = [];
|
|
20206
20827
|
let changed = false;
|
|
20207
|
-
const
|
|
20208
|
-
|
|
20209
|
-
|
|
20210
|
-
const pruned = pruneSequence(sequence, isDead);
|
|
20211
|
-
if (pruned === null) {
|
|
20212
|
-
nextSequences.push(sequence);
|
|
20828
|
+
for (const binding of bindings) {
|
|
20829
|
+
if (binding.kind === "sequence") {
|
|
20830
|
+
kept.push(binding);
|
|
20213
20831
|
continue;
|
|
20214
20832
|
}
|
|
20215
|
-
|
|
20216
|
-
|
|
20217
|
-
|
|
20218
|
-
|
|
20219
|
-
notes.removed(field, deviceSubjects(pruned.dead), "sequence-removed");
|
|
20220
|
-
} else {
|
|
20221
|
-
nextSequences.push(pruned.next);
|
|
20222
|
-
notes.removed(field, deviceSubjects(pruned.dead));
|
|
20833
|
+
if (binding.action.kind === "cap" && isDead(binding.action.deviceId)) {
|
|
20834
|
+
changed = true;
|
|
20835
|
+
notes.removed(field, deviceSubjects([binding.action.deviceId]), "binding-removed");
|
|
20836
|
+
continue;
|
|
20223
20837
|
}
|
|
20838
|
+
kept.push(binding);
|
|
20224
20839
|
}
|
|
20225
|
-
if (!changed) return
|
|
20226
|
-
|
|
20840
|
+
if (!changed) return {
|
|
20841
|
+
next: bindings,
|
|
20842
|
+
changed: false
|
|
20843
|
+
};
|
|
20227
20844
|
return {
|
|
20228
|
-
|
|
20229
|
-
|
|
20230
|
-
|
|
20845
|
+
next: kept.length > 0 ? kept : void 0,
|
|
20846
|
+
changed: true
|
|
20847
|
+
};
|
|
20848
|
+
}
|
|
20849
|
+
/** The actions after a device prune — `null` when no binding moved. */
|
|
20850
|
+
function pruneRuleActions(actions, isDead, notes) {
|
|
20851
|
+
const onActivation = pruneHookBindings(actions.onActivation, "actions.onActivation", isDead, notes);
|
|
20852
|
+
const onDeactivation = pruneHookBindings(actions.onDeactivation, "actions.onDeactivation", isDead, notes);
|
|
20853
|
+
const onTrigger = pruneHookBindings(actions.onTrigger, "actions.onTrigger", isDead, notes);
|
|
20854
|
+
const onTriggerOff = pruneHookBindings(actions.onTriggerOff, "actions.onTriggerOff", isDead, notes);
|
|
20855
|
+
if (!onActivation.changed && !onDeactivation.changed && !onTrigger.changed && !onTriggerOff.changed) return null;
|
|
20856
|
+
return {
|
|
20857
|
+
...onActivation.next !== void 0 ? { onActivation: [...onActivation.next] } : {},
|
|
20858
|
+
...onDeactivation.next !== void 0 ? { onDeactivation: [...onDeactivation.next] } : {},
|
|
20859
|
+
...onTrigger.next !== void 0 ? { onTrigger: [...onTrigger.next] } : {},
|
|
20860
|
+
...onTriggerOff.next !== void 0 ? { onTriggerOff: [...onTriggerOff.next] } : {},
|
|
20861
|
+
...actions.buttons !== void 0 ? { buttons: actions.buttons } : {}
|
|
20231
20862
|
};
|
|
20232
20863
|
}
|
|
20233
20864
|
function planRuleDevices(rule, isDead, guard) {
|
|
@@ -24462,6 +25093,10 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
|
|
|
24462
25093
|
return {
|
|
24463
25094
|
camera: deviceName,
|
|
24464
25095
|
class: detection?.className ?? (systemEvent !== void 0 ? "" : subject.className),
|
|
25096
|
+
classLabel: (() => {
|
|
25097
|
+
const wire = detection?.className ?? (systemEvent !== void 0 ? "" : subject.className);
|
|
25098
|
+
return wire.length === 0 ? "" : eventKindLabel(wire, texts.language);
|
|
25099
|
+
})(),
|
|
24465
25100
|
label: detection?.label ?? subject.label ?? "",
|
|
24466
25101
|
zones: zoneLabels.join(", "),
|
|
24467
25102
|
subject: detection?.className !== void 0 ? ncSubjectText(texts, {
|
|
@@ -28603,9 +29238,32 @@ function lastRunRow(data) {
|
|
|
28603
29238
|
}
|
|
28604
29239
|
//#endregion
|
|
28605
29240
|
//#region src/notification-center/rule-actions.ts
|
|
29241
|
+
function wrapDo(action) {
|
|
29242
|
+
const name = action.kind === "wait" ? `wait ${String(action.seconds)}s` : `${action.cap}.${action.method}`;
|
|
29243
|
+
return {
|
|
29244
|
+
id: `do:${name}`,
|
|
29245
|
+
name,
|
|
29246
|
+
enabled: true,
|
|
29247
|
+
actions: [action],
|
|
29248
|
+
throttle: false
|
|
29249
|
+
};
|
|
29250
|
+
}
|
|
29251
|
+
function resolveBinding(binding, lookup) {
|
|
29252
|
+
if (binding.kind === "do") return wrapDo(binding.action);
|
|
29253
|
+
const sequence = lookup(binding.sequenceId);
|
|
29254
|
+
if (sequence === null) return { missing: binding.sequenceId };
|
|
29255
|
+
return {
|
|
29256
|
+
id: sequence.id,
|
|
29257
|
+
name: sequence.name,
|
|
29258
|
+
enabled: true,
|
|
29259
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
29260
|
+
actions: sequence.actions,
|
|
29261
|
+
throttle: true
|
|
29262
|
+
};
|
|
29263
|
+
}
|
|
28606
29264
|
var NcRuleActionRunner = class {
|
|
28607
29265
|
deps;
|
|
28608
|
-
/** Last run per
|
|
29266
|
+
/** Last run per `sequenceId` — the throttle's memory (shared across rules). */
|
|
28609
29267
|
lastRunAt = /* @__PURE__ */ new Map();
|
|
28610
29268
|
now;
|
|
28611
29269
|
sleep;
|
|
@@ -28615,11 +29273,49 @@ var NcRuleActionRunner = class {
|
|
|
28615
29273
|
this.sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
28616
29274
|
}
|
|
28617
29275
|
/**
|
|
28618
|
-
*
|
|
29276
|
+
* Resolve bindings via the sequence store and run each, in order.
|
|
28619
29277
|
*
|
|
28620
29278
|
* Never throws: an actuation that fails must not cost the NOTIFICATION about
|
|
28621
|
-
* the very same event.
|
|
28622
|
-
|
|
29279
|
+
* the very same event.
|
|
29280
|
+
*/
|
|
29281
|
+
async runBindings(input) {
|
|
29282
|
+
const sequences = [];
|
|
29283
|
+
const missing = [];
|
|
29284
|
+
for (const binding of input.bindings) {
|
|
29285
|
+
const resolved = resolveBinding(binding, input.lookup);
|
|
29286
|
+
if ("missing" in resolved) {
|
|
29287
|
+
this.deps.logger.warn("rule action binding skipped — sequence missing", {
|
|
29288
|
+
tags: { deviceId: input.deviceId },
|
|
29289
|
+
meta: {
|
|
29290
|
+
ruleId: input.ruleId,
|
|
29291
|
+
sequenceId: resolved.missing
|
|
29292
|
+
}
|
|
29293
|
+
});
|
|
29294
|
+
missing.push({
|
|
29295
|
+
id: resolved.missing,
|
|
29296
|
+
name: resolved.missing,
|
|
29297
|
+
ran: false,
|
|
29298
|
+
stepsRun: 0,
|
|
29299
|
+
skippedReason: "missing"
|
|
29300
|
+
});
|
|
29301
|
+
continue;
|
|
29302
|
+
}
|
|
29303
|
+
sequences.push(resolved);
|
|
29304
|
+
}
|
|
29305
|
+
const ran = await this.run({
|
|
29306
|
+
ruleId: input.ruleId,
|
|
29307
|
+
ruleName: input.ruleName,
|
|
29308
|
+
deviceId: input.deviceId,
|
|
29309
|
+
sequences,
|
|
29310
|
+
...input.origin !== void 0 ? { origin: input.origin } : {}
|
|
29311
|
+
});
|
|
29312
|
+
return [...missing, ...ran];
|
|
29313
|
+
}
|
|
29314
|
+
/**
|
|
29315
|
+
* Run every enabled sequence, in order.
|
|
29316
|
+
*
|
|
29317
|
+
* Prefer {@link runBindings} at call sites that hold bindings. This remains
|
|
29318
|
+
* for button grants (force-enabled shared sequence) and tests.
|
|
28623
29319
|
*/
|
|
28624
29320
|
async run(input) {
|
|
28625
29321
|
const out = [];
|
|
@@ -28628,38 +29324,41 @@ var NcRuleActionRunner = class {
|
|
|
28628
29324
|
}
|
|
28629
29325
|
async runOne(input, sequence) {
|
|
28630
29326
|
if (!sequence.enabled) return {
|
|
29327
|
+
id: sequence.id,
|
|
28631
29328
|
name: sequence.name,
|
|
28632
29329
|
ran: false,
|
|
28633
29330
|
stepsRun: 0,
|
|
28634
29331
|
skippedReason: "disabled"
|
|
28635
29332
|
};
|
|
28636
29333
|
if (sequence.actions.length === 0) return {
|
|
29334
|
+
id: sequence.id,
|
|
28637
29335
|
name: sequence.name,
|
|
28638
29336
|
ran: false,
|
|
28639
29337
|
stepsRun: 0,
|
|
28640
29338
|
skippedReason: "empty"
|
|
28641
29339
|
};
|
|
28642
|
-
const key = `${input.ruleId}:${sequence.name}`;
|
|
28643
29340
|
const now = this.now();
|
|
28644
|
-
const minDelayMs = (sequence.minDelaySec ?? 0) * 1e3;
|
|
28645
|
-
const last = this.lastRunAt.get(
|
|
29341
|
+
const minDelayMs = sequence.throttle ? (sequence.minDelaySec ?? 0) * 1e3 : 0;
|
|
29342
|
+
const last = this.lastRunAt.get(sequence.id);
|
|
28646
29343
|
if (minDelayMs > 0 && last !== void 0 && now - last < minDelayMs) {
|
|
28647
29344
|
this.deps.logger.debug("rule action sequence throttled", {
|
|
28648
29345
|
tags: { deviceId: input.deviceId },
|
|
28649
29346
|
meta: {
|
|
28650
29347
|
ruleId: input.ruleId,
|
|
29348
|
+
sequenceId: sequence.id,
|
|
28651
29349
|
sequence: sequence.name,
|
|
28652
29350
|
sinceLastMs: now - last
|
|
28653
29351
|
}
|
|
28654
29352
|
});
|
|
28655
29353
|
return {
|
|
29354
|
+
id: sequence.id,
|
|
28656
29355
|
name: sequence.name,
|
|
28657
29356
|
ran: false,
|
|
28658
29357
|
stepsRun: 0,
|
|
28659
29358
|
skippedReason: "throttled"
|
|
28660
29359
|
};
|
|
28661
29360
|
}
|
|
28662
|
-
this.lastRunAt.set(
|
|
29361
|
+
if (sequence.throttle) this.lastRunAt.set(sequence.id, now);
|
|
28663
29362
|
let stepsRun = 0;
|
|
28664
29363
|
for (const [index, action] of sequence.actions.entries()) try {
|
|
28665
29364
|
await this.step(action, input.origin);
|
|
@@ -28670,6 +29369,7 @@ var NcRuleActionRunner = class {
|
|
|
28670
29369
|
meta: {
|
|
28671
29370
|
ruleId: input.ruleId,
|
|
28672
29371
|
rule: input.ruleName,
|
|
29372
|
+
sequenceId: sequence.id,
|
|
28673
29373
|
sequence: sequence.name,
|
|
28674
29374
|
step: index,
|
|
28675
29375
|
action: action.kind === "cap" ? `${action.cap}.${action.method}` : "wait",
|
|
@@ -28677,6 +29377,7 @@ var NcRuleActionRunner = class {
|
|
|
28677
29377
|
}
|
|
28678
29378
|
});
|
|
28679
29379
|
return {
|
|
29380
|
+
id: sequence.id,
|
|
28680
29381
|
name: sequence.name,
|
|
28681
29382
|
ran: true,
|
|
28682
29383
|
stepsRun,
|
|
@@ -28688,11 +29389,13 @@ var NcRuleActionRunner = class {
|
|
|
28688
29389
|
meta: {
|
|
28689
29390
|
ruleId: input.ruleId,
|
|
28690
29391
|
rule: input.ruleName,
|
|
29392
|
+
sequenceId: sequence.id,
|
|
28691
29393
|
sequence: sequence.name,
|
|
28692
29394
|
steps: stepsRun
|
|
28693
29395
|
}
|
|
28694
29396
|
});
|
|
28695
29397
|
return {
|
|
29398
|
+
id: sequence.id,
|
|
28696
29399
|
name: sequence.name,
|
|
28697
29400
|
ran: true,
|
|
28698
29401
|
stepsRun
|
|
@@ -28713,6 +29416,17 @@ var NcRuleActionRunner = class {
|
|
|
28713
29416
|
});
|
|
28714
29417
|
}
|
|
28715
29418
|
};
|
|
29419
|
+
/** Force-enable a shared sequence for a button tap (always runs, still throttled). */
|
|
29420
|
+
function asForcedSequence(sequence) {
|
|
29421
|
+
return {
|
|
29422
|
+
id: sequence.id,
|
|
29423
|
+
name: sequence.name,
|
|
29424
|
+
enabled: true,
|
|
29425
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
29426
|
+
actions: sequence.actions,
|
|
29427
|
+
throttle: true
|
|
29428
|
+
};
|
|
29429
|
+
}
|
|
28716
29430
|
//#endregion
|
|
28717
29431
|
//#region src/notification-center/rule-ai-analyst.ts
|
|
28718
29432
|
/**
|
|
@@ -30559,7 +31273,7 @@ function snoozeVerdict(input, deps) {
|
|
|
30559
31273
|
});
|
|
30560
31274
|
if (active === null) return { kind: "clear" };
|
|
30561
31275
|
const rule = deps.ruleFor(input.ruleId);
|
|
30562
|
-
if (rule !== null && isSnoozeExempt(rule)) return {
|
|
31276
|
+
if (rule !== null && isSnoozeExempt(rule, deps.sequenceActionsOf)) return {
|
|
30563
31277
|
kind: "exempt",
|
|
30564
31278
|
snooze: active
|
|
30565
31279
|
};
|
|
@@ -36671,6 +37385,8 @@ var NotificationCenter = class {
|
|
|
36671
37385
|
snoozes;
|
|
36672
37386
|
/** The permanent per-camera notification off switch — see `device-mute-store.ts`. */
|
|
36673
37387
|
deviceMutes;
|
|
37388
|
+
/** Shared action sequences rules and buttons bind to by id. */
|
|
37389
|
+
sequences;
|
|
36674
37390
|
/** The durable episode rows behind the sustained-state alerts. */
|
|
36675
37391
|
alertStore;
|
|
36676
37392
|
/** Battery band → notification, and the reminder sweep. */
|
|
@@ -36909,7 +37625,8 @@ var NotificationCenter = class {
|
|
|
36909
37625
|
const drafts = [];
|
|
36910
37626
|
const sequenceButtons = this.actionRunner === null || stored === null ? [] : buildActionButtons({
|
|
36911
37627
|
actions: stored.actions,
|
|
36912
|
-
|
|
37628
|
+
knownSequenceIds: this.sequences.knownIds(),
|
|
37629
|
+
mintUrl: ({ sequenceId, actionId }) => {
|
|
36913
37630
|
const id = randomUUID();
|
|
36914
37631
|
drafts.push({
|
|
36915
37632
|
id,
|
|
@@ -36919,7 +37636,7 @@ var NotificationCenter = class {
|
|
|
36919
37636
|
id,
|
|
36920
37637
|
ruleId: input.ruleId,
|
|
36921
37638
|
ruleName: input.ruleName,
|
|
36922
|
-
|
|
37639
|
+
sequenceId,
|
|
36923
37640
|
deviceId: input.deviceId,
|
|
36924
37641
|
actionId,
|
|
36925
37642
|
targetId: input.targetId,
|
|
@@ -36931,6 +37648,7 @@ var NotificationCenter = class {
|
|
|
36931
37648
|
});
|
|
36932
37649
|
const snoozeButtons = input.ownerUserId === void 0 || stored === null ? [] : buildSnoozeButtons({
|
|
36933
37650
|
rule: stored,
|
|
37651
|
+
sequenceActionsOf: this.sequenceActionsOf,
|
|
36934
37652
|
label: (minutes) => this.snoozeButtonLabel(minutes),
|
|
36935
37653
|
mintUrl: ({ actionId, minutes }) => {
|
|
36936
37654
|
const id = randomUUID();
|
|
@@ -37171,7 +37889,7 @@ var NotificationCenter = class {
|
|
|
37171
37889
|
*/
|
|
37172
37890
|
async applyGrantedSnooze(grant) {
|
|
37173
37891
|
const rule = this.rules.get(grant.ruleId);
|
|
37174
|
-
if (rule !== null && isSnoozeExempt(rule)) throw new Error(`"${grant.ruleName}" is an alarm rule and cannot be snoozed`);
|
|
37892
|
+
if (rule !== null && isSnoozeExempt(rule, this.sequenceActionsOf)) throw new Error(`"${grant.ruleName}" is an alarm rule and cannot be snoozed`);
|
|
37175
37893
|
const snooze = await this.snoozes.create({
|
|
37176
37894
|
userId: grant.userId,
|
|
37177
37895
|
scope: "device",
|
|
@@ -37470,16 +38188,13 @@ var NotificationCenter = class {
|
|
|
37470
38188
|
if (this.actionRunner === null) throw new Error("no actuator is wired on this node");
|
|
37471
38189
|
const rule = this.rules.get(grant.ruleId);
|
|
37472
38190
|
if (rule === null) throw new Error(`rule ${grant.ruleId} no longer exists`);
|
|
37473
|
-
const sequence =
|
|
37474
|
-
if (sequence ===
|
|
38191
|
+
const sequence = this.sequences.get(grant.sequenceId);
|
|
38192
|
+
if (sequence === null) throw new Error(`sequence "${grant.sequenceId}" no longer exists`);
|
|
37475
38193
|
const [outcome] = await this.actionRunner.run({
|
|
37476
38194
|
ruleId: rule.id,
|
|
37477
38195
|
ruleName: rule.name,
|
|
37478
38196
|
deviceId: grant.deviceId,
|
|
37479
|
-
sequences: [
|
|
37480
|
-
...sequence,
|
|
37481
|
-
enabled: true
|
|
37482
|
-
}]
|
|
38197
|
+
sequences: [asForcedSequence(sequence)]
|
|
37483
38198
|
});
|
|
37484
38199
|
if (outcome === void 0) throw new Error("the sequence did not run");
|
|
37485
38200
|
if (outcome.failedAt !== void 0) throw new Error(`step ${String(outcome.failedAt)} of "${sequence.name}" failed`);
|
|
@@ -37598,6 +38313,11 @@ var NotificationCenter = class {
|
|
|
37598
38313
|
logger: this.logger.child("device-mute"),
|
|
37599
38314
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
37600
38315
|
});
|
|
38316
|
+
this.sequences = new NcSequenceStore({
|
|
38317
|
+
store: deps.store,
|
|
38318
|
+
logger: this.logger.child("sequences"),
|
|
38319
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
38320
|
+
});
|
|
37601
38321
|
this.alertStore = new NcAlertStore({
|
|
37602
38322
|
store: deps.store,
|
|
37603
38323
|
logger: this.logger.child("device-alerts")
|
|
@@ -37864,6 +38584,7 @@ var NotificationCenter = class {
|
|
|
37864
38584
|
await NcStateSummaryService.declare(store);
|
|
37865
38585
|
await NcSnoozeStore.declare(store);
|
|
37866
38586
|
await NcDeviceMuteStore.declare(store);
|
|
38587
|
+
await NcSequenceStore.declare(store);
|
|
37867
38588
|
await TimelapseStore.declare(store);
|
|
37868
38589
|
await NcSummaryStore.declare(store);
|
|
37869
38590
|
await NcCalendarEventsStore.declare(store);
|
|
@@ -37925,6 +38646,8 @@ var NotificationCenter = class {
|
|
|
37925
38646
|
sinceStartMs: now - t0
|
|
37926
38647
|
} });
|
|
37927
38648
|
};
|
|
38649
|
+
await timed("sequences", () => this.sequences.load());
|
|
38650
|
+
await timed("migrateLegacyActions", () => this.migrateLegacyRuleActions());
|
|
37928
38651
|
await Promise.all([
|
|
37929
38652
|
timed("texts", () => this.reloadTexts()),
|
|
37930
38653
|
timed("rules", () => this.rules.load()),
|
|
@@ -39224,7 +39947,32 @@ var NotificationCenter = class {
|
|
|
39224
39947
|
return { success: true };
|
|
39225
39948
|
},
|
|
39226
39949
|
setRuleEnabled: async ({ ruleId, enabled }) => {
|
|
39227
|
-
await this.rules.setEnabled(ruleId, enabled);
|
|
39950
|
+
const updated = await this.rules.setEnabled(ruleId, enabled);
|
|
39951
|
+
const bindings = enabled ? updated.actions?.onActivation : updated.actions?.onDeactivation;
|
|
39952
|
+
if (bindings !== void 0 && bindings.length > 0 && this.actionRunner !== null) this.actionRunner.runBindings({
|
|
39953
|
+
ruleId: updated.id,
|
|
39954
|
+
ruleName: updated.name,
|
|
39955
|
+
deviceId: this.deviceIdForRuleActions(updated),
|
|
39956
|
+
bindings,
|
|
39957
|
+
lookup: (id) => this.sequences.get(id)
|
|
39958
|
+
});
|
|
39959
|
+
return { success: true };
|
|
39960
|
+
},
|
|
39961
|
+
listSequences: async () => ({ sequences: [...this.sequences.list()] }),
|
|
39962
|
+
getSequence: async ({ sequenceId }) => ({ sequence: this.sequences.get(sequenceId) }),
|
|
39963
|
+
createSequence: async ({ sequence }) => {
|
|
39964
|
+
const created = await this.sequences.create(sequence);
|
|
39965
|
+
this.logger.info("notification sequence created", { meta: { sequenceId: created.id } });
|
|
39966
|
+
return { sequence: created };
|
|
39967
|
+
},
|
|
39968
|
+
updateSequence: async ({ sequenceId, sequence }) => {
|
|
39969
|
+
const updated = await this.sequences.update(sequenceId, sequence);
|
|
39970
|
+
this.logger.info("notification sequence updated", { meta: { sequenceId } });
|
|
39971
|
+
return { sequence: updated };
|
|
39972
|
+
},
|
|
39973
|
+
deleteSequence: async ({ sequenceId }) => {
|
|
39974
|
+
await this.sequences.delete(sequenceId);
|
|
39975
|
+
this.logger.info("notification sequence deleted", { meta: { sequenceId } });
|
|
39228
39976
|
return { success: true };
|
|
39229
39977
|
},
|
|
39230
39978
|
testRule: async ({ rule, lookbackMinutes }) => ({ results: [...await this.dryRun(rule, lookbackMinutes)] }),
|
|
@@ -39281,7 +40029,18 @@ var NotificationCenter = class {
|
|
|
39281
40029
|
},
|
|
39282
40030
|
getAlarmConfig: async () => this.alarmConfig(),
|
|
39283
40031
|
getAlarmSensors: async (input) => this.alarmSensors(input),
|
|
39284
|
-
setAlarmConfig: async ({ patch }) => this.setAlarmConfig(patch)
|
|
40032
|
+
setAlarmConfig: async ({ patch }) => this.setAlarmConfig(patch),
|
|
40033
|
+
createAlarmProfile: async ({ name, cloneFromId }) => this.mutateAlarmProfile((panel) => panel.createProfile({
|
|
40034
|
+
name,
|
|
40035
|
+
newId: randomUUID(),
|
|
40036
|
+
source: cloneFromId === void 0 ? { kind: "defaults" } : {
|
|
40037
|
+
kind: "clone",
|
|
40038
|
+
fromId: cloneFromId
|
|
40039
|
+
}
|
|
40040
|
+
})),
|
|
40041
|
+
renameAlarmProfile: async ({ id, name }) => this.mutateAlarmProfile((panel) => panel.renameProfile(id, name)),
|
|
40042
|
+
deleteAlarmProfile: async ({ id }) => this.mutateAlarmProfile((panel) => panel.deleteProfile(id)),
|
|
40043
|
+
activateAlarmProfile: async ({ id }) => this.mutateAlarmProfile((panel) => panel.activateProfile(id))
|
|
39285
40044
|
};
|
|
39286
40045
|
}
|
|
39287
40046
|
/**
|
|
@@ -39574,6 +40333,13 @@ var NotificationCenter = class {
|
|
|
39574
40333
|
});
|
|
39575
40334
|
return this.alarmConfig();
|
|
39576
40335
|
}
|
|
40336
|
+
/** Profile CRUD — each mutation returns the whole config (settings applied). */
|
|
40337
|
+
async mutateAlarmProfile(run) {
|
|
40338
|
+
const panel = this.alarmPanel;
|
|
40339
|
+
if (panel === null) throw new Error("this node has no alarm panel");
|
|
40340
|
+
await run(panel);
|
|
40341
|
+
return this.alarmConfig();
|
|
40342
|
+
}
|
|
39577
40343
|
/**
|
|
39578
40344
|
* The panel as it is NOW — see `alarm-live.ts`.
|
|
39579
40345
|
*
|
|
@@ -39670,6 +40436,7 @@ var NotificationCenter = class {
|
|
|
39670
40436
|
return {
|
|
39671
40437
|
deviceId: panel.deviceId,
|
|
39672
40438
|
settings: panel.settings(),
|
|
40439
|
+
profiles: panel.profilesView(),
|
|
39673
40440
|
live: await this.alarmLive(panel),
|
|
39674
40441
|
coverage: this.coverageFor(panel).map((c) => ({
|
|
39675
40442
|
...c,
|
|
@@ -40702,7 +41469,7 @@ var NotificationCenter = class {
|
|
|
40702
41469
|
} : {}
|
|
40703
41470
|
}
|
|
40704
41471
|
});
|
|
40705
|
-
const unresolvable = unresolvableButtons(rule.actions);
|
|
41472
|
+
const unresolvable = unresolvableButtons(rule.actions, this.sequences.knownIds());
|
|
40706
41473
|
if (unresolvable.length > 0) this.logger.warn("rule buttons reference sequences that do not exist — not sent", {
|
|
40707
41474
|
tags: { deviceId: subject.deviceId },
|
|
40708
41475
|
meta: {
|
|
@@ -40829,20 +41596,85 @@ var NotificationCenter = class {
|
|
|
40829
41596
|
return this.groupStore.list(query);
|
|
40830
41597
|
}
|
|
40831
41598
|
/**
|
|
40832
|
-
*
|
|
41599
|
+
* Materialise legacy inline `onTrigger` sequences into the shared store and
|
|
41600
|
+
* rewrite each rule's actions to bindings + `buttons.sequenceId`. Runs on RAW
|
|
41601
|
+
* rows before {@link NcRuleStore.load} so Zod never skips them.
|
|
41602
|
+
*/
|
|
41603
|
+
async migrateLegacyRuleActions() {
|
|
41604
|
+
let rows;
|
|
41605
|
+
try {
|
|
41606
|
+
rows = await this.deps.store.query.query({
|
|
41607
|
+
collection: NC_RULES_COLLECTION,
|
|
41608
|
+
filter: { limit: 1e4 }
|
|
41609
|
+
});
|
|
41610
|
+
} catch (err) {
|
|
41611
|
+
this.logger.warn("legacy actions migration skipped — could not read rules", { meta: { error: String(err) } });
|
|
41612
|
+
return;
|
|
41613
|
+
}
|
|
41614
|
+
let migrated = 0;
|
|
41615
|
+
for (const row of rows) {
|
|
41616
|
+
const ruleBlob = row.data["rule"];
|
|
41617
|
+
if (typeof ruleBlob !== "object" || ruleBlob === null || Array.isArray(ruleBlob)) continue;
|
|
41618
|
+
const rule = { ...ruleBlob };
|
|
41619
|
+
const ruleId = typeof rule["id"] === "string" ? rule["id"] : void 0;
|
|
41620
|
+
if (ruleId === void 0) continue;
|
|
41621
|
+
const result = await migrateLegacyRuleActions(rule["actions"], async (input) => {
|
|
41622
|
+
return { id: (await this.sequences.create(input)).id };
|
|
41623
|
+
});
|
|
41624
|
+
if (!result.migrated) continue;
|
|
41625
|
+
if (result.actions !== void 0) rule["actions"] = result.actions;
|
|
41626
|
+
else delete rule["actions"];
|
|
41627
|
+
const { id: _rowId, ...columns } = row.data;
|
|
41628
|
+
await this.deps.store.set.mutate({
|
|
41629
|
+
collection: NC_RULES_COLLECTION,
|
|
41630
|
+
key: ruleId,
|
|
41631
|
+
value: {
|
|
41632
|
+
...columns,
|
|
41633
|
+
rule
|
|
41634
|
+
}
|
|
41635
|
+
});
|
|
41636
|
+
migrated += 1;
|
|
41637
|
+
}
|
|
41638
|
+
if (migrated > 0) this.logger.info("migrated legacy inline rule actions into shared sequences", { meta: {
|
|
41639
|
+
rules: migrated,
|
|
41640
|
+
sequences: this.sequences.list().length
|
|
41641
|
+
} });
|
|
41642
|
+
}
|
|
41643
|
+
/**
|
|
41644
|
+
* True when this match is a clear / leave edge — the subject for
|
|
41645
|
+
* `onTriggerOff`. Raise / enter / ordinary matches use `onTrigger`.
|
|
41646
|
+
*/
|
|
41647
|
+
isTriggerOffSubject(subject) {
|
|
41648
|
+
if (subject.sensorRaised === false) return true;
|
|
41649
|
+
if (subject.occupancy !== void 0 && subject.occupancy.occupied === false) return true;
|
|
41650
|
+
return false;
|
|
41651
|
+
}
|
|
41652
|
+
/** Device id for activation/deactivation log tags — first scoped device, else 0. */
|
|
41653
|
+
deviceIdForRuleActions(rule) {
|
|
41654
|
+
const devices = rule.conditions.devices;
|
|
41655
|
+
if (devices !== void 0 && devices.length > 0) {
|
|
41656
|
+
const first = devices[0];
|
|
41657
|
+
if (first !== void 0) return first;
|
|
41658
|
+
}
|
|
41659
|
+
return 0;
|
|
41660
|
+
}
|
|
41661
|
+
sequenceActionsOf = (sequenceId) => this.sequences.get(sequenceId)?.actions;
|
|
41662
|
+
/**
|
|
41663
|
+
* Run this rule's onTrigger / onTriggerOff bindings for this match.
|
|
40833
41664
|
*
|
|
40834
41665
|
* Called BEFORE the cooldown gate — see the call site for why. Everything
|
|
40835
41666
|
* here is fire-and-forget and nothing may throw into the evaluation.
|
|
40836
41667
|
*/
|
|
40837
41668
|
runRuleActions(rule, subject, kind, controls) {
|
|
40838
|
-
const sequences = rule.actions?.onTrigger;
|
|
40839
|
-
if (sequences === void 0 || sequences.length === 0) return;
|
|
40840
41669
|
if (this.actionRunner === null || controls.runActions === false) return;
|
|
40841
|
-
this.
|
|
41670
|
+
const bindings = this.isTriggerOffSubject(subject) ? rule.actions?.onTriggerOff : rule.actions?.onTrigger;
|
|
41671
|
+
if (bindings === void 0 || bindings.length === 0) return;
|
|
41672
|
+
this.actionRunner.runBindings({
|
|
40842
41673
|
ruleId: rule.id,
|
|
40843
41674
|
ruleName: rule.name,
|
|
40844
41675
|
deviceId: subject.deviceId,
|
|
40845
|
-
|
|
41676
|
+
bindings,
|
|
41677
|
+
lookup: (id) => this.sequences.get(id),
|
|
40846
41678
|
origin: fireRefFor(rule, subject, kind)
|
|
40847
41679
|
});
|
|
40848
41680
|
}
|
|
@@ -41746,7 +42578,8 @@ var NotificationCenter = class {
|
|
|
41746
42578
|
snoozeDecision(input) {
|
|
41747
42579
|
const verdict = snoozeVerdict(input, {
|
|
41748
42580
|
activeFor: (query) => this.snoozes.activeFor(query),
|
|
41749
|
-
ruleFor: (ruleId) => this.rules.get(ruleId)
|
|
42581
|
+
ruleFor: (ruleId) => this.rules.get(ruleId),
|
|
42582
|
+
sequenceActionsOf: this.sequenceActionsOf
|
|
41750
42583
|
});
|
|
41751
42584
|
if (verdict.kind === "exempt") this.logger.info("snooze does not apply — alarm rules are never silenced", {
|
|
41752
42585
|
tags: { deviceId: input.deviceId },
|