Skip to main content
Glama
RhombusSystems

Rhombus MCP Server

Official
events-tool-api.ts13.8 kB
import z from "zod"; import { FIVE_SECONDS_MS, THREE_HOURS_MS } from "../constants.js"; import { postApi } from "../network.js"; import type schema from "../types/schema.js"; import { formatTimestamp, type RequestModifiers } from "../util.js"; import { tempFunc, TempUnit } from "../utils/temp.js"; // Type definitions export const HumanEvent = z.object({ timestamp: z.number(), id: z.number(), }); export type HumanEvent = z.infer<typeof HumanEvent>; type MappedEnvironmentalEvent = { timestampString?: string; temp?: number | null; probeTemp?: number | null; humidity?: number | null; pm25?: number | null; co2?: number | null; vapeDetected?: boolean | null; }; type MappedClimateEvent = { timestampString?: string; timestampMs?: number | null; temp?: number | null; probeTempC?: number | null; humidity?: number | null; pm25?: number | null; co2?: number | null; tvoc?: number | null; iaq?: number | null; ethanol?: number | null; heatIndexDegF?: number | null; heatIndexRangeWarning?: string | null; vapeSmokeDetected?: boolean | null; vapeSmokePercent?: number | null; thcDetected?: boolean | null; thcPercent?: number | null; tampered?: boolean | null; batteryPercentage?: number | null; locationUuid?: string | null; orgUuid?: string | null; }; export async function getFaceEvents( _locationUuid: string | null | undefined, timeZone: string, requestModifiers?: RequestModifiers, sessionId?: string ) { const nowMs = Date.now(); const rangeStartMs = nowMs - THREE_HOURS_MS; const rangeEndMs = nowMs - FIVE_SECONDS_MS; const body = { pageRequest: { lastEvaluatedKey: undefined, maxPageSize: 75, }, searchFilter: { deviceUuids: [], faceNames: [], labels: [], locationUuids: [], personUuids: [], timestampFilter: { rangeStart: rangeStartMs, rangeEnd: rangeEndMs, }, }, }; const response = await postApi<schema["Facerecognition_faceevent_FindFaceEventsByOrgWSResponse"]>( { route: "/faceRecognition/faceEvent/findFaceEventsByOrg", body, modifiers: requestModifiers, sessionId, } ).then(response => { return { faceEvents: (response.faceEvents || []).map(event => ({ ...event, eventTimestamp: event.eventTimestamp ? formatTimestamp(event.eventTimestamp, timeZone) : undefined, })), }; }); return response; } export async function getAccessControlEvents( doorUuids: string[], startTime: number | undefined, endTime: number | undefined, timeZone: string, requestModifiers?: RequestModifiers, sessionId?: string ) { const bodies = doorUuids.map(doorUuid => ({ accessControlledDoorUuid: doorUuid, ...(startTime ? { createdAfterMs: startTime } : {}), ...(endTime ? { createdBeforeMs: endTime } : {}), typeFilter: ["CredentialReceivedEvent"], })); const responses = await Promise.all( bodies.map(body => postApi<schema["Component_FindComponentEventsByAccessControlledDoorWSResponse"]>({ route: "/component/findComponentEventsByAccessControlledDoor", body, modifiers: requestModifiers, sessionId, }).then(response => { return (response.componentEvents || []) .map((credEvent: schema["CredentialReceivedEventType"]) => { return { authenticationResult: credEvent?.authenticationResult, authorizationResult: credEvent?.authorizationResult, doorUuid: credEvent?.componentCompositeUuid, locationUuid: credEvent?.locationUuid, user: (credEvent?.originator as { username?: string } | undefined)?.username, credSource: credEvent?.credSource, datetime: credEvent?.timestampMs ? formatTimestamp(credEvent.timestampMs, timeZone) : undefined, }; }) .filter(Boolean); }) ) ); // Flatten all componentEvents into a single array const accessControlEvents = responses.flatMap(events => events); console.error(`componentEvents: ${JSON.stringify(accessControlEvents)}`); return accessControlEvents; } export async function getHumanMotionEvents( cameraUuid: string, duration: number, startTime: number, requestModifiers?: RequestModifiers, sessionId?: string ) { const body = { cameraUuid, duration, startTime: Math.round(startTime / 1000), }; const response = await postApi<schema["Camera_GetFootageSeekpointsV2WSResponse"]>({ route: "/camera/getFootageSeekpointsV2", body, modifiers: requestModifiers, sessionId, }).then(response => { const seekPoints = response.footageSeekPoints || []; const uniqueHumanEvents = seekPoints .reduceRight((acc: HumanEvent[], point) => { if ( point.a === "MOTION_HUMAN" && typeof point.ts === "number" && typeof point.id === "number" && !acc.some(existing => existing.id === point.id) ) { acc.unshift({ timestamp: point.ts, id: point.id }); } return acc; }, []) .map(event => ({ timestamp: event.timestamp, id: event.id, })); return { cameraUuid, uniqueHumanEvents }; }); return response; } const EVENT_COUNT_MAX_PER_RESPONSE = 2000; export async function getEventsForEnvironmentalGateway( deviceUuid: string, startTime: number | undefined, endTime: number | undefined, timeZone: string, tempUnit: TempUnit | null | undefined, requestModifiers?: RequestModifiers, sessionId?: string ) { let allEvents: MappedEnvironmentalEvent[] = []; let hasMore = true; let lastEvaluatedKey: string | undefined = undefined; while (hasMore) { const body: schema["Climate_GetEventsForEnvironmentalGatewayWSRequest"] = { deviceUuid, ...(startTime ? { createdAfterMs: startTime } : {}), ...(endTime ? { createdBeforeMs: endTime } : {}), maxPageSize: EVENT_COUNT_MAX_PER_RESPONSE, ...(lastEvaluatedKey ? { lastEvaluatedKey } : {}), }; const response = await postApi<schema["Climate_GetEventsForEnvironmentalGatewayWSResponse"]>({ route: "/climate/getEventsForEnvironmentalGateway", body, modifiers: requestModifiers, sessionId, }).then(response => { const tempFunc = tempUnit === TempUnit.FAHRENHEIT ? (temp: number) => (temp * 9) / 5 + 32 : (temp: number) => temp; return { events: (response.events || []).map(event => ({ timestampString: event.timestampMs ? formatTimestamp(event.timestampMs, timeZone) : undefined, temp: tempFunc(event.co2Sense?.tempC ?? 0), probeTemp: tempFunc(event.tempProbe?.tempC ?? 0), humidity: event.co2Sense?.relHumid, pm25: event.pmSense?.pm2p5, co2: event.co2Sense?.co2Ppm, vapeDetected: event.derivedValues?.vapeDetected, })), lastEvaluatedKey: response.lastEvaluatedKey, }; }); // Accumulate the events if (response.events) { allEvents.push(...response.events); } // Check if we should continue pagination if ( response.events && response.events.length === EVENT_COUNT_MAX_PER_RESPONSE && response.lastEvaluatedKey && response.lastEvaluatedKey !== null ) { // Update the lastEvaluatedKey for the next iteration lastEvaluatedKey = response.lastEvaluatedKey; } else { hasMore = false; } } return { events: allEvents, lastEvaluatedKey: undefined, // Don't return lastEvaluatedKey since we've fetched all pages }; } export async function getClimateEventsForSensor( sensorUuid: string, startTime: number | undefined, endTime: number | undefined, limit: number | null | undefined, timeZone: string, tempUnit: TempUnit | null | undefined, requestModifiers?: RequestModifiers, sessionId?: string ) { let allClimateEvents: MappedClimateEvent[] = []; let hasMore = true; let remainingLimit = limit || 1000; // Default to 1000 if no limit specified while (hasMore && allClimateEvents.length < remainingLimit) { const currentBatchSize = Math.min(100, remainingLimit - allClimateEvents.length); // Max 100 per request const body: schema["Climate_GetClimateEventsForSensorWSRequest"] = { sensorUuid, ...(startTime ? { createdAfterMs: startTime } : {}), ...(endTime ? { createdBeforeMs: endTime } : {}), limit: currentBatchSize, }; const response = await postApi<schema["Climate_GetClimateEventsForSensorWSResponse"]>({ route: "/climate/getClimateEventsForSensor", body, modifiers: requestModifiers, sessionId, }); if (response?.climateEvents) { // Map the climate events to include formatted timestamp and relevant fields const mappedEvents = response.climateEvents.map(event => ({ timestampString: event.timestampMs ? formatTimestamp(event.timestampMs, timeZone) : undefined, timestampMs: event.timestampMs, temp: event.temp, probeTemp: tempFunc(event.probeTempC ?? 0, tempUnit), // probeTempC: event.probeTempC, // probeTempF: event.probeTempC ? (event.probeTempC * 9) / 5 + 32 : undefined, humidity: event.humidity, pm25: event.pm25, co2: event.co2, tvoc: event.tvoc, iaq: event.iaq, ethanol: event.ethanol, heatIndexDegF: event.heatIndexDegF, heatIndexRangeWarning: event.heatIndexRangeWarning, vapeSmokeDetected: event.vapeSmokeDetected, vapeSmokePercent: event.vapeSmokePercent, thcDetected: event.thcDetected, thcPercent: event.thcPercent, tampered: event.tampered, batteryPercentage: event.batteryPercentage, locationUuid: event.locationUuid, orgUuid: event.orgUuid, })); allClimateEvents.push(...mappedEvents); // Check if we got a full batch and haven't reached the limit if ( response.climateEvents.length === currentBatchSize && allClimateEvents.length < remainingLimit ) { // Prepare for the next batch by updating the endTime to the oldest event's timestamp if (mappedEvents.length > 0) { const oldestEventTimestamp = mappedEvents[mappedEvents.length - 1].timestampMs; if (oldestEventTimestamp) { endTime = oldestEventTimestamp - 1; // Subtract 1ms to avoid duplicates } else { hasMore = false; } } else { hasMore = false; } } else { hasMore = false; } } else { hasMore = false; } } // Trim to the requested limit if we got more than needed if (limit && allClimateEvents.length > limit) { allClimateEvents = allClimateEvents.slice(0, limit); } return allClimateEvents; } export async function getComponentEventsByLocation( locationUuid: string, eventTypes: string[], // Still accept string[] for flexibility startTime: number | undefined, endTime: number | undefined, timeZone: string, requestModifiers?: RequestModifiers, sessionId?: string ) { const MAX_LIMIT = 1000; // API limit for component events const body: schema["Component_FindComponentEventsByLocationWSRequest"] = { locationUuid, typeFilter: eventTypes.length > 0 ? (eventTypes as any) : undefined, // API accepts string array ...(startTime ? { createdAfterMs: startTime } : {}), ...(endTime ? { createdBeforeMs: endTime } : {}), limit: MAX_LIMIT, }; const response = await postApi<schema["Component_FindComponentEventsByLocationWSResponse"]>({ route: "/component/findComponentEventsByLocation", body, modifiers: requestModifiers, sessionId, }); const events = (response.componentEvents || []).map(event => { // Map different event types to a common structure const baseEvent = { eventType: event.type, componentUuid: event.componentUuid, locationUuid: event.locationUuid, orgUuid: event.orgUuid, correlationId: event.correlationId, ownerDeviceUuid: event.ownerDeviceUuid, datetime: event.timestampMs ? formatTimestamp(event.timestampMs, timeZone) : undefined, timestampMs: event.timestampMs, uuid: event.uuid, }; // Add event-specific fields based on type if (event.type === "CredentialReceivedEvent") { const credEvent = event as any; return { ...baseEvent, authenticationResult: credEvent?.authenticationResult, authorizationResult: credEvent?.authorizationResult, user: credEvent?.originator?.username, credSource: credEvent?.credSource, doorUuid: credEvent?.componentCompositeUuid, }; } else if (event.type === "DoorbellEvent") { return { ...baseEvent, doorbellCameraUuid: event.componentUuid, }; } else if (event.type === "DoorStateChangeEvent") { const doorEvent = event as any; return { ...baseEvent, previousState: doorEvent?.previousState, newState: doorEvent?.newState, reason: doorEvent?.reason, }; } else if (event.type === "ButtonEvent" || event.type === "PanicButtonEvent") { const buttonEvent = event as any; return { ...baseEvent, buttonState: buttonEvent?.buttonState, }; } // For other event types, return the base event return baseEvent; }); // Sort events by timestamp (newest first) events.sort((a, b) => (b.timestampMs || 0) - (a.timestampMs || 0)); console.error(`componentEvents: ${JSON.stringify(events)}`); return events; }

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