u
This commit is contained in:
@@ -545,14 +545,21 @@ module.exports = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/devices
|
// 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) => {
|
app.get('/api/devices', (req, res) => {
|
||||||
try {
|
try {
|
||||||
if (!db) throw new Error('Database not connected');
|
if (!db) throw new Error('Database not connected');
|
||||||
// Filter to only numeric channels which can be charted
|
// Get sensor channels
|
||||||
const stmt = db.prepare("SELECT DISTINCT device, channel FROM sensor_events WHERE data_type = 'number' ORDER BY device, channel");
|
const sensorStmt = db.prepare("SELECT DISTINCT device, channel FROM sensor_events WHERE data_type = 'number' ORDER BY device, channel");
|
||||||
const rows = stmt.all();
|
const sensorRows = sensorStmt.all();
|
||||||
res.json(rows);
|
|
||||||
|
// Add output channels with 'output' as device
|
||||||
|
const outputRows = OUTPUT_CHANNELS.map(ch => ({
|
||||||
|
device: 'output',
|
||||||
|
channel: ch.channel
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json([...sensorRows, ...outputRows]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ error: err.message });
|
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 startTime = since || new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
||||||
const endTime = until || new Date().toISOString();
|
const endTime = until || new Date().toISOString();
|
||||||
|
|
||||||
// 1. Fetch main data window
|
const requestedSensorChannels = []; // [{device, channel}]
|
||||||
let sql = 'SELECT * FROM sensor_events WHERE timestamp > ? AND timestamp <= ? ';
|
const requestedOutputChannels = []; // [channel]
|
||||||
const params = [startTime, endTime];
|
|
||||||
|
|
||||||
const requestedChannels = []; // [{device, channel}]
|
|
||||||
|
|
||||||
if (req.query.selection) {
|
if (req.query.selection) {
|
||||||
const selections = req.query.selection.split(',');
|
const selections = req.query.selection.split(',');
|
||||||
if (selections.length > 0) {
|
selections.forEach(s => {
|
||||||
const placeholders = [];
|
const lastColonIndex = s.lastIndexOf(':');
|
||||||
selections.forEach(s => {
|
if (lastColonIndex !== -1) {
|
||||||
const lastColonIndex = s.lastIndexOf(':');
|
const d = s.substring(0, lastColonIndex);
|
||||||
if (lastColonIndex !== -1) {
|
const c = s.substring(lastColonIndex + 1);
|
||||||
const d = s.substring(0, lastColonIndex);
|
if (d === 'output') {
|
||||||
const c = s.substring(lastColonIndex + 1);
|
requestedOutputChannels.push(c);
|
||||||
placeholders.push('(device = ? AND channel = ?)');
|
} else {
|
||||||
params.push(d, c);
|
requestedSensorChannels.push({ device: d, channel: c });
|
||||||
requestedChannels.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 = {};
|
const result = {};
|
||||||
|
|
||||||
allRows.forEach(row => {
|
// 1. Fetch sensor data
|
||||||
const key = `${row.device}:${row.channel}`;
|
if (requestedSensorChannels.length > 0) {
|
||||||
if (!result[key]) result[key] = [];
|
let sql = 'SELECT * FROM sensor_events WHERE timestamp > ? AND timestamp <= ? ';
|
||||||
|
const params = [startTime, endTime];
|
||||||
|
|
||||||
const pt = [row.timestamp, row.value];
|
const placeholders = [];
|
||||||
if (row.until) pt.push(row.until);
|
requestedSensorChannels.forEach(ch => {
|
||||||
result[key].push(pt);
|
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);
|
res.json(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user