const { InfluxDB, Point } = require('@influxdata/influxdb-client'); const url = process.env.INFLX_URL; const token = process.env.INFLX_TOKEN; const org = process.env.INFLX_ORG; const boatTelemetry = "boat" const client = new InfluxDB({ url, token }) const write = client.getWriteApi(org, boatTelemetry); const querying = client.getQueryApi(org); console.log("InfluxDB client initialized with config:", { url, org, token }); async function append(measurement, sensor, data) { const point = new Point(measurement) .tag("sensor", sensor) .floatField('temperature', data.temperature) .floatField('humidity', data.humidity) write.writePoint(point); await write.flush(); } async function writeBatch(datas) { datas.forEach(data => { append(data.measurement, data.sensor, data.data); }) } async function query(bucket, relativeTime, measurement, sensor, field) { const fluxTimeRange = relativeTime || "-1h"; let fluxQuery = ` from(bucket: "${bucket}") |> range(start: ${fluxTimeRange}) |> filter(fn: (r) => r._measurement == "${measurement}")`; if (sensor) { fluxQuery += `\n |> filter(fn: (r) => r.sensor == "${sensor}")`; } if (field) { fluxQuery += `\n |> filter(fn: (r) => r._field == "${field}")`; } fluxQuery += `\n |> yield(name: "data")`; try { const data = []; for await (const { values, tableMeta } of querying.iterateRows(fluxQuery)) { data.push(tableMeta.toObject(values)); } return data; } catch (error) { console.error("Error in query:", error); return []; } } // Sorgente di verità per i logs di sessione: stesso bucket usato da // realtime/store/influx.js. Sovrascrivibile via env per ambiente. const sessionBucket = process.env.INFLX_BUCKET_LOGS || process.env.INFLX_BUCKET || 'logs'; /** * Query storica per una sessione di registrazione. * @param {string} sensor - nome sensore * @param {string} session - session_id (tag InfluxDB) * @param {string} since - ISO timestamp o duration (es. "-30d") * @param {string|null} until - ISO timestamp fine (opzionale) * @returns {Promise>} */ async function querySessionHistory(sensor, session, since, until = null) { const rangeStr = until ? `start: ${since}, stop: ${until}` : `start: ${since}`; const fluxQuery = ` from(bucket: "${sessionBucket}") |> range(${rangeStr}) |> filter(fn: (r) => r._measurement == "logs") |> filter(fn: (r) => r.sensor == "${sensor}") |> filter(fn: (r) => r.session == "${session}") |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") |> sort(columns: ["_time"]) `; const rows = []; return new Promise((resolve, reject) => { querying.queryRows(fluxQuery, { next(row, tableMeta) { rows.push(tableMeta.toObject(row)); }, error: reject, complete() { resolve(rows); }, }); }); } /** * Esporta i dati di una sessione come stringa CSV. * @param {string} sensor * @param {string} session * @param {string} since * @param {string|null} until * @returns {Promise} */ async function exportSessionCSV(sensor, session, since, until = null) { const rows = await querySessionHistory(sensor, session, since, until); if (rows.length === 0) return ''; const metaKeys = new Set(['result', 'table', '_start', '_stop', '_measurement', 'sensor', 'session', '']); const fieldNames = new Set(); for (const row of rows) { for (const key of Object.keys(row)) { if (!metaKeys.has(key) && key !== '_time') fieldNames.add(key); } } const fields = Array.from(fieldNames).sort(); const header = ['timestamp', ...fields].join(','); const csvRows = rows.map(row => { const values = fields.map(f => { const v = row[f]; return (v == null) ? '' : v; }); return [row._time || '', ...values].join(','); }); return header + '\n' + csvRows.join('\n') + '\n'; } /** * Utility interna: esegue una Flux query e restituisce le righe come array di oggetti. */ function runFlux(fluxQuery) { const rows = []; return new Promise((resolve, reject) => { querying.queryRows(fluxQuery, { next(row, tableMeta) { rows.push(tableMeta.toObject(row)); }, error: reject, complete() { resolve(rows); }, }); }); } /** * Elenca tutte le sessioni presenti in InfluxDB, con primo e ultimo timestamp. * Sorgente di verità: tag sensor + session sul measurement "logs". * @param {string} [lookback='-5y'] - range di ricerca (es. '-365d', '-5y') * @returns {Promise>} */ async function listInfluxSessions(lookback = '-5y') { const base = ` from(bucket: "${sessionBucket}") |> range(start: ${lookback}) |> filter(fn: (r) => r._measurement == "logs") |> group(columns: ["sensor", "session"]) `; const [firstRows, lastRows] = await Promise.all([ runFlux(base + '|> first() |> keep(columns: ["_time", "sensor", "session"])'), runFlux(base + '|> last() |> keep(columns: ["_time", "sensor", "session"])'), ]); const map = {}; firstRows.forEach(r => { if (!r.session) return; map[r.session] = { session: r.session, sensor: r.sensor, startTime: r._time }; }); lastRows.forEach(r => { if (map[r.session]) map[r.session].endTime = r._time; }); return Object.values(map).sort((a, b) => new Date(b.startTime) - new Date(a.startTime)); } async function checkInflux() { try { const result = await querying.collectRows( `from(bucket: "boat") |> range(start: -1s) |> limit(n:1)` ); console.log('InfluxDB: OK'); return { ok: true }; } catch (error) { console.error('InfluxDB check failed:', { message: error.message, statusCode: error.statusCode ?? 'N/A', body: error.body ?? 'N/A' }); return false } } module.exports = { write: append, writeBatch, query, listInfluxSessions, querySessionHistory, exportSessionCSV, checkInflux }