feat(mocks): retime chart mocks to today; add global/all-slot scripts
Add retimeAnalogInputs.mjs (all slots, single slot, or path) Add retimeAllCharts.mjs (recursive under chartsData) Update package.json with npm scripts: mocks:retime:ai (all analog slots) mocks:retime:ai:slot (single slot via %SLOT%) mocks:retime:all (entire chartsData tree) Preserve relative deltas; set first entry to today (same time); DIA2 daily at 00:00 Skip files/arrays without parsable "t" timestamps
This commit is contained in:
164
mocks/scripts/retimeAllCharts.mjs
Normal file
164
mocks/scripts/retimeAllCharts.mjs
Normal file
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Retime all chart JSON files under mocks/device-cgi-simulator/chartsData (recursively).
|
||||
* For any JSON array where elements contain a 't' timestamp string in format 'YYYY-MM-DD HH:mm:ss',
|
||||
* rebase timestamps so the first element's time-of-day is preserved but moved to today's date,
|
||||
* and all other elements keep their original relative deltas.
|
||||
*
|
||||
* Usage:
|
||||
* node ./mocks/scripts/retimeAllCharts.mjs [baseDir]
|
||||
* Default baseDir: mocks/device-cgi-simulator/chartsData
|
||||
*/
|
||||
|
||||
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) {
|
||||
const m =
|
||||
typeof str === "string" &&
|
||||
str.match(/(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})/);
|
||||
if (!m) return null;
|
||||
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();
|
||||
return new Date(
|
||||
today.getFullYear(),
|
||||
today.getMonth(),
|
||||
today.getDate(),
|
||||
baseTime.getHours(),
|
||||
baseTime.getMinutes(),
|
||||
baseTime.getSeconds(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
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 retimeArrayWithT(arr) {
|
||||
// Find first element with parsable 't'
|
||||
let idx0 = -1;
|
||||
let t0 = null;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const t = arr[i]?.t;
|
||||
const d = parseDateTime(t);
|
||||
if (d) {
|
||||
idx0 = i;
|
||||
t0 = d;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (idx0 === -1) return null; // nothing to retime
|
||||
|
||||
const newBase = withTodayDateAndTimeOf(t0);
|
||||
const t0ms = t0.getTime();
|
||||
|
||||
const updated = arr.map((item) => {
|
||||
if (!item || typeof item !== "object") return item;
|
||||
if (!("t" in item)) return item;
|
||||
const d = parseDateTime(item.t);
|
||||
if (!d) return item;
|
||||
const delta = t0ms - d.getTime();
|
||||
const newDate = new Date(newBase.getTime() - delta);
|
||||
return { ...item, t: formatDate(newDate) };
|
||||
});
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
async function processJsonFile(filePath) {
|
||||
try {
|
||||
const data = await readJson(filePath);
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
return { status: "skip", reason: "not an array or empty" };
|
||||
}
|
||||
|
||||
const updated = await retimeArrayWithT(data);
|
||||
if (!updated) {
|
||||
return { status: "skip", reason: "no parsable t fields" };
|
||||
}
|
||||
|
||||
await writeJson(filePath, updated);
|
||||
return { status: "ok" };
|
||||
} catch (e) {
|
||||
return { status: "error", message: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function walk(dir, visitor) {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const p = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
await walk(p, visitor);
|
||||
} else if (entry.isFile() && p.toLowerCase().endsWith(".json")) {
|
||||
await visitor(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const baseArg = process.argv[2];
|
||||
const baseDir = path.resolve(
|
||||
workspaceRoot,
|
||||
baseArg || "mocks/device-cgi-simulator/chartsData"
|
||||
);
|
||||
|
||||
let ok = 0,
|
||||
skipped = 0,
|
||||
errors = 0;
|
||||
const rel = (p) => path.relative(workspaceRoot, p) || p;
|
||||
|
||||
try {
|
||||
await walk(baseDir, async (file) => {
|
||||
const res = await processJsonFile(file);
|
||||
if (res.status === "ok") {
|
||||
ok++;
|
||||
console.log(`[ok] Updated ${rel(file)}`);
|
||||
} else if (res.status === "skip") {
|
||||
skipped++;
|
||||
console.log(`[skip] ${rel(file)}: ${res.reason}`);
|
||||
} else {
|
||||
errors++;
|
||||
console.log(`[error] ${rel(file)}: ${res.message}`);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to retime charts:", err);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\nDone. ok=${ok}, skipped=${skipped}, errors=${errors}`);
|
||||
}
|
||||
|
||||
await main();
|
||||
@@ -1,205 +0,0 @@
|
||||
#!/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
|
||||
* - <n> -> process slot n (e.g., 1 .. 8)
|
||||
* - <path/to/slotDir> -> 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();
|
||||
Reference in New Issue
Block a user