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