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.
Files changed (82) hide show
  1. package/README.md +8 -0
  2. package/dist/api/access-control-tool-api.js +256 -0
  3. package/dist/api/alarm-monitoring-tool-api.js +65 -0
  4. package/dist/api/camera-tool-api.js +35 -15
  5. package/dist/api/camera-uptime-tool-api.js +112 -0
  6. package/dist/api/clips-tool-api.js +110 -21
  7. package/dist/api/create-tool-api.js +143 -16
  8. package/dist/api/door-tool-api.js +66 -0
  9. package/dist/api/events-tool-api.js +151 -28
  10. package/dist/api/faces-tool-api.js +118 -91
  11. package/dist/api/get-entity-tool-api.js +2 -0
  12. package/dist/api/guest-management-tool-api.js +75 -0
  13. package/dist/api/location-tool-api.js +48 -1
  14. package/dist/api/lpr-tool-api.js +26 -0
  15. package/dist/api/policy-alerts-tool-api.js +54 -0
  16. package/dist/api/report-tool-api.js +345 -23
  17. package/dist/api/rules-tool-api.js +78 -0
  18. package/dist/api/search-tool-api.js +93 -0
  19. package/dist/api/time-tool-api.js +3 -1
  20. package/dist/api/update-tool-api.js +204 -0
  21. package/dist/api/user-access-trail-tool-api.js +45 -0
  22. package/dist/api/user-audit-tool-api.js +47 -0
  23. package/dist/api/user-tool-api.js +77 -0
  24. package/dist/createServer.js +3 -1
  25. package/dist/filtering-utils.js +298 -0
  26. package/dist/index.js +0 -9
  27. package/dist/logger.js +6 -5
  28. package/dist/network.js +18 -4
  29. package/dist/tools/access-control-tool.js +97 -0
  30. package/dist/tools/alarm-monitoring-tool.js +50 -0
  31. package/dist/tools/analytics-tool.js +383 -0
  32. package/dist/tools/camera-tool.js +27 -9
  33. package/dist/tools/camera-uptime-tool.js +50 -0
  34. package/dist/tools/clips-tool.js +46 -16
  35. package/dist/tools/door-tool.js +66 -0
  36. package/dist/tools/events-tool.js +103 -81
  37. package/dist/tools/faces-tool.js +175 -32
  38. package/dist/tools/get-entity-tool.js +6 -1
  39. package/dist/tools/guest-management-tool.js +50 -0
  40. package/dist/tools/location-tool.js +23 -4
  41. package/dist/tools/lpr-tool.js +26 -20
  42. package/dist/tools/policy-alerts-tool.js +34 -4
  43. package/dist/tools/report-tool.js +183 -46
  44. package/dist/tools/rules-tool.js +76 -0
  45. package/dist/tools/search-tool.js +69 -0
  46. package/dist/tools/update-tool.js +63 -2
  47. package/dist/tools/user-access-trail-tool.js +64 -0
  48. package/dist/tools/user-audit-tool.js +53 -0
  49. package/dist/tools/user-tool.js +55 -0
  50. package/dist/tools/video-walls-tool.js +41 -0
  51. package/dist/transports/streamable-http.js +25 -15
  52. package/dist/types/access-control-tool-types.js +120 -0
  53. package/dist/types/alarm-monitoring-tool-types.js +60 -0
  54. package/dist/types/analytics-tool-types.js +190 -0
  55. package/dist/types/camera-tool-types.js +3 -2
  56. package/dist/types/camera-uptime-tool-types.js +52 -0
  57. package/dist/types/clips-tool-types.js +60 -11
  58. package/dist/types/door-tool-types.js +64 -0
  59. package/dist/types/events-tools-types.js +71 -3
  60. package/dist/types/faces-tools-types.js +36 -0
  61. package/dist/types/guest-management-tool-types.js +57 -0
  62. package/dist/types/location-tool-types.js +10 -1
  63. package/dist/types/lpr-tool-types.js +14 -0
  64. package/dist/types/policy-alerts-tool-types.js +38 -3
  65. package/dist/types/report-tool-types.js +284 -42
  66. package/dist/types/rules-tool-types.js +58 -0
  67. package/dist/types/schema.js +3123 -1
  68. package/dist/types/search-tool-types.js +82 -0
  69. package/dist/types/update-tool-types.js +4 -1
  70. package/dist/types/user-access-trail-tool-types.js +54 -0
  71. package/dist/types/user-audit-tool-types.js +48 -0
  72. package/dist/types/user-tool-types.js +61 -0
  73. package/dist/types/video-walls-tool-types.js +59 -0
  74. package/dist/types/zod-schemas.js +3237 -986
  75. package/dist/types.js +0 -21
  76. package/dist/util.js +34 -57
  77. package/package.json +7 -5
  78. package/dist/disabled-tools/semantic-search-tool.js +0 -90
  79. package/dist/tools/create-tool.js +0 -30
  80. package/dist/types/create-tool-types.js +0 -8
  81. package/dist/types/schema-components.js +0 -7211
  82. 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 { appendQueryParams, AUTH_HEADERS, postApi, STATIC_HEADERS } from "../network.js";
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
- // construct request headers
26
- let requestHeaders = {
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
- logger.trace(`Fetching with headers\n${JSON.stringify(requestHeaders)}`);
35
- return await fetch(res.frameUri, {
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
- return await postApi({
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
- return await postApi({
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
  }