import http from 'http'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { WebSocketServer } from 'ws'; import sqlite3 from 'sqlite3'; 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); } } } // HTML Dashboard const dashboardHTML = ` Shelly Status Dashboard

Shelly Status Dashboard

Connecting...

Loading devices...

`; // 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' }); 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(); ws.send(JSON.stringify({ type: 'init', devices })); } catch (err) { console.error('[Status] Error fetching initial data:', 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(); }