UNPKG

domotz-mcp

Version:

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

269 lines (268 loc) 11.2 kB
import { z } from "zod"; import axios from "axios"; import { BaseDomotzTool } from "./BaseDomotzTool.js"; export class BMSMonitoringTool extends BaseDomotzTool { name = "bms_monitor"; description = "Start continuous monitoring of BMS controller data points"; schema = { ip: { type: z.string(), description: "IP address of the BMS controller" }, port: { type: z.number().min(1).max(65535).default(80), description: "Port number (default: 80)" }, protocol: { type: z.enum(['http', 'https']).default('http'), description: "Connection protocol" }, username: { type: z.string().optional(), description: "Username for authentication" }, password: { type: z.string().optional(), description: "Password for authentication" }, interval: { type: z.number().min(1).default(60), description: "Monitoring interval in seconds (default: 60)" }, data_points: { type: z.array(z.string()).optional(), description: "Specific data points to monitor" } }; monitoringIntervals = new Map(); monitoringData = new Map(); async execute(params) { try { const { ip, port = 80, protocol = 'http', username, password, interval = 60, data_points } = params; const monitoringId = `${ip}:${port}`; // Stop existing monitoring for this controller if any if (this.monitoringIntervals.has(monitoringId)) { clearInterval(this.monitoringIntervals.get(monitoringId)); this.monitoringIntervals.delete(monitoringId); } // Initialize data storage for this controller if (!this.monitoringData.has(monitoringId)) { this.monitoringData.set(monitoringId, []); } // Perform initial data collection const initialData = await this.collectData(ip, port, protocol, username, password, data_points); if (!initialData.system_status.online) { return { success: false, error: `Cannot start monitoring - controller at ${ip}:${port} is not accessible` }; } // Store initial data const dataHistory = this.monitoringData.get(monitoringId); dataHistory.push(initialData); // Keep only last 1000 data points to prevent memory issues if (dataHistory.length > 1000) { dataHistory.splice(0, dataHistory.length - 1000); } // Start continuous monitoring const intervalId = setInterval(async () => { try { const data = await this.collectData(ip, port, protocol, username, password, data_points); const dataHistory = this.monitoringData.get(monitoringId); dataHistory.push(data); // Keep only last 1000 data points if (dataHistory.length > 1000) { dataHistory.splice(0, dataHistory.length - 1000); } // Log significant changes or errors if (!data.system_status.online) { console.log(`BMS Controller ${monitoringId} went offline at ${data.timestamp}`); } // Check for alarm conditions this.checkAlarmConditions(data, monitoringId); } catch (error) { console.error(`Monitoring error for ${monitoringId}:`, error); } }, interval * 1000); this.monitoringIntervals.set(monitoringId, intervalId); return { success: true, monitoring_id: monitoringId, message: `Started monitoring BMS controller at ${ip}:${port} with ${interval}s interval`, current_data: initialData }; } catch (error) { return { success: false, error: `Failed to start monitoring: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } async collectData(ip, port, protocol, username, password, targetDataPoints) { const startTime = Date.now(); const baseUrl = `${protocol}://${ip}:${port}`; try { const axiosConfig = { timeout: 10000, validateStatus: (status) => status < 500, }; if (username && password) { axiosConfig.auth = { username, password }; } // Try to get data from the controller const response = await axios.get(baseUrl, axiosConfig); const responseTime = Date.now() - startTime; if (response.status !== 200) { return { timestamp: new Date(), controller_ip: ip, data_points: [], system_status: { online: false, response_time: responseTime, last_error: `HTTP ${response.status}: ${response.statusText}` } }; } // Extract data points from response const dataPoints = await this.extractDataFromResponse(response.data, targetDataPoints); return { timestamp: new Date(), controller_ip: ip, data_points: dataPoints, system_status: { online: true, response_time: responseTime } }; } catch (error) { return { timestamp: new Date(), controller_ip: ip, data_points: [], system_status: { online: false, response_time: Date.now() - startTime, last_error: error instanceof Error ? error.message : 'Unknown error' } }; } } async extractDataFromResponse(responseData, targetDataPoints) { const dataPoints = []; // This is a simplified extraction - would need to be customized for specific BMS types if (typeof responseData === 'string') { // Try to extract data from HTML or text response const lines = responseData.split('\n'); for (const line of lines) { if (this.looksLikeDataPoint(line)) { const parsed = this.parseDataPointLine(line); if (parsed && (!targetDataPoints || targetDataPoints.includes(parsed.name))) { dataPoints.push(parsed); } } } } else if (typeof responseData === 'object') { // Extract from JSON response this.extractFromJSONObject(responseData, dataPoints, '', targetDataPoints); } return dataPoints; } looksLikeDataPoint(line) { // Simple heuristic to identify data point lines const keywords = ['temp', 'pressure', 'flow', 'setpoint', 'status', 'alarm', 'energy', 'power']; const lineLower = line.toLowerCase(); return keywords.some(keyword => lineLower.includes(keyword)) && (line.includes(':') || line.includes('=')); } parseDataPointLine(line) { // Simple parsing - would need enhancement for real BMS data const parts = line.split(/[:=]/); if (parts.length >= 2) { const name = parts[0].trim(); const valueStr = parts[1].trim(); // Extract numeric value and unit const match = valueStr.match(/^([\d.-]+)\s*([A-Za-z°%]+)?/); if (match) { const value = parseFloat(match[1]); const unit = match[2]; return { name, value, unit, status: this.determineStatus(name, value) }; } } return null; } extractFromJSONObject(obj, dataPoints, prefix, targetDataPoints) { for (const [key, value] of Object.entries(obj)) { const fullKey = prefix ? `${prefix}.${key}` : key; if (typeof value === 'object' && value !== null && !Array.isArray(value)) { this.extractFromJSONObject(value, dataPoints, fullKey, targetDataPoints); } else if (typeof value === 'number') { if (!targetDataPoints || targetDataPoints.includes(fullKey)) { dataPoints.push({ name: fullKey, value, status: this.determineStatus(fullKey, value) }); } } } } determineStatus(name, value) { // Simple status determination - would need customization for specific systems if (typeof value !== 'number') return 'ok'; const nameLower = name.toLowerCase(); // Temperature checks if (nameLower.includes('temp')) { if (value < 0 || value > 100) return 'error'; if (value < 5 || value > 85) return 'warning'; } // Pressure checks if (nameLower.includes('pressure')) { if (value < 0) return 'error'; if (value > 1000) return 'warning'; } return 'ok'; } checkAlarmConditions(data, monitoringId) { // Check for alarm conditions and log them const errorPoints = data.data_points.filter(dp => dp.status === 'error'); const warningPoints = data.data_points.filter(dp => dp.status === 'warning'); if (errorPoints.length > 0) { console.log(`ALARM: ${monitoringId} has ${errorPoints.length} error conditions:`, errorPoints.map(dp => `${dp.name}: ${dp.value}`)); } if (warningPoints.length > 0) { console.log(`WARNING: ${monitoringId} has ${warningPoints.length} warning conditions:`, warningPoints.map(dp => `${dp.name}: ${dp.value}`)); } } // Method to get historical data async getHistoricalData(monitoringId, hours = 24) { const data = this.monitoringData.get(monitoringId) || []; const cutoffTime = new Date(Date.now() - hours * 60 * 60 * 1000); return data.filter(d => d.timestamp >= cutoffTime); } // Method to stop monitoring async stopMonitoring(monitoringId) { if (this.monitoringIntervals.has(monitoringId)) { clearInterval(this.monitoringIntervals.get(monitoringId)); this.monitoringIntervals.delete(monitoringId); return true; } return false; } } export default BMSMonitoringTool;