rhombus-node-mcp 0.1.21 → 0.1.23
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/README.md +8 -0
- package/dist/api/access-control-tool-api.js +256 -0
- package/dist/api/alarm-monitoring-tool-api.js +65 -0
- package/dist/api/camera-tool-api.js +35 -15
- package/dist/api/camera-uptime-tool-api.js +112 -0
- package/dist/api/clips-tool-api.js +110 -21
- package/dist/api/create-tool-api.js +143 -16
- package/dist/api/door-tool-api.js +66 -0
- package/dist/api/events-tool-api.js +151 -28
- package/dist/api/faces-tool-api.js +118 -91
- package/dist/api/get-entity-tool-api.js +2 -0
- package/dist/api/guest-management-tool-api.js +75 -0
- package/dist/api/location-tool-api.js +48 -1
- package/dist/api/lpr-tool-api.js +26 -0
- package/dist/api/policy-alerts-tool-api.js +54 -0
- package/dist/api/report-tool-api.js +345 -23
- package/dist/api/rules-tool-api.js +78 -0
- package/dist/api/search-tool-api.js +93 -0
- package/dist/api/time-tool-api.js +3 -1
- package/dist/api/update-tool-api.js +204 -0
- package/dist/api/user-access-trail-tool-api.js +45 -0
- package/dist/api/user-audit-tool-api.js +47 -0
- package/dist/api/user-tool-api.js +77 -0
- package/dist/createServer.js +3 -1
- package/dist/filtering-utils.js +298 -0
- package/dist/index.js +0 -9
- package/dist/logger.js +6 -5
- package/dist/network.js +18 -4
- package/dist/tools/access-control-tool.js +97 -0
- package/dist/tools/alarm-monitoring-tool.js +50 -0
- package/dist/tools/analytics-tool.js +383 -0
- package/dist/tools/camera-tool.js +27 -9
- package/dist/tools/camera-uptime-tool.js +50 -0
- package/dist/tools/clips-tool.js +46 -16
- package/dist/tools/door-tool.js +66 -0
- package/dist/tools/events-tool.js +103 -81
- package/dist/tools/faces-tool.js +175 -32
- package/dist/tools/get-entity-tool.js +6 -1
- package/dist/tools/guest-management-tool.js +50 -0
- package/dist/tools/location-tool.js +23 -4
- package/dist/tools/lpr-tool.js +26 -20
- package/dist/tools/policy-alerts-tool.js +34 -4
- package/dist/tools/report-tool.js +183 -46
- package/dist/tools/rules-tool.js +76 -0
- package/dist/tools/search-tool.js +69 -0
- package/dist/tools/update-tool.js +63 -2
- package/dist/tools/user-access-trail-tool.js +64 -0
- package/dist/tools/user-audit-tool.js +53 -0
- package/dist/tools/user-tool.js +55 -0
- package/dist/tools/video-walls-tool.js +41 -0
- package/dist/transports/streamable-http.js +25 -15
- package/dist/types/access-control-tool-types.js +120 -0
- package/dist/types/alarm-monitoring-tool-types.js +60 -0
- package/dist/types/analytics-tool-types.js +190 -0
- package/dist/types/camera-tool-types.js +3 -2
- package/dist/types/camera-uptime-tool-types.js +52 -0
- package/dist/types/clips-tool-types.js +60 -11
- package/dist/types/door-tool-types.js +64 -0
- package/dist/types/events-tools-types.js +71 -3
- package/dist/types/faces-tools-types.js +36 -0
- package/dist/types/guest-management-tool-types.js +57 -0
- package/dist/types/location-tool-types.js +10 -1
- package/dist/types/lpr-tool-types.js +14 -0
- package/dist/types/policy-alerts-tool-types.js +38 -3
- package/dist/types/report-tool-types.js +284 -42
- package/dist/types/rules-tool-types.js +58 -0
- package/dist/types/schema.js +3123 -1
- package/dist/types/search-tool-types.js +82 -0
- package/dist/types/update-tool-types.js +4 -1
- package/dist/types/user-access-trail-tool-types.js +54 -0
- package/dist/types/user-audit-tool-types.js +48 -0
- package/dist/types/user-tool-types.js +61 -0
- package/dist/types/video-walls-tool-types.js +59 -0
- package/dist/types/zod-schemas.js +3237 -986
- package/dist/types.js +0 -21
- package/dist/util.js +34 -57
- package/package.json +7 -5
- package/dist/disabled-tools/semantic-search-tool.js +0 -90
- package/dist/tools/create-tool.js +0 -30
- package/dist/types/create-tool-types.js +0 -8
- package/dist/types/schema-components.js +0 -7211
- package/dist/types/semantic-search-tool-types.js +0 -5
package/README.md
CHANGED
|
@@ -24,6 +24,14 @@ Your insights will directly influence our development roadmap and help us create
|
|
|
24
24
|
|
|
25
25
|
## 🔧 Quick Setup
|
|
26
26
|
|
|
27
|
+
### Installing via Smithery
|
|
28
|
+
|
|
29
|
+
To install Rhombus MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@RhombusSystems/rhombus-node-mcp):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx -y @smithery/cli install @RhombusSystems/rhombus-node-mcp --client claude
|
|
33
|
+
```
|
|
34
|
+
|
|
27
35
|
### Step 1: Give Your Rhombus Console Superpowers
|
|
28
36
|
|
|
29
37
|
1. Login to your Rhombus Console - [Login Here](https://console.rhombus.com)! 🔐
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { postApi } from "../network.js";
|
|
2
|
+
const PERMISSION_RANK = {
|
|
3
|
+
LIVEONLY: 0,
|
|
4
|
+
READONLY: 1,
|
|
5
|
+
ADMIN: 2,
|
|
6
|
+
};
|
|
7
|
+
function hasAtLeastReadonly(perm) {
|
|
8
|
+
return PERMISSION_RANK[perm ?? ""] >= PERMISSION_RANK.READONLY;
|
|
9
|
+
}
|
|
10
|
+
export async function unlockDoor(doorUuid, requestModifiers, sessionId) {
|
|
11
|
+
const res = await postApi({
|
|
12
|
+
route: "/accesscontrol/unlockAccessControlledDoor",
|
|
13
|
+
body: { accessControlledDoorUuid: doorUuid },
|
|
14
|
+
modifiers: requestModifiers,
|
|
15
|
+
sessionId,
|
|
16
|
+
});
|
|
17
|
+
if (res.error) {
|
|
18
|
+
throw new Error(JSON.stringify(res));
|
|
19
|
+
}
|
|
20
|
+
return { success: true, doorUuid };
|
|
21
|
+
}
|
|
22
|
+
export async function getAccessControlGroups(requestModifiers, sessionId) {
|
|
23
|
+
const res = await postApi({
|
|
24
|
+
route: "/accesscontrol/findAccessControlGroupsByOrg",
|
|
25
|
+
body: {},
|
|
26
|
+
modifiers: requestModifiers,
|
|
27
|
+
sessionId,
|
|
28
|
+
});
|
|
29
|
+
if (res.error) {
|
|
30
|
+
throw new Error(JSON.stringify(res));
|
|
31
|
+
}
|
|
32
|
+
return (res.groups?.map((group) => ({
|
|
33
|
+
uuid: group.uuid ?? undefined,
|
|
34
|
+
name: group.name ?? undefined,
|
|
35
|
+
description: group.description ?? undefined,
|
|
36
|
+
orgUuid: group.orgUuid ?? undefined,
|
|
37
|
+
userUuids: group.userUuids?.filter((u) => u !== null) ?? [],
|
|
38
|
+
})) ?? []);
|
|
39
|
+
}
|
|
40
|
+
export async function getCredentialsByUser(userUuid, requestModifiers, sessionId) {
|
|
41
|
+
const res = await postApi({
|
|
42
|
+
route: "/accesscontrol/findAccessControlCredentialByUser",
|
|
43
|
+
body: { userUuid },
|
|
44
|
+
modifiers: requestModifiers,
|
|
45
|
+
sessionId,
|
|
46
|
+
});
|
|
47
|
+
if (res.error) {
|
|
48
|
+
throw new Error(JSON.stringify(res));
|
|
49
|
+
}
|
|
50
|
+
return (res.credentials?.map((cred) => ({
|
|
51
|
+
uuid: cred.uuid ?? undefined,
|
|
52
|
+
userUuid: cred.userUuid ?? undefined,
|
|
53
|
+
credentialType: cred.credentialType ?? undefined,
|
|
54
|
+
status: cred.workflowStatus ?? undefined,
|
|
55
|
+
note: cred.note ?? undefined,
|
|
56
|
+
})) ?? []);
|
|
57
|
+
}
|
|
58
|
+
export async function getLockdownPlans(requestModifiers, sessionId) {
|
|
59
|
+
const res = await postApi({
|
|
60
|
+
route: "/accesscontrol/lockdownPlan/findLockdownPlans",
|
|
61
|
+
body: {},
|
|
62
|
+
modifiers: requestModifiers,
|
|
63
|
+
sessionId,
|
|
64
|
+
});
|
|
65
|
+
if (res.error) {
|
|
66
|
+
throw new Error(JSON.stringify(res));
|
|
67
|
+
}
|
|
68
|
+
return (res.lockdownPlans?.map((plan) => ({
|
|
69
|
+
uuid: plan.uuid ?? undefined,
|
|
70
|
+
name: plan.name ?? undefined,
|
|
71
|
+
locationUuid: plan.locationUuid ?? undefined,
|
|
72
|
+
description: plan.description ?? undefined,
|
|
73
|
+
active: plan.active ?? undefined,
|
|
74
|
+
})) ?? []);
|
|
75
|
+
}
|
|
76
|
+
export async function activateLockdown(locationUuid, lockdownPlanUuid, requestModifiers, sessionId) {
|
|
77
|
+
const res = await postApi({
|
|
78
|
+
route: "/accesscontrol/lockdownPlan/activateLockdownForLocation",
|
|
79
|
+
body: { locationUuid, lockdownPlanUuids: [lockdownPlanUuid] },
|
|
80
|
+
modifiers: requestModifiers,
|
|
81
|
+
sessionId,
|
|
82
|
+
});
|
|
83
|
+
if (res.error) {
|
|
84
|
+
throw new Error(JSON.stringify(res));
|
|
85
|
+
}
|
|
86
|
+
return { success: true, locationUuid, action: "activated" };
|
|
87
|
+
}
|
|
88
|
+
export async function deactivateLockdown(locationUuid, lockdownPlanUuid, requestModifiers, sessionId) {
|
|
89
|
+
const res = await postApi({
|
|
90
|
+
route: "/accesscontrol/lockdownPlan/deactivateLockdownForLocation",
|
|
91
|
+
body: { locationUuid, lockdownPlanUuids: [lockdownPlanUuid] },
|
|
92
|
+
modifiers: requestModifiers,
|
|
93
|
+
sessionId,
|
|
94
|
+
});
|
|
95
|
+
if (res.error) {
|
|
96
|
+
throw new Error(JSON.stringify(res));
|
|
97
|
+
}
|
|
98
|
+
return { success: true, locationUuid, action: "deactivated" };
|
|
99
|
+
}
|
|
100
|
+
export async function getDoorScheduleExceptions(locationUuid, requestModifiers, sessionId) {
|
|
101
|
+
const res = await postApi({
|
|
102
|
+
route: "/accesscontrol/doorScheduleException/findExceptionsV2",
|
|
103
|
+
body: { locationUuid },
|
|
104
|
+
modifiers: requestModifiers,
|
|
105
|
+
sessionId,
|
|
106
|
+
});
|
|
107
|
+
if (res.error) {
|
|
108
|
+
throw new Error(JSON.stringify(res));
|
|
109
|
+
}
|
|
110
|
+
return (res.exceptions?.map((exc) => ({
|
|
111
|
+
uuid: exc.uuid ?? undefined,
|
|
112
|
+
name: exc.name ?? undefined,
|
|
113
|
+
startTime: exc.startTimeMs ?? undefined,
|
|
114
|
+
endTime: exc.endTimeMs ?? undefined,
|
|
115
|
+
doorUuids: exc.doorUuids?.filter((d) => d !== null) ?? [],
|
|
116
|
+
})) ?? []);
|
|
117
|
+
}
|
|
118
|
+
export async function getAccessGrants(locationUuid, requestModifiers, sessionId) {
|
|
119
|
+
const res = locationUuid
|
|
120
|
+
? await postApi({
|
|
121
|
+
route: "/accesscontrol/findLocationAccessGrantsByLocation",
|
|
122
|
+
body: { locationUuid },
|
|
123
|
+
modifiers: requestModifiers,
|
|
124
|
+
sessionId,
|
|
125
|
+
})
|
|
126
|
+
: await postApi({
|
|
127
|
+
route: "/accesscontrol/findLocationAccessGrantsByOrg",
|
|
128
|
+
body: {},
|
|
129
|
+
modifiers: requestModifiers,
|
|
130
|
+
sessionId,
|
|
131
|
+
});
|
|
132
|
+
if (res.error) {
|
|
133
|
+
throw new Error(JSON.stringify(res));
|
|
134
|
+
}
|
|
135
|
+
const filterNulls = (arr) => arr?.filter((v) => v !== null) ?? [];
|
|
136
|
+
return (res.accessGrants?.map((grant) => ({
|
|
137
|
+
uuid: grant.uuid ?? undefined,
|
|
138
|
+
name: grant.name ?? undefined,
|
|
139
|
+
locationUuid: grant.locationUuid ?? undefined,
|
|
140
|
+
userUuids: filterNulls(grant.userUuids),
|
|
141
|
+
groupUuids: filterNulls(grant.groupUuids),
|
|
142
|
+
doorUuids: filterNulls(grant.accessControlledDoorUuids),
|
|
143
|
+
scheduleUuid: grant.scheduleUuid ?? undefined,
|
|
144
|
+
})) ?? []);
|
|
145
|
+
}
|
|
146
|
+
function canRoleUnlockDoor(role, doorLocationUuid, doorAssociatedCameras) {
|
|
147
|
+
if (role.superAdmin)
|
|
148
|
+
return true;
|
|
149
|
+
const hasDoorAdmin = role.functionalityList?.includes("DOOR_ACCESS_ADMINISTRATION");
|
|
150
|
+
if (!hasDoorAdmin)
|
|
151
|
+
return false;
|
|
152
|
+
const acMap = role.accessControlLocationAccessMap ?? {};
|
|
153
|
+
const granularMap = role.locationGranularAccessMap ?? {};
|
|
154
|
+
const acLocationPerm = acMap[doorLocationUuid];
|
|
155
|
+
const granularLocationPerms = granularMap[doorLocationUuid] ?? {};
|
|
156
|
+
const accessConditionsPerm = granularLocationPerms["ACCESS_CONDITIONS"];
|
|
157
|
+
if (hasAtLeastReadonly(acLocationPerm) && hasAtLeastReadonly(accessConditionsPerm)) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
if (doorAssociatedCameras.length > 0) {
|
|
161
|
+
const locationMap = role.locationAccessMap ?? {};
|
|
162
|
+
const deviceMap = role.deviceAccessMap ?? {};
|
|
163
|
+
if (hasAtLeastReadonly(locationMap[doorLocationUuid]))
|
|
164
|
+
return true;
|
|
165
|
+
for (const cameraUuid of doorAssociatedCameras) {
|
|
166
|
+
if (hasAtLeastReadonly(deviceMap[cameraUuid]))
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
export async function getRemoteUnlockUsers(locationUuid, requestModifiers, sessionId) {
|
|
173
|
+
const [permGroupsRes, doorsRes, usersRes] = await Promise.all([
|
|
174
|
+
postApi({
|
|
175
|
+
route: "/permission/getPermissionGroups",
|
|
176
|
+
body: {},
|
|
177
|
+
modifiers: requestModifiers,
|
|
178
|
+
sessionId,
|
|
179
|
+
}),
|
|
180
|
+
postApi({
|
|
181
|
+
route: "/component/findAccessControlledDoors",
|
|
182
|
+
body: {},
|
|
183
|
+
modifiers: requestModifiers,
|
|
184
|
+
sessionId,
|
|
185
|
+
}),
|
|
186
|
+
postApi({
|
|
187
|
+
route: "/user/getUsersInOrg",
|
|
188
|
+
body: {},
|
|
189
|
+
modifiers: requestModifiers,
|
|
190
|
+
sessionId,
|
|
191
|
+
}),
|
|
192
|
+
]);
|
|
193
|
+
if (permGroupsRes.error)
|
|
194
|
+
throw new Error(JSON.stringify(permGroupsRes));
|
|
195
|
+
if (doorsRes.error)
|
|
196
|
+
throw new Error(JSON.stringify(doorsRes));
|
|
197
|
+
if (usersRes.error)
|
|
198
|
+
throw new Error(JSON.stringify(usersRes));
|
|
199
|
+
const permissionGroups = permGroupsRes.permissionGroups ?? [];
|
|
200
|
+
const groupMembership = {};
|
|
201
|
+
for (const [groupUuid, userUuids] of Object.entries(permGroupsRes.groupMembership ?? {})) {
|
|
202
|
+
groupMembership[groupUuid] = (userUuids ?? []).filter((u) => u !== null);
|
|
203
|
+
}
|
|
204
|
+
const userMap = new Map();
|
|
205
|
+
for (const user of usersRes.users ?? []) {
|
|
206
|
+
if (user.uuid) {
|
|
207
|
+
userMap.set(user.uuid, {
|
|
208
|
+
uuid: user.uuid,
|
|
209
|
+
firstName: user.firstName ?? undefined,
|
|
210
|
+
lastName: user.lastName ?? undefined,
|
|
211
|
+
email: user.email ?? undefined,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const doors = (doorsRes.accessControlledDoors ?? []).filter((door) => door.locationUuid === locationUuid && door.remoteUnlockEnabled === true);
|
|
216
|
+
const doorNames = doors.map((d) => d.name ?? "Unknown");
|
|
217
|
+
const totalDoors = doorNames.length;
|
|
218
|
+
const groupResults = new Map();
|
|
219
|
+
const seenUsers = new Set();
|
|
220
|
+
for (const door of doors) {
|
|
221
|
+
const doorLocationUuid = door.locationUuid ?? "";
|
|
222
|
+
const doorName = door.name ?? "Unknown";
|
|
223
|
+
const associatedCameras = door.associatedCameras?.filter((c) => c !== null) ?? [];
|
|
224
|
+
for (const role of permissionGroups) {
|
|
225
|
+
if (!role.uuid)
|
|
226
|
+
continue;
|
|
227
|
+
if (!canRoleUnlockDoor(role, doorLocationUuid, associatedCameras))
|
|
228
|
+
continue;
|
|
229
|
+
const roleName = role.name ?? "Unknown";
|
|
230
|
+
let group = groupResults.get(roleName);
|
|
231
|
+
if (!group) {
|
|
232
|
+
group = { doorNames: new Set(), userEntries: new Set() };
|
|
233
|
+
groupResults.set(roleName, group);
|
|
234
|
+
}
|
|
235
|
+
group.doorNames.add(doorName);
|
|
236
|
+
for (const userUuid of groupMembership[role.uuid] ?? []) {
|
|
237
|
+
if (!userMap.has(userUuid) || seenUsers.has(userUuid))
|
|
238
|
+
continue;
|
|
239
|
+
seenUsers.add(userUuid);
|
|
240
|
+
const user = userMap.get(userUuid);
|
|
241
|
+
const name = [user.firstName, user.lastName].filter(Boolean).join(" ");
|
|
242
|
+
const label = name
|
|
243
|
+
? `${name} (${user.email ?? "no email"})`
|
|
244
|
+
: (user.email ?? userUuid);
|
|
245
|
+
group.userEntries.add(label);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const groups = Array.from(groupResults.entries()).map(([name, g]) => ({
|
|
250
|
+
permissionGroup: name,
|
|
251
|
+
doors: g.doorNames.size === totalDoors ? "all" : Array.from(g.doorNames),
|
|
252
|
+
users: Array.from(g.userEntries),
|
|
253
|
+
}));
|
|
254
|
+
const totalUsers = seenUsers.size;
|
|
255
|
+
return { doors: doorNames, totalUsers, groups };
|
|
256
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { postApi } from "../network.js";
|
|
2
|
+
export async function getOrgAlarmStatus(requestModifiers, sessionId) {
|
|
3
|
+
const res = await postApi({
|
|
4
|
+
route: "/alertmonitoring/orgStatus",
|
|
5
|
+
body: {},
|
|
6
|
+
modifiers: requestModifiers,
|
|
7
|
+
sessionId,
|
|
8
|
+
});
|
|
9
|
+
if (res.error) {
|
|
10
|
+
throw new Error(res.errorMsg ?? "Failed to get org alarm status");
|
|
11
|
+
}
|
|
12
|
+
const statuses = res.locationStatuses ?? {};
|
|
13
|
+
return Object.entries(statuses).map(([locationUuid, status]) => ({
|
|
14
|
+
locationUuid,
|
|
15
|
+
enabled: status?.enabled ?? undefined,
|
|
16
|
+
enabledOnMs: status?.enabledOnMs ?? undefined,
|
|
17
|
+
disabledOnMs: status?.disabledOnMs ?? undefined,
|
|
18
|
+
notEnabledReason: status?.notEnabledReason ?? undefined,
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
export async function getAlertMonitoringThreatCases(startTimeMs, endTimeMs, maxResults, requestModifiers, sessionId) {
|
|
22
|
+
const body = {};
|
|
23
|
+
if (startTimeMs)
|
|
24
|
+
body.afterTimestampMs = startTimeMs;
|
|
25
|
+
if (endTimeMs)
|
|
26
|
+
body.beforeTimestampMs = endTimeMs;
|
|
27
|
+
if (maxResults)
|
|
28
|
+
body.maxResults = maxResults;
|
|
29
|
+
const res = await postApi({
|
|
30
|
+
route: "/event/getAlertMonitoringThreatCases",
|
|
31
|
+
body,
|
|
32
|
+
modifiers: requestModifiers,
|
|
33
|
+
sessionId,
|
|
34
|
+
});
|
|
35
|
+
if (res.error) {
|
|
36
|
+
throw new Error(res.errorMsg ?? "Failed to get threat cases");
|
|
37
|
+
}
|
|
38
|
+
return (res.threatCases?.map(tc => ({
|
|
39
|
+
uuid: tc.uuid ?? undefined,
|
|
40
|
+
status: tc.status ?? undefined,
|
|
41
|
+
locationName: tc.locationName ?? undefined,
|
|
42
|
+
locationUuid: tc.locationUuid ?? undefined,
|
|
43
|
+
deviceUuid: tc.deviceUuid ?? undefined,
|
|
44
|
+
createdAtMillis: tc.createdAtMillis ?? undefined,
|
|
45
|
+
promptTitle: tc.promptTitle ?? undefined,
|
|
46
|
+
})) ?? []);
|
|
47
|
+
}
|
|
48
|
+
export async function getLocationAlarmStatus(locationUuid, requestModifiers, sessionId) {
|
|
49
|
+
const res = await postApi({
|
|
50
|
+
route: "/alertmonitoring/locationStatus",
|
|
51
|
+
body: { locationUuid },
|
|
52
|
+
modifiers: requestModifiers,
|
|
53
|
+
sessionId,
|
|
54
|
+
});
|
|
55
|
+
if (res.error) {
|
|
56
|
+
throw new Error(res.errorMsg ?? "Failed to get location alarm status");
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
locationUuid,
|
|
60
|
+
enabled: res.status?.enabled ?? undefined,
|
|
61
|
+
enabledOnMs: res.status?.enabledOnMs ?? undefined,
|
|
62
|
+
disabledOnMs: res.status?.disabledOnMs ?? undefined,
|
|
63
|
+
notEnabledReason: res.status?.notEnabledReason ?? undefined,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getLogger } from "../logger.js";
|
|
2
|
-
import {
|
|
2
|
+
import { constructRequestHeaders, postApi } from "../network.js";
|
|
3
3
|
import { removeNullFields } from "../util.js";
|
|
4
4
|
const logger = getLogger("camera-tool");
|
|
5
5
|
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
@@ -8,13 +8,10 @@ export async function getImageForCameraAtTime(cameraUuid, timestampMs, requestMo
|
|
|
8
8
|
cameraUuid: cameraUuid,
|
|
9
9
|
downscaleFactor: 10,
|
|
10
10
|
jpgQuality: 70,
|
|
11
|
-
permyriadCropHeight: 10000,
|
|
12
|
-
permyriadCropWidth: 5625,
|
|
13
|
-
permyriadCropX: 2188,
|
|
14
|
-
permyriadCropY: 0,
|
|
15
11
|
timestampMs: timestampMs,
|
|
16
12
|
};
|
|
17
13
|
logger.debug(`Getting frameUri from UUID: ${cameraUuid} at timestampMs: ${timestampMs}`);
|
|
14
|
+
let frameUri;
|
|
18
15
|
const base64Image = await postApi({
|
|
19
16
|
route: "/video/getExactFrameUri",
|
|
20
17
|
body,
|
|
@@ -22,17 +19,17 @@ export async function getImageForCameraAtTime(cameraUuid, timestampMs, requestMo
|
|
|
22
19
|
sessionId,
|
|
23
20
|
}).then(async (res) => {
|
|
24
21
|
logger.debug(`Received frameUri ${res.frameUri}`);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
...(requestModifiers?.headers ?? AUTH_HEADERS),
|
|
28
|
-
...STATIC_HEADERS,
|
|
29
|
-
};
|
|
30
|
-
// add query params
|
|
31
|
-
if (requestModifiers?.query) {
|
|
32
|
-
res.frameUri = appendQueryParams(res.frameUri, requestModifiers.query);
|
|
22
|
+
if (!res.frameUri) {
|
|
23
|
+
throw new Error("No frameUri given. Maybe camera does not support it.");
|
|
33
24
|
}
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
// construct request headers
|
|
26
|
+
const { url: _frameUri, requestHeaders } = constructRequestHeaders(res.frameUri, requestModifiers, sessionId);
|
|
27
|
+
frameUri = _frameUri;
|
|
28
|
+
// remove content type
|
|
29
|
+
delete requestHeaders["Content-Type"];
|
|
30
|
+
delete requestHeaders["accept"];
|
|
31
|
+
logger.debug(`Fetching with headers\n${JSON.stringify(requestHeaders, null, 2)}`);
|
|
32
|
+
return await fetch(frameUri, {
|
|
36
33
|
method: "GET",
|
|
37
34
|
headers: requestHeaders,
|
|
38
35
|
}).then(async (res) => {
|
|
@@ -57,6 +54,7 @@ export async function getImageForCameraAtTime(cameraUuid, timestampMs, requestMo
|
|
|
57
54
|
return {
|
|
58
55
|
success: true,
|
|
59
56
|
status: "successfully fetched image",
|
|
57
|
+
frameUri: frameUri ?? "",
|
|
60
58
|
imageType: "base64",
|
|
61
59
|
imageData: base64Image,
|
|
62
60
|
};
|
|
@@ -134,6 +132,28 @@ export async function getCameraSettings(cameraUuid, requestModifiers, sessionId)
|
|
|
134
132
|
status: "fetched camera settings",
|
|
135
133
|
};
|
|
136
134
|
}
|
|
135
|
+
export async function getCameraMediaUris(cameraUuid, requestModifiers, sessionId) {
|
|
136
|
+
const res = await postApi({
|
|
137
|
+
route: "/camera/getCameraMediaUris",
|
|
138
|
+
body: { deviceUuid: cameraUuid },
|
|
139
|
+
modifiers: requestModifiers,
|
|
140
|
+
sessionId,
|
|
141
|
+
});
|
|
142
|
+
if (res.error)
|
|
143
|
+
throw new Error(JSON.stringify(res));
|
|
144
|
+
return res;
|
|
145
|
+
}
|
|
146
|
+
export async function getCameraAIThresholds(cameraUuid, requestModifiers, sessionId) {
|
|
147
|
+
const res = await postApi({
|
|
148
|
+
route: "/camera/getCameraAIThresholds",
|
|
149
|
+
body: { deviceUuid: cameraUuid },
|
|
150
|
+
modifiers: requestModifiers,
|
|
151
|
+
sessionId,
|
|
152
|
+
});
|
|
153
|
+
if (res.error)
|
|
154
|
+
throw new Error(JSON.stringify(res));
|
|
155
|
+
return res;
|
|
156
|
+
}
|
|
137
157
|
export async function updateCameraSettings(cameraUuid, update, requestModifiers, sessionId) {
|
|
138
158
|
// remove any "null" values
|
|
139
159
|
const res = await postApi({
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { postApi } from "../network.js";
|
|
2
|
+
function computeUptimeStats(cameraUuid, cameraName, locationUuid, windows, startTimeSec, endTimeSec) {
|
|
3
|
+
const totalPeriodSeconds = endTimeSec - startTimeSec;
|
|
4
|
+
let totalUptimeSeconds = 0;
|
|
5
|
+
const sortedWindows = windows
|
|
6
|
+
.filter(w => w.startSeconds != null && w.durationSeconds != null)
|
|
7
|
+
.sort((a, b) => (a.startSeconds ?? 0) - (b.startSeconds ?? 0));
|
|
8
|
+
for (const w of sortedWindows) {
|
|
9
|
+
const wStart = Math.max(w.startSeconds, startTimeSec);
|
|
10
|
+
const wEnd = Math.min(w.startSeconds + w.durationSeconds, endTimeSec);
|
|
11
|
+
if (wEnd > wStart) {
|
|
12
|
+
totalUptimeSeconds += wEnd - wStart;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
let outageCount = 0;
|
|
16
|
+
let longestOutageSeconds = 0;
|
|
17
|
+
let lastEnd = startTimeSec;
|
|
18
|
+
for (const w of sortedWindows) {
|
|
19
|
+
const wStart = Math.max(w.startSeconds, startTimeSec);
|
|
20
|
+
if (wStart > lastEnd) {
|
|
21
|
+
outageCount++;
|
|
22
|
+
longestOutageSeconds = Math.max(longestOutageSeconds, wStart - lastEnd);
|
|
23
|
+
}
|
|
24
|
+
const wEnd = Math.min(w.startSeconds + w.durationSeconds, endTimeSec);
|
|
25
|
+
lastEnd = Math.max(lastEnd, wEnd);
|
|
26
|
+
}
|
|
27
|
+
if (lastEnd < endTimeSec) {
|
|
28
|
+
outageCount++;
|
|
29
|
+
longestOutageSeconds = Math.max(longestOutageSeconds, endTimeSec - lastEnd);
|
|
30
|
+
}
|
|
31
|
+
const uptimePercentage = totalPeriodSeconds > 0
|
|
32
|
+
? Math.round((totalUptimeSeconds / totalPeriodSeconds) * 10000) / 100
|
|
33
|
+
: 0;
|
|
34
|
+
return {
|
|
35
|
+
cameraUuid,
|
|
36
|
+
cameraName,
|
|
37
|
+
locationUuid,
|
|
38
|
+
totalUptimeSeconds,
|
|
39
|
+
totalPeriodSeconds,
|
|
40
|
+
uptimePercentage,
|
|
41
|
+
outageCount,
|
|
42
|
+
longestOutageSeconds,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export async function getCameraUptime(cameraUuid, startTimeSec, endTimeSec, requestModifiers, sessionId) {
|
|
46
|
+
const res = await postApi({
|
|
47
|
+
route: "/camera/getUptimeWindows",
|
|
48
|
+
body: {
|
|
49
|
+
cameraUuid,
|
|
50
|
+
startTime: startTimeSec,
|
|
51
|
+
endTime: endTimeSec,
|
|
52
|
+
},
|
|
53
|
+
modifiers: requestModifiers,
|
|
54
|
+
sessionId,
|
|
55
|
+
});
|
|
56
|
+
if (res.error) {
|
|
57
|
+
throw new Error(res.errorMsg ?? "Failed to get camera uptime windows");
|
|
58
|
+
}
|
|
59
|
+
return computeUptimeStats(cameraUuid, undefined, undefined, (res.uptimeWindows ?? []), startTimeSec, endTimeSec);
|
|
60
|
+
}
|
|
61
|
+
export async function getFleetUptime(startTimeSec, endTimeSec, requestModifiers, sessionId) {
|
|
62
|
+
const cameraListRes = await postApi({
|
|
63
|
+
route: "/camera/getMinimalCameraStateList",
|
|
64
|
+
body: {},
|
|
65
|
+
modifiers: requestModifiers,
|
|
66
|
+
sessionId,
|
|
67
|
+
});
|
|
68
|
+
const cameras = (cameraListRes.cameraStates ?? [])
|
|
69
|
+
.filter((c) => c.locationUuid)
|
|
70
|
+
.map((c) => ({
|
|
71
|
+
uuid: c.uuid,
|
|
72
|
+
name: c.name ?? c.uuid,
|
|
73
|
+
locationUuid: c.locationUuid,
|
|
74
|
+
}));
|
|
75
|
+
const uptimeResults = [];
|
|
76
|
+
const batchSize = 10;
|
|
77
|
+
for (let i = 0; i < cameras.length; i += batchSize) {
|
|
78
|
+
const batch = cameras.slice(i, i + batchSize);
|
|
79
|
+
const batchResults = await Promise.all(batch.map(async (cam) => {
|
|
80
|
+
try {
|
|
81
|
+
const res = await postApi({
|
|
82
|
+
route: "/camera/getUptimeWindows",
|
|
83
|
+
body: {
|
|
84
|
+
cameraUuid: cam.uuid,
|
|
85
|
+
startTime: startTimeSec,
|
|
86
|
+
endTime: endTimeSec,
|
|
87
|
+
},
|
|
88
|
+
modifiers: requestModifiers,
|
|
89
|
+
sessionId,
|
|
90
|
+
});
|
|
91
|
+
return computeUptimeStats(cam.uuid, cam.name, cam.locationUuid, (res.uptimeWindows ?? []), startTimeSec, endTimeSec);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return computeUptimeStats(cam.uuid, cam.name, cam.locationUuid, [], startTimeSec, endTimeSec);
|
|
95
|
+
}
|
|
96
|
+
}));
|
|
97
|
+
uptimeResults.push(...batchResults);
|
|
98
|
+
}
|
|
99
|
+
uptimeResults.sort((a, b) => a.uptimePercentage - b.uptimePercentage);
|
|
100
|
+
const avgUptime = uptimeResults.length > 0
|
|
101
|
+
? Math.round((uptimeResults.reduce((sum, r) => sum + r.uptimePercentage, 0) / uptimeResults.length) * 100) / 100
|
|
102
|
+
: 0;
|
|
103
|
+
return {
|
|
104
|
+
cameras: uptimeResults,
|
|
105
|
+
summary: {
|
|
106
|
+
totalCameras: uptimeResults.length,
|
|
107
|
+
averageUptimePercentage: avgUptime,
|
|
108
|
+
worstCamera: uptimeResults[0]?.cameraName,
|
|
109
|
+
worstUptimePercentage: uptimeResults[0]?.uptimePercentage,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -1,36 +1,125 @@
|
|
|
1
1
|
import { postApi } from "../network.js";
|
|
2
2
|
export async function getSavedClips(args, requestModifiers, sessionId) {
|
|
3
|
-
|
|
3
|
+
const res = await postApi({
|
|
4
4
|
route: "/event/getClipsWithProgress",
|
|
5
5
|
body: args,
|
|
6
6
|
modifiers: requestModifiers,
|
|
7
7
|
sessionId,
|
|
8
|
-
}).then(response => {
|
|
9
|
-
if (response.error)
|
|
10
|
-
return response;
|
|
11
|
-
else
|
|
12
|
-
return (response.savedClips?.map((clip) => ({
|
|
13
|
-
...clip,
|
|
14
|
-
createdAtTimestamp: clip.createdAtMs ? new Date(clip.createdAtMs).toISOString() : null,
|
|
15
|
-
})) ?? []);
|
|
16
8
|
});
|
|
9
|
+
if (res.error)
|
|
10
|
+
return { error: res.errorMsg ?? undefined };
|
|
11
|
+
return {
|
|
12
|
+
savedClips: res.savedClips?.map((clip) => ({
|
|
13
|
+
...clip,
|
|
14
|
+
createdAtTimestamp: clip.createdAtMs
|
|
15
|
+
? new Date(clip.createdAtMs).toISOString()
|
|
16
|
+
: null,
|
|
17
|
+
})) ?? [],
|
|
18
|
+
};
|
|
17
19
|
}
|
|
18
20
|
export async function getExpiringClips(args, requestModifiers, sessionId) {
|
|
19
|
-
|
|
21
|
+
const res = await postApi({
|
|
20
22
|
route: "/event/getExpiringClipsForOrg",
|
|
21
23
|
body: args,
|
|
22
24
|
modifiers: requestModifiers,
|
|
23
25
|
sessionId,
|
|
24
|
-
}).then(response => {
|
|
25
|
-
if (response.error)
|
|
26
|
-
return response;
|
|
27
|
-
else
|
|
28
|
-
return {
|
|
29
|
-
...response,
|
|
30
|
-
savedClips: (response.savedClips || []).map((clip) => ({
|
|
31
|
-
...clip,
|
|
32
|
-
createdAtTimestamp: clip.createdAtMs ? new Date(clip.createdAtMs).toISOString() : null,
|
|
33
|
-
})),
|
|
34
|
-
};
|
|
35
26
|
});
|
|
27
|
+
if (res.error)
|
|
28
|
+
return { error: res.errorMsg ?? undefined };
|
|
29
|
+
return {
|
|
30
|
+
expiringClips: res.savedClips?.map((clip) => ({
|
|
31
|
+
...clip,
|
|
32
|
+
createdAtTimestamp: clip.createdAtMs
|
|
33
|
+
? new Date(clip.createdAtMs).toISOString()
|
|
34
|
+
: null,
|
|
35
|
+
})) ?? [],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export async function getSharedLiveStreams(_args, requestModifiers, sessionId) {
|
|
39
|
+
const res = await postApi({
|
|
40
|
+
route: "/camera/findAllSharedLiveVideoStreams",
|
|
41
|
+
body: {},
|
|
42
|
+
modifiers: requestModifiers,
|
|
43
|
+
sessionId,
|
|
44
|
+
});
|
|
45
|
+
if (res.error)
|
|
46
|
+
return { error: res.errorMsg ?? undefined };
|
|
47
|
+
return {
|
|
48
|
+
sharedLiveVideoStreams: res.sharedLiveVideoStreams ?? [],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export async function getTimelapseClips(_args, requestModifiers, sessionId) {
|
|
52
|
+
const res = await postApi({
|
|
53
|
+
route: "/video/getTimelapseClips",
|
|
54
|
+
body: {},
|
|
55
|
+
modifiers: requestModifiers,
|
|
56
|
+
sessionId,
|
|
57
|
+
});
|
|
58
|
+
if (res.error)
|
|
59
|
+
return { error: res.errorMsg ?? undefined };
|
|
60
|
+
return {
|
|
61
|
+
timelapseClips: res.timelapseClips ?? [],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export async function getClipGroups(requestModifiers, sessionId) {
|
|
65
|
+
const res = await postApi({
|
|
66
|
+
route: "/event/getClipGroupsForOrg",
|
|
67
|
+
body: {},
|
|
68
|
+
modifiers: requestModifiers,
|
|
69
|
+
sessionId,
|
|
70
|
+
});
|
|
71
|
+
if (res.error)
|
|
72
|
+
throw new Error(JSON.stringify(res));
|
|
73
|
+
return {
|
|
74
|
+
clipGroups: (res.clipGroups || []).map((group) => ({
|
|
75
|
+
uuid: group.uuid ?? undefined,
|
|
76
|
+
name: group.name ?? undefined,
|
|
77
|
+
clipCount: group.clipCount ?? undefined,
|
|
78
|
+
})),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export async function getSharedClipGroups(requestModifiers, sessionId) {
|
|
82
|
+
const res = await postApi({
|
|
83
|
+
route: "/event/getSharedClipGroupsV2",
|
|
84
|
+
body: {},
|
|
85
|
+
modifiers: requestModifiers,
|
|
86
|
+
sessionId,
|
|
87
|
+
});
|
|
88
|
+
if (res.error)
|
|
89
|
+
throw new Error(JSON.stringify(res));
|
|
90
|
+
return {
|
|
91
|
+
sharedClipGroups: (res.sharedClipGroups || []).map((group) => ({
|
|
92
|
+
uuid: group.uuid ?? undefined,
|
|
93
|
+
name: group.title ?? undefined,
|
|
94
|
+
})),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export async function createClip(cameraUuid, startTimeMs, endTimeMs, requestModifiers, sessionId) {
|
|
98
|
+
const durationSec = Math.round((endTimeMs - startTimeMs) / 1000);
|
|
99
|
+
const body = {
|
|
100
|
+
deviceUuids: [cameraUuid],
|
|
101
|
+
startTimeMillis: startTimeMs,
|
|
102
|
+
durationSec,
|
|
103
|
+
saveToConsole: true,
|
|
104
|
+
};
|
|
105
|
+
const res = await postApi({
|
|
106
|
+
route: "/video/spliceV3",
|
|
107
|
+
body,
|
|
108
|
+
modifiers: requestModifiers,
|
|
109
|
+
sessionId,
|
|
110
|
+
});
|
|
111
|
+
if (res.error)
|
|
112
|
+
throw new Error(JSON.stringify(res));
|
|
113
|
+
return { spliceResult: { success: true, clipUuid: res.clipUuid ?? undefined } };
|
|
114
|
+
}
|
|
115
|
+
export async function deleteClip(clipUuid, requestModifiers, sessionId) {
|
|
116
|
+
const res = await postApi({
|
|
117
|
+
route: "/event/deleteSavedClip",
|
|
118
|
+
body: { savedClipUuid: clipUuid },
|
|
119
|
+
modifiers: requestModifiers,
|
|
120
|
+
sessionId,
|
|
121
|
+
});
|
|
122
|
+
if (res.error)
|
|
123
|
+
throw new Error(JSON.stringify(res));
|
|
124
|
+
return { deleteResult: { success: true } };
|
|
36
125
|
}
|