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
@@ -1,6 +1,123 @@
1
1
  import { logger } from "../logger.js";
2
2
  import { postApi } from "../network.js";
3
3
  export async function createVideoWall(options, requestModifiers, sessionId) {
4
+ // our grid layout settings is a little complicated. we'll dumb it down
5
+ // to certain presets based on length of device list
6
+ const numDevices = options?.settings.numVisibleDevicesAtOnce ??
7
+ options?.deviceList?.length ??
8
+ 1;
9
+ let gridSettings;
10
+ if (numDevices <= 1) {
11
+ gridSettings = {
12
+ gridLayout: "1",
13
+ gridSize: {
14
+ width: 1,
15
+ height: 1,
16
+ },
17
+ };
18
+ }
19
+ else if (numDevices <= 2) {
20
+ gridSettings = {
21
+ gridLayout: "1\n2",
22
+ gridSize: {
23
+ width: 1,
24
+ height: 2,
25
+ },
26
+ };
27
+ }
28
+ else if (numDevices <= 3) {
29
+ gridSettings = {
30
+ gridLayout: "1 2\n1 3",
31
+ gridSize: {
32
+ width: 2,
33
+ height: 2,
34
+ },
35
+ };
36
+ }
37
+ else if (numDevices <= 4) {
38
+ gridSettings = {
39
+ gridLayout: "1 2\n3 4",
40
+ gridSize: {
41
+ width: 4,
42
+ height: 4,
43
+ },
44
+ };
45
+ }
46
+ else if (numDevices <= 5) {
47
+ gridSettings = {
48
+ gridLayout: "1 1 1 2\n 1 1 1 3\n 1 1 1 4\n 1 1 1 5",
49
+ gridSize: {
50
+ width: 4,
51
+ height: 4,
52
+ },
53
+ };
54
+ }
55
+ else if (numDevices <= 6) {
56
+ gridSettings = {
57
+ gridLayout: "1 2 3\n4 5 6",
58
+ gridSize: {
59
+ width: 3,
60
+ height: 2,
61
+ },
62
+ };
63
+ }
64
+ else if (numDevices <= 7) {
65
+ gridSettings = {
66
+ gridLayout: "1 1 2\n 1 1 3\n 4 5 6",
67
+ gridSize: {
68
+ width: 3,
69
+ height: 3,
70
+ },
71
+ };
72
+ }
73
+ else if (numDevices <= 9) {
74
+ gridSettings = {
75
+ gridLayout: "1 2 3\n4 5 6\n7 8 9",
76
+ gridSize: { width: 3, height: 3 },
77
+ };
78
+ }
79
+ else if (numDevices <= 12) {
80
+ gridSettings = {
81
+ gridLayout: "1 2 3 4\n5 6 7 8\n9 10 11 12",
82
+ gridSize: { width: 4, height: 3 },
83
+ };
84
+ }
85
+ else if (numDevices <= 16) {
86
+ gridSettings = {
87
+ gridLayout: "1 2 3 4\n5 6 7 8\n9 10 11 12\n13 14 15 16",
88
+ gridSize: { width: 4, height: 4 },
89
+ };
90
+ }
91
+ else if (numDevices <= 25) {
92
+ gridSettings = {
93
+ gridLayout: "1 2 3 4 5\n6 7 8 9 10\n11 12 13 14 15\n16 17 18 19 20\n21 22 23 24 25",
94
+ gridSize: { width: 5, height: 5 },
95
+ };
96
+ }
97
+ else if (numDevices <= 30) {
98
+ gridSettings = {
99
+ gridLayout: "1 2 3 4 5 6\n7 8 9 10 11 12\n13 14 15 16 17 18\n19 20 21 22 23 24\n25 26 27 28 29 30",
100
+ gridSize: { width: 6, height: 5 },
101
+ };
102
+ }
103
+ else if (numDevices <= 36) {
104
+ gridSettings = {
105
+ gridLayout: "1 2 3 4 5 6\n7 8 9 10 11 12\n13 14 15 16 17 18\n19 20 21 22 23 24\n25 26 27 28 29 30\n31 32 33 34 35 36",
106
+ gridSize: { width: 6, height: 6 },
107
+ };
108
+ }
109
+ else if (numDevices <= 49) {
110
+ gridSettings = {
111
+ gridLayout: "1 2 3 4 5 6 7\n8 9 10 11 12 13 14\n15 16 17 18 19 20 21\n22 23 24 25 26 27 28\n29 30 31 32 33 34 35\n36 37 38 39 40 41 42\n43 44 45 46 47 48 49",
112
+ gridSize: { width: 7, height: 7 },
113
+ };
114
+ }
115
+ else {
116
+ gridSettings = {
117
+ gridLayout: "1 2 3 4 5 6 7 8\n9 10 11 12 13 14 15 16\n17 18 19 20 21 22 23 24\n25 26 27 28 29 30 31 32\n33 34 35 36 37 38 39 40\n41 42 43 44 45 46 47 48\n49 50 51 52 53 54 55 56\n57 58 59 60 61 62 63 64",
118
+ gridSize: { width: 8, height: 8 },
119
+ };
120
+ }
4
121
  const body = {
5
122
  videoWall: {
6
123
  displayName: options?.displayName,
@@ -9,8 +126,7 @@ export async function createVideoWall(options, requestModifiers, sessionId) {
9
126
  orgUuid: options?.orgUuid,
10
127
  shared: true,
11
128
  settings: {
12
- gridSize: { width: options?.settings.columnCount, height: options?.settings.columnCount },
13
- gridLayout: "1 2\n3 4",
129
+ ...gridSettings,
14
130
  intervalSeconds: options?.settings.intervalSeconds || 5,
15
131
  },
16
132
  },
@@ -23,31 +139,42 @@ export async function createVideoWall(options, requestModifiers, sessionId) {
23
139
  });
24
140
  return response;
25
141
  }
142
+ export async function getVideoWalls(requestModifiers, sessionId) {
143
+ const response = await postApi({
144
+ route: "/camera/getVideoWalls",
145
+ body: {},
146
+ modifiers: requestModifiers,
147
+ sessionId,
148
+ });
149
+ if (response?.error) {
150
+ return {
151
+ error: response.errorMsg ?? "No error message.",
152
+ };
153
+ }
154
+ return {
155
+ videoWalls: response?.videoWalls ?? [],
156
+ };
157
+ }
26
158
  export async function handleCreateVideoWallRequest(videoWallCreateOptions, requestModifiers, sessionId) {
27
- let text = "Unable to create video wall!";
28
159
  logger.info("🔨 Creating video wall");
29
160
  if (!videoWallCreateOptions?.displayName) {
30
- text = JSON.stringify({
161
+ return {
31
162
  needUserInput: true,
32
163
  commandForUser: "What should the name of the video wall be?",
33
- });
164
+ };
34
165
  }
35
166
  else if ((videoWallCreateOptions?.deviceList || []).length === 0) {
36
- text = JSON.stringify({
167
+ return {
37
168
  needUserInput: true,
38
169
  commandForUser: "Which cameras would you like on this video wall?",
39
- });
170
+ };
40
171
  }
41
172
  else {
42
173
  logger.info("Creating video wall with options: ", JSON.stringify(videoWallCreateOptions));
43
- text = JSON.stringify(await createVideoWall(videoWallCreateOptions, requestModifiers, sessionId));
174
+ const response = await createVideoWall(videoWallCreateOptions, requestModifiers, sessionId);
175
+ return {
176
+ uuid: response?.uuid ?? undefined,
177
+ error: response?.errorMsg ?? undefined,
178
+ };
44
179
  }
45
- return Promise.resolve({
46
- content: [
47
- {
48
- type: "text",
49
- text,
50
- },
51
- ],
52
- });
53
180
  }
@@ -0,0 +1,66 @@
1
+ import { postApi } from "../network.js";
2
+ export async function getDoorControllerRules(doorControllerUuid, requestModifiers, sessionId) {
3
+ const res = await postApi({
4
+ route: "/doorcontroller/getDoorControllerRules",
5
+ body: { doorControllerUuid },
6
+ modifiers: requestModifiers,
7
+ sessionId,
8
+ });
9
+ if (res.error) {
10
+ throw new Error(JSON.stringify(res));
11
+ }
12
+ return (res.rules?.map((rule) => ({
13
+ uuid: rule.uuid ?? undefined,
14
+ name: rule.name ?? undefined,
15
+ deviceUuid: rule.deviceUuid ?? undefined,
16
+ enabled: rule.enabled ?? undefined,
17
+ ruleType: rule.ruleType ?? undefined,
18
+ })) ?? []);
19
+ }
20
+ export async function createDoorControllerRule(ruleConfig, requestModifiers, sessionId) {
21
+ const res = await postApi({
22
+ route: "/doorcontroller/createDoorControllerRule",
23
+ body: ruleConfig,
24
+ modifiers: requestModifiers,
25
+ sessionId,
26
+ });
27
+ if (res.error) {
28
+ throw new Error(JSON.stringify(res));
29
+ }
30
+ return {
31
+ uuid: res.ruleUuid ?? undefined,
32
+ success: true,
33
+ };
34
+ }
35
+ export async function getDoorPolicies(requestModifiers, sessionId) {
36
+ const res = await postApi({
37
+ route: "/policy/getDoorPolicies",
38
+ body: {},
39
+ modifiers: requestModifiers,
40
+ sessionId,
41
+ });
42
+ if (res.error) {
43
+ throw new Error(JSON.stringify(res));
44
+ }
45
+ return (res.policies?.map((policy) => ({
46
+ uuid: policy.uuid ?? undefined,
47
+ name: policy.name ?? undefined,
48
+ orgUuid: policy.orgUuid ?? undefined,
49
+ enabled: policy.enabled ?? undefined,
50
+ })) ?? []);
51
+ }
52
+ export async function createDoorPolicy(policyConfig, requestModifiers, sessionId) {
53
+ const res = await postApi({
54
+ route: "/policy/createDoorPolicy",
55
+ body: policyConfig,
56
+ modifiers: requestModifiers,
57
+ sessionId,
58
+ });
59
+ if (res.error) {
60
+ throw new Error(JSON.stringify(res));
61
+ }
62
+ return {
63
+ uuid: res.policyUuid ?? undefined,
64
+ success: true,
65
+ };
66
+ }
@@ -8,6 +8,73 @@ export const HumanEvent = z.object({
8
8
  timestamp: z.number(),
9
9
  id: z.number(),
10
10
  });
11
+ const ACCESS_CONTROL_EVENT_BATCH_SIZE = 1000;
12
+ const ACCESS_CONTROL_EVENT_TYPE_FILTER = ["CredentialReceivedEvent"];
13
+ function mapAccessControlEvent(credEvent, timeZone) {
14
+ return {
15
+ authenticationResult: credEvent?.authenticationResult,
16
+ authorizationResult: credEvent?.authorizationResult,
17
+ doorUuid: credEvent?.componentCompositeUuid,
18
+ locationUuid: credEvent?.locationUuid,
19
+ user: credEvent?.originator?.username,
20
+ credSource: credEvent?.credSource,
21
+ timestampMs: credEvent?.timestampMs,
22
+ datetime: credEvent?.timestampMs ? formatTimestamp(credEvent.timestampMs, timeZone) : undefined,
23
+ };
24
+ }
25
+ async function getAccessControlEventsForDoor(doorUuid, startTime, endTime, timeZone, requestModifiers, sessionId) {
26
+ const allEvents = [];
27
+ let createdBeforeMs = endTime;
28
+ while (true) {
29
+ const body = {
30
+ accessControlledDoorUuid: doorUuid,
31
+ ...(startTime !== undefined ? { createdAfterMs: startTime } : {}),
32
+ ...(createdBeforeMs !== undefined ? { createdBeforeMs } : {}),
33
+ limit: ACCESS_CONTROL_EVENT_BATCH_SIZE,
34
+ typeFilter: ACCESS_CONTROL_EVENT_TYPE_FILTER,
35
+ };
36
+ const response = await postApi({
37
+ route: "/component/findComponentEventsByAccessControlledDoor",
38
+ body,
39
+ modifiers: requestModifiers,
40
+ sessionId,
41
+ });
42
+ const componentEvents = response.componentEvents || [];
43
+ if (componentEvents.length === 0) {
44
+ break;
45
+ }
46
+ const mappedEvents = componentEvents
47
+ .map((credEvent) => mapAccessControlEvent(credEvent, timeZone))
48
+ .filter(Boolean);
49
+ allEvents.push(...mappedEvents);
50
+ if (componentEvents.length < ACCESS_CONTROL_EVENT_BATCH_SIZE) {
51
+ break;
52
+ }
53
+ const oldestTimestamp = componentEvents.reduce((oldest, event) => {
54
+ const ts = event?.timestampMs;
55
+ if (typeof ts !== "number") {
56
+ return oldest;
57
+ }
58
+ if (oldest === null || ts < oldest) {
59
+ return ts;
60
+ }
61
+ return oldest;
62
+ }, null);
63
+ if (oldestTimestamp === null) {
64
+ break;
65
+ }
66
+ // createdBeforeMs is exclusive. Moving the window to the oldest seen timestamp
67
+ // prevents duplicate pages and keeps fetching older events until the range is exhausted.
68
+ if (createdBeforeMs !== undefined && oldestTimestamp >= createdBeforeMs) {
69
+ break;
70
+ }
71
+ if (startTime !== undefined && oldestTimestamp <= startTime) {
72
+ break;
73
+ }
74
+ createdBeforeMs = oldestTimestamp;
75
+ }
76
+ return allEvents;
77
+ }
11
78
  export async function getFaceEvents(_locationUuid, timeZone, requestModifiers, sessionId) {
12
79
  const nowMs = Date.now();
13
80
  const rangeStartMs = nowMs - THREE_HOURS_MS;
@@ -47,36 +114,10 @@ export async function getFaceEvents(_locationUuid, timeZone, requestModifiers, s
47
114
  return response;
48
115
  }
49
116
  export async function getAccessControlEvents(doorUuids, startTime, endTime, timeZone, requestModifiers, sessionId) {
50
- const bodies = doorUuids.map(doorUuid => ({
51
- accessControlledDoorUuid: doorUuid,
52
- ...(startTime ? { createdAfterMs: startTime } : {}),
53
- ...(endTime ? { createdBeforeMs: endTime } : {}),
54
- typeFilter: ["CredentialReceivedEvent"],
55
- }));
56
- const responses = await Promise.all(bodies.map(body => postApi({
57
- route: "/component/findComponentEventsByAccessControlledDoor",
58
- body,
59
- modifiers: requestModifiers,
60
- sessionId,
61
- }).then(response => {
62
- return (response.componentEvents || [])
63
- .map((credEvent) => {
64
- return {
65
- authenticationResult: credEvent?.authenticationResult,
66
- authorizationResult: credEvent?.authorizationResult,
67
- doorUuid: credEvent?.componentCompositeUuid,
68
- locationUuid: credEvent?.locationUuid,
69
- user: credEvent?.originator?.username,
70
- credSource: credEvent?.credSource,
71
- datetime: credEvent?.timestampMs
72
- ? formatTimestamp(credEvent.timestampMs, timeZone)
73
- : undefined,
74
- };
75
- })
76
- .filter(Boolean);
77
- })));
117
+ const responses = await Promise.all(doorUuids.map(doorUuid => getAccessControlEventsForDoor(doorUuid, startTime, endTime, timeZone, requestModifiers, sessionId)));
78
118
  // Flatten all componentEvents into a single array
79
119
  const accessControlEvents = responses.flatMap(events => events);
120
+ accessControlEvents.sort((a, b) => (b.timestampMs || 0) - (a.timestampMs || 0));
80
121
  console.error(`componentEvents: ${JSON.stringify(accessControlEvents)}`);
81
122
  return accessControlEvents;
82
123
  }
@@ -318,3 +359,85 @@ startTime, endTime, timeZone, requestModifiers, sessionId) {
318
359
  console.error(`componentEvents: ${JSON.stringify(events)}`);
319
360
  return events;
320
361
  }
362
+ export async function getButtonPressEvents(sensorUuid, startTime, endTime, timeZone, requestModifiers, sessionId) {
363
+ const body = {
364
+ sensorUuid,
365
+ ...(startTime ? { createdAfterMs: startTime } : {}),
366
+ ...(endTime ? { createdBeforeMs: endTime } : {}),
367
+ };
368
+ const response = await postApi({
369
+ route: "/button/getButtonPressEventsForSensor",
370
+ body,
371
+ modifiers: requestModifiers,
372
+ sessionId,
373
+ });
374
+ return (response.events || []).map(event => ({
375
+ timestampMs: event?.timestampMs ?? undefined,
376
+ datetime: event?.timestampMs ? formatTimestamp(event.timestampMs, timeZone) : undefined,
377
+ sensorUuid: event?.componentUuid ?? undefined,
378
+ buttonState: event?.buttonPress ?? undefined,
379
+ }));
380
+ }
381
+ export async function getOccupancyEvents(sensorUuid, startTime, endTime, timeZone, requestModifiers, sessionId) {
382
+ const body = {
383
+ sensorUuid,
384
+ ...(startTime ? { createdAfterMs: startTime } : {}),
385
+ ...(endTime ? { createdBeforeMs: endTime } : {}),
386
+ };
387
+ const response = await postApi({
388
+ route: "/occupancy/getOccupancyEventsForSensor",
389
+ body,
390
+ modifiers: requestModifiers,
391
+ sessionId,
392
+ });
393
+ return (response.occupancyEvents || response.events || []).map(event => ({
394
+ timestampMs: event.timestampMs ?? undefined,
395
+ datetime: event.timestampMs ? formatTimestamp(event.timestampMs, timeZone) : undefined,
396
+ sensorUuid: event.sensorUuid ?? event.componentUuid ?? undefined,
397
+ count: event.count ?? undefined,
398
+ }));
399
+ }
400
+ export async function getProximityEvents(tagUuids, startTime, endTime, timeZone, requestModifiers, sessionId) {
401
+ const allEvents = [];
402
+ for (const tagUuid of tagUuids) {
403
+ const body = {
404
+ tagUuid,
405
+ ...(startTime ? { createdAfterMs: startTime } : {}),
406
+ ...(endTime ? { createdBeforeMs: endTime } : {}),
407
+ };
408
+ const response = await postApi({
409
+ route: "/proximity/getProximityEventsForTag",
410
+ body,
411
+ modifiers: requestModifiers,
412
+ sessionId,
413
+ });
414
+ const mapped = (response.proximityEvents || []).map(event => ({
415
+ timestampMs: event.startTimeMs ?? undefined,
416
+ datetime: event.startTimeMs ? formatTimestamp(event.startTimeMs, timeZone) : undefined,
417
+ tagUuid: event.bleDeviceUuid ?? undefined,
418
+ rssi: event.bleRssi ?? undefined,
419
+ }));
420
+ allEvents.push(...mapped);
421
+ }
422
+ return allEvents;
423
+ }
424
+ export async function getDoorbellEvents(doorbellCameraUuid, startTime, endTime, timeZone, requestModifiers, sessionId) {
425
+ const body = {
426
+ doorbellCameraUuid,
427
+ limit: 100,
428
+ ...(startTime ? { createdAfterMs: startTime } : {}),
429
+ ...(endTime ? { createdBeforeMs: endTime } : {}),
430
+ };
431
+ const response = await postApi({
432
+ route: "/doorbellcamera/findComponentEventsForDoorbellCamera",
433
+ body,
434
+ modifiers: requestModifiers,
435
+ sessionId,
436
+ });
437
+ return (response.componentEvents || []).map(event => ({
438
+ timestampMs: event.timestampMs ?? undefined,
439
+ datetime: event.timestampMs ? formatTimestamp(event.timestampMs, timeZone) : undefined,
440
+ doorbellCameraUuid: event.componentUuid ?? undefined,
441
+ eventType: event.type ?? undefined,
442
+ }));
443
+ }
@@ -2,103 +2,58 @@ import { postApi } from "../network.js";
2
2
  import { formatTimestamp } from "../util.js";
3
3
  import { removeNulls } from "../utils/remove-nulls.js";
4
4
  export async function getFaceEvents(args, timeZone, requestModifiers, sessionId) {
5
- let allFaceEvents = [];
6
- let currentArgs = { ...args };
7
- let hasMore = true;
8
- let lastResponse = null;
9
- while (hasMore) {
10
- // Filter out empty/undefined fields from currentArgs
11
- // OptionalNullable so that we can remove some fields before returning them to the MCP client
12
- let filteredArgs = { ...currentArgs };
13
- if (filteredArgs.pageRequest) {
14
- if (filteredArgs.pageRequest.lastEvaluatedKey === "") {
15
- delete filteredArgs.pageRequest.lastEvaluatedKey;
16
- }
5
+ // OptionalNullable so that we can remove some fields before sending to the API
6
+ let filteredArgs = { ...args };
7
+ if (filteredArgs.pageRequest && filteredArgs.pageRequest.lastEvaluatedKey === "") {
8
+ delete filteredArgs.pageRequest.lastEvaluatedKey;
9
+ }
10
+ if (filteredArgs.searchFilter) {
11
+ if (filteredArgs.searchFilter.faceNames &&
12
+ filteredArgs.searchFilter.faceNames.length === 0) {
13
+ // @ts-expect-error - we can break typing
14
+ delete filteredArgs.searchFilter.faceNames;
17
15
  }
18
- if (filteredArgs.searchFilter) {
19
- // Remove empty arrays
20
- // if (
21
- // filteredArgs.searchFilter.deviceUuids &&
22
- // filteredArgs.searchFilter.deviceUuids.length === 0
23
- // ) {
24
- // // @ts-expect-error - we can break typing
25
- // delete filteredArgs.searchFilter.deviceUuids;
26
- // }
27
- if (filteredArgs.searchFilter.faceNames && filteredArgs.searchFilter.faceNames.length === 0) {
28
- // @ts-expect-error - we can break typing
29
- delete filteredArgs.searchFilter.faceNames;
30
- }
31
- if (filteredArgs.searchFilter.labels && filteredArgs.searchFilter.labels.length === 0) {
32
- // @ts-expect-error - we can break typing
33
- delete filteredArgs.searchFilter.labels;
34
- }
35
- // Remove empty strings
36
- // if (filteredArgs.searchFilter.faceNameContains === "") {
37
- // delete filteredArgs.searchFilter.faceNameContains;
38
- // }
39
- // Remove false booleans
40
- if (filteredArgs.searchFilter.hasEmbedding === false) {
41
- delete filteredArgs.searchFilter.hasEmbedding;
42
- }
43
- if (filteredArgs.searchFilter.hasName === false) {
44
- delete filteredArgs.searchFilter.hasName;
45
- }
46
- // if there is no timestampFilter, we can remove it
47
- // rangeEnd and rangeStart are ISO 8601 strings
48
- if (filteredArgs.searchFilter.timestampFilter) {
49
- if (filteredArgs.searchFilter.timestampFilter.rangeEnd === null) {
50
- delete filteredArgs.searchFilter.timestampFilter.rangeEnd;
51
- }
52
- if (filteredArgs.searchFilter.timestampFilter.rangeStart === null) {
53
- delete filteredArgs.searchFilter.timestampFilter.rangeStart;
54
- }
55
- }
16
+ if (filteredArgs.searchFilter.labels &&
17
+ filteredArgs.searchFilter.labels.length === 0) {
18
+ // @ts-expect-error - we can break typing
19
+ delete filteredArgs.searchFilter.labels;
20
+ }
21
+ if (filteredArgs.searchFilter.hasEmbedding === false) {
22
+ delete filteredArgs.searchFilter.hasEmbedding;
56
23
  }
57
- // one last sweep of null values
58
- console.log("filteredArgs", filteredArgs);
59
- filteredArgs = removeNulls(filteredArgs);
60
- const response = await postApi({
61
- route: "/faceRecognition/faceEvent/findFaceEventsByOrg",
62
- body: filteredArgs,
63
- modifiers: requestModifiers,
64
- sessionId,
65
- });
66
- lastResponse = response;
67
- // Filter the faceEvents to only include the specified fields
68
- if (response && response.faceEvents) {
69
- const filteredEvents = response.faceEvents.map((event) => ({
70
- deviceUuid: event.deviceUuid,
71
- eventTimestampMs: event.eventTimestamp,
72
- eventTimestamp: formatTimestamp(event.eventTimestamp, timeZone),
73
- faceName: event.faceName,
74
- locationUuid: event.locationUuid,
75
- personUuid: event.personUuid,
76
- // selectedPersonMatch: event.selectedPersonMatch,
77
- thumbnailS3Key: event.thumbnailS3Key,
78
- // topPersonMatches: event.topPersonMatches,
79
- uuid: event.uuid,
80
- }));
81
- allFaceEvents.push(...filteredEvents);
82
- // Check if we should continue pagination
83
- if (response.faceEvents.length === 100 && response.lastEvaluatedKey) {
84
- // Update the pageRequest for the next iteration
85
- currentArgs = {
86
- ...currentArgs,
87
- pageRequest: {
88
- maxPageSize: currentArgs.pageRequest?.maxPageSize || 100,
89
- lastEvaluatedKey: response.lastEvaluatedKey,
90
- },
91
- };
24
+ if (filteredArgs.searchFilter.hasName === false) {
25
+ delete filteredArgs.searchFilter.hasName;
26
+ }
27
+ if (filteredArgs.searchFilter.timestampFilter) {
28
+ if (filteredArgs.searchFilter.timestampFilter.rangeEnd === null) {
29
+ delete filteredArgs.searchFilter.timestampFilter.rangeEnd;
92
30
  }
93
- else {
94
- hasMore = false;
31
+ if (filteredArgs.searchFilter.timestampFilter.rangeStart === null) {
32
+ delete filteredArgs.searchFilter.timestampFilter.rangeStart;
95
33
  }
96
34
  }
97
- else {
98
- hasMore = false;
99
- }
100
35
  }
101
- return allFaceEvents;
36
+ filteredArgs = removeNulls(filteredArgs);
37
+ const response = await postApi({
38
+ route: "/faceRecognition/faceEvent/findFaceEventsByOrg",
39
+ body: filteredArgs,
40
+ modifiers: requestModifiers,
41
+ sessionId,
42
+ });
43
+ const faceEvents = (response?.faceEvents ?? []).map((event) => ({
44
+ deviceUuid: event.deviceUuid,
45
+ eventTimestampMs: event.eventTimestamp,
46
+ eventTimestamp: formatTimestamp(event.eventTimestamp, timeZone),
47
+ faceName: event.faceName,
48
+ locationUuid: event.locationUuid,
49
+ personUuid: event.personUuid,
50
+ thumbnailS3Key: event.thumbnailS3Key,
51
+ uuid: event.uuid,
52
+ }));
53
+ return {
54
+ faceEvents,
55
+ lastEvaluatedKey: response?.lastEvaluatedKey,
56
+ };
102
57
  }
103
58
  export async function getRegisteredFaces(_args, requestModifiers, sessionId) {
104
59
  return await postApi({
@@ -108,3 +63,75 @@ export async function getRegisteredFaces(_args, requestModifiers, sessionId) {
108
63
  sessionId,
109
64
  });
110
65
  }
66
+ export async function getPersonLabels(requestModifiers, sessionId) {
67
+ return await postApi({
68
+ route: "/faceRecognition/person/findPersonLabelsByOrg",
69
+ body: {},
70
+ modifiers: requestModifiers,
71
+ sessionId,
72
+ });
73
+ }
74
+ export async function searchSimilarFaces(faceEventUuid, timeZone, requestModifiers, sessionId) {
75
+ const res = await postApi({
76
+ route: "/faceRecognition/faceEvent/findSimilarFaceEvents",
77
+ body: { faceEventUuid },
78
+ modifiers: requestModifiers,
79
+ sessionId,
80
+ });
81
+ if (res.error)
82
+ throw new Error(JSON.stringify(res));
83
+ return (res.faceEvents || []).map((event) => ({
84
+ uuid: event.uuid ?? undefined,
85
+ personUuid: event.personUuid ?? undefined,
86
+ similarity: event.similarity ?? undefined,
87
+ eventTimestamp: event.eventTimestamp
88
+ ? formatTimestamp(event.eventTimestamp, timeZone)
89
+ : undefined,
90
+ }));
91
+ }
92
+ export async function getFaceMatchmakers(requestModifiers, sessionId) {
93
+ const res = await postApi({
94
+ route: "/faceRecognition/matchmaker/findFaceMatchmakersByOrg",
95
+ body: {},
96
+ modifiers: requestModifiers,
97
+ sessionId,
98
+ });
99
+ if (res.error)
100
+ throw new Error(JSON.stringify(res));
101
+ return (res.faceMatchmakers || []).map((m) => ({
102
+ uuid: m.uuid ?? undefined,
103
+ personUuid: m.personUuid ?? undefined,
104
+ name: m.name ?? undefined,
105
+ }));
106
+ }
107
+ export async function getFaceEventsByPerson(personUuid, timeZone, requestModifiers, sessionId) {
108
+ const res = await postApi({
109
+ route: "/faceRecognition/faceEvent/findFaceEventsByOrg",
110
+ body: {
111
+ pageRequest: { maxPageSize: 75 },
112
+ searchFilter: {
113
+ personUuids: [personUuid],
114
+ deviceUuids: [],
115
+ faceNames: [],
116
+ labels: [],
117
+ locationUuids: [],
118
+ },
119
+ },
120
+ modifiers: requestModifiers,
121
+ sessionId,
122
+ });
123
+ if (res.error)
124
+ throw new Error(JSON.stringify(res));
125
+ const faceEvents = (res.faceEvents || []).map((event) => ({
126
+ uuid: event.uuid ?? undefined,
127
+ personUuid: event.personUuid ?? undefined,
128
+ eventTimestamp: event.eventTimestamp
129
+ ? formatTimestamp(event.eventTimestamp, timeZone)
130
+ : undefined,
131
+ deviceUuid: event.deviceUuid ?? undefined,
132
+ }));
133
+ return {
134
+ faceEvents,
135
+ lastEvaluatedKey: res.lastEvaluatedKey,
136
+ };
137
+ }
@@ -47,6 +47,8 @@ export async function getAccessControlledDoors(requestModifiers, sessionId) {
47
47
  geofenceEnabled: door.geofenceEnabled,
48
48
  locationUuid: door.locationUuid,
49
49
  policyUuid: door.policyUuid,
50
+ remoteUnlockEnabled: door.remoteUnlockEnabled,
51
+ associatedCameras: door.associatedCameras?.filter((c) => c !== null) ?? [],
50
52
  name: door.name,
51
53
  uuid: door.uuid,
52
54
  };