rhombus-node-mcp
Version:
MCP server for Rhombus API
185 lines (184 loc) • 6.83 kB
JavaScript
import { getLogger } from "../logger.js";
import { postApi } from "../network/network.js";
import { removeNullFields } from "../util.js";
const logger = getLogger("camera-tool");
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
/** Convert a 0-100 percentage to permyriad (0-10000), clamped to the valid range. */
function pctToPermyriad(pct) {
return Math.round(Math.max(0, Math.min(100, pct)) * 100);
}
export async function getImageForCameraAtTime(cameraUuid, timestampMs, requestModifiers, sessionId, options) {
// biome-ignore lint/suspicious/noExplicitAny: request body is a loose JSON shape
const body = {
cameraUuid,
timestampMs,
downscaleFactor: options?.downscaleFactor ?? 10,
jpgQuality: 70,
};
const crop = options?.crop;
const hasCrop = crop != null &&
(crop.x != null || crop.y != null || crop.width != null || crop.height != null);
const resolvedCrop = hasCrop
? {
x: crop.x ?? 0,
y: crop.y ?? 0,
width: crop.width ?? 100,
height: crop.height ?? 100,
}
: null;
if (resolvedCrop) {
body.permyriadCropX = pctToPermyriad(resolvedCrop.x);
body.permyriadCropY = pctToPermyriad(resolvedCrop.y);
body.permyriadCropWidth = pctToPermyriad(resolvedCrop.width);
body.permyriadCropHeight = pctToPermyriad(resolvedCrop.height);
}
logger.debug(`Getting exact frame data for UUID: ${cameraUuid} at timestampMs: ${timestampMs}` +
(hasCrop ? ` (cropped)` : ``));
const res = await postApi({
route: "/video/getExactFrameData",
body,
modifiers: requestModifiers,
sessionId,
});
if (res.error || !res.frameData) {
logger.error(`getExactFrameData failed: ${JSON.stringify({ error: res.error, errorMsg: res.errorMsg, status: res.status, hasFrameData: !!res.frameData })}`);
return {
success: false,
status: "failed to fetch image",
// Prefer a structured business error from the endpoint, then any transport-level
// status set by postApi (e.g. permission/HTTP errors), and only fall back to the
// no-VOD explanation when no error detail is available.
message: res.errorMsg ??
res.status ??
"Camera snapshot unavailable: this camera may have no recorded video (VOD) at the requested time. " +
"It may not have any footage stored, or the requested timestamp may be outside its retention window.",
};
}
return {
success: true,
status: "successfully fetched image",
imageType: "base64",
imageData: res.frameData,
crop: resolvedCrop,
};
}
async function getCameraStorageData(cameraUuid, requestModifiers, sessionId) {
// First, get the camera's full state to determine the time range and cloud archive days
const stateResponse = await postApi({
route: "/camera/getFullCameraState",
body: {
cameraUuid: cameraUuid,
},
modifiers: requestModifiers,
sessionId,
});
const cloudArchiveDays = stateResponse.fullCameraState?.onCloudState?.cloud_archive_days ?? null;
// Get the oldest segment time (in seconds) to use as start time
const oldestSegmentSecs = stateResponse.fullCameraState?.onCameraState?.oldest_segment_secs ?? 0;
const endTimeSec = Math.floor(Date.now() / 1000);
const startTimeSec = oldestSegmentSecs || 0;
const durationSec = endTimeSec - startTimeSec;
const presenceResponse = await postApi({
route: "/camera/getPresenceWindows",
body: {
cameraUuid: cameraUuid,
startTimeSec: startTimeSec,
durationSec: durationSec,
},
modifiers: requestModifiers,
sessionId,
});
const calculateTotalDays = (timeWindows) => {
if (!timeWindows || timeWindows.length === 0) {
return 0;
}
let totalMilliseconds = 0;
timeWindows.forEach(window => {
if (window.startSeconds && window.durationSeconds) {
const durationMs = window.durationSeconds * 1000;
totalMilliseconds += durationMs;
}
});
return Math.floor(totalMilliseconds / MILLISECONDS_PER_DAY);
};
const presenceWindows = presenceResponse.presenceWindows;
const daysOnCamera = calculateTotalDays(presenceWindows?.VideoLocal);
const daysInCloud = calculateTotalDays(presenceWindows?.VideoCloud);
return {
daysInCloud,
daysOnCamera,
cloudArchiveDays,
};
}
export async function getCameraSettings(cameraUuid, requestModifiers, sessionId) {
const res = await postApi({
route: "/camera/getFacetedConfig",
body: {
deviceUuid: cameraUuid,
},
modifiers: requestModifiers,
sessionId,
});
if (res.error) {
return {
success: false,
status: "failed to fetch camera settings",
};
}
const storageData = await getCameraStorageData(cameraUuid, requestModifiers, sessionId);
return {
success: true,
config: res.config,
daysInCloud: storageData.daysInCloud,
daysOnCamera: storageData.daysOnCamera,
cloudArchiveDays: storageData.cloudArchiveDays,
status: "fetched camera settings",
};
}
export async function getCameraMediaUris(cameraUuid, requestModifiers, sessionId) {
const res = await postApi({
route: "/camera/getCameraMediaUris",
body: { deviceUuid: cameraUuid },
modifiers: requestModifiers,
sessionId,
});
if (res.error)
throw new Error(JSON.stringify(res));
return res;
}
export async function getCameraAIThresholds(cameraUuid, requestModifiers, sessionId) {
const res = await postApi({
route: "/camera/getCameraAIThresholds",
body: { deviceUuid: cameraUuid },
modifiers: requestModifiers,
sessionId,
});
if (res.error)
throw new Error(JSON.stringify(res));
return res;
}
export async function updateCameraSettings(cameraUuid, update, requestModifiers, sessionId) {
// remove any "null" values
const res = await postApi({
route: "/camera/updateFacetedConfig",
body: {
configUpdate: {
deviceUuid: cameraUuid,
...removeNullFields(update),
},
},
modifiers: requestModifiers,
sessionId,
});
if (res.error) {
return {
success: false,
status: "failed to update camera settings",
};
}
return {
success: true,
config: res.config,
status: "updated camera settings",
};
}