rhombus-node-mcp 0.1.15 → 0.1.17

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 (80) hide show
  1. package/README.md +9 -1
  2. package/dist/api/camera-tool-api.js +161 -0
  3. package/dist/api/clips-tool-api.js +36 -0
  4. package/dist/api/create-camera-policy-tool-api.js +9 -0
  5. package/dist/api/create-tool-api.js +53 -0
  6. package/dist/api/entity-lookup-tool-api.js +65 -0
  7. package/dist/api/events-tool-api.js +320 -0
  8. package/dist/api/faces-tool-api.js +110 -0
  9. package/dist/api/get-entity-tool-api.js +176 -0
  10. package/dist/api/get-org-information-tool-api.js +9 -0
  11. package/dist/api/location-tool-api.js +9 -0
  12. package/dist/api/lpr-tool-api.js +68 -0
  13. package/dist/api/policy-alerts-tool-api.js +42 -0
  14. package/dist/api/reboot-cameras-tool-api.js +34 -0
  15. package/dist/api/report-tool-api.js +426 -0
  16. package/dist/api/time-tool-api.js +90 -0
  17. package/dist/api/update-tool-api.js +148 -0
  18. package/dist/createServer.js +7 -1
  19. package/dist/disabled-tools/endpoint-to-keys-tool.js +84 -0
  20. package/dist/disabled-tools/semantic-search-tool.js +90 -0
  21. package/dist/index.js +3 -5
  22. package/dist/logger.js +4 -3
  23. package/dist/network.js +42 -15
  24. package/dist/resources/routes.json.js +1 -1
  25. package/dist/services/embedding-service.js +153 -0
  26. package/dist/services/faiss-search-service.js +261 -0
  27. package/dist/tools/camera-tool.js +100 -0
  28. package/dist/tools/clips-tool.js +23 -37
  29. package/dist/tools/count-tool.js +25 -0
  30. package/dist/tools/create-camera-policy-tool.js +214 -0
  31. package/dist/tools/create-tool.js +25 -74
  32. package/dist/tools/entity-lookup-tool.js +36 -0
  33. package/dist/tools/events-tool.js +188 -92
  34. package/dist/tools/faces-tool.js +59 -132
  35. package/dist/tools/get-entity-tool.js +78 -0
  36. package/dist/tools/get-org-information-tool.js +18 -0
  37. package/dist/tools/location-tool.js +24 -31
  38. package/dist/tools/lpr-tool.js +79 -0
  39. package/dist/tools/policy-alerts-tool.js +34 -42
  40. package/dist/tools/reboot-cameras-tool.js +34 -0
  41. package/dist/tools/report-tool.js +190 -0
  42. package/dist/tools/time-conversion-tool.js +43 -0
  43. package/dist/tools/time-tool.js +17 -57
  44. package/dist/tools/update-tool.js +262 -0
  45. package/dist/transports/streamable-http.js +160 -57
  46. package/dist/{tools/devices/camera-tool/types.js → types/camera-tool-types.js} +52 -0
  47. package/dist/types/clips-tool-types.js +41 -0
  48. package/dist/types/create-camera-policy-tool-types.js +44 -0
  49. package/dist/types/create-tool-types.js +8 -0
  50. package/dist/types/deviceType.js +1 -0
  51. package/dist/types/endpoint-to-keys-tool-types.js +7 -0
  52. package/dist/types/entity-lookup-tool-types.js +70 -0
  53. package/dist/types/events-tools-types.js +257 -0
  54. package/dist/types/faces-tools-types.js +143 -0
  55. package/dist/types/get-entity-tool-types.js +25 -0
  56. package/dist/types/get-org-information-tool-types.js +3 -0
  57. package/dist/types/location-tool-types.js +11 -0
  58. package/dist/types/lpr-tool-types.js +97 -0
  59. package/dist/types/policy-alerts-tool-types.js +74 -0
  60. package/dist/types/reboot-cameras-tool-types.js +8 -0
  61. package/dist/types/report-tool-types.js +268 -0
  62. package/dist/types/schema-components.js +7093 -0
  63. package/dist/types/schema.js +1 -0
  64. package/dist/types/semantic-search-tool-types.js +5 -0
  65. package/dist/types/time-conversion-tool-types.js +8 -0
  66. package/dist/types/time-tool-types.js +11 -0
  67. package/dist/types/update-tool-types.js +186 -0
  68. package/dist/types/zod-schemas.js +21315 -0
  69. package/dist/types.js +17 -7
  70. package/dist/util.js +94 -2
  71. package/dist/utils/confirmation.js +1 -1
  72. package/dist/utils/reduce-output.js +35 -0
  73. package/dist/utils/remove-nulls.js +28 -0
  74. package/dist/utils/temp.js +8 -0
  75. package/dist/utils/timestampInput.js +12 -0
  76. package/package.json +23 -3
  77. package/dist/tools/devices/camera-tool/camera-tool.js +0 -218
  78. package/dist/tools/devices/get-entity-tool.js +0 -118
  79. package/dist/tools/get-org-information.js +0 -17
  80. package/dist/tools/reboot-cameras.js +0 -62
