rhombus-node-mcp 0.1.13 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -3
- package/dist/api/camera-tool-api.js +108 -0
- package/dist/api/clips-tool-api.js +36 -0
- package/dist/api/create-camera-policy-tool-api.js +9 -0
- package/dist/api/create-tool-api.js +53 -0
- package/dist/api/entity-lookup-tool-api.js +37 -0
- package/dist/api/events-tool-api.js +320 -0
- package/dist/api/faces-tool-api.js +110 -0
- package/dist/api/get-entity-tool-api.js +176 -0
- package/dist/api/get-org-information-tool-api.js +9 -0
- package/dist/api/location-tool-api.js +9 -0
- package/dist/api/lpr-tool-api.js +68 -0
- package/dist/api/policy-alerts-tool-api.js +42 -0
- package/dist/api/reboot-cameras-tool-api.js +34 -0
- package/dist/api/report-tool-api.js +426 -0
- package/dist/api/time-tool-api.js +90 -0
- package/dist/api/update-tool-api.js +148 -0
- package/dist/createServer.js +50 -0
- package/dist/disabled-tools/endpoint-to-keys-tool.js +84 -0
- package/dist/disabled-tools/semantic-search-tool.js +90 -0
- package/dist/index.js +25 -32
- package/dist/logger.js +5 -4
- package/dist/network.js +45 -14
- package/dist/resources/routes.json.js +1 -1
- package/dist/services/embedding-service.js +153 -0
- package/dist/services/faiss-search-service.js +261 -0
- package/dist/tools/camera-tool.js +100 -0
- package/dist/tools/clips-tool.js +23 -38
- package/dist/tools/count-tool.js +25 -0
- package/dist/tools/create-camera-policy-tool.js +214 -0
- package/dist/tools/create-tool.js +25 -75
- package/dist/tools/entity-lookup-tool.js +35 -0
- package/dist/tools/events-tool.js +188 -93
- package/dist/tools/faces-tool.js +59 -133
- package/dist/tools/get-entity-tool.js +78 -0
- package/dist/tools/get-org-information-tool.js +18 -0
- package/dist/tools/location-tool.js +24 -32
- package/dist/tools/lpr-tool.js +71 -0
- package/dist/tools/policy-alerts-tool.js +32 -43
- package/dist/tools/reboot-cameras-tool.js +34 -0
- package/dist/tools/report-tool.js +190 -0
- package/dist/tools/time-conversion-tool.js +43 -0
- package/dist/tools/time-tool.js +17 -55
- package/dist/tools/update-tool.js +262 -0
- package/dist/transports/stdio.js +10 -0
- package/dist/transports/streamable-http.js +201 -0
- package/dist/{tools/devices/camera-tool/types.js → types/camera-tool-types.js} +13 -0
- package/dist/types/clips-tool-types.js +41 -0
- package/dist/types/create-camera-policy-tool-types.js +44 -0
- package/dist/types/create-tool-types.js +8 -0
- package/dist/types/deviceType.js +1 -0
- package/dist/types/endpoint-to-keys-tool-types.js +7 -0
- package/dist/types/entity-lookup-tool-types.js +70 -0
- package/dist/types/events-tools-types.js +257 -0
- package/dist/types/faces-tools-types.js +143 -0
- package/dist/types/get-entity-tool-types.js +25 -0
- package/dist/types/get-org-information-tool-types.js +3 -0
- package/dist/types/location-tool-types.js +11 -0
- package/dist/types/lpr-tool-types.js +97 -0
- package/dist/types/policy-alerts-tool-types.js +74 -0
- package/dist/types/reboot-cameras-tool-types.js +8 -0
- package/dist/types/report-tool-types.js +268 -0
- package/dist/types/schema-components.js +7093 -0
- package/dist/types/schema.js +1 -0
- package/dist/types/semantic-search-tool-types.js +5 -0
- package/dist/types/time-conversion-tool-types.js +8 -0
- package/dist/types/time-tool-types.js +11 -0
- package/dist/types/update-tool-types.js +186 -0
- package/dist/types/zod-schemas.js +21315 -0
- package/dist/types.js +17 -7
- package/dist/util.js +96 -14
- package/dist/utils/confirmation.js +1 -7
- package/dist/utils/reduce-output.js +35 -0
- package/dist/utils/remove-nulls.js +28 -0
- package/dist/utils/temp.js +8 -0
- package/dist/utils/timestampInput.js +12 -0
- package/package.json +28 -3
- package/dist/tools/devices/camera-tool/camera-tool.js +0 -218
- package/dist/tools/devices/get-entity-tool.js +0 -117
- package/dist/tools/get-org-information.js +0 -18
- package/dist/tools/reboot-cameras.js +0 -63
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ Your insights will directly influence our development roadmap and help us create
|
|
|
49
49
|
"--rm",
|
|
50
50
|
"-e",
|
|
51
51
|
"RHOMBUS_API_KEY=YOUR_API_KEY_HERE",
|
|
52
|
-
"
|
|
52
|
+
"rhombussystems/mcp-server-rhombus"
|
|
53
53
|
],
|
|
54
54
|
"env": {
|
|
55
55
|
"RHOMBUS_API_KEY": "YOUR_API_KEY_HERE"
|
|
@@ -106,7 +106,7 @@ docker build -t mcp-server-rhombus .
|
|
|
106
106
|
### 2. Update Your Claude Config for Local Use ⚡
|
|
107
107
|
Now, you'll need to adjust your `claude_desktop_config.json` to point to your newly built local Docker image.
|
|
108
108
|
|
|
109
|
-
> ***Note:*** When running locally, the Docker image name changes to `mcp-server-rhombus` from `
|
|
109
|
+
> ***Note:*** When running locally, the Docker image name changes to `mcp-server-rhombus` from `rhombussystems/mcp-server-rhombus`. Make sure to update this in your configuration!
|
|
110
110
|
|
|
111
111
|
```json
|
|
112
112
|
{
|
|
@@ -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
|
-
|
|
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,108 @@
|
|
|
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
|
+
export async function getImageForCameraAtTime(cameraUuid, timestampMs, requestModifiers, sessionId) {
|
|
6
|
+
const body = {
|
|
7
|
+
cameraUuid: cameraUuid,
|
|
8
|
+
downscaleFactor: 10,
|
|
9
|
+
jpgQuality: 70,
|
|
10
|
+
permyriadCropHeight: 10000,
|
|
11
|
+
permyriadCropWidth: 5625,
|
|
12
|
+
permyriadCropX: 2188,
|
|
13
|
+
permyriadCropY: 0,
|
|
14
|
+
timestampMs: timestampMs,
|
|
15
|
+
};
|
|
16
|
+
logger.debug(`Getting frameUri from UUID: ${cameraUuid} at timestampMs: ${timestampMs}`);
|
|
17
|
+
const base64Image = await postApi({
|
|
18
|
+
route: "/video/getExactFrameUri",
|
|
19
|
+
body,
|
|
20
|
+
modifiers: requestModifiers,
|
|
21
|
+
sessionId,
|
|
22
|
+
}).then(async (res) => {
|
|
23
|
+
logger.debug(`Received frameUri ${res.frameUri}`);
|
|
24
|
+
// construct request headers
|
|
25
|
+
let requestHeaders = {
|
|
26
|
+
...(requestModifiers?.headers ?? AUTH_HEADERS),
|
|
27
|
+
...STATIC_HEADERS,
|
|
28
|
+
};
|
|
29
|
+
// add query params
|
|
30
|
+
if (requestModifiers?.query) {
|
|
31
|
+
res.frameUri = appendQueryParams(res.frameUri, requestModifiers.query);
|
|
32
|
+
}
|
|
33
|
+
logger.trace(`Fetching with headers\n${JSON.stringify(requestHeaders)}`);
|
|
34
|
+
return await fetch(res.frameUri, {
|
|
35
|
+
method: "GET",
|
|
36
|
+
headers: requestHeaders,
|
|
37
|
+
}).then(async (res) => {
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
logger.error(`Failed to fetch image: ${await res.text()}`);
|
|
40
|
+
logger.error(res);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const arrayBuffer = await res.arrayBuffer();
|
|
44
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
45
|
+
const base64 = buffer.toString("base64");
|
|
46
|
+
logger.debug(`Received image base64:\n ${base64}`);
|
|
47
|
+
return base64;
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
if (!base64Image) {
|
|
51
|
+
return {
|
|
52
|
+
success: false,
|
|
53
|
+
status: "failed to fetch image",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
success: true,
|
|
58
|
+
status: "successfully fetched image",
|
|
59
|
+
imageType: "base64",
|
|
60
|
+
imageData: base64Image,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export async function getCameraSettings(cameraUuid, requestModifiers, sessionId) {
|
|
64
|
+
const res = await postApi({
|
|
65
|
+
route: "/camera/getFacetedConfig",
|
|
66
|
+
body: {
|
|
67
|
+
deviceUuid: cameraUuid,
|
|
68
|
+
},
|
|
69
|
+
modifiers: requestModifiers,
|
|
70
|
+
sessionId,
|
|
71
|
+
});
|
|
72
|
+
if (res.error) {
|
|
73
|
+
return {
|
|
74
|
+
success: false,
|
|
75
|
+
status: "failed to fetch camera settings",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
success: true,
|
|
80
|
+
config: res.config,
|
|
81
|
+
status: "fetched camera settings",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export async function updateCameraSettings(cameraUuid, update, requestModifiers, sessionId) {
|
|
85
|
+
// remove any "null" values
|
|
86
|
+
const res = await postApi({
|
|
87
|
+
route: "/camera/updateFacetedConfig",
|
|
88
|
+
body: {
|
|
89
|
+
configUpdate: {
|
|
90
|
+
deviceUuid: cameraUuid,
|
|
91
|
+
...removeNullFields(update),
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
modifiers: requestModifiers,
|
|
95
|
+
sessionId,
|
|
96
|
+
});
|
|
97
|
+
if (res.error) {
|
|
98
|
+
return {
|
|
99
|
+
success: false,
|
|
100
|
+
status: "failed to update camera settings",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
success: true,
|
|
105
|
+
config: res.config,
|
|
106
|
+
status: "updated camera settings",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -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,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,37 @@
|
|
|
1
|
+
import { getAccessControlledDoors, getAudioGateways, getBadgeReaders, getButtons, getCameraList, getDoorbellCameras, getDoorSensors, getEnvironmentalGateways, getEnvironmentalSensors, getKeypads, getMotionSensors, } from "./get-entity-tool-api.js";
|
|
2
|
+
export async function getAllEntities(deviceUuids, timeZone, tempUnit, requestModifiers, sessionId) {
|
|
3
|
+
const promises = [
|
|
4
|
+
getCameraList(requestModifiers, sessionId),
|
|
5
|
+
getDoorbellCameras(requestModifiers, sessionId),
|
|
6
|
+
getBadgeReaders(requestModifiers, sessionId),
|
|
7
|
+
getAccessControlledDoors(requestModifiers, sessionId),
|
|
8
|
+
getAudioGateways(timeZone, requestModifiers, sessionId),
|
|
9
|
+
getDoorSensors(requestModifiers, sessionId),
|
|
10
|
+
getEnvironmentalSensors(timeZone, tempUnit, requestModifiers, sessionId),
|
|
11
|
+
getMotionSensors(requestModifiers, sessionId),
|
|
12
|
+
getButtons(timeZone, requestModifiers, sessionId),
|
|
13
|
+
getKeypads(requestModifiers, sessionId),
|
|
14
|
+
getEnvironmentalGateways(timeZone, requestModifiers, sessionId),
|
|
15
|
+
];
|
|
16
|
+
const responses = await Promise.all(promises);
|
|
17
|
+
// Filter each response to only include devices with matching UUIDs
|
|
18
|
+
for (let i = 0; i < responses.length; i++) {
|
|
19
|
+
const response = responses[i];
|
|
20
|
+
// Look through keys and find any with an array
|
|
21
|
+
for (const key of Object.keys(response)) {
|
|
22
|
+
const value = response[key];
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
// Filter to only include devices with matching UUIDs
|
|
25
|
+
response[key] = value.filter((item) => {
|
|
26
|
+
return deviceUuids.includes(item.uuid);
|
|
27
|
+
});
|
|
28
|
+
response[`${key}Count`] = response[key].length;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const result = {};
|
|
33
|
+
for (const response of responses) {
|
|
34
|
+
Object.assign(result, response);
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
@@ -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
|
+
}
|