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
JavaScript
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);
});
});