Skip to main content
Glama
RhombusSystems

Rhombus MCP Server

Official
report-tool-api.ts17.1 kB
import { getLogger } from "../logger.js"; import { postApi } from "../network.js"; import { formatTimestamp } from "../util.js"; import schema from "../types/schema.js"; import { OutputSchema, SanitizedTimeSeriesDataPoint } from "../types/report-tool-types.js"; import { GetCountReportV2WSRequestTypesEnum } from "../types/schema-components.js"; import { DateTime } from "luxon"; const REPORT_TYPES_THAT_RETURN_UTC = new Set([ GetCountReportV2WSRequestTypesEnum.BANDWIDTH, GetCountReportV2WSRequestTypesEnum.VEHICLES, GetCountReportV2WSRequestTypesEnum.MOTION, GetCountReportV2WSRequestTypesEnum.DWELL, ]); const logger = getLogger(); function getUtcTime( datetime: string, reportType: GetCountReportV2WSRequestTypesEnum, timeZone?: string ): string { if (REPORT_TYPES_THAT_RETURN_UTC.has(reportType)) { return datetime; } else { logger.info("timeZone", datetime); return DateTime.fromISO(datetime, { zone: timeZone }).toUTC().toISO()!; } } export async function getOccupancyCountReport( deviceUuid: string, startTimeMs: number, endTimeMs: number, interval: "MINUTELY" | "HOURLY" | "DAILY" | "WEEKLY" | "MONTHLY" | "YEARLY", requestModifiers?: any, sessionId?: string ) { const body = { deviceUuid, startTimeMs, endTimeMs, interval, }; const response: schema["Report_GetOccupancyCountsWSResponse"] = await postApi({ route: "/report/getOccupancyCounts", body, modifiers: requestModifiers, sessionId, }); response.timeSeriesDataPoints = (response.timeSeriesDataPoints || []).map(dataPoint => { const dateLocalMs = new Date(dataPoint.dateLocal || "").getTime(); const dateUtcMs = new Date(dataPoint.dateUtc || "").getTime(); logger.info("timeSeriesDataPoints dataPoint:", JSON.stringify(dataPoint)); return { ...dataPoint, dateLocalString: formatTimestamp(dateLocalMs), dateUtcString: formatTimestamp(dateUtcMs), }; }); return response; } export async function getSummaryCountReport( interval: "MINUTELY" | "QUARTERHOURLY" | "HOURLY" | "DAILY" | "WEEKLY" | "MONTHLY" | "YEARLY", scope: "REGION" | "LOCATION" | "DEVICE" | "ORG", types: ( | "CROWD" | "PEOPLE" | "FACES" | "MOTION" | "BANDWIDTH" | "VEHICLES" | "LICENSEPLATES" | "ALERTS" | "AM_VERIFICATION" | "DWELL" )[], uuid: string | undefined, endTimeMs: number, startTimeMs: number, requestModifiers?: any, sessionId?: string, timeZone?: string ): Promise<OutputSchema["summaryCountReport"]> { logger.info( "📊 Getting summary count report", JSON.stringify({ interval, scope, types, endTimeMs, startTimeMs, uuid, }) ); const body = { endTimeMs, interval, scope, startTimeMs, types, ...(uuid ? { uuid } : {}), }; const response = await postApi<schema["Report_GetCountReportWSResponse"]>({ route: "/report/getCountReportV2", body, modifiers: requestModifiers, sessionId, }); // Process response to convert date strings to UTC milliseconds timestamps let newTimeSeriesDataPoints: SanitizedTimeSeriesDataPoint[] | undefined = undefined; if (response && response.timeSeriesDataPoints && Array.isArray(response.timeSeriesDataPoints)) { newTimeSeriesDataPoints = response.timeSeriesDataPoints.map((dataPoint: any) => { const sanitizedDataPoint: SanitizedTimeSeriesDataPoint = { eventCountMap: dataPoint.eventCountMap, dateUtcString: getUtcTime(dataPoint.dateLocal, dataPoint.type, timeZone), }; return sanitizedDataPoint; }); } return { error: response.error, errorMsg: response.errorMsg ?? undefined, timeSeriesDataPoints: newTimeSeriesDataPoints, }; } export async function getOccupancyEnabledCameras( requestModifiers?: any, sessionId?: string ): Promise<OutputSchema["occupancyEnabledCamerasReport"]> { logger.info("📷 Getting occupancy enabled cameras"); const body = {}; const response = await postApi<schema["Camera_GetFacetedCameraDetailsWSResponse"]>({ route: "/camera/getOccupancyEnabledCameras", body, modifiers: requestModifiers, sessionId, }); // Transform the response to handle null values const cameras = response.cameras ? response.cameras.map(camera => ({ uuid: camera.uuid ?? undefined, deviceUuid: camera.deviceUuid ?? undefined, name: camera.name ?? undefined, serialNumber: camera.serialNumber ?? undefined, locationUuid: camera.locationUuid ?? undefined, facetNameMap: camera.facetNameMap ?? undefined, deleted: camera.deleted ?? undefined, pending: camera.pending ?? undefined, mummified: camera.mummified ?? undefined, })) : undefined; return { error: response.error ?? undefined, errorMsg: response.errorMsg ?? undefined, cameras, }; } export async function getLineCrossingEnabledCameras( locationUuid: string, requestModifiers?: any, sessionId?: string ): Promise<OutputSchema["lineCrossingEnabledCamerasReport"]> { logger.info("🚶 Getting line crossing enabled cameras for location", locationUuid); const body = { locationUuid, }; const response = await postApi< schema["Camera_GetLineCrossingEnabledCamerasForLocationWSResponse"] >({ route: "/camera/getLineCrossingEnabledCamerasForLocation", body, modifiers: requestModifiers, sessionId, }); return { error: response.error ?? undefined, errorMsg: response.errorMsg ?? undefined, camerasToConfigs: response.camerasToConfigs ?? undefined, }; } export async function getThresholdCrossingCountReport( deviceUuid: string, startTimeMs: number, endTimeMs: number, bucketSize: "QUARTER_HOUR" | "HOUR" | "DAY" | "WEEK", crossingObject: "HUMAN" | "VEHICLE" | "UNKNOWN", dedupe: boolean, requestModifiers?: any, sessionId?: string ): Promise<OutputSchema["thresholdCrossingCountReport"]> { logger.info( "🚪 Getting threshold crossing count report", JSON.stringify({ deviceUuid, startTimeMs, endTimeMs, bucketSize, crossingObject, dedupe, }) ); const body = { deviceUuid, startTimeMs, endTimeMs, bucketSize, crossingObject, dedupe, }; const response = await postApi<schema["Report_GetThresholdCrossingCountReportWSResponse"]>({ route: "/report/getThresholdCrossingCountReport", body, modifiers: requestModifiers, sessionId, }); // Transform the response to handle null values const crossingCounts = response.crossingCounts ? response.crossingCounts.map(count => ({ timestampMs: count.timestampMs ?? undefined, ingressCount: count.ingressCount ?? undefined, egressCount: count.egressCount ?? undefined, })) : undefined; // Calculate metrics if we have crossing counts and the bucket size is appropriate let metrics: NonNullable<OutputSchema["thresholdCrossingCountReport"]>["metrics"] = undefined; if (crossingCounts && crossingCounts.length > 0) { // Calculate total entries and exits let totalEntries = 0; let totalExits = 0; let maxEntries = 0; let maxExits = 0; let maxTotal = 0; let maxEntriesTimestamp: number | undefined; let maxExitsTimestamp: number | undefined; let maxTotalTimestamp: number | undefined; let maxTotalEntries = 0; let maxTotalExits = 0; crossingCounts.forEach(count => { const entries = count.ingressCount || 0; const exits = count.egressCount || 0; const total = entries + exits; totalEntries += entries; totalExits += exits; // Track max entries if (entries > maxEntries) { maxEntries = entries; maxEntriesTimestamp = count.timestampMs; } // Track max exits if (exits > maxExits) { maxExits = exits; maxExitsTimestamp = count.timestampMs; } // Track busiest hour (max total) if (total > maxTotal) { maxTotal = total; maxTotalTimestamp = count.timestampMs; maxTotalEntries = entries; maxTotalExits = exits; } }); // Calculate hours based on bucket size let hoursInPeriod = 1; const periodMs = endTimeMs - startTimeMs; if (bucketSize === "HOUR") { hoursInPeriod = periodMs / (1000 * 60 * 60); } else if (bucketSize === "QUARTER_HOUR") { // For quarter hour buckets, we need to aggregate to hourly hoursInPeriod = periodMs / (1000 * 60 * 60); // Note: For more accurate hourly calculations with quarter-hour buckets, // we'd need to group by hour, but for now we'll use the total period } else if (bucketSize === "DAY") { hoursInPeriod = periodMs / (1000 * 60 * 60); } else if (bucketSize === "WEEK") { hoursInPeriod = periodMs / (1000 * 60 * 60); } // Helper function to format hour labels const formatHourLabel = (timestampMs: number | undefined): string => { if (!timestampMs) return "Unknown"; const date = DateTime.fromMillis(timestampMs); if (bucketSize === "HOUR") { const endHour = date.plus({ hours: 1 }); return `${date.toFormat("h:mm a")} - ${endHour.toFormat("h:mm a")} (${date.toFormat("MMM d, yyyy")})`; } else if (bucketSize === "QUARTER_HOUR") { const endTime = date.plus({ minutes: 15 }); return `${date.toFormat("h:mm a")} - ${endTime.toFormat("h:mm a")} (${date.toFormat("MMM d, yyyy")})`; } else if (bucketSize === "DAY") { return date.toFormat("MMM d, yyyy"); } else if (bucketSize === "WEEK") { const endWeek = date.plus({ weeks: 1 }).minus({ days: 1 }); return `Week of ${date.toFormat("MMM d")} - ${endWeek.toFormat("MMM d, yyyy")}`; } return date.toISO() || "Unknown"; }; metrics = { averageEntriesPerHour: totalEntries / hoursInPeriod, averageExitsPerHour: totalExits / hoursInPeriod, mostEntriesInHour: { count: maxEntries, timestamp: maxEntriesTimestamp ? DateTime.fromMillis(maxEntriesTimestamp).toISO()! : "", hourLabel: formatHourLabel(maxEntriesTimestamp), }, mostExitsInHour: { count: maxExits, timestamp: maxExitsTimestamp ? DateTime.fromMillis(maxExitsTimestamp).toISO()! : "", hourLabel: formatHourLabel(maxExitsTimestamp), }, busiestHour: { totalCount: maxTotal, timestamp: maxTotalTimestamp ? DateTime.fromMillis(maxTotalTimestamp).toISO()! : "", hourLabel: formatHourLabel(maxTotalTimestamp), entries: maxTotalEntries, exits: maxTotalExits, }, }; } return { error: response.error ?? undefined, errorMsg: response.errorMsg ?? undefined, crossingCounts, metrics, }; } export async function findPromptConfigurations( requestModifiers?: any, sessionId?: string ): Promise<OutputSchema["promptConfigurationsReport"]> { logger.info("🔍 Finding custom event prompt configurations"); const body = {}; const response = await postApi<schema["Scenequery_FindAllPromptConfigurationsWSResponse"]>({ route: "/scenequery/findPromptConfigurations", body, modifiers: requestModifiers, sessionId, }); // Transform the response to handle null values const promptConfigurations = response.promptConfigurations ? response.promptConfigurations.map(config => ({ active: config.active ?? undefined, cameraConfigurations: config.cameraConfigurations ?? undefined, checkCondition: config.checkCondition ?? undefined, description: config.description ?? undefined, name: config.name ?? undefined, orgUuid: config.orgUuid ?? undefined, prompt: config.prompt ?? undefined, promptType: config.promptType ?? undefined, scheduleUuid: config.scheduleUuid ?? undefined, shortName: config.shortName ?? undefined, uuid: config.uuid ?? undefined, })) : undefined; return { error: response.error ?? undefined, errorMsg: response.errorMsg ?? undefined, promptConfigurations, }; } export async function getCustomLLMReport( promptUuid: string, promptType: "COUNT" | "PERCENT" | "BOOLEAN", startTimeMs: number, endTimeMs: number, interval: "MINUTELY" | "QUARTERHOURLY" | "HOURLY" | "DAILY" | "WEEKLY" | "MONTHLY", requestModifiers?: any, sessionId?: string ): Promise<OutputSchema["customLLMReport"]> { logger.info( "📊 Getting custom LLM report", JSON.stringify({ promptUuid, promptType, startTimeMs, endTimeMs, interval, }) ); const body = { promptUuid, startTimeMs, endTimeMs, interval, }; // Determine the correct endpoint based on promptType let route: string; switch (promptType) { case "COUNT": route = "/report/getCustomLLMNumericCounts"; break; case "PERCENT": route = "/report/getCustomLLMReport"; break; case "BOOLEAN": route = "/report/getCustomLLMBinaryCounts"; break; default: throw new Error(`Unknown prompt type: ${promptType}`); } const response = await postApi<any>({ route, body, modifiers: requestModifiers, sessionId, }); logger.info( "📊 Custom LLM API response:", JSON.stringify({ promptType, hasReports: !!response.reports, hasTimeSeriesDataPoints: !!response.timeSeriesDataPoints, error: response.error, }) ); // Different prompt types return different response structures let timeSeriesDataPoints; if ((promptType === "BOOLEAN" || promptType === "PERCENT") && response.reports) { // BOOLEAN and PERCENT types return a reports object with device UUIDs as keys const reportEntries = Object.entries(response.reports); logger.info( `📊 ${promptType} response structure:`, JSON.stringify({ reportEntries: reportEntries.length }) ); // Aggregate data from all devices const aggregatedData: Record<string, any> = {}; reportEntries.forEach(([deviceId, reportData]) => { if (Array.isArray(reportData)) { reportData.forEach((item: any) => { const dateKey = item.localDate || new Date().toISOString(); if (!aggregatedData[dateKey]) { aggregatedData[dateKey] = { dateLocal: item.localDate, eventCount: 0, true: 0, false: 0, // For PERCENT type, store percentage values percentages: {}, }; } if (promptType === "BOOLEAN") { aggregatedData[dateKey].eventCount += item.eventCount || 0; aggregatedData[dateKey].true += item.true || 0; aggregatedData[dateKey].false += item.false || 0; } else if (promptType === "PERCENT") { // For PERCENT type, aggregate percentage data aggregatedData[dateKey].eventCount += item.eventCount || 0; // Store any percentage-related fields from the response Object.entries(item).forEach(([key, value]) => { if (key !== "localDate" && key !== "eventCount") { // Store any numeric values as percentages (could be number or string percentage) aggregatedData[dateKey].percentages[key] = value; } }); } }); } }); // Convert aggregated data to timeSeriesDataPoints format timeSeriesDataPoints = Object.entries(aggregatedData).map(([date, data]) => ({ dateLocal: data.dateLocal ?? undefined, dateUtc: data.dateLocal ?? undefined, // Convert to UTC if needed eventCountMap: promptType === "BOOLEAN" ? { total: data.eventCount, true: data.true, false: data.false, } : { total: data.eventCount, ...data.percentages, }, })); } else if (response.timeSeriesDataPoints) { // COUNT type uses the standard timeSeriesDataPoints format timeSeriesDataPoints = response.timeSeriesDataPoints.map((dataPoint: any) => ({ dateLocal: dataPoint.dateLocal ?? undefined, dateUtc: dataPoint.dateUtc ?? undefined, eventCountMap: dataPoint.eventCountMap ? Object.entries(dataPoint.eventCountMap).reduce( (acc, [key, value]) => { if (value !== null && value !== undefined) { acc[key] = value as number | boolean | string; } return acc; }, {} as Record<string, number | boolean | string> ) : undefined, })); } const result = { error: response.error ?? undefined, errorMsg: response.errorMsg ?? undefined, timeSeriesDataPoints, }; logger.info( "📊 Custom LLM report result:", JSON.stringify({ hasError: !!result.error, dataPointsCount: timeSeriesDataPoints?.length ?? 0, firstDataPoint: timeSeriesDataPoints?.[0], }) ); return result; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/RhombusSystems/rhombus-node-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server