import http from 'http';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { WebSocketServer } from 'ws';
import sqlite3 from 'sqlite3';
import { getRulesStatus, setRuleConfig } from './rule_engine.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PORT = 8081;
const MODEL_PICS_DIR = path.join(__dirname, 'modelPics');
// SQLite database connection (read-only for status queries)
const db = new sqlite3.Database('devices.db', sqlite3.OPEN_READONLY);
// WebSocket clients for broadcasting
const wsClients = new Set();
// Query initial status data
function getStatusData() {
return new Promise((resolve, reject) => {
const sql = `
SELECT d.mac, d.model, d.connected, d.last_seen,
c.id as channel_id, c.component, c.field, c.type,
(SELECT e.event FROM events e WHERE e.channel_id = c.id ORDER BY e.id DESC LIMIT 1) as last_event,
(SELECT e.timestamp FROM events e WHERE e.channel_id = c.id ORDER BY e.id DESC LIMIT 1) as last_ts
FROM devices d
LEFT JOIN channels c ON c.mac = d.mac
ORDER BY d.mac, c.component, c.field
`;
db.all(sql, [], (err, rows) => {
if (err) reject(err);
else {
// Group by device
const devices = {};
for (const row of rows) {
if (!devices[row.mac]) {
devices[row.mac] = {
mac: row.mac,
model: row.model,
connected: row.connected,
last_seen: row.last_seen,
channels: []
};
}
if (row.channel_id) {
devices[row.mac].channels.push({
id: row.channel_id,
component: row.component,
field: row.field,
type: row.type,
event: row.last_event,
timestamp: row.last_ts
});
}
}
resolve(Object.values(devices));
}
});
});
}
// Broadcast event to all connected WebSocket clients
export function broadcastEvent(mac, component, field, type, event) {
const message = JSON.stringify({
type: 'event',
mac,
component,
field,
eventType: type,
event,
timestamp: new Date().toISOString()
});
for (const client of wsClients) {
if (client.readyState === 1) { // OPEN
client.send(message);
}
}
}
// Broadcast rule status update to all connected WebSocket clients
export function broadcastRuleUpdate(ruleName, status) {
const message = JSON.stringify({
type: 'rule_update',
name: ruleName,
status,
timestamp: new Date().toISOString()
});
for (const client of wsClients) {
if (client.readyState === 1) { // OPEN
client.send(message);
}
}
}
// HTML Dashboard
const dashboardHTML = `
IoT Status
`;
// Create HTTP server
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
// Serve model images
if (url.pathname.startsWith('/modelPics/')) {
const filename = path.basename(url.pathname);
const filepath = path.join(MODEL_PICS_DIR, filename);
if (fs.existsSync(filepath)) {
res.writeHead(200, { 'Content-Type': 'image/png' });
fs.createReadStream(filepath).pipe(res);
} else {
res.writeHead(404);
res.end('Not found');
}
return;
}
// Serve dashboard
if (url.pathname === '/' || url.pathname === '/index.html') {
res.writeHead(200, {
'Content-Type': 'text/html',
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
});
res.end(dashboardHTML);
return;
}
res.writeHead(404);
res.end('Not found');
});
// Create WebSocket server attached to HTTP server
const wss = new WebSocketServer({ server });
wss.on('connection', async (ws) => {
console.log('[Status] Browser client connected');
wsClients.add(ws);
// Send initial status data
try {
const devices = await getStatusData();
const rules = await getRulesStatus();
ws.send(JSON.stringify({ type: 'init', devices, rules }));
} catch (err) {
console.error('[Status] Error fetching initial data:', err);
}
ws.on('message', async (message) => {
try {
const data = JSON.parse(message);
if (data.type === 'set_config') {
const { ruleName, key, value } = data;
const success = setRuleConfig(ruleName, key, value);
if (success) {
// Broadcast updated status to all clients
const rules = await getRulesStatus();
const updateMsg = JSON.stringify({ type: 'rules_update', rules });
for (const client of wsClients) {
if (client.readyState === 1) {
client.send(updateMsg);
}
}
}
ws.send(JSON.stringify({ type: 'set_config_result', success }));
} else if (data.type === 'get_status') {
// Client requested full status refresh
const devices = await getStatusData();
const rules = await getRulesStatus();
ws.send(JSON.stringify({ type: 'init', devices, rules }));
}
} catch (err) {
console.error('[Status] Error processing message:', err);
}
});
ws.on('close', () => {
console.log('[Status] Browser client disconnected');
wsClients.delete(ws);
});
});
// Start server function
export function startStatusServer() {
server.listen(PORT, () => {
console.log(`[Status] Dashboard server running at http://localhost:${PORT}`);
});
}
// Allow running standalone
if (process.argv[1] === fileURLToPath(import.meta.url)) {
startStatusServer();
}