package/README.md CHANGED
@@ -129,7 +129,15 @@ Now, you'll need to adjust your `claude_desktop_config.json` to point to your ne
129
129
  }
130
130
  ```
131
131
 
132
- ### Hitting a Snag? We've Got You! 🛟
132
+ ## Creating a new tool
133
+
134
+ We've written a script that you can run to get kickstarted on developing a new tool in addition to our existing tool set. You can simply call:
135
+
136
+ ```sh
137
+ npm run create:tool
138
+ ```
139
+
140
+ ## Hitting a Snag? We've Got You! 🛟
133
141
 
134
142
  Check out Claude's fantastic [troubleshooting guide](https://modelcontextprotocol.io/docs/tools/debugging) for quick fixes! Still stuck? Our team of experts is ready to help!
135
143
 
@@ -0,0 +1,161 @@
1
+ import { getLogger } from "../logger.js";
2
+ import { appendQueryParams, AUTH_HEADERS, postApi, STATIC_HEADERS } from "../network.js";
3
+ import { removeNullFields } from "../util.js";
4
+ const logger = getLogger("camera-tool");
5
+ const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
6
+ export async function getImageForCameraAtTime(cameraUuid, timestampMs, requestModifiers, sessionId) {
7
+ const body = {
8
+ cameraUuid: cameraUuid,
9
+ downscaleFactor: 10,
10
+ jpgQuality: 70,
11
+ permyriadCropHeight: 10000,
12
+ permyriadCropWidth: 5625,
13
+ permyriadCropX: 2188,
14
+ permyriadCropY: 0,
15
+ timestampMs: timestampMs,
16
+ };
17
+ logger.debug(`Getting frameUri from UUID: ${cameraUuid} at timestampMs: ${timestampMs}`);
18
+ const base64Image = await postApi({
19
+ route: "/video/getExactFrameUri",
20
+ body,
21
+ modifiers: requestModifiers,
22
+ sessionId,
23
+ }).then(async (res) => {
24
+ 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);
33
+ }
34
+ logger.trace(`Fetching with headers\n${JSON.stringify(requestHeaders)}`);
35
+ return await fetch(res.frameUri, {
36
+ method: "GET",
37
+ headers: requestHeaders,
38
+ }).then(async (res) => {
39
+ if (!res.ok) {
40
+ logger.error(`Failed to fetch image: ${await res.text()}`);
41
+ logger.error(res);
42
+ return null;
43
+ }
44
+ const arrayBuffer = await res.arrayBuffer();
45
+ const buffer = Buffer.from(arrayBuffer);
46
+ const base64 = buffer.toString("base64");
47
+ logger.debug(`Received image base64:\n ${base64}`);
48
+ return base64;
49
+ });
50
+ });
51
+ if (!base64Image) {
52
+ return {
53
+ success: false,
54
+ status: "failed to fetch image",
55
+ };
56
+ }
57
+ return {
58
+ success: true,
59
+ status: "successfully fetched image",
60
+ imageType: "base64",
61
+ imageData: base64Image,
62
+ };
63
+ }
64
+ async function getCameraStorageData(cameraUuid, requestModifiers, sessionId) {
65
+ // First, get the camera's full state to determine the time range and cloud archive days
66
+ const stateResponse = await postApi({
67
+ route: "/camera/getFullCameraState",
68
+ body: {
69
+ cameraUuid: cameraUuid,
70
+ },
71
+ modifiers: requestModifiers,
72
+ sessionId,
73
+ });
74
+ const cloudArchiveDays = stateResponse.fullCameraState?.onCloudState?.cloud_archive_days ?? null;
75
+ // Get the oldest segment time (in seconds) to use as start time
76
+ const oldestSegmentSecs = stateResponse.fullCameraState?.onCameraState?.oldest_segment_secs ?? 0;
77
+ const endTimeSec = Math.floor(Date.now() / 1000);
78
+ const startTimeSec = oldestSegmentSecs || 0;
79
+ const durationSec = endTimeSec - startTimeSec;
80
+ const presenceResponse = await postApi({
81
+ route: "/camera/getPresenceWindows",
82
+ body: {
83
+ cameraUuid: cameraUuid,
84
+ startTimeSec: startTimeSec,
85
+ durationSec: durationSec,
86
+ },
87
+ modifiers: requestModifiers,
88
+ sessionId,
89
+ });
90
+ const calculateTotalDays = (timeWindows) => {
91
+ if (!timeWindows || timeWindows.length === 0) {
92
+ return 0;
93
+ }
94
+ let totalMilliseconds = 0;
95
+ timeWindows.forEach(window => {
96
+ if (window.startSeconds && window.durationSeconds) {
97
+ const durationMs = window.durationSeconds * 1000;
98
+ totalMilliseconds += durationMs;
99
+ }
100
+ });
101
+ return Math.floor(totalMilliseconds / MILLISECONDS_PER_DAY);
102
+ };
103
+ const presenceWindows = presenceResponse.presenceWindows;
104
+ const daysOnCamera = calculateTotalDays(presenceWindows?.VideoLocal);
105
+ const daysInCloud = calculateTotalDays(presenceWindows?.VideoCloud);
106
+ return {
107
+ daysInCloud,
108
+ daysOnCamera,
109
+ cloudArchiveDays,
110
+ };
111
+ }
112
+ export async function getCameraSettings(cameraUuid, requestModifiers, sessionId) {
113
+ const res = await postApi({
114
+ route: "/camera/getFacetedConfig",
115
+ body: {
116
+ deviceUuid: cameraUuid,
117
+ },
118
+ modifiers: requestModifiers,
119
+ sessionId,
120
+ });
121
+ if (res.error) {
122
+ return {
123
+ success: false,
124
+ status: "failed to fetch camera settings",
125
+ };
126
+ }
127
+ const storageData = await getCameraStorageData(cameraUuid, requestModifiers, sessionId);
128
+ return {
129
+ success: true,
130
+ config: res.config,
131
+ daysInCloud: storageData.daysInCloud,
132
+ daysOnCamera: storageData.daysOnCamera,
133
+ cloudArchiveDays: storageData.cloudArchiveDays,
134
+ status: "fetched camera settings",
135
+ };
136
+ }
137
+ export async function updateCameraSettings(cameraUuid, update, requestModifiers, sessionId) {
138
+ // remove any "null" values
139
+ const res = await postApi({
140
+ route: "/camera/updateFacetedConfig",
141
+ body: {
142
+ configUpdate: {
143
+ deviceUuid: cameraUuid,
144
+ ...removeNullFields(update),
145
+ },
146
+ },
147
+ modifiers: requestModifiers,
148
+ sessionId,
149
+ });
150
+ if (res.error) {
151
+ return {
152
+ success: false,
153
+ status: "failed to update camera settings",
154
+ };
155
+ }
156
+ return {
157
+ success: true,
158
+ config: res.config,
159
+ status: "updated camera settings",
160
+ };
161
+ }
@@ -0,0 +1,36 @@
1
+ import { postApi } from "../network.js";
2
+ export async function getSavedClips(args, requestModifiers, sessionId) {
3
+ return await postApi({
4
+ route: "/event/getClipsWithProgress",
5
+ body: args,
6
+ modifiers: requestModifiers,
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
+ });
17
+ }
18
+ export async function getExpiringClips(args, requestModifiers, sessionId) {
19
+ return await postApi({
20
+ route: "/event/getExpiringClipsForOrg",
21
+ body: args,
22
+ modifiers: requestModifiers,
23
+ 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
+ });
36
+ }
@@ -0,0 +1,9 @@
1
+ import { postApi } from "../network.js";
2
+ export async function createCameraPolicy(args, requestModifiers, sessionId) {
3
+ return await postApi({
4
+ route: "/policy/createCameraPolicy",
5
+ body: args,
6
+ modifiers: requestModifiers,
7
+ sessionId,
8
+ });
9
+ }
@@ -0,0 +1,53 @@
1
+ import { logger } from "../logger.js";
2
+ import { postApi } from "../network.js";
3
+ export async function createVideoWall(options, requestModifiers, sessionId) {
4
+ const body = {
5
+ videoWall: {
6
+ displayName: options?.displayName,
7
+ deviceList: options?.deviceList,
8
+ othersCanEdit: true,
9
+ orgUuid: options?.orgUuid,
10
+ shared: true,
11
+ settings: {
12
+ gridSize: { width: options?.settings.columnCount, height: options?.settings.columnCount },
13
+ gridLayout: "1 2\n3 4",
14
+ intervalSeconds: options?.settings.intervalSeconds || 5,
15
+ },
16
+ },
17
+ };
18
+ const response = await postApi({
19
+ route: "/camera/createVideoWall",
20
+ body,
21
+ modifiers: requestModifiers,
22
+ sessionId,
23
+ });
24
+ return response;
25
+ }
26
+ export async function handleCreateVideoWallRequest(videoWallCreateOptions, requestModifiers, sessionId) {
27
+ let text = "Unable to create video wall!";
28
+ logger.info("🔨 Creating video wall");
29
+ if (!videoWallCreateOptions?.displayName) {
30
+ text = JSON.stringify({
31
+ needUserInput: true,
32
+ commandForUser: "What should the name of the video wall be?",
33
+ });
34
+ }
35
+ else if ((videoWallCreateOptions?.deviceList || []).length === 0) {
36
+ text = JSON.stringify({
37
+ needUserInput: true,
38
+ commandForUser: "Which cameras would you like on this video wall?",
39
+ });
40
+ }
41
+ else {
42
+ logger.info("Creating video wall with options: ", JSON.stringify(videoWallCreateOptions));
43
+ text = JSON.stringify(await createVideoWall(videoWallCreateOptions, requestModifiers, sessionId));
44
+ }
45
+ return Promise.resolve({
46
+ content: [
47
+ {
48
+ type: "text",
49
+ text,
50
+ },
51
+ ],
52
+ });
53
+ }
@@ -0,0 +1,65 @@
1
+ import { postApi } from "../network.js";
2
+ import { getAccessControlledDoors, getAudioGateways, getBadgeReaders, getButtons, getCameraList, getDoorbellCameras, getDoorSensors, getEnvironmentalGateways, getEnvironmentalSensors, getKeypads, getMotionSensors, } from "./get-entity-tool-api.js";
3
+ async function getDeviceFeatures(deviceUuid, requestModifiers, sessionId) {
4
+ const response = await postApi({
5
+ route: "/feature/getDeviceFeatures",
6
+ body: { deviceUuid },
7
+ modifiers: requestModifiers,
8
+ sessionId,
9
+ });
10
+ if (response.error || !response.features) {
11
+ return null;
12
+ }
13
+ return {
14
+ assignedLicense: response.features.assignedLicense,
15
+ featureMap: response.features.featureMap,
16
+ };
17
+ }
18
+ export async function getAllEntities(deviceUuids, timeZone, tempUnit, requestModifiers, sessionId) {
19
+ const promises = [
20
+ getCameraList(requestModifiers, sessionId),
21
+ getDoorbellCameras(requestModifiers, sessionId),
22
+ getBadgeReaders(requestModifiers, sessionId),
23
+ getAccessControlledDoors(requestModifiers, sessionId),
24
+ getAudioGateways(timeZone, requestModifiers, sessionId),
25
+ getDoorSensors(requestModifiers, sessionId),
26
+ getEnvironmentalSensors(timeZone, tempUnit, requestModifiers, sessionId),
27
+ getMotionSensors(requestModifiers, sessionId),
28
+ getButtons(timeZone, requestModifiers, sessionId),
29
+ getKeypads(requestModifiers, sessionId),
30
+ getEnvironmentalGateways(timeZone, requestModifiers, sessionId),
31
+ ];
32
+ const responses = await Promise.all(promises);
33
+ // Filter each response to only include devices with matching UUIDs and fetch device features
34
+ for (let i = 0; i < responses.length; i++) {
35
+ const response = responses[i];
36
+ // Look through keys and find any with an array
37
+ for (const key of Object.keys(response)) {
38
+ const value = response[key];
39
+ if (Array.isArray(value)) {
40
+ // Filter to only include devices with matching UUIDs
41
+ const filteredDevices = value.filter((item) => {
42
+ return deviceUuids.includes(item.uuid);
43
+ });
44
+ // Fetch device features for each filtered device
45
+ const devicesWithFeatures = await Promise.all(filteredDevices.map(async (device) => {
46
+ const features = await getDeviceFeatures(device.uuid, requestModifiers, sessionId);
47
+ return {
48
+ ...device,
49
+ ...(features && {
50
+ assignedLicense: features.assignedLicense,
51
+ featureMap: features.featureMap,
52
+ }),
53
+ };
54
+ }));
55
+ response[key] = devicesWithFeatures;
56
+ response[`${key}Count`] = devicesWithFeatures.length;
57
+ }
58
+ }
59
+ }
60
+ const result = {};
61
+ for (const response of responses) {
62
+ Object.assign(result, response);
63
+ }
64
+ return result;
65
+ }
@@ -0,0 +1,320 @@
1
+ import z from "zod";
2
+ import { FIVE_SECONDS_MS, THREE_HOURS_MS } from "../constants.js";
3
+ import { postApi } from "../network.js";
4
+ import { formatTimestamp } from "../util.js";
5
+ import { tempFunc, TempUnit } from "../utils/temp.js";
6
+ // Type definitions
7
+ export const HumanEvent = z.object({
8
+ timestamp: z.number(),
9
+ id: z.number(),
10
+ });
11
+ export async function getFaceEvents(_locationUuid, timeZone, requestModifiers, sessionId) {
12
+ const nowMs = Date.now();
13
+ const rangeStartMs = nowMs - THREE_HOURS_MS;
14
+ const rangeEndMs = nowMs - FIVE_SECONDS_MS;
15
+ const body = {
16
+ pageRequest: {
17
+ lastEvaluatedKey: undefined,
18
+ maxPageSize: 75,
19
+ },
20
+ searchFilter: {
21
+ deviceUuids: [],
22
+ faceNames: [],
23
+ labels: [],
24
+ locationUuids: [],
25
+ personUuids: [],
26
+ timestampFilter: {
27
+ rangeStart: rangeStartMs,
28
+ rangeEnd: rangeEndMs,
29
+ },
30
+ },
31
+ };
32
+ const response = await postApi({
33
+ route: "/faceRecognition/faceEvent/findFaceEventsByOrg",
34
+ body,
35
+ modifiers: requestModifiers,
36
+ sessionId,
37
+ }).then(response => {
38
+ return {
39
+ faceEvents: (response.faceEvents || []).map(event => ({
40
+ ...event,
41
+ eventTimestamp: event.eventTimestamp
42
+ ? formatTimestamp(event.eventTimestamp, timeZone)
43
+ : undefined,
44
+ })),
45
+ };
46
+ });
47
+ return response;
48
+ }
49
+ 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
+ })));
78
+ // Flatten all componentEvents into a single array
79
+ const accessControlEvents = responses.flatMap(events => events);
80
+ console.error(`componentEvents: ${JSON.stringify(accessControlEvents)}`);
81
+ return accessControlEvents;
82
+ }
83
+ export async function getHumanMotionEvents(cameraUuid, duration, startTime, requestModifiers, sessionId) {
84
+ const body = {
85
+ cameraUuid,
86
+ duration,
87
+ startTime: Math.round(startTime / 1000),
88
+ };
89
+ const response = await postApi({
90
+ route: "/camera/getFootageSeekpointsV2",
91
+ body,
92
+ modifiers: requestModifiers,
93
+ sessionId,
94
+ }).then(response => {
95
+ const seekPoints = response.footageSeekPoints || [];
96
+ const uniqueHumanEvents = seekPoints
97
+ .reduceRight((acc, point) => {
98
+ if (point.a === "MOTION_HUMAN" &&
99
+ typeof point.ts === "number" &&
100
+ typeof point.id === "number" &&
101
+ !acc.some(existing => existing.id === point.id)) {
102
+ acc.unshift({ timestamp: point.ts, id: point.id });
103
+ }
104
+ return acc;
105
+ }, [])
106
+ .map(event => ({
107
+ timestamp: event.timestamp,
108
+ id: event.id,
109
+ }));
110
+ return { cameraUuid, uniqueHumanEvents };
111
+ });
112
+ return response;
113
+ }
114
+ const EVENT_COUNT_MAX_PER_RESPONSE = 2000;
115
+ export async function getEventsForEnvironmentalGateway(deviceUuid, startTime, endTime, timeZone, tempUnit, requestModifiers, sessionId) {
116
+ let allEvents = [];
117
+ let hasMore = true;
118
+ let lastEvaluatedKey = undefined;
119
+ while (hasMore) {
120
+ const body = {
121
+ deviceUuid,
122
+ ...(startTime ? { createdAfterMs: startTime } : {}),
123
+ ...(endTime ? { createdBeforeMs: endTime } : {}),
124
+ maxPageSize: EVENT_COUNT_MAX_PER_RESPONSE,
125
+ ...(lastEvaluatedKey ? { lastEvaluatedKey } : {}),
126
+ };
127
+ const response = await postApi({
128
+ route: "/climate/getEventsForEnvironmentalGateway",
129
+ body,
130
+ modifiers: requestModifiers,
131
+ sessionId,
132
+ }).then(response => {
133
+ const tempFunc = tempUnit === TempUnit.FAHRENHEIT
134
+ ? (temp) => (temp * 9) / 5 + 32
135
+ : (temp) => temp;
136
+ return {
137
+ events: (response.events || []).map(event => ({
138
+ timestampString: event.timestampMs
139
+ ? formatTimestamp(event.timestampMs, timeZone)
140
+ : undefined,
141
+ temp: tempFunc(event.co2Sense?.tempC ?? 0),
142
+ probeTemp: tempFunc(event.tempProbe?.tempC ?? 0),
143
+ humidity: event.co2Sense?.relHumid,
144
+ pm25: event.pmSense?.pm2p5,
145
+ co2: event.co2Sense?.co2Ppm,
146
+ vapeDetected: event.derivedValues?.vapeDetected,
147
+ })),
148
+ lastEvaluatedKey: response.lastEvaluatedKey,
149
+ };
150
+ });
151
+ // Accumulate the events
152
+ if (response.events) {
153
+ allEvents.push(...response.events);
154
+ }
155
+ // Check if we should continue pagination
156
+ if (response.events &&
157
+ response.events.length === EVENT_COUNT_MAX_PER_RESPONSE &&
158
+ response.lastEvaluatedKey &&
159
+ response.lastEvaluatedKey !== null) {
160
+ // Update the lastEvaluatedKey for the next iteration
161
+ lastEvaluatedKey = response.lastEvaluatedKey;
162
+ }
163
+ else {
164
+ hasMore = false;
165
+ }
166
+ }
167
+ return {
168
+ events: allEvents,
169
+ lastEvaluatedKey: undefined, // Don't return lastEvaluatedKey since we've fetched all pages
170
+ };
171
+ }
172
+ export async function getClimateEventsForSensor(sensorUuid, startTime, endTime, limit, timeZone, tempUnit, requestModifiers, sessionId) {
173
+ let allClimateEvents = [];
174
+ let hasMore = true;
175
+ let remainingLimit = limit || 1000; // Default to 1000 if no limit specified
176
+ while (hasMore && allClimateEvents.length < remainingLimit) {
177
+ const currentBatchSize = Math.min(100, remainingLimit - allClimateEvents.length); // Max 100 per request
178
+ const body = {
179
+ sensorUuid,
180
+ ...(startTime ? { createdAfterMs: startTime } : {}),
181
+ ...(endTime ? { createdBeforeMs: endTime } : {}),
182
+ limit: currentBatchSize,
183
+ };
184
+ const response = await postApi({
185
+ route: "/climate/getClimateEventsForSensor",
186
+ body,
187
+ modifiers: requestModifiers,
188
+ sessionId,
189
+ });
190
+ if (response?.climateEvents) {
191
+ // Map the climate events to include formatted timestamp and relevant fields
192
+ const mappedEvents = response.climateEvents.map(event => ({
193
+ timestampString: event.timestampMs
194
+ ? formatTimestamp(event.timestampMs, timeZone)
195
+ : undefined,
196
+ timestampMs: event.timestampMs,
197
+ temp: event.temp,
198
+ probeTemp: tempFunc(event.probeTempC ?? 0, tempUnit),
199
+ // probeTempC: event.probeTempC,
200
+ // probeTempF: event.probeTempC ? (event.probeTempC * 9) / 5 + 32 : undefined,
201
+ humidity: event.humidity,
202
+ pm25: event.pm25,
203
+ co2: event.co2,
204
+ tvoc: event.tvoc,
205
+ iaq: event.iaq,
206
+ ethanol: event.ethanol,
207
+ heatIndexDegF: event.heatIndexDegF,
208
+ heatIndexRangeWarning: event.heatIndexRangeWarning,
209
+ vapeSmokeDetected: event.vapeSmokeDetected,
210
+ vapeSmokePercent: event.vapeSmokePercent,
211
+ thcDetected: event.thcDetected,
212
+ thcPercent: event.thcPercent,
213
+ tampered: event.tampered,
214
+ batteryPercentage: event.batteryPercentage,
215
+ locationUuid: event.locationUuid,
216
+ orgUuid: event.orgUuid,
217
+ }));
218
+ allClimateEvents.push(...mappedEvents);
219
+ // Check if we got a full batch and haven't reached the limit
220
+ if (response.climateEvents.length === currentBatchSize &&
221
+ allClimateEvents.length < remainingLimit) {
222
+ // Prepare for the next batch by updating the endTime to the oldest event's timestamp
223
+ if (mappedEvents.length > 0) {
224
+ const oldestEventTimestamp = mappedEvents[mappedEvents.length - 1].timestampMs;
225
+ if (oldestEventTimestamp) {
226
+ endTime = oldestEventTimestamp - 1; // Subtract 1ms to avoid duplicates
227
+ }
228
+ else {
229
+ hasMore = false;
230
+ }
231
+ }
232
+ else {
233
+ hasMore = false;
234
+ }
235
+ }
236
+ else {
237
+ hasMore = false;
238
+ }
239
+ }
240
+ else {
241
+ hasMore = false;
242
+ }
243
+ }
244
+ // Trim to the requested limit if we got more than needed
245
+ if (limit && allClimateEvents.length > limit) {
246
+ allClimateEvents = allClimateEvents.slice(0, limit);
247
+ }
248
+ return allClimateEvents;
249
+ }
250
+ export async function getComponentEventsByLocation(locationUuid, eventTypes, // Still accept string[] for flexibility
251
+ startTime, endTime, timeZone, requestModifiers, sessionId) {
252
+ const MAX_LIMIT = 1000; // API limit for component events
253
+ const body = {
254
+ locationUuid,
255
+ typeFilter: eventTypes.length > 0 ? eventTypes : undefined, // API accepts string array
256
+ ...(startTime ? { createdAfterMs: startTime } : {}),
257
+ ...(endTime ? { createdBeforeMs: endTime } : {}),
258
+ limit: MAX_LIMIT,
259
+ };
260
+ const response = await postApi({
261
+ route: "/component/findComponentEventsByLocation",
262
+ body,
263
+ modifiers: requestModifiers,
264
+ sessionId,
265
+ });
266
+ const events = (response.componentEvents || []).map(event => {
267
+ // Map different event types to a common structure
268
+ const baseEvent = {
269
+ eventType: event.type,
270
+ componentUuid: event.componentUuid,
271
+ locationUuid: event.locationUuid,
272
+ orgUuid: event.orgUuid,
273
+ correlationId: event.correlationId,
274
+ ownerDeviceUuid: event.ownerDeviceUuid,
275
+ datetime: event.timestampMs ? formatTimestamp(event.timestampMs, timeZone) : undefined,
276
+ timestampMs: event.timestampMs,
277
+ uuid: event.uuid,
278
+ };
279
+ // Add event-specific fields based on type
280
+ if (event.type === "CredentialReceivedEvent") {
281
+ const credEvent = event;
282
+ return {
283
+ ...baseEvent,
284
+ authenticationResult: credEvent?.authenticationResult,
285
+ authorizationResult: credEvent?.authorizationResult,
286
+ user: credEvent?.originator?.username,
287
+ credSource: credEvent?.credSource,
288
+ doorUuid: credEvent?.componentCompositeUuid,
289
+ };
290
+ }
291
+ else if (event.type === "DoorbellEvent") {
292
+ return {
293
+ ...baseEvent,
294
+ doorbellCameraUuid: event.componentUuid,
295
+ };
296
+ }
297
+ else if (event.type === "DoorStateChangeEvent") {
298
+ const doorEvent = event;
299
+ return {
300
+ ...baseEvent,
301
+ previousState: doorEvent?.previousState,
302
+ newState: doorEvent?.newState,
303
+ reason: doorEvent?.reason,
304
+ };
305
+ }
306
+ else if (event.type === "ButtonEvent" || event.type === "PanicButtonEvent") {
307
+ const buttonEvent = event;
308
+ return {
309
+ ...baseEvent,
310
+ buttonState: buttonEvent?.buttonState,
311
+ };
312
+ }
313
+ // For other event types, return the base event
314
+ return baseEvent;
315
+ });
316
+ // Sort events by timestamp (newest first)
317
+ events.sort((a, b) => (b.timestampMs || 0) - (a.timestampMs || 0));
318
+ console.error(`componentEvents: ${JSON.stringify(events)}`);
319
+ return events;
320
+ }