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.
Files changed (102) hide show
  1. package/dist/api/access-control-tool-api.js +231 -32
  2. package/dist/api/alarm-monitoring-tool-api.js +50 -1
  3. package/dist/api/automated-prompts-tool-api.js +9 -17
  4. package/dist/api/camera-policy-tool-api.js +122 -0
  5. package/dist/api/camera-tool-api.js +3 -5
  6. package/dist/api/camera-uptime-tool-api.js +62 -25
  7. package/dist/api/clips-tool-api.js +45 -9
  8. package/dist/api/create-tool-api.js +55 -7
  9. package/dist/api/door-tool-api.js +74 -13
  10. package/dist/api/entity-lookup-tool-api.js +8 -1
  11. package/dist/api/events-tool-api.js +79 -6
  12. package/dist/api/faces-tool-api.js +112 -7
  13. package/dist/api/get-accessible-apps.js +68 -6
  14. package/dist/api/get-entity-tool-api.js +12 -12
  15. package/dist/api/get-org-information-tool-api.js +2 -2
  16. package/dist/api/guest-management-tool-api.js +75 -1
  17. package/dist/api/location-tool-api.js +120 -28
  18. package/dist/api/lpr-tool-api.js +71 -16
  19. package/dist/api/policy-alerts-tool-api.js +7 -3
  20. package/dist/api/report-tool-api.js +4 -1
  21. package/dist/api/rules-tool-api.js +6 -16
  22. package/dist/api/schedule-tool-api.js +107 -0
  23. package/dist/api/search-tool-api.js +5 -13
  24. package/dist/api/update-tool-api.js +96 -1
  25. package/dist/api/user-tool-api.js +58 -13
  26. package/dist/createServer.js +18 -6
  27. package/dist/filtering-utils.js +421 -29
  28. package/dist/index.js +1 -1
  29. package/dist/network/network.js +122 -10
  30. package/dist/network/org-reference-cache.js +74 -0
  31. package/dist/telemetry/tracingProxy.js +4 -0
  32. package/dist/tools/entity-lookup-tool.js +6 -4
  33. package/dist/tools/get-entity-tool.js +40 -20
  34. package/dist/tools/get-org-information-tool.js +30 -2
  35. package/dist/tools/time-conversion-tool.js +59 -36
  36. package/dist/tools/time-tool.js +12 -3
  37. package/dist/tools/user-tool.js +100 -2
  38. package/dist/tools-console/access-anomaly-tool.js +2 -19
  39. package/dist/tools-console/access-control-tool.js +328 -2
  40. package/dist/tools-console/alarm-monitoring-tool.js +72 -2
  41. package/dist/tools-console/analytics-tool.js +2 -2
  42. package/dist/tools-console/badge-timeline-tool.js +2 -21
  43. package/dist/tools-console/camera-policy-tool.js +252 -0
  44. package/dist/tools-console/camera-tool.js +111 -89
  45. package/dist/tools-console/clips-tool.js +21 -31
  46. package/dist/tools-console/count-tool.js +8 -0
  47. package/dist/tools-console/create-camera-policy-tool.js +302 -122
  48. package/dist/tools-console/door-schedule-exception-tool.js +2 -33
  49. package/dist/tools-console/door-tool.js +130 -2
  50. package/dist/tools-console/elements-access-anomaly-tool.js +2 -19
  51. package/dist/tools-console/elements-badge-timeline-tool.js +2 -21
  52. package/dist/tools-console/elements-lost-badge-tool.js +2 -17
  53. package/dist/tools-console/elements-tool.js +2 -29
  54. package/dist/tools-console/events-tool.js +22 -101
  55. package/dist/tools-console/faces-tool.js +254 -28
  56. package/dist/tools-console/guest-management-tool.js +127 -2
  57. package/dist/tools-console/location-tool.js +156 -35
  58. package/dist/tools-console/lost-badge-tool.js +2 -17
  59. package/dist/tools-console/lpr-tool.js +89 -23
  60. package/dist/tools-console/netbox-access-anomaly-tool.js +2 -19
  61. package/dist/tools-console/netbox-badge-timeline-tool.js +2 -21
  62. package/dist/tools-console/netbox-lost-badge-tool.js +2 -17
  63. package/dist/tools-console/netbox-tool.js +2 -29
  64. package/dist/tools-console/onguard-tool.js +2 -30
  65. package/dist/tools-console/person-tracking-tool.js +2 -2
  66. package/dist/tools-console/policy-alerts-tool.js +5 -29
  67. package/dist/tools-console/reboot-cameras-tool.js +17 -2
  68. package/dist/tools-console/report-tool.js +10 -40
  69. package/dist/tools-console/schedule-tool.js +190 -0
  70. package/dist/tools-console/update-tool.js +298 -168
  71. package/dist/tools-console/video-walls-tool.js +93 -21
  72. package/dist/types/access-anomaly-tool-types.js +22 -2
  73. package/dist/types/access-control-tool-types.js +116 -1
  74. package/dist/types/alarm-monitoring-tool-types.js +42 -1
  75. package/dist/types/badge-timeline-tool-types.js +18 -3
  76. package/dist/types/camera-policy-tool-types.js +104 -0
  77. package/dist/types/camera-tool-types.js +80 -1
  78. package/dist/types/clips-tool-types.js +27 -3
  79. package/dist/types/create-camera-policy-tool-types.js +275 -26
  80. package/dist/types/door-schedule-exception-tool-types.js +20 -6
  81. package/dist/types/door-tool-types.js +36 -4
  82. package/dist/types/entity-lookup-tool-types.js +6 -1
  83. package/dist/types/events-tools-types.js +24 -16
  84. package/dist/types/faces-tools-types.js +106 -3
  85. package/dist/types/get-entity-tool-types.js +76 -12
  86. package/dist/types/guest-management-tool-types.js +103 -1
  87. package/dist/types/location-tool-types.js +100 -14
  88. package/dist/types/lost-badge-tool-types.js +17 -2
  89. package/dist/types/lpr-tool-types.js +47 -8
  90. package/dist/types/onguard-tool-types.js +21 -1
  91. package/dist/types/policy-alerts-tool-types.js +1 -1
  92. package/dist/types/report-tool-types.js +29 -1
  93. package/dist/types/schedule-tool-types.js +370 -0
  94. package/dist/types/schema.js +479 -9
  95. package/dist/types/time-conversion-tool-types.js +3 -4
  96. package/dist/types/update-tool-types.js +40 -18
  97. package/dist/types/user-tool-types.js +44 -1
  98. package/dist/types/video-walls-tool-types.js +30 -2
  99. package/dist/types/zod-schemas.js +5128 -996
  100. package/dist/util.js +62 -3
  101. package/dist/utils/entity-name-match.js +11 -0
  102. 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
- startTime: startTimeSec,
51
- endTime: endTimeSec,
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 postApi({
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 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);
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
- if (res.error)
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
- if (res.error)
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
- if (res.error)
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
- if (res.error)
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
- 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;
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
- if (res.error) {
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
- if (res.error) {
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
- if (res.error) {
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
- if (res.error) {
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
- async function getAccessControlEventsForDoor(doorUuid, startTime, endTime, timeZone, requestModifiers, sessionId) {
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
- return allEvents;
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(([brivoDoornId, info]) => ({
103
- brivoDoornId,
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
- console.error(`componentEvents: ${JSON.stringify(accessControlEvents)}`);
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
- console.error(`componentEvents: ${JSON.stringify(events)}`);
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) {