UNPKG

domotz-mcp

Version:

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

232 lines (231 loc) 9.45 kB
import { z } from "zod"; import axios from "axios"; import * as cheerio from "cheerio"; import { BaseDomotzTool } from "./BaseDomotzTool.js"; export class BMSConnectionTool extends BaseDomotzTool { name = "bms_connect"; description = "Connect to BMS controller and retrieve 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', 'bacnet', 'modbus']).default('http'), description: "Connection protocol" }, username: { type: z.string().optional(), description: "Username for authentication" }, password: { type: z.string().optional(), description: "Password for authentication" } }; async execute(params) { try { const { ip, port = 80, protocol = 'http', username, password } = params; console.log(`Attempting to connect to BMS at ${ip}:${port} using ${protocol}`); // Try different connection methods based on protocol switch (protocol) { case 'http': case 'https': return await this.connectHTTP(ip, port, protocol, username, password); case 'bacnet': return await this.connectBACnet(ip, port); case 'modbus': return await this.connectModbus(ip, port); default: return { success: false, error: `Unsupported protocol: ${protocol}` }; } } catch (error) { return { success: false, error: `Connection failed: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } async connectHTTP(ip, port, protocol, username, password) { const baseUrl = `${protocol}://${ip}:${port}`; try { // Configure axios with authentication if provided const axiosConfig = { timeout: 10000, validateStatus: (status) => status < 500, // Accept 4xx as valid responses }; if (username && password) { axiosConfig.auth = { username, password }; } // Try common BMS web interface paths const commonPaths = ['/', '/index.html', '/login', '/main', '/dashboard', '/trend']; let response; let accessiblePath = ''; for (const path of commonPaths) { try { response = await axios.get(`${baseUrl}${path}`, axiosConfig); if (response.status === 200) { accessiblePath = path; break; } } catch (err) { // Continue trying other paths continue; } } if (!response || response.status !== 200) { return { success: false, error: `No accessible web interface found. Tried paths: ${commonPaths.join(', ')}` }; } // Parse the HTML response to extract information const $ = cheerio.load(response.data); const title = $('title').text() || 'Unknown BMS'; // Look for common BMS indicators in the HTML const bodyText = $('body').text().toLowerCase(); const isTrendBMS = bodyText.includes('trend') || title.toLowerCase().includes('trend'); const isHoneywellBMS = bodyText.includes('honeywell') || title.toLowerCase().includes('honeywell'); const isJohnsonBMS = bodyText.includes('johnson') || title.toLowerCase().includes('johnson'); // Try to extract data points from common BMS web interfaces const dataPoints = await this.extractWebDataPoints($, baseUrl, axiosConfig); return { success: true, controller_info: { ip, port, protocol, accessible_path: accessiblePath, title, detected_type: isTrendBMS ? 'Trend Controls' : isHoneywellBMS ? 'Honeywell' : isJohnsonBMS ? 'Johnson Controls' : 'Unknown', response_size: response.data.length, headers: response.headers }, data_points: dataPoints }; } catch (error) { return { success: false, error: `HTTP connection failed: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } async extractWebDataPoints($, baseUrl, axiosConfig) { const dataPoints = []; // Look for common data point patterns in the HTML // This is a basic implementation - would need customization for specific BMS types // Look for tables with data $('table').each((i, table) => { $(table).find('tr').each((j, row) => { const cells = $(row).find('td'); if (cells.length >= 2) { const name = $(cells[0]).text().trim(); const value = $(cells[1]).text().trim(); if (name && value && this.isDataPointName(name)) { dataPoints.push({ name, value: this.parseValue(value), timestamp: new Date(), type: this.inferDataPointType(name) }); } } }); }); // Look for JSON data endpoints try { const jsonPaths = ['/api/data', '/data.json', '/status.json', '/points.json']; for (const path of jsonPaths) { try { const response = await axios.get(`${baseUrl}${path}`, axiosConfig); if (response.data && typeof response.data === 'object') { const jsonDataPoints = this.extractJSONDataPoints(response.data); dataPoints.push(...jsonDataPoints); } } catch (err) { // Continue trying other paths } } } catch (error) { // JSON extraction failed, continue with HTML data } return dataPoints; } isDataPointName(name) { const keywords = ['temp', 'pressure', 'flow', 'setpoint', 'status', 'alarm', 'energy', 'power', 'humidity']; return keywords.some(keyword => name.toLowerCase().includes(keyword)); } parseValue(value) { // Remove units and parse numeric values const numericValue = parseFloat(value.replace(/[^\d.-]/g, '')); return isNaN(numericValue) ? value : numericValue; } inferDataPointType(name) { const nameLower = name.toLowerCase(); if (nameLower.includes('temp')) return 'temperature'; if (nameLower.includes('pressure')) return 'pressure'; if (nameLower.includes('flow')) return 'flow'; if (nameLower.includes('energy') || nameLower.includes('power')) return 'energy'; if (nameLower.includes('setpoint')) return 'setpoint'; if (nameLower.includes('status') || nameLower.includes('alarm')) return 'status'; return 'other'; } extractJSONDataPoints(data) { const dataPoints = []; // Recursively extract data points from JSON structure const extractFromObject = (obj, prefix = '') => { for (const [key, value] of Object.entries(obj)) { const fullKey = prefix ? `${prefix}.${key}` : key; if (typeof value === 'object' && value !== null && !Array.isArray(value)) { extractFromObject(value, fullKey); } else if (typeof value === 'number' || typeof value === 'string') { if (this.isDataPointName(key)) { dataPoints.push({ name: fullKey, value, timestamp: new Date(), type: this.inferDataPointType(key) }); } } } }; extractFromObject(data); return dataPoints; } async connectBACnet(ip, port) { // BACnet connection implementation would go here // This requires the node-bacnet library return { success: false, error: "BACnet connection not yet implemented" }; } async connectModbus(ip, port) { // Modbus connection implementation would go here // This requires the modbus-serial library return { success: false, error: "Modbus connection not yet implemented" }; } } export default BMSConnectionTool;