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

IoT Status

Connecting...

Loading devices...

Rules
`; // 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(); }