Weather Station / ECOWITT / DNT
0

Configure Feed

Select the types of activity you want to include in your feed.

at stats2.0 4.7 kB View raw
1import { promises as fs } from "fs"; 2import path from "path"; 3import { parseTimestamp, floorToResolution, keyForResolution, type Resolution } from "@/lib/time"; 4 5/** 6 * Represents a row of data from a CSV file. 7 * The `time` property is always a string, while other properties can be strings, numbers, or null. 8 */ 9export type Row = { [key: string]: string | number | null } & { time: string }; 10 11/** 12 * Reads a CSV file from a relative path. 13 * @param {string} relPath - The relative path to the CSV file. 14 * @returns {Promise<string>} A promise that resolves with the content of the file as a string. 15 */ 16export async function readCsvFile(relPath: string): Promise<string> { 17 const base = process.cwd(); 18 const abs = path.join(base, relPath); 19 return fs.readFile(abs, "utf8"); 20} 21 22/** 23 * Parses a CSV string into a header array and an array of row objects. 24 * @param {string} content - The CSV content as a string. 25 * @returns {{ header: string[]; rows: Row[] }} An object containing the header and rows. 26 */ 27export function parseCsv(content: string): { header: string[]; rows: Row[] } { 28 const lines = content.split(/\r?\n/).filter((l) => l.trim().length > 0); 29 if (lines.length === 0) return { header: [], rows: [] }; 30 // Strip potential UTF-8 BOM 31 if (lines[0].charCodeAt(0) === 0xfeff) { 32 lines[0] = lines[0].slice(1); 33 } 34 const header = lines[0].split(",").map((s) => s.trim()); 35 const rows: Row[] = []; 36 for (let i = 1; i < lines.length; i++) { 37 const cols = lines[i].split(","); 38 if (cols.length === 0) continue; 39 const row: Row = { time: "" } as Row; 40 for (let c = 0; c < header.length; c++) { 41 const key = header[c]; 42 const valRaw = cols[c] ?? ""; 43 const val = valRaw.trim(); 44 // Always treat first column as time; header may vary or include BOM 45 if (c === 0) { 46 row.time = val; 47 continue; 48 } 49 if (val === "--" || val === "") row[key] = null; 50 else if (!isNaN(Number(val))) row[key] = Number(val); 51 else row[key] = val; 52 } 53 if (row.time) rows.push(row); 54 } 55 return { header, rows }; 56} 57 58/** 59 * Aggregates rows of data by a given time resolution. 60 * @param {Row[]} rows - The array of rows to aggregate. 61 * @param {Resolution} resolution - The time resolution to group by (e.g., "minute", "hour", "day"). 62 * @param {Date} [start] - An optional start date to filter the rows. 63 * @param {Date} [end] - An optional end date to filter the rows. 64 * @returns {Array<Row & { key: string }>} An array of aggregated rows, with an added `key` property for the time bucket. 65 */ 66export function aggregateRows(rows: Row[], resolution: Resolution, start?: Date, end?: Date): Array<Row & { key: string }> { 67 // Group by floored time 68 const map = new Map<string, { t: Date; acc: Record<string, number>; cnt: Record<string, number> }>(); 69 for (const r of rows) { 70 const dt = parseTimestamp(r.time); 71 if (!dt) continue; 72 if (start && dt < start) continue; 73 if (end && dt > end) continue; 74 const bucket = floorToResolution(dt, resolution); 75 const k = keyForResolution(bucket, resolution); 76 let entry = map.get(k); 77 if (!entry) { 78 entry = { t: bucket, acc: {}, cnt: {} }; 79 map.set(k, entry); 80 } 81 for (const [k2, v] of Object.entries(r)) { 82 if (k2 === "time") continue; 83 if (typeof v === "number") { 84 entry.acc[k2] = (entry.acc[k2] ?? 0) + v; 85 entry.cnt[k2] = (entry.cnt[k2] ?? 0) + 1; 86 } 87 } 88 } 89 // Build aggregated rows (averages) 90 const out: (Row & { key: string })[] = []; 91 for (const [k, e] of Array.from(map.entries()).sort((a, b) => a[0].localeCompare(b[0]))) { 92 const row: Row & { key: string } = { key: k, time: k } as any; 93 for (const [col, sum] of Object.entries(e.acc)) { 94 const n = e.cnt[col] ?? 1; 95 row[col] = sum / n; 96 } 97 out.push(row); 98 } 99 return out; 100} 101 102/** 103 * Infers the keys for temperature, humidity, dew point, and heat index from a CSV header. 104 * @param {string[]} header - The array of header strings. 105 * @returns {{ temp: string[]; hum: string[]; dew: string[]; heat: string[] }} An object containing arrays of keys for each metric. 106 */ 107export function inferAllsensorKeys(header: string[]): { temp: string[]; hum: string[]; dew: string[]; heat: string[] } { 108 const temp: string[] = []; 109 const hum: string[] = []; 110 const dew: string[] = []; 111 const heat: string[] = []; 112 for (const h of header) { 113 if (/^CH\d+ Temperature/.test(h)) temp.push(h); 114 else if (/^CH\d+ Luftfeuchtigkeit/.test(h) || /^WN35CH\d+hum/.test(h)) hum.push(h); 115 else if (/^CH\d+ Taupunkt/.test(h)) dew.push(h); 116 else if (/^CH\d+ Wärmeindex/.test(h)) heat.push(h); 117 } 118 return { temp, hum, dew, heat }; 119}