rhombus-node-mcp 0.1.56 → 0.1.57
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/api/access-control-tool-api.js +231 -32
- package/dist/api/alarm-monitoring-tool-api.js +50 -1
- package/dist/api/automated-prompts-tool-api.js +9 -17
- package/dist/api/camera-policy-tool-api.js +122 -0
- package/dist/api/camera-tool-api.js +3 -5
- package/dist/api/camera-uptime-tool-api.js +62 -25
- package/dist/api/clips-tool-api.js +45 -9
- package/dist/api/create-tool-api.js +55 -7
- package/dist/api/door-tool-api.js +74 -13
- package/dist/api/entity-lookup-tool-api.js +8 -1
- package/dist/api/events-tool-api.js +79 -6
- package/dist/api/faces-tool-api.js +112 -7
- package/dist/api/get-accessible-apps.js +68 -6
- package/dist/api/get-entity-tool-api.js +12 -12
- package/dist/api/get-org-information-tool-api.js +2 -2
- package/dist/api/guest-management-tool-api.js +75 -1
- package/dist/api/location-tool-api.js +120 -28
- package/dist/api/lpr-tool-api.js +71 -16
- package/dist/api/policy-alerts-tool-api.js +7 -3
- package/dist/api/report-tool-api.js +4 -1
- package/dist/api/rules-tool-api.js +6 -16
- package/dist/api/schedule-tool-api.js +107 -0
- package/dist/api/search-tool-api.js +5 -13
- package/dist/api/update-tool-api.js +96 -1
- package/dist/api/user-tool-api.js +58 -13
- package/dist/createServer.js +18 -6
- package/dist/filtering-utils.js +421 -29
- package/dist/index.js +1 -1
- package/dist/network/network.js +122 -10
- package/dist/network/org-reference-cache.js +74 -0
- package/dist/telemetry/tracingProxy.js +4 -0
- package/dist/tools/entity-lookup-tool.js +6 -4
- package/dist/tools/get-entity-tool.js +40 -20
- package/dist/tools/get-org-information-tool.js +30 -2
- package/dist/tools/time-conversion-tool.js +59 -36
- package/dist/tools/time-tool.js +12 -3
- package/dist/tools/user-tool.js +100 -2
- package/dist/tools-console/access-anomaly-tool.js +2 -19
- package/dist/tools-console/access-control-tool.js +328 -2
- package/dist/tools-console/alarm-monitoring-tool.js +72 -2
- package/dist/tools-console/analytics-tool.js +2 -2
- package/dist/tools-console/badge-timeline-tool.js +2 -21
- package/dist/tools-console/camera-policy-tool.js +252 -0
- package/dist/tools-console/camera-tool.js +111 -89
- package/dist/tools-console/clips-tool.js +21 -31
- package/dist/tools-console/count-tool.js +8 -0
- package/dist/tools-console/create-camera-policy-tool.js +302 -122
- package/dist/tools-console/door-schedule-exception-tool.js +2 -33
- package/dist/tools-console/door-tool.js +130 -2
- package/dist/tools-console/elements-access-anomaly-tool.js +2 -19
- package/dist/tools-console/elements-badge-timeline-tool.js +2 -21
- package/dist/tools-console/elements-lost-badge-tool.js +2 -17
- package/dist/tools-console/elements-tool.js +2 -29
- package/dist/tools-console/events-tool.js +22 -101
- package/dist/tools-console/faces-tool.js +254 -28
- package/dist/tools-console/guest-management-tool.js +127 -2
- package/dist/tools-console/location-tool.js +156 -35
- package/dist/tools-console/lost-badge-tool.js +2 -17
- package/dist/tools-console/lpr-tool.js +89 -23
- package/dist/tools-console/netbox-access-anomaly-tool.js +2 -19
- package/dist/tools-console/netbox-badge-timeline-tool.js +2 -21
- package/dist/tools-console/netbox-lost-badge-tool.js +2 -17
- package/dist/tools-console/netbox-tool.js +2 -29
- package/dist/tools-console/onguard-tool.js +2 -30
- package/dist/tools-console/person-tracking-tool.js +2 -2
- package/dist/tools-console/policy-alerts-tool.js +5 -29
- package/dist/tools-console/reboot-cameras-tool.js +17 -2
- package/dist/tools-console/report-tool.js +10 -40
- package/dist/tools-console/schedule-tool.js +190 -0
- package/dist/tools-console/update-tool.js +298 -168
- package/dist/tools-console/video-walls-tool.js +93 -21
- package/dist/types/access-anomaly-tool-types.js +22 -2
- package/dist/types/access-control-tool-types.js +116 -1
- package/dist/types/alarm-monitoring-tool-types.js +42 -1
- package/dist/types/badge-timeline-tool-types.js +18 -3
- package/dist/types/camera-policy-tool-types.js +104 -0
- package/dist/types/camera-tool-types.js +80 -1
- package/dist/types/clips-tool-types.js +27 -3
- package/dist/types/create-camera-policy-tool-types.js +275 -26
- package/dist/types/door-schedule-exception-tool-types.js +20 -6
- package/dist/types/door-tool-types.js +36 -4
- package/dist/types/entity-lookup-tool-types.js +6 -1
- package/dist/types/events-tools-types.js +24 -16
- package/dist/types/faces-tools-types.js +106 -3
- package/dist/types/get-entity-tool-types.js +76 -12
- package/dist/types/guest-management-tool-types.js +103 -1
- package/dist/types/location-tool-types.js +100 -14
- package/dist/types/lost-badge-tool-types.js +17 -2
- package/dist/types/lpr-tool-types.js +47 -8
- package/dist/types/onguard-tool-types.js +21 -1
- package/dist/types/policy-alerts-tool-types.js +1 -1
- package/dist/types/report-tool-types.js +29 -1
- package/dist/types/schedule-tool-types.js +370 -0
- package/dist/types/schema.js +479 -9
- package/dist/types/time-conversion-tool-types.js +3 -4
- package/dist/types/update-tool-types.js +40 -18
- package/dist/types/user-tool-types.js +44 -1
- package/dist/types/video-walls-tool-types.js +30 -2
- package/dist/types/zod-schemas.js +5128 -996
- package/dist/util.js +62 -3
- package/dist/utils/entity-name-match.js +11 -0
- package/package.json +3 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { logger } from "../logger.js";
|
|
1
2
|
import { postApi } from "../network/network.js";
|
|
3
|
+
import { cachedPostApi } from "../network/org-reference-cache.js";
|
|
2
4
|
function computeUptimeStats(cameraUuid, cameraName, locationUuid, windows, startTimeSec, endTimeSec) {
|
|
3
5
|
const totalPeriodSeconds = endTimeSec - startTimeSec;
|
|
4
6
|
let totalUptimeSeconds = 0;
|
|
@@ -47,8 +49,11 @@ export async function getCameraUptime(cameraUuid, startTimeSec, endTimeSec, requ
|
|
|
47
49
|
route: "/camera/getUptimeWindows",
|
|
48
50
|
body: {
|
|
49
51
|
cameraUuid,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
// The endpoint takes MILLISECONDS despite its (formerly wrong) swagger
|
|
53
|
+
// docs saying seconds — passing seconds makes the server clamp the
|
|
54
|
+
// range away entirely and return zero windows for every camera.
|
|
55
|
+
startTime: startTimeSec * 1000,
|
|
56
|
+
endTime: endTimeSec * 1000,
|
|
52
57
|
},
|
|
53
58
|
modifiers: requestModifiers,
|
|
54
59
|
sessionId,
|
|
@@ -58,8 +63,31 @@ export async function getCameraUptime(cameraUuid, startTimeSec, endTimeSec, requ
|
|
|
58
63
|
}
|
|
59
64
|
return computeUptimeStats(cameraUuid, undefined, undefined, (res.uptimeWindows ?? []), startTimeSec, endTimeSec);
|
|
60
65
|
}
|
|
66
|
+
async function getFleetUptimeWindowsForOrg(startTimeSec, endTimeSec, requestModifiers, sessionId) {
|
|
67
|
+
const res = await postApi({
|
|
68
|
+
route: "/camera/getUptimeWindowsForOrg",
|
|
69
|
+
body: {
|
|
70
|
+
startTimeMs: startTimeSec * 1000,
|
|
71
|
+
endTimeMs: endTimeSec * 1000,
|
|
72
|
+
},
|
|
73
|
+
modifiers: requestModifiers,
|
|
74
|
+
sessionId,
|
|
75
|
+
});
|
|
76
|
+
if (res.error || !Array.isArray(res.uptimeByDevice)) {
|
|
77
|
+
// Most likely an older webservice without the batch route yet.
|
|
78
|
+
logger.warn(`getUptimeWindowsForOrg unavailable (${res.status ?? "no uptimeByDevice in response"}); falling back to per-camera fan-out`);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const windowsByCamera = new Map();
|
|
82
|
+
for (const entry of res.uptimeByDevice) {
|
|
83
|
+
if (entry?.deviceUuid) {
|
|
84
|
+
windowsByCamera.set(entry.deviceUuid, entry.uptimeWindows ?? []);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return windowsByCamera;
|
|
88
|
+
}
|
|
61
89
|
export async function getFleetUptime(startTimeSec, endTimeSec, requestModifiers, sessionId) {
|
|
62
|
-
const cameraListRes = await
|
|
90
|
+
const cameraListRes = await cachedPostApi({
|
|
63
91
|
route: "/camera/getMinimalCameraStateList",
|
|
64
92
|
body: {},
|
|
65
93
|
modifiers: requestModifiers,
|
|
@@ -73,28 +101,37 @@ export async function getFleetUptime(startTimeSec, endTimeSec, requestModifiers,
|
|
|
73
101
|
locationUuid: c.locationUuid,
|
|
74
102
|
}));
|
|
75
103
|
const uptimeResults = [];
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
const batchWindows = await getFleetUptimeWindowsForOrg(startTimeSec, endTimeSec, requestModifiers, sessionId);
|
|
105
|
+
if (batchWindows) {
|
|
106
|
+
for (const cam of cameras) {
|
|
107
|
+
uptimeResults.push(computeUptimeStats(cam.uuid, cam.name, cam.locationUuid, batchWindows.get(cam.uuid) ?? [], startTimeSec, endTimeSec));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
const batchSize = 10;
|
|
112
|
+
for (let i = 0; i < cameras.length; i += batchSize) {
|
|
113
|
+
const batch = cameras.slice(i, i + batchSize);
|
|
114
|
+
const batchResults = await Promise.all(batch.map(async (cam) => {
|
|
115
|
+
try {
|
|
116
|
+
const res = await postApi({
|
|
117
|
+
route: "/camera/getUptimeWindows",
|
|
118
|
+
body: {
|
|
119
|
+
cameraUuid: cam.uuid,
|
|
120
|
+
// milliseconds — see getCameraUptime
|
|
121
|
+
startTime: startTimeSec * 1000,
|
|
122
|
+
endTime: endTimeSec * 1000,
|
|
123
|
+
},
|
|
124
|
+
modifiers: requestModifiers,
|
|
125
|
+
sessionId,
|
|
126
|
+
});
|
|
127
|
+
return computeUptimeStats(cam.uuid, cam.name, cam.locationUuid, (res.uptimeWindows ?? []), startTimeSec, endTimeSec);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return computeUptimeStats(cam.uuid, cam.name, cam.locationUuid, [], startTimeSec, endTimeSec);
|
|
131
|
+
}
|
|
132
|
+
}));
|
|
133
|
+
uptimeResults.push(...batchResults);
|
|
134
|
+
}
|
|
98
135
|
}
|
|
99
136
|
uptimeResults.sort((a, b) => a.uptimePercentage - b.uptimePercentage);
|
|
100
137
|
const avgUptime = uptimeResults.length > 0
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { postApi } from "../network/network.js";
|
|
1
|
+
import { postApi, throwIfApiError } from "../network/network.js";
|
|
2
2
|
export async function getSavedClips(args, requestModifiers, sessionId) {
|
|
3
3
|
const res = await postApi({
|
|
4
4
|
route: "/event/getClipsWithProgress",
|
|
@@ -68,8 +68,7 @@ export async function getClipGroups(requestModifiers, sessionId) {
|
|
|
68
68
|
modifiers: requestModifiers,
|
|
69
69
|
sessionId,
|
|
70
70
|
});
|
|
71
|
-
|
|
72
|
-
throw new Error(JSON.stringify(res));
|
|
71
|
+
throwIfApiError(res);
|
|
73
72
|
return {
|
|
74
73
|
clipGroups: (res.clipGroups || []).map((group) => ({
|
|
75
74
|
uuid: group.uuid ?? undefined,
|
|
@@ -85,8 +84,7 @@ export async function getSharedClipGroups(requestModifiers, sessionId) {
|
|
|
85
84
|
modifiers: requestModifiers,
|
|
86
85
|
sessionId,
|
|
87
86
|
});
|
|
88
|
-
|
|
89
|
-
throw new Error(JSON.stringify(res));
|
|
87
|
+
throwIfApiError(res);
|
|
90
88
|
return {
|
|
91
89
|
sharedClipGroups: (res.sharedClipGroups || []).map((group) => ({
|
|
92
90
|
uuid: group.uuid ?? undefined,
|
|
@@ -108,8 +106,7 @@ export async function createClip(cameraUuid, startTimeMs, endTimeMs, requestModi
|
|
|
108
106
|
modifiers: requestModifiers,
|
|
109
107
|
sessionId,
|
|
110
108
|
});
|
|
111
|
-
|
|
112
|
-
throw new Error(JSON.stringify(res));
|
|
109
|
+
throwIfApiError(res);
|
|
113
110
|
return { spliceResult: { success: true, clipUuid: res.clipUuid ?? undefined } };
|
|
114
111
|
}
|
|
115
112
|
export async function deleteClip(clipUuid, requestModifiers, sessionId) {
|
|
@@ -119,7 +116,46 @@ export async function deleteClip(clipUuid, requestModifiers, sessionId) {
|
|
|
119
116
|
modifiers: requestModifiers,
|
|
120
117
|
sessionId,
|
|
121
118
|
});
|
|
122
|
-
|
|
123
|
-
throw new Error(JSON.stringify(res));
|
|
119
|
+
throwIfApiError(res);
|
|
124
120
|
return { deleteResult: { success: true } };
|
|
125
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Retitle / re-describe a saved clip.
|
|
124
|
+
*
|
|
125
|
+
* `updateSavedClip` is a whole-object write, not a selective one: omitting
|
|
126
|
+
* `title` blanks the title rather than leaving it. So this reads the clip first
|
|
127
|
+
* and merges, which also gives the caller a real "no such clip" error instead of
|
|
128
|
+
* a silent no-op on a bad uuid.
|
|
129
|
+
*/
|
|
130
|
+
export async function updateClip(clipUuid, changes, requestModifiers, sessionId) {
|
|
131
|
+
const current = await postApi({
|
|
132
|
+
route: "/event/getSavedClipDetails",
|
|
133
|
+
body: { clipUuid },
|
|
134
|
+
modifiers: requestModifiers,
|
|
135
|
+
sessionId,
|
|
136
|
+
});
|
|
137
|
+
throwIfApiError(current);
|
|
138
|
+
const existing = current.savedClip;
|
|
139
|
+
if (!existing) {
|
|
140
|
+
return {
|
|
141
|
+
error: `No saved clip has the uuid "${clipUuid}". Use requestType 'saved' to list the clips that exist — do not guess a uuid.`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const res = await postApi({
|
|
145
|
+
route: "/event/updateSavedClip",
|
|
146
|
+
body: {
|
|
147
|
+
savedClipUuid: clipUuid,
|
|
148
|
+
title: changes.title ?? existing.title ?? undefined,
|
|
149
|
+
description: changes.description ?? existing.description ?? undefined,
|
|
150
|
+
accessSettings: existing.accessSettings ?? undefined,
|
|
151
|
+
},
|
|
152
|
+
modifiers: requestModifiers,
|
|
153
|
+
sessionId,
|
|
154
|
+
});
|
|
155
|
+
throwIfApiError(res);
|
|
156
|
+
return {
|
|
157
|
+
updateResult: { success: true, clipUuid },
|
|
158
|
+
warningMsg: res.warningMsg ?? undefined,
|
|
159
|
+
existingTitle: existing.title ?? undefined,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { logger } from "../logger.js";
|
|
2
|
-
import { postApi } from "../network/network.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import { postApi, throwIfApiError } from "../network/network.js";
|
|
3
|
+
/**
|
|
4
|
+
* Our grid layout settings are a little complicated, so we dumb them down to
|
|
5
|
+
* presets chosen by how many devices are visible at once.
|
|
6
|
+
*
|
|
7
|
+
* Exported because UPDATE has to recompute it: a wall grown from 4 cameras to 9
|
|
8
|
+
* while keeping its 2x2 layout would render five of them invisibly.
|
|
9
|
+
*/
|
|
10
|
+
export function computeGridSettings(numDevices) {
|
|
9
11
|
let gridSettings;
|
|
10
12
|
if (numDevices <= 1) {
|
|
11
13
|
gridSettings = {
|
|
@@ -118,6 +120,10 @@ export async function createVideoWall(options, requestModifiers, sessionId) {
|
|
|
118
120
|
gridSize: { width: 8, height: 8 },
|
|
119
121
|
};
|
|
120
122
|
}
|
|
123
|
+
return gridSettings;
|
|
124
|
+
}
|
|
125
|
+
export async function createVideoWall(options, requestModifiers, sessionId) {
|
|
126
|
+
const gridSettings = computeGridSettings(options?.settings.numVisibleDevicesAtOnce ?? options?.deviceList?.length ?? 1);
|
|
121
127
|
const body = {
|
|
122
128
|
videoWall: {
|
|
123
129
|
displayName: options?.displayName,
|
|
@@ -178,3 +184,45 @@ export async function handleCreateVideoWallRequest(videoWallCreateOptions, reque
|
|
|
178
184
|
};
|
|
179
185
|
}
|
|
180
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* api2's `updateVideoWalls` REPLACES the whole videoWall object, so the caller
|
|
189
|
+
* must read the current wall and merge before calling this — see the tool's
|
|
190
|
+
* update branch.
|
|
191
|
+
*/
|
|
192
|
+
export async function updateVideoWall(videoWall, requestModifiers, sessionId) {
|
|
193
|
+
const res = await postApi({
|
|
194
|
+
route: "/camera/updateVideoWalls",
|
|
195
|
+
body: { videoWall },
|
|
196
|
+
modifiers: requestModifiers,
|
|
197
|
+
sessionId,
|
|
198
|
+
});
|
|
199
|
+
throwIfApiError(res);
|
|
200
|
+
return {
|
|
201
|
+
updated: { success: true, uuid: videoWall.uuid },
|
|
202
|
+
warningMsg: res.warningMsg ?? undefined,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
export async function deleteVideoWall(uuid, requestModifiers, sessionId) {
|
|
206
|
+
const res = await postApi({
|
|
207
|
+
route: "/camera/deleteVideoWall",
|
|
208
|
+
body: { uuid },
|
|
209
|
+
modifiers: requestModifiers,
|
|
210
|
+
sessionId,
|
|
211
|
+
});
|
|
212
|
+
throwIfApiError(res);
|
|
213
|
+
return {
|
|
214
|
+
deleted: { success: true, uuid },
|
|
215
|
+
warningMsg: res.warningMsg ?? undefined,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/** The raw wall, for the read-modify-write that update requires. */
|
|
219
|
+
export async function findVideoWall(uuid, requestModifiers, sessionId) {
|
|
220
|
+
const res = await postApi({
|
|
221
|
+
route: "/camera/getVideoWalls",
|
|
222
|
+
body: {},
|
|
223
|
+
modifiers: requestModifiers,
|
|
224
|
+
sessionId,
|
|
225
|
+
});
|
|
226
|
+
throwIfApiError(res);
|
|
227
|
+
return res.videoWalls?.find(wall => wall?.uuid === uuid) ?? undefined;
|
|
228
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { postApi } from "../network/network.js";
|
|
1
|
+
import { postApi, throwIfApiError } from "../network/network.js";
|
|
2
2
|
export async function getDoorControllerRules(doorControllerUuid, requestModifiers, sessionId) {
|
|
3
3
|
const res = await postApi({
|
|
4
4
|
route: "/doorcontroller/getDoorControllerRules",
|
|
@@ -6,9 +6,7 @@ export async function getDoorControllerRules(doorControllerUuid, requestModifier
|
|
|
6
6
|
modifiers: requestModifiers,
|
|
7
7
|
sessionId,
|
|
8
8
|
});
|
|
9
|
-
|
|
10
|
-
throw new Error(JSON.stringify(res));
|
|
11
|
-
}
|
|
9
|
+
throwIfApiError(res);
|
|
12
10
|
return (res.rules?.map((rule) => ({
|
|
13
11
|
uuid: rule.uuid ?? undefined,
|
|
14
12
|
name: rule.name ?? undefined,
|
|
@@ -24,9 +22,7 @@ export async function createDoorControllerRule(ruleConfig, requestModifiers, ses
|
|
|
24
22
|
modifiers: requestModifiers,
|
|
25
23
|
sessionId,
|
|
26
24
|
});
|
|
27
|
-
|
|
28
|
-
throw new Error(JSON.stringify(res));
|
|
29
|
-
}
|
|
25
|
+
throwIfApiError(res);
|
|
30
26
|
return {
|
|
31
27
|
uuid: res.ruleUuid ?? undefined,
|
|
32
28
|
success: true,
|
|
@@ -39,9 +35,7 @@ export async function getDoorPolicies(requestModifiers, sessionId) {
|
|
|
39
35
|
modifiers: requestModifiers,
|
|
40
36
|
sessionId,
|
|
41
37
|
});
|
|
42
|
-
|
|
43
|
-
throw new Error(JSON.stringify(res));
|
|
44
|
-
}
|
|
38
|
+
throwIfApiError(res);
|
|
45
39
|
return (res.policies?.map((policy) => ({
|
|
46
40
|
uuid: policy.uuid ?? undefined,
|
|
47
41
|
name: policy.name ?? undefined,
|
|
@@ -56,11 +50,78 @@ export async function createDoorPolicy(policyConfig, requestModifiers, sessionId
|
|
|
56
50
|
modifiers: requestModifiers,
|
|
57
51
|
sessionId,
|
|
58
52
|
});
|
|
59
|
-
|
|
60
|
-
throw new Error(JSON.stringify(res));
|
|
61
|
-
}
|
|
53
|
+
throwIfApiError(res);
|
|
62
54
|
return {
|
|
63
55
|
uuid: res.policyUuid ?? undefined,
|
|
64
56
|
success: true,
|
|
65
57
|
};
|
|
66
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* api2's `updateDoorPolicy` REPLACES the policy object, so the caller must read
|
|
61
|
+
* the current policy and merge before calling this.
|
|
62
|
+
*/
|
|
63
|
+
export async function updateDoorPolicy(policyConfig, requestModifiers, sessionId) {
|
|
64
|
+
const res = await postApi({
|
|
65
|
+
route: "/policy/updateDoorPolicy",
|
|
66
|
+
body: { policy: policyConfig },
|
|
67
|
+
modifiers: requestModifiers,
|
|
68
|
+
sessionId,
|
|
69
|
+
});
|
|
70
|
+
throwIfApiError(res);
|
|
71
|
+
return { success: true, warningMsg: res.warningMsg ?? undefined };
|
|
72
|
+
}
|
|
73
|
+
export async function deleteDoorPolicy(policyUuid, requestModifiers, sessionId) {
|
|
74
|
+
const res = await postApi({
|
|
75
|
+
route: "/policy/deleteDoorPolicy",
|
|
76
|
+
body: { policyUuid },
|
|
77
|
+
modifiers: requestModifiers,
|
|
78
|
+
sessionId,
|
|
79
|
+
});
|
|
80
|
+
throwIfApiError(res);
|
|
81
|
+
return { success: true, uuid: policyUuid, warningMsg: res.warningMsg ?? undefined };
|
|
82
|
+
}
|
|
83
|
+
export async function getDoorControllerRule(doorControllerUuid, ruleUuid, requestModifiers, sessionId) {
|
|
84
|
+
const res = await postApi({
|
|
85
|
+
route: "/doorcontroller/getDoorControllerRule",
|
|
86
|
+
body: {
|
|
87
|
+
doorControllerUuid,
|
|
88
|
+
ruleUuid,
|
|
89
|
+
},
|
|
90
|
+
modifiers: requestModifiers,
|
|
91
|
+
sessionId,
|
|
92
|
+
});
|
|
93
|
+
throwIfApiError(res);
|
|
94
|
+
return res.rule ?? undefined;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* api2's `updateDoorControllerRule` REPLACES the rule, so the caller must read
|
|
98
|
+
* the current rule and merge before calling this.
|
|
99
|
+
*/
|
|
100
|
+
export async function updateDoorControllerRule(doorControllerUuid, ruleConfig, requestModifiers, sessionId) {
|
|
101
|
+
const res = await postApi({
|
|
102
|
+
route: "/doorcontroller/updateDoorControllerRule",
|
|
103
|
+
body: {
|
|
104
|
+
doorControllerUuid,
|
|
105
|
+
rule: ruleConfig,
|
|
106
|
+
},
|
|
107
|
+
modifiers: requestModifiers,
|
|
108
|
+
sessionId,
|
|
109
|
+
});
|
|
110
|
+
throwIfApiError(res);
|
|
111
|
+
// Doorcontroller_UpdateDoorControllerRuleWSResponse carries only `rule` — no
|
|
112
|
+
// warningMsg channel exists on it, unlike the policy endpoints.
|
|
113
|
+
return { success: true, rule: res.rule ?? undefined };
|
|
114
|
+
}
|
|
115
|
+
export async function deleteDoorControllerRule(doorControllerUuid, ruleUuid, requestModifiers, sessionId) {
|
|
116
|
+
const res = await postApi({
|
|
117
|
+
route: "/doorcontroller/deleteDoorControllerRule",
|
|
118
|
+
body: {
|
|
119
|
+
doorControllerUuid,
|
|
120
|
+
ruleUuid,
|
|
121
|
+
},
|
|
122
|
+
modifiers: requestModifiers,
|
|
123
|
+
sessionId,
|
|
124
|
+
});
|
|
125
|
+
throwIfApiError(res);
|
|
126
|
+
return { success: true, uuid: ruleUuid };
|
|
127
|
+
}
|
|
@@ -30,13 +30,20 @@ export async function getAllEntities(deviceUuids, timeZone, tempUnit, requestMod
|
|
|
30
30
|
getEnvironmentalGateways(timeZone, requestModifiers, sessionId),
|
|
31
31
|
];
|
|
32
32
|
const responses = await Promise.all(promises);
|
|
33
|
-
// Filter each response to only include devices with matching UUIDs and fetch device features
|
|
33
|
+
// Filter each response to only include devices with matching UUIDs and fetch device features.
|
|
34
|
+
// deviceUuids === null means "no UUID filter" (return all entities); in that case skip the
|
|
35
|
+
// per-device getDeviceFeatures fan-out — running it for every device in the org would be an
|
|
36
|
+
// N+1 feature-call storm. License/feature details are only attached for explicit UUID lists.
|
|
34
37
|
for (let i = 0; i < responses.length; i++) {
|
|
35
38
|
const response = responses[i];
|
|
36
39
|
// Look through keys and find any with an array
|
|
37
40
|
for (const key of Object.keys(response)) {
|
|
38
41
|
const value = response[key];
|
|
39
42
|
if (Array.isArray(value)) {
|
|
43
|
+
if (deviceUuids === null) {
|
|
44
|
+
response[`${key}Count`] = value.length;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
40
47
|
// Filter to only include devices with matching UUIDs
|
|
41
48
|
const filteredDevices = value.filter((item) => {
|
|
42
49
|
return deviceUuids.includes(item.uuid);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import z from "zod";
|
|
2
2
|
import { FIVE_SECONDS_MS, THREE_HOURS_MS } from "../constants.js";
|
|
3
|
+
import { logger } from "../logger.js";
|
|
3
4
|
import { postApi } from "../network/network.js";
|
|
5
|
+
import { getAccessControlledDoors } from "./get-entity-tool-api.js";
|
|
4
6
|
import { formatTimestamp } from "../util.js";
|
|
5
7
|
import { tempFunc, TempUnit } from "../utils/temp.js";
|
|
6
8
|
// Type definitions
|
|
@@ -33,7 +35,13 @@ function mapAccessControlEvent(credEvent, timeZone) {
|
|
|
33
35
|
datetime: credEvent?.timestampMs ? formatTimestamp(credEvent.timestampMs, timeZone) : undefined,
|
|
34
36
|
};
|
|
35
37
|
}
|
|
36
|
-
|
|
38
|
+
// Newest-N cap per door: the pagination loop below walks backwards through a
|
|
39
|
+
// door's history and would otherwise fetch every event in a wide time window.
|
|
40
|
+
const MAX_ACCESS_CONTROL_EVENTS_PER_DOOR = 500;
|
|
41
|
+
// Defensive default window when no startTime reaches this layer.
|
|
42
|
+
const DEFAULT_ACCESS_CONTROL_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
43
|
+
async function getAccessControlEventsForDoor(doorUuid, startTimeArg, endTime, timeZone, requestModifiers, sessionId) {
|
|
44
|
+
const startTime = startTimeArg ?? Date.now() - DEFAULT_ACCESS_CONTROL_WINDOW_MS;
|
|
37
45
|
const allEvents = [];
|
|
38
46
|
let createdBeforeMs = endTime;
|
|
39
47
|
while (true) {
|
|
@@ -58,6 +66,10 @@ async function getAccessControlEventsForDoor(doorUuid, startTime, endTime, timeZ
|
|
|
58
66
|
.map((credEvent) => mapAccessControlEvent(credEvent, timeZone))
|
|
59
67
|
.filter(Boolean);
|
|
60
68
|
allEvents.push(...mappedEvents);
|
|
69
|
+
if (allEvents.length >= MAX_ACCESS_CONTROL_EVENTS_PER_DOOR) {
|
|
70
|
+
logger.info(`access-control events for door ${doorUuid} capped at newest ${MAX_ACCESS_CONTROL_EVENTS_PER_DOOR}`);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
61
73
|
if (componentEvents.length < ACCESS_CONTROL_EVENT_BATCH_SIZE) {
|
|
62
74
|
break;
|
|
63
75
|
}
|
|
@@ -84,7 +96,68 @@ async function getAccessControlEventsForDoor(doorUuid, startTime, endTime, timeZ
|
|
|
84
96
|
}
|
|
85
97
|
createdBeforeMs = oldestTimestamp;
|
|
86
98
|
}
|
|
87
|
-
|
|
99
|
+
// Pages accumulate newest → oldest, so slicing keeps the newest events.
|
|
100
|
+
return allEvents.slice(0, MAX_ACCESS_CONTROL_EVENTS_PER_DOOR);
|
|
101
|
+
}
|
|
102
|
+
/** Substrings marking a component event type that only an access-controlled door emits. */
|
|
103
|
+
const DOOR_RELATED_COMPONENT_EVENT_MARKERS = [
|
|
104
|
+
"Door",
|
|
105
|
+
"Credential",
|
|
106
|
+
"RequestToExit",
|
|
107
|
+
"AccessControlUnit",
|
|
108
|
+
"Aperio",
|
|
109
|
+
];
|
|
110
|
+
function isDoorRelatedComponentEventQuery(eventTypes) {
|
|
111
|
+
// No filter means "all types", which includes the door ones.
|
|
112
|
+
if (eventTypes.length === 0)
|
|
113
|
+
return true;
|
|
114
|
+
return eventTypes.some(t => DOOR_RELATED_COMPONENT_EVENT_MARKERS.some(marker => t.includes(marker)));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Explains an empty component-events result when the caller scoped it to a location
|
|
118
|
+
* that has no access-controlled doors.
|
|
119
|
+
*
|
|
120
|
+
* Without this the tool returns a bare `{componentEvents: []}` and the model reports
|
|
121
|
+
* "no door activity" — which is what happened in prod when it queried a real but
|
|
122
|
+
* doorless location while 42 doors sat at seven other locations. The door list is a
|
|
123
|
+
* cached call, and this only runs on the empty-result path, so it costs nothing on
|
|
124
|
+
* the hot path.
|
|
125
|
+
*/
|
|
126
|
+
export async function describeEmptyComponentEventResult(locationUuid, eventTypes, requestModifiers, sessionId) {
|
|
127
|
+
if (!isDoorRelatedComponentEventQuery(eventTypes))
|
|
128
|
+
return undefined;
|
|
129
|
+
let doors;
|
|
130
|
+
try {
|
|
131
|
+
const response = await getAccessControlledDoors(requestModifiers, sessionId);
|
|
132
|
+
doors = response.accessControlledDoors ?? [];
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
logger.debug(`Could not load doors to annotate empty component-events result: ${error}`);
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
if (doors.some(door => door.locationUuid === locationUuid))
|
|
139
|
+
return undefined;
|
|
140
|
+
const doorsPerLocation = new Map();
|
|
141
|
+
for (const door of doors) {
|
|
142
|
+
if (door.locationUuid) {
|
|
143
|
+
doorsPerLocation.set(door.locationUuid, (doorsPerLocation.get(door.locationUuid) ?? 0) + 1);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (doorsPerLocation.size === 0) {
|
|
147
|
+
return (`No component events were returned, and this organization has no access-controlled doors at any location. ` +
|
|
148
|
+
`Do not report this as "no door activity" without saying that no doors are configured.`);
|
|
149
|
+
}
|
|
150
|
+
const MAX_LISTED_LOCATIONS = 20;
|
|
151
|
+
const listed = [...doorsPerLocation.entries()]
|
|
152
|
+
.sort((a, b) => b[1] - a[1])
|
|
153
|
+
.slice(0, MAX_LISTED_LOCATIONS)
|
|
154
|
+
.map(([uuid, count]) => `${uuid} (${count})`)
|
|
155
|
+
.join(", ");
|
|
156
|
+
const omitted = doorsPerLocation.size - Math.min(doorsPerLocation.size, MAX_LISTED_LOCATIONS);
|
|
157
|
+
return (`Location ${locationUuid} has no access-controlled doors, so this query could not return door events — ` +
|
|
158
|
+
`the empty result does not mean there was no door activity. Doors exist at these locations ` +
|
|
159
|
+
`(locationUuid and door count): ${listed}${omitted > 0 ? `, and ${omitted} more` : ""}. ` +
|
|
160
|
+
`Re-run this query for those locations before answering.`);
|
|
88
161
|
}
|
|
89
162
|
export async function getBrivoAccessControlEvents(startTime, endTime, timeZone, requestModifiers, sessionId) {
|
|
90
163
|
// Step 1: fetch Brivo integration config to get configured doors and their location UUIDs
|
|
@@ -99,8 +172,8 @@ export async function getBrivoAccessControlEvents(startTime, endTime, timeZone,
|
|
|
99
172
|
const doorInfoMap = brivoSettings?.doorInfoMap ?? {};
|
|
100
173
|
const brivoDoors = Object.entries(doorInfoMap)
|
|
101
174
|
.filter(([, info]) => info != null)
|
|
102
|
-
.map(([
|
|
103
|
-
|
|
175
|
+
.map(([brivoDoorId, info]) => ({
|
|
176
|
+
brivoDoorId,
|
|
104
177
|
doorName: info.doorName ?? undefined,
|
|
105
178
|
locationUuid: info.locationUuid ?? undefined,
|
|
106
179
|
}));
|
|
@@ -194,7 +267,7 @@ export async function getAccessControlEvents(doorUuids, startTime, endTime, time
|
|
|
194
267
|
// Flatten all componentEvents into a single array
|
|
195
268
|
const accessControlEvents = responses.flatMap(events => events);
|
|
196
269
|
accessControlEvents.sort((a, b) => (b.timestampMs || 0) - (a.timestampMs || 0));
|
|
197
|
-
|
|
270
|
+
logger.debug(`componentEvents: ${accessControlEvents.length} access-control events`);
|
|
198
271
|
return accessControlEvents;
|
|
199
272
|
}
|
|
200
273
|
export async function getCameraFootageSeekpointEvents(cameraUuid, duration, startTime, requestModifiers, sessionId) {
|
|
@@ -438,7 +511,7 @@ startTime, endTime, timeZone, requestModifiers, sessionId) {
|
|
|
438
511
|
});
|
|
439
512
|
// Sort events by timestamp (newest first)
|
|
440
513
|
events.sort((a, b) => (b.timestampMs || 0) - (a.timestampMs || 0));
|
|
441
|
-
|
|
514
|
+
logger.debug(`componentEvents: ${events.length} events`);
|
|
442
515
|
return events;
|
|
443
516
|
}
|
|
444
517
|
export async function getButtonPressEvents(sensorUuid, startTime, endTime, timeZone, requestModifiers, sessionId) {
|