diff --git a/uiserver/webpack.config.js b/uiserver/webpack.config.js index 133ed34..89214de 100644 --- a/uiserver/webpack.config.js +++ b/uiserver/webpack.config.js @@ -545,14 +545,21 @@ module.exports = { }); // GET /api/devices - // Returns list of unique device/channel pairs + // Returns list of unique device/channel pairs (sensors + outputs) app.get('/api/devices', (req, res) => { try { if (!db) throw new Error('Database not connected'); - // Filter to only numeric channels which can be charted - const stmt = db.prepare("SELECT DISTINCT device, channel FROM sensor_events WHERE data_type = 'number' ORDER BY device, channel"); - const rows = stmt.all(); - res.json(rows); + // Get sensor channels + const sensorStmt = db.prepare("SELECT DISTINCT device, channel FROM sensor_events WHERE data_type = 'number' ORDER BY device, channel"); + const sensorRows = sensorStmt.all(); + + // Add output channels with 'output' as device + const outputRows = OUTPUT_CHANNELS.map(ch => ({ + device: 'output', + channel: ch.channel + })); + + res.json([...sensorRows, ...outputRows]); } catch (err) { res.status(500).json({ error: err.message }); } @@ -576,75 +583,108 @@ module.exports = { const startTime = since || new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); const endTime = until || new Date().toISOString(); - // 1. Fetch main data window - let sql = 'SELECT * FROM sensor_events WHERE timestamp > ? AND timestamp <= ? '; - const params = [startTime, endTime]; - - const requestedChannels = []; // [{device, channel}] + const requestedSensorChannels = []; // [{device, channel}] + const requestedOutputChannels = []; // [channel] if (req.query.selection) { const selections = req.query.selection.split(','); - if (selections.length > 0) { - const placeholders = []; - selections.forEach(s => { - const lastColonIndex = s.lastIndexOf(':'); - if (lastColonIndex !== -1) { - const d = s.substring(0, lastColonIndex); - const c = s.substring(lastColonIndex + 1); - placeholders.push('(device = ? AND channel = ?)'); - params.push(d, c); - requestedChannels.push({ device: d, channel: c }); + selections.forEach(s => { + const lastColonIndex = s.lastIndexOf(':'); + if (lastColonIndex !== -1) { + const d = s.substring(0, lastColonIndex); + const c = s.substring(lastColonIndex + 1); + if (d === 'output') { + requestedOutputChannels.push(c); + } else { + requestedSensorChannels.push({ device: d, channel: c }); } - }); - if (placeholders.length > 0) { - sql += `AND (${placeholders.join(' OR ')}) `; } - } + }); } - sql += 'ORDER BY timestamp ASC'; - - const stmt = db.prepare(sql); - const rows = stmt.all(...params); - - // 2. Backfill: Ensure we have a starting point for each channel - // For each requested channel, check if we have data at/near start. - // If the first point for a channel is > startTime, we should try to find the previous value. - // We check for the value that started before (or at) startTime AND ended after (or at) startTime (or hasn't ended). - - const backfillRows = []; - // Find the record that started most recently before startTime BUT was still valid at startTime - const backfillStmt = db.prepare(` - SELECT * FROM sensor_events - WHERE device = ? AND channel = ? - AND timestamp <= ? - AND (until >= ? OR until IS NULL) - ORDER BY timestamp DESC LIMIT 1 - `); - - requestedChannels.forEach(ch => { - const prev = backfillStmt.get(ch.device, ch.channel, startTime, startTime); - if (prev) { - // We found a point that covers the startTime. - backfillRows.push(prev); - } - }); - - // Combine and sort - const allRows = [...backfillRows, ...rows]; - - // Transform to Compact Dictionary Format - // { "device:channel": [ [timestamp, value, until], ... ] } const result = {}; - allRows.forEach(row => { - const key = `${row.device}:${row.channel}`; - if (!result[key]) result[key] = []; + // 1. Fetch sensor data + if (requestedSensorChannels.length > 0) { + let sql = 'SELECT * FROM sensor_events WHERE timestamp > ? AND timestamp <= ? '; + const params = [startTime, endTime]; - const pt = [row.timestamp, row.value]; - if (row.until) pt.push(row.until); - result[key].push(pt); - }); + const placeholders = []; + requestedSensorChannels.forEach(ch => { + placeholders.push('(device = ? AND channel = ?)'); + params.push(ch.device, ch.channel); + }); + if (placeholders.length > 0) { + sql += `AND (${placeholders.join(' OR ')}) `; + } + sql += 'ORDER BY timestamp ASC'; + + const rows = db.prepare(sql).all(...params); + + // Backfill for sensors + const backfillStmt = db.prepare(` + SELECT * FROM sensor_events + WHERE device = ? AND channel = ? + AND timestamp <= ? + AND (until >= ? OR until IS NULL) + ORDER BY timestamp DESC LIMIT 1 + `); + + const backfillRows = []; + requestedSensorChannels.forEach(ch => { + const prev = backfillStmt.get(ch.device, ch.channel, startTime, startTime); + if (prev) backfillRows.push(prev); + }); + + [...backfillRows, ...rows].forEach(row => { + const key = `${row.device}:${row.channel}`; + if (!result[key]) result[key] = []; + const pt = [row.timestamp, row.value]; + if (row.until) pt.push(row.until); + result[key].push(pt); + }); + } + + // 2. Fetch output data + if (requestedOutputChannels.length > 0) { + let sql = 'SELECT * FROM output_events WHERE timestamp > ? AND timestamp <= ? '; + const params = [startTime, endTime]; + + const placeholders = requestedOutputChannels.map(() => 'channel = ?'); + sql += `AND (${placeholders.join(' OR ')}) `; + params.push(...requestedOutputChannels); + sql += 'ORDER BY timestamp ASC'; + + const rows = db.prepare(sql).all(...params); + + // Backfill for outputs + const backfillStmt = db.prepare(` + SELECT * FROM output_events + WHERE channel = ? + AND timestamp <= ? + AND (until >= ? OR until IS NULL) + ORDER BY timestamp DESC LIMIT 1 + `); + + const backfillRows = []; + requestedOutputChannels.forEach(ch => { + const prev = backfillStmt.get(ch, startTime, startTime); + if (prev) { + backfillRows.push(prev); + } else { + // No data at all - add default 0 value at startTime + backfillRows.push({ channel: ch, timestamp: startTime, value: 0, until: null }); + } + }); + + [...backfillRows, ...rows].forEach(row => { + const key = `output:${row.channel}`; + if (!result[key]) result[key] = []; + const pt = [row.timestamp, row.value]; + if (row.until) pt.push(row.until); + result[key].push(pt); + }); + } res.json(result); } catch (err) {