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.
- package/README.md +9 -1
- package/dist/api/camera-tool-api.js +161 -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 +65 -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 +7 -1
- 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 +3 -5
- package/dist/logger.js +4 -3
- package/dist/network.js +42 -15
- 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 -37
- 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 -74
- package/dist/tools/entity-lookup-tool.js +36 -0
- package/dist/tools/events-tool.js +188 -92
- package/dist/tools/faces-tool.js +59 -132
- 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 -31
- package/dist/tools/lpr-tool.js +79 -0
- package/dist/tools/policy-alerts-tool.js +34 -42
- 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 -57
- package/dist/tools/update-tool.js +262 -0
- package/dist/transports/streamable-http.js +160 -57
- package/dist/{tools/devices/camera-tool/types.js → types/camera-tool-types.js} +52 -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 +94 -2
- package/dist/utils/confirmation.js +1 -1
- 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 +23 -3
- package/dist/tools/devices/camera-tool/camera-tool.js +0 -218
- package/dist/tools/devices/get-entity-tool.js +0 -118
- package/dist/tools/get-org-information.js +0 -17
- package/dist/tools/reboot-cameras.js +0 -62
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { getLogger } from "../logger.js";
|
|
2
|
+
import { postApi } from "../network.js";
|
|
3
|
+
import { formatTimestamp } from "../util.js";
|
|
4
|
+
import { GetCountReportV2WSRequestTypesEnum } from "../types/schema-components.js";
|
|
5
|
+
import { DateTime } from "luxon";
|
|
6
|
+
const REPORT_TYPES_THAT_RETURN_UTC = new Set([
|
|
7
|
+
GetCountReportV2WSRequestTypesEnum.BANDWIDTH,
|
|
8
|
+
GetCountReportV2WSRequestTypesEnum.VEHICLES,
|
|
9
|
+
GetCountReportV2WSRequestTypesEnum.MOTION,
|
|
10
|
+
GetCountReportV2WSRequestTypesEnum.DWELL,
|
|
11
|
+
]);
|
|
12
|
+
const logger = getLogger();
|
|
13
|
+
function getUtcTime(datetime, reportType, timeZone) {
|
|
14
|
+
if (REPORT_TYPES_THAT_RETURN_UTC.has(reportType)) {
|
|
15
|
+
return datetime;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
logger.info("timeZone", datetime);
|
|
19
|
+
return DateTime.fromISO(datetime, { zone: timeZone }).toUTC().toISO();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function getOccupancyCountReport(deviceUuid, startTimeMs, endTimeMs, interval, requestModifiers, sessionId) {
|
|
23
|
+
const body = {
|
|
24
|
+
deviceUuid,
|
|
25
|
+
startTimeMs,
|
|
26
|
+
endTimeMs,
|
|
27
|
+
interval,
|
|
28
|
+
};
|
|
29
|
+
const response = await postApi({
|
|
30
|
+
route: "/report/getOccupancyCounts",
|
|
31
|
+
body,
|
|
32
|
+
modifiers: requestModifiers,
|
|
33
|
+
sessionId,
|
|
34
|
+
});
|
|
35
|
+
response.timeSeriesDataPoints = (response.timeSeriesDataPoints || []).map(dataPoint => {
|
|
36
|
+
const dateLocalMs = new Date(dataPoint.dateLocal || "").getTime();
|
|
37
|
+
const dateUtcMs = new Date(dataPoint.dateUtc || "").getTime();
|
|
38
|
+
logger.info("timeSeriesDataPoints dataPoint:", JSON.stringify(dataPoint));
|
|
39
|
+
return {
|
|
40
|
+
...dataPoint,
|
|
41
|
+
dateLocalString: formatTimestamp(dateLocalMs),
|
|
42
|
+
dateUtcString: formatTimestamp(dateUtcMs),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
return response;
|
|
46
|
+
}
|
|
47
|
+
export async function getSummaryCountReport(interval, scope, types, uuid, endTimeMs, startTimeMs, requestModifiers, sessionId, timeZone) {
|
|
48
|
+
logger.info("📊 Getting summary count report", JSON.stringify({
|
|
49
|
+
interval,
|
|
50
|
+
scope,
|
|
51
|
+
types,
|
|
52
|
+
endTimeMs,
|
|
53
|
+
startTimeMs,
|
|
54
|
+
uuid,
|
|
55
|
+
}));
|
|
56
|
+
const body = {
|
|
57
|
+
endTimeMs,
|
|
58
|
+
interval,
|
|
59
|
+
scope,
|
|
60
|
+
startTimeMs,
|
|
61
|
+
types,
|
|
62
|
+
...(uuid ? { uuid } : {}),
|
|
63
|
+
};
|
|
64
|
+
const response = await postApi({
|
|
65
|
+
route: "/report/getCountReportV2",
|
|
66
|
+
body,
|
|
67
|
+
modifiers: requestModifiers,
|
|
68
|
+
sessionId,
|
|
69
|
+
});
|
|
70
|
+
// Process response to convert date strings to UTC milliseconds timestamps
|
|
71
|
+
let newTimeSeriesDataPoints = undefined;
|
|
72
|
+
if (response && response.timeSeriesDataPoints && Array.isArray(response.timeSeriesDataPoints)) {
|
|
73
|
+
newTimeSeriesDataPoints = response.timeSeriesDataPoints.map((dataPoint) => {
|
|
74
|
+
const sanitizedDataPoint = {
|
|
75
|
+
eventCountMap: dataPoint.eventCountMap,
|
|
76
|
+
dateUtcString: getUtcTime(dataPoint.dateLocal, dataPoint.type, timeZone),
|
|
77
|
+
};
|
|
78
|
+
return sanitizedDataPoint;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
error: response.error,
|
|
83
|
+
errorMsg: response.errorMsg ?? undefined,
|
|
84
|
+
timeSeriesDataPoints: newTimeSeriesDataPoints,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export async function getOccupancyEnabledCameras(requestModifiers, sessionId) {
|
|
88
|
+
logger.info("📷 Getting occupancy enabled cameras");
|
|
89
|
+
const body = {};
|
|
90
|
+
const response = await postApi({
|
|
91
|
+
route: "/camera/getOccupancyEnabledCameras",
|
|
92
|
+
body,
|
|
93
|
+
modifiers: requestModifiers,
|
|
94
|
+
sessionId,
|
|
95
|
+
});
|
|
96
|
+
// Transform the response to handle null values
|
|
97
|
+
const cameras = response.cameras
|
|
98
|
+
? response.cameras.map(camera => ({
|
|
99
|
+
uuid: camera.uuid ?? undefined,
|
|
100
|
+
deviceUuid: camera.deviceUuid ?? undefined,
|
|
101
|
+
name: camera.name ?? undefined,
|
|
102
|
+
serialNumber: camera.serialNumber ?? undefined,
|
|
103
|
+
locationUuid: camera.locationUuid ?? undefined,
|
|
104
|
+
facetNameMap: camera.facetNameMap ?? undefined,
|
|
105
|
+
deleted: camera.deleted ?? undefined,
|
|
106
|
+
pending: camera.pending ?? undefined,
|
|
107
|
+
mummified: camera.mummified ?? undefined,
|
|
108
|
+
}))
|
|
109
|
+
: undefined;
|
|
110
|
+
return {
|
|
111
|
+
error: response.error ?? undefined,
|
|
112
|
+
errorMsg: response.errorMsg ?? undefined,
|
|
113
|
+
cameras,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export async function getLineCrossingEnabledCameras(locationUuid, requestModifiers, sessionId) {
|
|
117
|
+
logger.info("🚶 Getting line crossing enabled cameras for location", locationUuid);
|
|
118
|
+
const body = {
|
|
119
|
+
locationUuid,
|
|
120
|
+
};
|
|
121
|
+
const response = await postApi({
|
|
122
|
+
route: "/camera/getLineCrossingEnabledCamerasForLocation",
|
|
123
|
+
body,
|
|
124
|
+
modifiers: requestModifiers,
|
|
125
|
+
sessionId,
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
error: response.error ?? undefined,
|
|
129
|
+
errorMsg: response.errorMsg ?? undefined,
|
|
130
|
+
camerasToConfigs: response.camerasToConfigs ?? undefined,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export async function getThresholdCrossingCountReport(deviceUuid, startTimeMs, endTimeMs, bucketSize, crossingObject, dedupe, requestModifiers, sessionId) {
|
|
134
|
+
logger.info("🚪 Getting threshold crossing count report", JSON.stringify({
|
|
135
|
+
deviceUuid,
|
|
136
|
+
startTimeMs,
|
|
137
|
+
endTimeMs,
|
|
138
|
+
bucketSize,
|
|
139
|
+
crossingObject,
|
|
140
|
+
dedupe,
|
|
141
|
+
}));
|
|
142
|
+
const body = {
|
|
143
|
+
deviceUuid,
|
|
144
|
+
startTimeMs,
|
|
145
|
+
endTimeMs,
|
|
146
|
+
bucketSize,
|
|
147
|
+
crossingObject,
|
|
148
|
+
dedupe,
|
|
149
|
+
};
|
|
150
|
+
const response = await postApi({
|
|
151
|
+
route: "/report/getThresholdCrossingCountReport",
|
|
152
|
+
body,
|
|
153
|
+
modifiers: requestModifiers,
|
|
154
|
+
sessionId,
|
|
155
|
+
});
|
|
156
|
+
// Transform the response to handle null values
|
|
157
|
+
const crossingCounts = response.crossingCounts
|
|
158
|
+
? response.crossingCounts.map(count => ({
|
|
159
|
+
timestampMs: count.timestampMs ?? undefined,
|
|
160
|
+
ingressCount: count.ingressCount ?? undefined,
|
|
161
|
+
egressCount: count.egressCount ?? undefined,
|
|
162
|
+
}))
|
|
163
|
+
: undefined;
|
|
164
|
+
// Calculate metrics if we have crossing counts and the bucket size is appropriate
|
|
165
|
+
let metrics = undefined;
|
|
166
|
+
if (crossingCounts && crossingCounts.length > 0) {
|
|
167
|
+
// Calculate total entries and exits
|
|
168
|
+
let totalEntries = 0;
|
|
169
|
+
let totalExits = 0;
|
|
170
|
+
let maxEntries = 0;
|
|
171
|
+
let maxExits = 0;
|
|
172
|
+
let maxTotal = 0;
|
|
173
|
+
let maxEntriesTimestamp;
|
|
174
|
+
let maxExitsTimestamp;
|
|
175
|
+
let maxTotalTimestamp;
|
|
176
|
+
let maxTotalEntries = 0;
|
|
177
|
+
let maxTotalExits = 0;
|
|
178
|
+
crossingCounts.forEach(count => {
|
|
179
|
+
const entries = count.ingressCount || 0;
|
|
180
|
+
const exits = count.egressCount || 0;
|
|
181
|
+
const total = entries + exits;
|
|
182
|
+
totalEntries += entries;
|
|
183
|
+
totalExits += exits;
|
|
184
|
+
// Track max entries
|
|
185
|
+
if (entries > maxEntries) {
|
|
186
|
+
maxEntries = entries;
|
|
187
|
+
maxEntriesTimestamp = count.timestampMs;
|
|
188
|
+
}
|
|
189
|
+
// Track max exits
|
|
190
|
+
if (exits > maxExits) {
|
|
191
|
+
maxExits = exits;
|
|
192
|
+
maxExitsTimestamp = count.timestampMs;
|
|
193
|
+
}
|
|
194
|
+
// Track busiest hour (max total)
|
|
195
|
+
if (total > maxTotal) {
|
|
196
|
+
maxTotal = total;
|
|
197
|
+
maxTotalTimestamp = count.timestampMs;
|
|
198
|
+
maxTotalEntries = entries;
|
|
199
|
+
maxTotalExits = exits;
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
// Calculate hours based on bucket size
|
|
203
|
+
let hoursInPeriod = 1;
|
|
204
|
+
const periodMs = endTimeMs - startTimeMs;
|
|
205
|
+
if (bucketSize === "HOUR") {
|
|
206
|
+
hoursInPeriod = periodMs / (1000 * 60 * 60);
|
|
207
|
+
}
|
|
208
|
+
else if (bucketSize === "QUARTER_HOUR") {
|
|
209
|
+
// For quarter hour buckets, we need to aggregate to hourly
|
|
210
|
+
hoursInPeriod = periodMs / (1000 * 60 * 60);
|
|
211
|
+
// Note: For more accurate hourly calculations with quarter-hour buckets,
|
|
212
|
+
// we'd need to group by hour, but for now we'll use the total period
|
|
213
|
+
}
|
|
214
|
+
else if (bucketSize === "DAY") {
|
|
215
|
+
hoursInPeriod = periodMs / (1000 * 60 * 60);
|
|
216
|
+
}
|
|
217
|
+
else if (bucketSize === "WEEK") {
|
|
218
|
+
hoursInPeriod = periodMs / (1000 * 60 * 60);
|
|
219
|
+
}
|
|
220
|
+
// Helper function to format hour labels
|
|
221
|
+
const formatHourLabel = (timestampMs) => {
|
|
222
|
+
if (!timestampMs)
|
|
223
|
+
return "Unknown";
|
|
224
|
+
const date = DateTime.fromMillis(timestampMs);
|
|
225
|
+
if (bucketSize === "HOUR") {
|
|
226
|
+
const endHour = date.plus({ hours: 1 });
|
|
227
|
+
return `${date.toFormat("h:mm a")} - ${endHour.toFormat("h:mm a")} (${date.toFormat("MMM d, yyyy")})`;
|
|
228
|
+
}
|
|
229
|
+
else if (bucketSize === "QUARTER_HOUR") {
|
|
230
|
+
const endTime = date.plus({ minutes: 15 });
|
|
231
|
+
return `${date.toFormat("h:mm a")} - ${endTime.toFormat("h:mm a")} (${date.toFormat("MMM d, yyyy")})`;
|
|
232
|
+
}
|
|
233
|
+
else if (bucketSize === "DAY") {
|
|
234
|
+
return date.toFormat("MMM d, yyyy");
|
|
235
|
+
}
|
|
236
|
+
else if (bucketSize === "WEEK") {
|
|
237
|
+
const endWeek = date.plus({ weeks: 1 }).minus({ days: 1 });
|
|
238
|
+
return `Week of ${date.toFormat("MMM d")} - ${endWeek.toFormat("MMM d, yyyy")}`;
|
|
239
|
+
}
|
|
240
|
+
return date.toISO() || "Unknown";
|
|
241
|
+
};
|
|
242
|
+
metrics = {
|
|
243
|
+
averageEntriesPerHour: totalEntries / hoursInPeriod,
|
|
244
|
+
averageExitsPerHour: totalExits / hoursInPeriod,
|
|
245
|
+
mostEntriesInHour: {
|
|
246
|
+
count: maxEntries,
|
|
247
|
+
timestamp: maxEntriesTimestamp ? DateTime.fromMillis(maxEntriesTimestamp).toISO() : "",
|
|
248
|
+
hourLabel: formatHourLabel(maxEntriesTimestamp),
|
|
249
|
+
},
|
|
250
|
+
mostExitsInHour: {
|
|
251
|
+
count: maxExits,
|
|
252
|
+
timestamp: maxExitsTimestamp ? DateTime.fromMillis(maxExitsTimestamp).toISO() : "",
|
|
253
|
+
hourLabel: formatHourLabel(maxExitsTimestamp),
|
|
254
|
+
},
|
|
255
|
+
busiestHour: {
|
|
256
|
+
totalCount: maxTotal,
|
|
257
|
+
timestamp: maxTotalTimestamp ? DateTime.fromMillis(maxTotalTimestamp).toISO() : "",
|
|
258
|
+
hourLabel: formatHourLabel(maxTotalTimestamp),
|
|
259
|
+
entries: maxTotalEntries,
|
|
260
|
+
exits: maxTotalExits,
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
error: response.error ?? undefined,
|
|
266
|
+
errorMsg: response.errorMsg ?? undefined,
|
|
267
|
+
crossingCounts,
|
|
268
|
+
metrics,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
export async function findPromptConfigurations(requestModifiers, sessionId) {
|
|
272
|
+
logger.info("🔍 Finding custom event prompt configurations");
|
|
273
|
+
const body = {};
|
|
274
|
+
const response = await postApi({
|
|
275
|
+
route: "/scenequery/findPromptConfigurations",
|
|
276
|
+
body,
|
|
277
|
+
modifiers: requestModifiers,
|
|
278
|
+
sessionId,
|
|
279
|
+
});
|
|
280
|
+
// Transform the response to handle null values
|
|
281
|
+
const promptConfigurations = response.promptConfigurations
|
|
282
|
+
? response.promptConfigurations.map(config => ({
|
|
283
|
+
active: config.active ?? undefined,
|
|
284
|
+
cameraConfigurations: config.cameraConfigurations ?? undefined,
|
|
285
|
+
checkCondition: config.checkCondition ?? undefined,
|
|
286
|
+
description: config.description ?? undefined,
|
|
287
|
+
name: config.name ?? undefined,
|
|
288
|
+
orgUuid: config.orgUuid ?? undefined,
|
|
289
|
+
prompt: config.prompt ?? undefined,
|
|
290
|
+
promptType: config.promptType ?? undefined,
|
|
291
|
+
scheduleUuid: config.scheduleUuid ?? undefined,
|
|
292
|
+
shortName: config.shortName ?? undefined,
|
|
293
|
+
uuid: config.uuid ?? undefined,
|
|
294
|
+
}))
|
|
295
|
+
: undefined;
|
|
296
|
+
return {
|
|
297
|
+
error: response.error ?? undefined,
|
|
298
|
+
errorMsg: response.errorMsg ?? undefined,
|
|
299
|
+
promptConfigurations,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
export async function getCustomLLMReport(promptUuid, promptType, startTimeMs, endTimeMs, interval, requestModifiers, sessionId) {
|
|
303
|
+
logger.info("📊 Getting custom LLM report", JSON.stringify({
|
|
304
|
+
promptUuid,
|
|
305
|
+
promptType,
|
|
306
|
+
startTimeMs,
|
|
307
|
+
endTimeMs,
|
|
308
|
+
interval,
|
|
309
|
+
}));
|
|
310
|
+
const body = {
|
|
311
|
+
promptUuid,
|
|
312
|
+
startTimeMs,
|
|
313
|
+
endTimeMs,
|
|
314
|
+
interval,
|
|
315
|
+
};
|
|
316
|
+
// Determine the correct endpoint based on promptType
|
|
317
|
+
let route;
|
|
318
|
+
switch (promptType) {
|
|
319
|
+
case "COUNT":
|
|
320
|
+
route = "/report/getCustomLLMNumericCounts";
|
|
321
|
+
break;
|
|
322
|
+
case "PERCENT":
|
|
323
|
+
route = "/report/getCustomLLMReport";
|
|
324
|
+
break;
|
|
325
|
+
case "BOOLEAN":
|
|
326
|
+
route = "/report/getCustomLLMBinaryCounts";
|
|
327
|
+
break;
|
|
328
|
+
default:
|
|
329
|
+
throw new Error(`Unknown prompt type: ${promptType}`);
|
|
330
|
+
}
|
|
331
|
+
const response = await postApi({
|
|
332
|
+
route,
|
|
333
|
+
body,
|
|
334
|
+
modifiers: requestModifiers,
|
|
335
|
+
sessionId,
|
|
336
|
+
});
|
|
337
|
+
logger.info("📊 Custom LLM API response:", JSON.stringify({
|
|
338
|
+
promptType,
|
|
339
|
+
hasReports: !!response.reports,
|
|
340
|
+
hasTimeSeriesDataPoints: !!response.timeSeriesDataPoints,
|
|
341
|
+
error: response.error,
|
|
342
|
+
}));
|
|
343
|
+
// Different prompt types return different response structures
|
|
344
|
+
let timeSeriesDataPoints;
|
|
345
|
+
if ((promptType === "BOOLEAN" || promptType === "PERCENT") && response.reports) {
|
|
346
|
+
// BOOLEAN and PERCENT types return a reports object with device UUIDs as keys
|
|
347
|
+
const reportEntries = Object.entries(response.reports);
|
|
348
|
+
logger.info(`📊 ${promptType} response structure:`, JSON.stringify({ reportEntries: reportEntries.length }));
|
|
349
|
+
// Aggregate data from all devices
|
|
350
|
+
const aggregatedData = {};
|
|
351
|
+
reportEntries.forEach(([deviceId, reportData]) => {
|
|
352
|
+
if (Array.isArray(reportData)) {
|
|
353
|
+
reportData.forEach((item) => {
|
|
354
|
+
const dateKey = item.localDate || new Date().toISOString();
|
|
355
|
+
if (!aggregatedData[dateKey]) {
|
|
356
|
+
aggregatedData[dateKey] = {
|
|
357
|
+
dateLocal: item.localDate,
|
|
358
|
+
eventCount: 0,
|
|
359
|
+
true: 0,
|
|
360
|
+
false: 0,
|
|
361
|
+
// For PERCENT type, store percentage values
|
|
362
|
+
percentages: {},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (promptType === "BOOLEAN") {
|
|
366
|
+
aggregatedData[dateKey].eventCount += item.eventCount || 0;
|
|
367
|
+
aggregatedData[dateKey].true += item.true || 0;
|
|
368
|
+
aggregatedData[dateKey].false += item.false || 0;
|
|
369
|
+
}
|
|
370
|
+
else if (promptType === "PERCENT") {
|
|
371
|
+
// For PERCENT type, aggregate percentage data
|
|
372
|
+
aggregatedData[dateKey].eventCount += item.eventCount || 0;
|
|
373
|
+
// Store any percentage-related fields from the response
|
|
374
|
+
Object.entries(item).forEach(([key, value]) => {
|
|
375
|
+
if (key !== "localDate" && key !== "eventCount") {
|
|
376
|
+
// Store any numeric values as percentages (could be number or string percentage)
|
|
377
|
+
aggregatedData[dateKey].percentages[key] = value;
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
// Convert aggregated data to timeSeriesDataPoints format
|
|
385
|
+
timeSeriesDataPoints = Object.entries(aggregatedData).map(([date, data]) => ({
|
|
386
|
+
dateLocal: data.dateLocal ?? undefined,
|
|
387
|
+
dateUtc: data.dateLocal ?? undefined, // Convert to UTC if needed
|
|
388
|
+
eventCountMap: promptType === "BOOLEAN"
|
|
389
|
+
? {
|
|
390
|
+
total: data.eventCount,
|
|
391
|
+
true: data.true,
|
|
392
|
+
false: data.false,
|
|
393
|
+
}
|
|
394
|
+
: {
|
|
395
|
+
total: data.eventCount,
|
|
396
|
+
...data.percentages,
|
|
397
|
+
},
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
else if (response.timeSeriesDataPoints) {
|
|
401
|
+
// COUNT type uses the standard timeSeriesDataPoints format
|
|
402
|
+
timeSeriesDataPoints = response.timeSeriesDataPoints.map((dataPoint) => ({
|
|
403
|
+
dateLocal: dataPoint.dateLocal ?? undefined,
|
|
404
|
+
dateUtc: dataPoint.dateUtc ?? undefined,
|
|
405
|
+
eventCountMap: dataPoint.eventCountMap
|
|
406
|
+
? Object.entries(dataPoint.eventCountMap).reduce((acc, [key, value]) => {
|
|
407
|
+
if (value !== null && value !== undefined) {
|
|
408
|
+
acc[key] = value;
|
|
409
|
+
}
|
|
410
|
+
return acc;
|
|
411
|
+
}, {})
|
|
412
|
+
: undefined,
|
|
413
|
+
}));
|
|
414
|
+
}
|
|
415
|
+
const result = {
|
|
416
|
+
error: response.error ?? undefined,
|
|
417
|
+
errorMsg: response.errorMsg ?? undefined,
|
|
418
|
+
timeSeriesDataPoints,
|
|
419
|
+
};
|
|
420
|
+
logger.info("📊 Custom LLM report result:", JSON.stringify({
|
|
421
|
+
hasError: !!result.error,
|
|
422
|
+
dataPointsCount: timeSeriesDataPoints?.length ?? 0,
|
|
423
|
+
firstDataPoint: timeSeriesDataPoints?.[0],
|
|
424
|
+
}));
|
|
425
|
+
return result;
|
|
426
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { parse } from "chrono-node";
|
|
2
|
+
import { DateTime } from "luxon";
|
|
3
|
+
import { logger } from "../logger.js";
|
|
4
|
+
export function nullToUndefined(value) {
|
|
5
|
+
return value === null ? undefined : value;
|
|
6
|
+
}
|
|
7
|
+
function normalizeTimeDescription(description) {
|
|
8
|
+
const normalized = description.toLowerCase().trim();
|
|
9
|
+
if (normalized.includes("current time")) {
|
|
10
|
+
return "now";
|
|
11
|
+
}
|
|
12
|
+
// Handle plain day references as start of day
|
|
13
|
+
if (normalized === "today") {
|
|
14
|
+
return "today at 00:00";
|
|
15
|
+
}
|
|
16
|
+
if (normalized === "yesterday") {
|
|
17
|
+
return "yesterday at 00:00";
|
|
18
|
+
}
|
|
19
|
+
if (normalized === "tomorrow") {
|
|
20
|
+
return "tomorrow at 00:00";
|
|
21
|
+
}
|
|
22
|
+
// Handle "this" time periods - always refer to today
|
|
23
|
+
if (normalized === "this morning") {
|
|
24
|
+
return "today at 06:00";
|
|
25
|
+
}
|
|
26
|
+
if (normalized === "this afternoon") {
|
|
27
|
+
return "today at 12:00";
|
|
28
|
+
}
|
|
29
|
+
if (normalized === "this evening") {
|
|
30
|
+
return "today at 18:00";
|
|
31
|
+
}
|
|
32
|
+
if (normalized === "this night" || normalized === "tonight") {
|
|
33
|
+
return "today at 20:00";
|
|
34
|
+
}
|
|
35
|
+
if (normalized.includes("start of today") || normalized.includes("beginning of today")) {
|
|
36
|
+
return "today at 00:00";
|
|
37
|
+
}
|
|
38
|
+
if (normalized.includes("start of yesterday") || normalized.includes("beginning of yesterday")) {
|
|
39
|
+
return "yesterday at 00:00";
|
|
40
|
+
}
|
|
41
|
+
if (normalized.includes("start of tomorrow") || normalized.includes("beginning of tomorrow")) {
|
|
42
|
+
return "tomorrow at 00:00";
|
|
43
|
+
}
|
|
44
|
+
if (normalized.includes("end of today")) {
|
|
45
|
+
return "today at 23:59:59";
|
|
46
|
+
}
|
|
47
|
+
if (normalized.includes("end of yesterday")) {
|
|
48
|
+
return "yesterday at 23:59:59";
|
|
49
|
+
}
|
|
50
|
+
if (normalized.includes("end of tomorrow")) {
|
|
51
|
+
return "tomorrow at 23:59:59";
|
|
52
|
+
}
|
|
53
|
+
return description;
|
|
54
|
+
}
|
|
55
|
+
export function parseTimeDescription(time_description, timezone, extra) {
|
|
56
|
+
logger.info("EXTRA", extra);
|
|
57
|
+
const now = new Date(DateTime.now().setZone(timezone || "America/Los_Angeles").toISO({ includeOffset: false }));
|
|
58
|
+
const normalizedDescription = normalizeTimeDescription(time_description);
|
|
59
|
+
logger.info(`TIME TOOL ${timezone}: Normalized "${time_description}" to "${normalizedDescription}"`);
|
|
60
|
+
// Use the timezone-adjusted date as the reference date for chrono-node
|
|
61
|
+
const parsed = parse(normalizedDescription, now);
|
|
62
|
+
if (!parsed || parsed.length === 0) {
|
|
63
|
+
throw new Error(`Could not parse time description: ${time_description}`);
|
|
64
|
+
}
|
|
65
|
+
logger.info(`TIME TOOLPARSED ${time_description}`, JSON.stringify(parsed));
|
|
66
|
+
const dateComponents = parsed[0].start;
|
|
67
|
+
if (!dateComponents) {
|
|
68
|
+
throw new Error("Parsed time has no start component");
|
|
69
|
+
}
|
|
70
|
+
const dt = DateTime.fromObject({
|
|
71
|
+
year: nullToUndefined(dateComponents.get("year")),
|
|
72
|
+
month: nullToUndefined(dateComponents.get("month")),
|
|
73
|
+
day: nullToUndefined(dateComponents.get("day")),
|
|
74
|
+
hour: nullToUndefined(dateComponents.get("hour")),
|
|
75
|
+
minute: nullToUndefined(dateComponents.get("minute")),
|
|
76
|
+
second: nullToUndefined(dateComponents.get("second")),
|
|
77
|
+
millisecond: 0,
|
|
78
|
+
}, {
|
|
79
|
+
zone: timezone || "local",
|
|
80
|
+
});
|
|
81
|
+
if (!dt.isValid) {
|
|
82
|
+
throw new Error(`Could not construct valid DateTime: ${dt.invalidReason}`);
|
|
83
|
+
}
|
|
84
|
+
const timestamp = dt.toMillis();
|
|
85
|
+
return {
|
|
86
|
+
timestamp,
|
|
87
|
+
iso: dt.toISO(),
|
|
88
|
+
timezone: dt.zoneName,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { postApi } from "../network.js";
|
|
2
|
+
/**
|
|
3
|
+
* Updates camera configuration using the faceted config API
|
|
4
|
+
*/
|
|
5
|
+
export async function updateCameraConfig(payload, requestModifiers, sessionId) {
|
|
6
|
+
try {
|
|
7
|
+
const result = await postApi({
|
|
8
|
+
route: "/camera/updateFacetedConfig",
|
|
9
|
+
body: payload,
|
|
10
|
+
modifiers: requestModifiers,
|
|
11
|
+
sessionId,
|
|
12
|
+
});
|
|
13
|
+
if (result.error) {
|
|
14
|
+
return {
|
|
15
|
+
success: false,
|
|
16
|
+
error: result.errorMsg || "Failed to update camera configuration",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
success: true,
|
|
21
|
+
updatedSettings: payload.configUpdate,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return {
|
|
26
|
+
success: false,
|
|
27
|
+
error: error instanceof Error ? error.message : "Unknown error occurred",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Gets current camera configuration
|
|
33
|
+
*/
|
|
34
|
+
export async function getCameraDetails(cameraUuid, requestModifiers, sessionId) {
|
|
35
|
+
try {
|
|
36
|
+
const result = await postApi({
|
|
37
|
+
route: "/camera/getDetailsV2",
|
|
38
|
+
body: {
|
|
39
|
+
uuid: cameraUuid,
|
|
40
|
+
},
|
|
41
|
+
modifiers: requestModifiers,
|
|
42
|
+
sessionId,
|
|
43
|
+
});
|
|
44
|
+
if (result.error) {
|
|
45
|
+
return {
|
|
46
|
+
success: false,
|
|
47
|
+
error: result.errorMsg || "Failed to get camera details",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
success: true,
|
|
52
|
+
data: result.camera,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
return {
|
|
57
|
+
success: false,
|
|
58
|
+
error: error instanceof Error ? error.message : "Unknown error occurred",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Helper function to remove null/undefined fields from update payload
|
|
64
|
+
*/
|
|
65
|
+
export function cleanUpdatePayload(payload) {
|
|
66
|
+
const cleaned = {};
|
|
67
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
68
|
+
if (value !== null && value !== undefined) {
|
|
69
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
70
|
+
const cleanedValue = cleanUpdatePayload(value);
|
|
71
|
+
if (Object.keys(cleanedValue).length > 0) {
|
|
72
|
+
cleaned[key] = cleanedValue;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
cleaned[key] = value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return cleaned;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Formats camera settings for display
|
|
84
|
+
*/
|
|
85
|
+
export function formatCameraSettings(settings) {
|
|
86
|
+
const sections = [];
|
|
87
|
+
if (settings.videoFacetSettings) {
|
|
88
|
+
const videoSettings = Object.values(settings.videoFacetSettings)[0];
|
|
89
|
+
if (videoSettings) {
|
|
90
|
+
const videoItems = [];
|
|
91
|
+
if (videoSettings.resolution)
|
|
92
|
+
videoItems.push(`Resolution: ${videoSettings.resolution.width}x${videoSettings.resolution.height}`);
|
|
93
|
+
if (videoSettings.img_brightness !== undefined)
|
|
94
|
+
videoItems.push(`Brightness: ${videoSettings.img_brightness}`);
|
|
95
|
+
if (videoSettings.img_contrast !== undefined)
|
|
96
|
+
videoItems.push(`Contrast: ${videoSettings.img_contrast}`);
|
|
97
|
+
if (videoSettings.img_saturation !== undefined)
|
|
98
|
+
videoItems.push(`Saturation: ${videoSettings.img_saturation}`);
|
|
99
|
+
if (videoSettings.img_sharpness !== undefined)
|
|
100
|
+
videoItems.push(`Sharpness: ${videoSettings.img_sharpness}`);
|
|
101
|
+
if (videoSettings.hdr_enabled !== undefined)
|
|
102
|
+
videoItems.push(`HDR: ${videoSettings.hdr_enabled ? "On" : "Off"}`);
|
|
103
|
+
if (videoSettings.wdr_enabled !== undefined)
|
|
104
|
+
videoItems.push(`WDR: ${videoSettings.wdr_enabled ? "On" : "Off"}`);
|
|
105
|
+
if (videoSettings.wdr_strength !== undefined)
|
|
106
|
+
videoItems.push(`WDR Strength: ${videoSettings.wdr_strength}`);
|
|
107
|
+
if (videoSettings.video_persist_disabled !== undefined)
|
|
108
|
+
videoItems.push(`Video Persist: ${videoSettings.video_persist_disabled ? "Disabled" : "Enabled"}`);
|
|
109
|
+
if (videoSettings.zero_motion_video_bitrate_percent !== undefined)
|
|
110
|
+
videoItems.push(`Zero Motion Bitrate: ${videoSettings.zero_motion_video_bitrate_percent}%`);
|
|
111
|
+
if (videoItems.length > 0) {
|
|
112
|
+
sections.push("**Video Settings:**\n" + videoItems.map(item => `• ${item}`).join("\n"));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (settings.audioFacetSettings) {
|
|
117
|
+
const audioSettings = Object.values(settings.audioFacetSettings)[0];
|
|
118
|
+
if (audioSettings) {
|
|
119
|
+
const audioItems = [];
|
|
120
|
+
if (audioSettings.audio_record !== undefined)
|
|
121
|
+
audioItems.push(`Recording: ${audioSettings.audio_record ? "On" : "Off"}`);
|
|
122
|
+
if (audioSettings.device_mic_enabled !== undefined)
|
|
123
|
+
audioItems.push(`Microphone: ${audioSettings.device_mic_enabled ? "Enabled" : "Disabled"}`);
|
|
124
|
+
if (audioSettings.device_speaker_enabled !== undefined)
|
|
125
|
+
audioItems.push(`Speaker: ${audioSettings.device_speaker_enabled ? "Enabled" : "Disabled"}`);
|
|
126
|
+
if (audioItems.length > 0) {
|
|
127
|
+
sections.push("**Audio Settings:**\n" + audioItems.map(item => `• ${item}`).join("\n"));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (settings.deviceSettings) {
|
|
132
|
+
const deviceItems = [];
|
|
133
|
+
if (settings.deviceSettings.camera_name)
|
|
134
|
+
deviceItems.push(`Name: ${settings.deviceSettings.camera_name}`);
|
|
135
|
+
if (settings.deviceSettings.camera_timezone)
|
|
136
|
+
deviceItems.push(`Timezone: ${settings.deviceSettings.camera_timezone}`);
|
|
137
|
+
if (settings.deviceSettings.led_mode)
|
|
138
|
+
deviceItems.push(`LED Mode: ${settings.deviceSettings.led_mode}`);
|
|
139
|
+
if (settings.deviceSettings.led_intensity !== undefined)
|
|
140
|
+
deviceItems.push(`LED Intensity: ${settings.deviceSettings.led_intensity}`);
|
|
141
|
+
if (settings.deviceSettings.led_stealth_mode !== undefined)
|
|
142
|
+
deviceItems.push(`LED Stealth Mode: ${settings.deviceSettings.led_stealth_mode ? "On" : "Off"}`);
|
|
143
|
+
if (deviceItems.length > 0) {
|
|
144
|
+
sections.push("**Device Settings:**\n" + deviceItems.map(item => `• ${item}`).join("\n"));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return sections.join("\n\n");
|
|
148
|
+
}
|
package/dist/createServer.js
CHANGED
|
@@ -36,7 +36,13 @@ export default async function createServer() {
|
|
|
36
36
|
}
|
|
37
37
|
logger.info(`🛠️ Registered ${resources.length} resources`);
|
|
38
38
|
for (const tool of tools) {
|
|
39
|
-
|
|
39
|
+
try {
|
|
40
|
+
await tool.create(server);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
logger.error(`Failed to register tool ${tool.name}:`, error);
|
|
44
|
+
// Continue with other tools instead of failing completely
|
|
45
|
+
}
|
|
40
46
|
}
|
|
41
47
|
logger.info(`🛠️ Registered ${tools.length} tools`);
|
|
42
48
|
logger.info(`✅ Server created`);
|