domotz-mcp
Version:
MCP server for Domotz API and BMS (Building Management System) integration with IoT device monitoring
218 lines (215 loc) • 7.78 kB
JavaScript
import Database from 'better-sqlite3';
import { mkdirSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Ensure data directory exists
const dataDir = join(__dirname, '..', '..', 'data');
mkdirSync(dataDir, { recursive: true });
// Database singleton
let db = null;
export class MQTTDatabaseManager {
static instance;
db;
constructor() {
const dbPath = join(dataDir, 'mqtt_messages.db');
this.db = new Database(dbPath);
this.initializeDatabase();
}
static getInstance() {
if (!MQTTDatabaseManager.instance) {
MQTTDatabaseManager.instance = new MQTTDatabaseManager();
}
return MQTTDatabaseManager.instance;
}
initializeDatabase() {
// Create messages table
this.db.exec(`
CREATE TABLE IF NOT EXISTS mqtt_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
connection_id TEXT NOT NULL,
topic TEXT NOT NULL,
message TEXT NOT NULL,
qos INTEGER DEFAULT 0,
retain BOOLEAN DEFAULT FALSE,
timestamp TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Create index for faster queries
this.db.exec(`
CREATE INDEX IF NOT EXISTS idx_mqtt_messages_topic
ON mqtt_messages(topic);
CREATE INDEX IF NOT EXISTS idx_mqtt_messages_connection
ON mqtt_messages(connection_id);
CREATE INDEX IF NOT EXISTS idx_mqtt_messages_timestamp
ON mqtt_messages(timestamp);
`);
// Create subscriptions table
this.db.exec(`
CREATE TABLE IF NOT EXISTS mqtt_subscriptions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
connection_id TEXT NOT NULL,
topic_pattern TEXT NOT NULL,
qos INTEGER DEFAULT 0,
active BOOLEAN DEFAULT TRUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(connection_id, topic_pattern)
)
`);
// Create settings table for retention policy
this.db.exec(`
CREATE TABLE IF NOT EXISTS mqtt_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
`);
// Set default retention period (7 days)
const stmt = this.db.prepare(`
INSERT OR IGNORE INTO mqtt_settings (key, value)
VALUES ('retention_days', '7')
`);
stmt.run();
}
// Store a received message
storeMessage(message) {
const stmt = this.db.prepare(`
INSERT INTO mqtt_messages
(connection_id, topic, message, qos, retain, timestamp)
VALUES (, , , , , )
`);
const result = stmt.run(message);
return result.lastInsertRowid;
}
// Store a subscription
storeSubscription(subscription) {
const stmt = this.db.prepare(`
INSERT OR REPLACE INTO mqtt_subscriptions
(connection_id, topic_pattern, qos, active)
VALUES (, , , )
`);
const result = stmt.run(subscription);
return result.lastInsertRowid;
}
// Get messages by topic pattern
getMessagesByTopic(topicPattern, limit = 100, offset = 0) {
// Convert MQTT wildcards to SQL LIKE pattern
const sqlPattern = topicPattern
.replace(/\+/g, '_') // + matches single level
.replace(/#/g, '%'); // # matches multiple levels
const stmt = this.db.prepare(`
SELECT * FROM mqtt_messages
WHERE topic LIKE ?
ORDER BY timestamp DESC
LIMIT ? OFFSET ?
`);
return stmt.all(sqlPattern, limit, offset);
}
// Get messages by connection
getMessagesByConnection(connectionId, limit = 100) {
const stmt = this.db.prepare(`
SELECT * FROM mqtt_messages
WHERE connection_id = ?
ORDER BY timestamp DESC
LIMIT ?
`);
return stmt.all(connectionId, limit);
}
// Get messages by time range
getMessagesByTimeRange(startTime, endTime, topic) {
let query = `
SELECT * FROM mqtt_messages
WHERE timestamp >= ? AND timestamp <= ?
`;
if (topic) {
const sqlPattern = topic.replace(/\+/g, '_').replace(/#/g, '%');
query += ` AND topic LIKE ?`;
const stmt = this.db.prepare(query + ` ORDER BY timestamp DESC`);
return stmt.all(startTime, endTime, sqlPattern);
}
else {
const stmt = this.db.prepare(query + ` ORDER BY timestamp DESC`);
return stmt.all(startTime, endTime);
}
}
// Get active subscriptions
getActiveSubscriptions(connectionId) {
if (connectionId) {
const stmt = this.db.prepare(`
SELECT s.*, COUNT(m.id) as message_count
FROM mqtt_subscriptions s
LEFT JOIN mqtt_messages m ON s.connection_id = m.connection_id
AND m.topic LIKE REPLACE(REPLACE(s.topic_pattern, '+', '_'), '#', '%')
WHERE s.connection_id = ? AND s.active = TRUE
GROUP BY s.id
`);
return stmt.all(connectionId);
}
else {
const stmt = this.db.prepare(`
SELECT s.*, COUNT(m.id) as message_count
FROM mqtt_subscriptions s
LEFT JOIN mqtt_messages m ON s.connection_id = m.connection_id
AND m.topic LIKE REPLACE(REPLACE(s.topic_pattern, '+', '_'), '#', '%')
WHERE s.active = TRUE
GROUP BY s.id
`);
return stmt.all();
}
}
// Deactivate subscription
deactivateSubscription(connectionId, topicPattern) {
const stmt = this.db.prepare(`
UPDATE mqtt_subscriptions
SET active = FALSE
WHERE connection_id = ? AND topic_pattern = ?
`);
stmt.run(connectionId, topicPattern);
}
// Clean up old messages based on retention policy
cleanupOldMessages() {
const retentionDays = this.getRetentionDays();
const stmt = this.db.prepare(`
DELETE FROM mqtt_messages
WHERE created_at < datetime('now', '-' || ? || ' days')
`);
const result = stmt.run(retentionDays);
return result.changes;
}
// Get retention days setting
getRetentionDays() {
const stmt = this.db.prepare(`
SELECT value FROM mqtt_settings WHERE key = 'retention_days'
`);
const result = stmt.get();
return result ? parseInt(result.value) : 7;
}
// Set retention days
setRetentionDays(days) {
const stmt = this.db.prepare(`
UPDATE mqtt_settings SET value = ? WHERE key = 'retention_days'
`);
stmt.run(days.toString());
}
// Get database statistics
getStatistics() {
const messageCount = this.db.prepare('SELECT COUNT(*) as count FROM mqtt_messages').get();
const subscriptionCount = this.db.prepare('SELECT COUNT(*) as count FROM mqtt_subscriptions WHERE active = TRUE').get();
const topicCount = this.db.prepare('SELECT COUNT(DISTINCT topic) as count FROM mqtt_messages').get();
const oldestMessage = this.db.prepare('SELECT MIN(timestamp) as oldest FROM mqtt_messages').get();
const newestMessage = this.db.prepare('SELECT MAX(timestamp) as newest FROM mqtt_messages').get();
return {
total_messages: messageCount.count,
active_subscriptions: subscriptionCount.count,
unique_topics: topicCount.count,
oldest_message: oldestMessage.oldest,
newest_message: newestMessage.newest,
retention_days: this.getRetentionDays()
};
}
// Close database connection
close() {
this.db.close();
}
}