import { createHash } from "node:crypto"; export function normalizeUnit(unit, { tenantId, connectionId, receivedAt = new Date() }) { const sourceUnitId = positiveInteger(first(unit?.id, unit?.unit_id, unit?.unitId)); if (!sourceUnitId) return null; const lastMessage = firstObject(unit?.lmsg, unit?.last_msg, unit?.lastMsg, unit?.lastMessage) || {}; const latitude = finiteNumber(first(lastMessage.lat, lastMessage.latitude, unit?.lat, unit?.latitude)); const longitude = finiteNumber(first(lastMessage.lon, lastMessage.lng, lastMessage.longitude, unit?.lon, unit?.lng, unit?.longitude)); const observedAt = timestamp(first(lastMessage.time, lastMessage.ts, lastMessage.timestamp, unit?.last_msg_time, unit?.lmsg_time), receivedAt); const telemetry = { sourceUnitId, name: optionalString(first(unit?.name, unit?.unit_name)), observedAt: observedAt.toISOString(), latitude, longitude, speed: finiteNumber(first(lastMessage.speed, unit?.speed)), course: finiteNumber(first(lastMessage.course, lastMessage.angle, unit?.course)), gpsValid: booleanOrNull(first(lastMessage.gps_valid, lastMessage.gpsValid, unit?.gps_valid)), satellites: finiteNumber(first(lastMessage.sats, lastMessage.satellites, unit?.sats)), hdop: finiteNumber(first(lastMessage.hdop, unit?.hdop)), accuracyM: finiteNumber(first(lastMessage.accuracy, lastMessage.accuracy_m, unit?.accuracy_m)), }; return { tenantId, connectionId, sourceUnitId, stableSubjectId: `gelios.unit:${connectionId}:${sourceUnitId}`, name: telemetry.name || `Gelios unit ${sourceUnitId}`, observedAt, receivedAt, latitude, longitude, speed: telemetry.speed, course: telemetry.course, gpsValid: telemetry.gpsValid, satellites: telemetry.satellites, hdop: telemetry.hdop, accuracyM: telemetry.accuracyM, telemetry, fingerprint: hash(telemetry), }; } export function extractUnitItems(payload) { if (Array.isArray(payload?.items)) return payload.items; if (Array.isArray(payload?.units)) return payload.units; if (Array.isArray(payload?.data)) return payload.data; return []; } export function hash(value) { return createHash("sha256").update(JSON.stringify(value)).digest("hex"); } function first(...values) { return values.find((value) => value !== undefined && value !== null && value !== ""); } function firstObject(...values) { return values.find((value) => value && typeof value === "object" && !Array.isArray(value)); } function positiveInteger(value) { const parsed = Number.parseInt(value, 10); return Number.isInteger(parsed) && parsed > 0 ? parsed : null; } function finiteNumber(value) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; } function optionalString(value) { const normalized = String(value ?? "").trim(); return normalized || null; } function booleanOrNull(value) { if (value === true || value === 1 || value === "1" || value === "true") return true; if (value === false || value === 0 || value === "0" || value === "false") return false; return null; } function timestamp(value, fallback) { const numeric = Number(value); if (!Number.isFinite(numeric) || numeric <= 0) return fallback; const milliseconds = numeric < 1_000_000_000_000 ? numeric * 1000 : numeric; const parsed = new Date(milliseconds); return Number.isNaN(parsed.getTime()) ? fallback : parsed; }