93 lines
3.4 KiB
JavaScript
93 lines
3.4 KiB
JavaScript
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;
|
|
}
|