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