UNPKG

domotz-mcp

Version:

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

222 lines (221 loc) 8.32 kB
import { mqttConnections } from "./MQTTConnectionTool.js"; import { MQTTDatabaseManager } from "./MQTTDatabaseManager.js"; export class MQTTPersistentManager { static instance; subscriptions = new Map(); db; cleanupInterval = null; constructor() { this.db = MQTTDatabaseManager.getInstance(); // Start cleanup task (runs every hour) this.cleanupInterval = setInterval(() => { const deleted = this.db.cleanupOldMessages(); if (deleted > 0) { console.error(`[MQTTPersistentManager] Cleaned up ${deleted} old messages`); } }, 3600000); // 1 hour } static getInstance() { if (!MQTTPersistentManager.instance) { MQTTPersistentManager.instance = new MQTTPersistentManager(); } return MQTTPersistentManager.instance; } // Start persistent subscription startPersistentSubscription(connectionId, topicPattern, qos = 0) { // Get the MQTT client from active connections const client = mqttConnections.get(connectionId); if (!client) { return { success: false, error: "No active MQTT connection found with the specified ID" }; } if (!client.connected) { return { success: false, error: "MQTT client is not connected" }; } // Get or create persistent subscription for this connection let subscription = this.subscriptions.get(connectionId); if (!subscription) { subscription = { client, connectionId, topics: new Map(), isActive: true }; this.subscriptions.set(connectionId, subscription); } // Check if already subscribed to this topic if (subscription.topics.has(topicPattern)) { return { success: false, error: `Already subscribed to topic pattern: ${topicPattern}` }; } // Create message handler for this topic const messageHandler = (receivedTopic, message) => { // Check if the received topic matches the pattern if (this.topicMatches(receivedTopic, topicPattern)) { const mqttMessage = { connection_id: connectionId, topic: receivedTopic, message: message.toString(), qos: 0, // QoS of received message (not available in callback) retain: false, // Retain flag (not available in callback) timestamp: new Date().toISOString() }; // Store in database try { this.db.storeMessage(mqttMessage); } catch (error) { console.error(`[MQTTPersistentManager] Failed to store message: ${error}`); } } }; // Subscribe to the topic client.subscribe(topicPattern, { qos: qos }, (err) => { if (err) { console.error(`[MQTTPersistentManager] Subscribe error: ${err.message}`); } else { // Store subscription in database this.db.storeSubscription({ connection_id: connectionId, topic_pattern: topicPattern, qos, active: true }); } }); // Add message handler client.on("message", messageHandler); subscription.topics.set(topicPattern, { qos, messageHandler }); const subscriptionId = `${connectionId}:${topicPattern}`; return { success: true, subscription_id: subscriptionId }; } // Stop persistent subscription stopPersistentSubscription(connectionId, topicPattern) { const subscription = this.subscriptions.get(connectionId); if (!subscription) { return { success: false, error: "No active subscription found for this connection" }; } const topicInfo = subscription.topics.get(topicPattern); if (!topicInfo) { return { success: false, error: `Not subscribed to topic pattern: ${topicPattern}` }; } // Remove message handler subscription.client.removeListener("message", topicInfo.messageHandler); // Unsubscribe from topic subscription.client.unsubscribe(topicPattern, (err) => { if (err) { console.error(`[MQTTPersistentManager] Unsubscribe error: ${err.message}`); } }); // Remove from topics map subscription.topics.delete(topicPattern); // Deactivate in database this.db.deactivateSubscription(connectionId, topicPattern); // If no more topics, remove the subscription if (subscription.topics.size === 0) { this.subscriptions.delete(connectionId); } return { success: true, unsubscribed: true }; } // Get all active persistent subscriptions getActivePersistentSubscriptions(connectionId) { if (connectionId) { const subscription = this.subscriptions.get(connectionId); if (!subscription) { return []; } return Array.from(subscription.topics.entries()).map(([topic, info]) => ({ connection_id: connectionId, topic_pattern: topic, qos: info.qos, active: true })); } // Return all subscriptions const allSubscriptions = []; this.subscriptions.forEach((subscription, connId) => { subscription.topics.forEach((info, topic) => { allSubscriptions.push({ connection_id: connId, topic_pattern: topic, qos: info.qos, active: true }); }); }); return allSubscriptions; } // Check if a topic matches a pattern (handles MQTT wildcards) topicMatches(topic, pattern) { // Handle exact match if (topic === pattern) return true; // Convert MQTT pattern to regex const regexPattern = pattern .split('/') .map(level => { if (level === '+') return '[^/]+'; // Single level wildcard if (level === '#') return '.*'; // Multi-level wildcard (must be last) return level.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Escape special chars }) .join('/'); // # can only be at the end and matches everything after const regex = new RegExp(`^${regexPattern}$`); return regex.test(topic); } // Clean up when a connection is closed cleanupConnection(connectionId) { const subscription = this.subscriptions.get(connectionId); if (subscription) { // Remove all message handlers subscription.topics.forEach((info, topic) => { subscription.client.removeListener("message", info.messageHandler); }); // Mark all subscriptions as inactive in database subscription.topics.forEach((info, topic) => { this.db.deactivateSubscription(connectionId, topic); }); // Remove from map this.subscriptions.delete(connectionId); } } // Get database manager for direct queries getDatabaseManager() { return this.db; } // Cleanup on shutdown shutdown() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } // Clean up all subscriptions this.subscriptions.forEach((subscription, connectionId) => { this.cleanupConnection(connectionId); }); this.db.close(); } } // Export singleton instance export const mqttPersistentManager = MQTTPersistentManager.getInstance();