UNPKG

domotz-mcp

Version:

MCP server for Domotz API and BMS (Building Management System) integration with IoT device monitoring

550 lines (511 loc) 19.4 kB
import express from 'express'; import { WebSocketServer } from 'ws'; import { createServer } from 'http'; import path from 'path'; import { BMSConnectionTool } from '../tools/BMSConnectionTool.js'; import { BMSMonitoringTool } from '../tools/BMSMonitoringTool.js'; const app = express(); const server = createServer(app); const wss = new WebSocketServer({ server }); // Initialize BMS tools const bmsConnection = new BMSConnectionTool(); const bmsMonitoring = new BMSMonitoringTool(); // Middleware app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); // Store connected WebSocket clients const clients = new Set(); // WebSocket connection handling wss.on('connection', (ws) => { console.log('New WebSocket client connected'); clients.add(ws); ws.on('close', () => { console.log('WebSocket client disconnected'); clients.delete(ws); }); ws.on('error', (error) => { console.error('WebSocket error:', error); clients.delete(ws); }); }); // Broadcast data to all connected clients function broadcastToClients(data) { const message = JSON.stringify(data); clients.forEach(client => { if (client.readyState === 1) { // WebSocket.OPEN client.send(message); } }); } // API Routes // Test BMS connection app.post('/api/bms/connect', async (req, res) => { try { const { ip, port, protocol, username, password } = req.body; if (!ip) { return res.status(400).json({ error: 'IP address is required' }); } const result = await bmsConnection.execute({ ip, port: port || 80, protocol: protocol || 'http', username, password }); res.json(result); } catch (error) { res.status(500).json({ error: `Connection test failed: ${error instanceof Error ? error.message : 'Unknown error'}` }); } }); // Start monitoring app.post('/api/bms/monitor/start', async (req, res) => { try { const { ip, port, protocol, username, password, interval, data_points } = req.body; if (!ip) { return res.status(400).json({ error: 'IP address is required' }); } const result = await bmsMonitoring.execute({ ip, port: port || 80, protocol: protocol || 'http', username, password, interval: interval || 60, data_points }); if (result.success) { // Start broadcasting monitoring data const monitoringId = result.monitoring_id; // Set up periodic broadcast of monitoring data const broadcastInterval = setInterval(async () => { try { const historicalData = await bmsMonitoring.getHistoricalData(monitoringId, 1); // Last hour if (historicalData.length > 0) { broadcastToClients({ type: 'monitoring_data', monitoring_id: monitoringId, data: historicalData[historicalData.length - 1] // Latest data }); } } catch (error) { console.error('Error broadcasting monitoring data:', error); } }, 30000); // Broadcast every 30 seconds // Store interval for cleanup global.broadcastIntervals = global.broadcastIntervals || new Map(); global.broadcastIntervals.set(monitoringId, broadcastInterval); } res.json(result); } catch (error) { res.status(500).json({ error: `Failed to start monitoring: ${error instanceof Error ? error.message : 'Unknown error'}` }); } }); // Stop monitoring app.post('/api/bms/monitor/stop', async (req, res) => { try { const { monitoring_id } = req.body; if (!monitoring_id) { return res.status(400).json({ error: 'Monitoring ID is required' }); } const stopped = await bmsMonitoring.stopMonitoring(monitoring_id); // Stop broadcasting const broadcastIntervals = global.broadcastIntervals; if (broadcastIntervals && broadcastIntervals.has(monitoring_id)) { clearInterval(broadcastIntervals.get(monitoring_id)); broadcastIntervals.delete(monitoring_id); } res.json({ success: stopped, message: stopped ? 'Monitoring stopped' : 'Monitoring was not active' }); } catch (error) { res.status(500).json({ error: `Failed to stop monitoring: ${error instanceof Error ? error.message : 'Unknown error'}` }); } }); // Get historical data app.get('/api/bms/monitor/:monitoring_id/history', async (req, res) => { try { const { monitoring_id } = req.params; const { hours = 24 } = req.query; const data = await bmsMonitoring.getHistoricalData(monitoring_id, Number(hours)); res.json({ success: true, data }); } catch (error) { res.status(500).json({ error: `Failed to get historical data: ${error instanceof Error ? error.message : 'Unknown error'}` }); } }); // Serve the dashboard HTML app.get('/', (req, res) => { res.send(` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>BMS Dashboard</title> <style> body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; } .container { max-width: 1200px; margin: 0 auto; } .card { background: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; font-weight: bold; } input, select, button { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; } button { background-color: #007bff; color: white; border: none; cursor: pointer; margin-top: 10px; } button:hover { background-color: #0056b3; } button:disabled { background-color: #ccc; cursor: not-allowed; } .status { padding: 10px; border-radius: 4px; margin: 10px 0; } .status.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; } .status.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } .data-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; margin-top: 20px; } .data-point { background: #f8f9fa; padding: 15px; border-radius: 6px; border-left: 4px solid #007bff; } .data-point.warning { border-left-color: #ffc107; } .data-point.error { border-left-color: #dc3545; } .data-point-name { font-weight: bold; margin-bottom: 5px; } .data-point-value { font-size: 1.2em; color: #333; } .data-point-unit { color: #666; font-size: 0.9em; } .connection-status { display: inline-block; padding: 4px 8px; border-radius: 12px; font-size: 0.8em; font-weight: bold; } .connection-status.online { background-color: #28a745; color: white; } .connection-status.offline { background-color: #dc3545; color: white; } #log { background-color: #000; color: #0f0; padding: 15px; border-radius: 4px; font-family: monospace; height: 200px; overflow-y: auto; white-space: pre-wrap; } </style> </head> <body> <div class="container"> <h1>🏭 Industrial BMS Dashboard</h1> <div class="card"> <h2>BMS Connection</h2> <div class="form-group"> <label for="ip">IP Address:</label> <input type="text" id="ip" value="192.168.1.246" placeholder="192.168.1.246"> </div> <div class="form-group"> <label for="port">Port:</label> <input type="number" id="port" value="80" placeholder="80"> </div> <div class="form-group"> <label for="protocol">Protocol:</label> <select id="protocol"> <option value="http">HTTP</option> <option value="https">HTTPS</option> <option value="bacnet">BACnet</option> <option value="modbus">Modbus</option> </select> </div> <div class="form-group"> <label for="username">Username (optional):</label> <input type="text" id="username" placeholder="admin"> </div> <div class="form-group"> <label for="password">Password (optional):</label> <input type="password" id="password" placeholder="password"> </div> <button onclick="testConnection()">Test Connection</button> <button onclick="startMonitoring()" id="startBtn">Start Monitoring</button> <button onclick="stopMonitoring()" id="stopBtn" disabled>Stop Monitoring</button> </div> <div class="card"> <h2>System Status</h2> <div id="systemStatus"> <span class="connection-status offline">Disconnected</span> <span id="responseTime"></span> </div> </div> <div class="card"> <h2>Live Data Points</h2> <div id="dataPoints" class="data-grid"> <p>No data available. Connect to a BMS controller to see live data.</p> </div> </div> <div class="card"> <h2>Activity Log</h2> <div id="log"></div> </div> </div> <script> let ws; let currentMonitoringId = null; // Initialize WebSocket connection function initWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; ws = new WebSocket(\`\${protocol}//\${window.location.host}\`); ws.onopen = function() { log('WebSocket connected'); }; ws.onmessage = function(event) { const data = JSON.parse(event.data); handleWebSocketMessage(data); }; ws.onclose = function() { log('WebSocket disconnected'); setTimeout(initWebSocket, 5000); // Reconnect after 5 seconds }; ws.onerror = function(error) { log('WebSocket error: ' + error); }; } function handleWebSocketMessage(data) { if (data.type === 'monitoring_data') { updateDataPoints(data.data); updateSystemStatus(data.data.system_status); } } function log(message) { const logElement = document.getElementById('log'); const timestamp = new Date().toLocaleTimeString(); logElement.textContent += \`[\${timestamp}] \${message}\\n\`; logElement.scrollTop = logElement.scrollHeight; } async function testConnection() { const ip = document.getElementById('ip').value; const port = document.getElementById('port').value; const protocol = document.getElementById('protocol').value; const username = document.getElementById('username').value; const password = document.getElementById('password').value; if (!ip) { alert('Please enter an IP address'); return; } log(\`Testing connection to \${ip}:\${port}...\`); try { const response = await fetch('/api/bms/connect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ip, port: parseInt(port), protocol, username, password }) }); const result = await response.json(); if (result.success) { log(\`✅ Connection successful! Detected: \${result.controller_info.detected_type}\`); log(\`Found \${result.data_points?.length || 0} data points\`); if (result.data_points && result.data_points.length > 0) { updateDataPoints({ data_points: result.data_points, timestamp: new Date() }); } } else { log(\`❌ Connection failed: \${result.error}\`); } } catch (error) { log(\`❌ Connection error: \${error.message}\`); } } async function startMonitoring() { const ip = document.getElementById('ip').value; const port = document.getElementById('port').value; const protocol = document.getElementById('protocol').value; const username = document.getElementById('username').value; const password = document.getElementById('password').value; if (!ip) { alert('Please enter an IP address'); return; } log(\`Starting monitoring for \${ip}:\${port}...\`); try { const response = await fetch('/api/bms/monitor/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ip, port: parseInt(port), protocol, username, password, interval: 60 }) }); const result = await response.json(); if (result.success) { currentMonitoringId = result.monitoring_id; log(\`✅ Monitoring started! ID: \${currentMonitoringId}\`); document.getElementById('startBtn').disabled = true; document.getElementById('stopBtn').disabled = false; if (result.current_data) { updateDataPoints(result.current_data); updateSystemStatus(result.current_data.system_status); } } else { log(\`❌ Failed to start monitoring: \${result.error}\`); } } catch (error) { log(\`❌ Monitoring error: \${error.message}\`); } } async function stopMonitoring() { if (!currentMonitoringId) return; log(\`Stopping monitoring for \${currentMonitoringId}...\`); try { const response = await fetch('/api/bms/monitor/stop', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ monitoring_id: currentMonitoringId }) }); const result = await response.json(); if (result.success) { log(\`✅ Monitoring stopped\`); currentMonitoringId = null; document.getElementById('startBtn').disabled = false; document.getElementById('stopBtn').disabled = true; updateSystemStatus({ online: false, response_time: 0 }); } else { log(\`❌ Failed to stop monitoring\`); } } catch (error) { log(\`❌ Stop monitoring error: \${error.message}\`); } } function updateDataPoints(data) { const container = document.getElementById('dataPoints'); if (!data.data_points || data.data_points.length === 0) { container.innerHTML = '<p>No data points available</p>'; return; } container.innerHTML = data.data_points.map(dp => \` <div class="data-point \${dp.status}"> <div class="data-point-name">\${dp.name}</div> <div class="data-point-value"> \${dp.value} <span class="data-point-unit">\${dp.unit || ''}</span> </div> </div> \`).join(''); } function updateSystemStatus(status) { const statusElement = document.getElementById('systemStatus'); const responseTimeElement = document.getElementById('responseTime'); statusElement.querySelector('.connection-status').className = \`connection-status \${status.online ? 'online' : 'offline'}\`; statusElement.querySelector('.connection-status').textContent = status.online ? 'Connected' : 'Disconnected'; responseTimeElement.textContent = status.online ? \`Response: \${status.response_time}ms\` : ''; } // Initialize WebSocket when page loads window.onload = function() { initWebSocket(); log('BMS Dashboard initialized'); }; </script> </body> </html> `); }); const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log(`🏭 BMS Dashboard server running on http://localhost:${PORT}`); console.log(`📊 Dashboard available at: http://localhost:${PORT}`); console.log(`🔌 WebSocket server ready for real-time data`); }); // Graceful shutdown process.on('SIGTERM', () => { console.log('Shutting down BMS Dashboard server...'); // Stop all monitoring const broadcastIntervals = global.broadcastIntervals; if (broadcastIntervals) { broadcastIntervals.forEach((interval) => { clearInterval(interval); }); } server.close(() => { console.log('Server shut down gracefully'); process.exit(0); }); });