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