#!/usr/bin/env node /** * Retime mock chart data so the newest entry is "today" and older entries are shifted accordingly. * * Files handled: * - DIA0.json: high-resolution data with many entries, each having a "t" timestamp. * Strategy: keep original intervals by computing deltas from the first item and rebase to today's date * at the time-of-day of the original first item. * - DIA1.json: typically contains only values (no "t"). We skip if no "t" exists. * - DIA2.json: daily aggregation with "t" at 00:00:00. Strategy: set top item to today 00:00:00, * then each subsequent item to one day earlier. * * Usage: * node ./mocks/scripts/retimeAnalogInputs.mjs [target] * target options: * - all -> process all slots under analogInputs * - -> process slot n (e.g., 1 .. 8) * - -> process the given directory containing DIA*.json * Default: all slots under mocks/device-cgi-simulator/chartsData/analogInputs */ import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const workspaceRoot = path.resolve(__dirname, "../.."); function pad(n) { return String(n).padStart(2, "0"); } function formatDate(d) { const yyyy = d.getFullYear(); const MM = pad(d.getMonth() + 1); const dd = pad(d.getDate()); const hh = pad(d.getHours()); const mm = pad(d.getMinutes()); const ss = pad(d.getSeconds()); return `${yyyy}-${MM}-${dd} ${hh}:${mm}:${ss}`; } function parseDateTime(str) { // Expecting "YYYY-MM-DD HH:mm:ss" // Replace space with 'T' to ensure consistent parsing without timezone conversion. // We treat times as local; constructing a Date with year, month-1, day, hours, mins, secs is safer. const m = str.match(/(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})/); if (!m) return new Date(str); const [, y, mo, d, h, mi, s] = m.map(Number); return new Date(y, mo - 1, d, h, mi, s, 0); } function withTodayDateAndTimeOf(baseTime) { const today = new Date(); const d = new Date( today.getFullYear(), today.getMonth(), today.getDate(), baseTime.getHours(), baseTime.getMinutes(), baseTime.getSeconds(), 0 ); return d; } async function readJson(filePath) { const raw = await fs.readFile(filePath, "utf-8"); return JSON.parse(raw); } async function writeJson(filePath, data) { const content = JSON.stringify(data, null, 2); await fs.writeFile(filePath, content + "\n", "utf-8"); } async function retimeDIA0(filePath) { const arr = await readJson(filePath); if (!Array.isArray(arr) || arr.length === 0) return false; if (!("t" in arr[0])) return false; // Compute deltas from first item's timestamp const t0 = parseDateTime(arr[0].t); const newBase = withTodayDateAndTimeOf(t0); const t0ms = t0.getTime(); const updated = arr.map((item) => { if (!item.t) return item; // leave untouched const ti = parseDateTime(item.t).getTime(); const delta = t0ms - ti; // ms to subtract from base const newDate = new Date(newBase.getTime() - delta); return { ...item, t: formatDate(newDate) }; }); await writeJson(filePath, updated); return true; } async function retimeDIA2(filePath) { const arr = await readJson(filePath); if (!Array.isArray(arr) || arr.length === 0) return false; if (!("t" in arr[0])) return false; const todayMidnight = new Date(); todayMidnight.setHours(0, 0, 0, 0); const updated = arr.map((item, idx) => { const d = new Date(todayMidnight.getTime() - idx * 24 * 60 * 60 * 1000); return { ...item, t: formatDate(d) }; }); await writeJson(filePath, updated); return true; } async function maybeLogSkip(filePath, reason) { console.log(`[skip] ${path.basename(filePath)}: ${reason}`); } async function processSlotDir(targetDir) { const dia0 = path.join(targetDir, "DIA0.json"); const dia1 = path.join(targetDir, "DIA1.json"); const dia2 = path.join(targetDir, "DIA2.json"); // DIA0 try { const changed = await retimeDIA0(dia0); if (!changed) await maybeLogSkip(dia0, "no data or no t field"); else console.log(`[ok] Updated ${path.basename(dia0)}`); } catch (e) { console.error(`[error] ${path.basename(dia0)}:`, e.message); } // DIA1: usually no 't' -> skip gracefully. If it has 't', treat like DIA0 (keep intervals) try { const arr = await readJson(dia1); if (Array.isArray(arr) && arr.length && "t" in arr[0]) { const changed = await retimeDIA0(dia1); if (changed) console.log(`[ok] Updated ${path.basename(dia1)}`); else await maybeLogSkip(dia1, "no data or no t field"); } else { await maybeLogSkip(dia1, "no t field present"); } } catch (e) { console.error(`[error] ${path.basename(dia1)}:`, e.message); } // DIA2 try { const changed = await retimeDIA2(dia2); if (!changed) await maybeLogSkip(dia2, "no data or no t field"); else console.log(`[ok] Updated ${path.basename(dia2)}`); } catch (e) { console.error(`[error] ${path.basename(dia2)}:`, e.message); } } async function main() { try { const arg = process.argv[2]; const analogBase = path.resolve( workspaceRoot, "mocks/device-cgi-simulator/chartsData/analogInputs" ); let targets = []; if (!arg || arg === "all") { // process all numeric slot directories under analogInputs const entries = await fs.readdir(analogBase, { withFileTypes: true }); targets = entries .filter((e) => e.isDirectory() && /^\d+$/.test(e.name)) .map((e) => path.join(analogBase, e.name)) .sort((a, b) => Number(path.basename(a)) - Number(path.basename(b))); } else if (/^\d+$/.test(arg)) { targets = [path.join(analogBase, arg)]; } else { // treat as a path const p = path.isAbsolute(arg) ? arg : path.resolve(workspaceRoot, arg); targets = [p]; } if (!targets.length) { console.log("No analog input slot directories found."); return; } for (const dir of targets) { try { await fs.access(dir); } catch { console.log(`[skip] ${dir}: not found`); continue; } console.log(`\n=== Processing slot: ${path.basename(dir)} ===`); await processSlotDir(dir); } } catch (err) { console.error("Failed to retime analog input data:", err); process.exitCode = 1; } } await main();