UNPKG

energy-manager-iot

Version:

Library for energy management in IoT devices via MQTT protocol. Documentation: https://jonhvmp.github.io/energy-manager-iot-docs/

361 lines (360 loc) 15.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MqttHandler = void 0; const mqtt = __importStar(require("mqtt")); const events_1 = require("events"); const logger_1 = __importDefault(require("../utils/logger")); const validators_1 = require("../utils/validators"); const error_handler_1 = require("../utils/error-handler"); /** * Handles MQTT connections and message routing * * This class provides a wrapper around the MQTT client library with * additional features for connection management, topic subscription, * and message handling. * * @remarks * MqttHandler extends EventEmitter to provide notification of connection * state changes and received messages. */ class MqttHandler extends events_1.EventEmitter { client = null; options; brokerUrl = ''; isConnected = false; topicSubscriptions; logger = logger_1.default.child('MqttHandler'); /** * Creates a new MQTT handler instance * * @param options - Configuration options for the MQTT connection */ constructor(options = {}) { super(); // Default options this.options = { clientId: `energy-manager-${Math.random().toString(16).substring(2, 10)}`, clean: true, keepalive: 60, reconnectPeriod: 5000, connectTimeout: 30000, ...options }; this.topicSubscriptions = new Map(); } /** * Connects to the MQTT broker * * @param brokerUrl - URL of the MQTT broker * @param options - Additional connection options to override defaults * @returns Promise that resolves when connected or rejects on error * @throws {EnergyManagerError} If broker URL is invalid or connection fails */ connect(brokerUrl, options) { // Create a connection-specific logger with unique correlation ID const connectionId = `mqtt_${Date.now()}`; const connLogger = this.logger.withCorrelationId(connectionId); return new Promise((resolve, reject) => { connLogger.info('Connecting to MQTT broker', { brokerUrl, clientId: this.options.clientId }); if (!(0, validators_1.validateMqttBrokerUrl)(brokerUrl)) { connLogger.error('Connection failed - Invalid MQTT broker URL', { brokerUrl }); reject(new error_handler_1.EnergyManagerError('Invalid MQTT broker URL', error_handler_1.ErrorType.VALIDATION)); return; } this.brokerUrl = brokerUrl; // Merge options const mqttOptions = { ...this.options, ...options }; try { connLogger.debug('Initializing MQTT client connection', { options: { ...mqttOptions, // Hide password for security password: mqttOptions.password ? '********' : undefined } }); this.client = mqtt.connect(brokerUrl, mqttOptions); this.client.on('connect', () => { connLogger.info('Successfully connected to MQTT broker', { broker: brokerUrl, clientId: mqttOptions.clientId }); this.isConnected = true; this.emit('connect'); resolve(); }); this.client.on('message', (topic, payload) => { this.handleMessage(topic, payload); }); this.client.on('reconnect', () => { connLogger.warn('Attempting to reconnect to MQTT broker', { broker: this.brokerUrl, reconnectPeriod: mqttOptions.reconnectPeriod }); this.emit('reconnect'); }); this.client.on('error', (err) => { connLogger.error('MQTT connection error', { broker: this.brokerUrl, errorMessage: err.message, errorStack: err.stack }); this.emit('error', err); reject(new error_handler_1.EnergyManagerError(`MQTT connection failed: ${err.message}`, error_handler_1.ErrorType.CONNECTION, err)); }); this.client.on('offline', () => { connLogger.warn('MQTT client disconnected', { broker: this.brokerUrl, previouslyConnected: this.isConnected }); this.isConnected = false; this.emit('offline'); }); } catch (err) { connLogger.error('Exception connecting to MQTT broker', { errorMessage: err.message, errorStack: err.stack, broker: brokerUrl }); reject(new error_handler_1.EnergyManagerError(`MQTT connection exception: ${err.message}`, error_handler_1.ErrorType.CONNECTION, err)); } }); } /** * Disconnects from the MQTT broker * * @returns Promise that resolves when disconnected or rejects on error * @throws {EnergyManagerError} If disconnection fails */ disconnect() { const disconnectLogger = this.logger.withCorrelationId(`disconnect_${Date.now()}`); return new Promise((resolve, reject) => { if (!this.client) { disconnectLogger.debug('Disconnect called but client is not initialized'); resolve(); return; } disconnectLogger.info('Disconnecting from MQTT broker', { broker: this.brokerUrl, clientId: this.options.clientId }); this.client.end(false, {}, (err) => { if (err) { disconnectLogger.error('Error disconnecting from MQTT broker', { errorMessage: err.message, errorStack: err.stack }); reject(new error_handler_1.EnergyManagerError(`Disconnect error: ${err.message}`, error_handler_1.ErrorType.CONNECTION, err)); } else { disconnectLogger.info('Successfully disconnected from MQTT broker', { broker: this.brokerUrl }); this.isConnected = false; this.client = null; resolve(); } }); }); } /** * Publishes a message to an MQTT topic * * @param topic - MQTT topic to publish to * @param message - Message content (string or object that will be JSON stringified) * @param options - Publish options like QoS level and retain flag * @returns Promise that resolves when published or rejects on error * @throws {EnergyManagerError} If not connected or publish fails */ publish(topic, message, options) { const publishId = `pub_${Date.now()}`; const pubLogger = this.logger.withCorrelationId(publishId); return new Promise((resolve, reject) => { if (!this.client || !this.isConnected) { pubLogger.error('Publish failed - not connected to MQTT broker', { topic }); reject(new error_handler_1.EnergyManagerError('Not connected to MQTT broker', error_handler_1.ErrorType.CONNECTION)); return; } const payload = typeof message === 'string' ? message : JSON.stringify(message); pubLogger.debug('Publishing message to MQTT broker', { topic, payloadSize: payload.length, messageType: typeof message }); const defaultOptions = { qos: 1, retain: false }; const publishOptions = { ...defaultOptions, ...options }; this.client.publish(topic, payload, publishOptions, (err) => { if (err) { pubLogger.error('Failed to publish message', { topic, errorMessage: err.message, errorStack: err.stack }); reject(new error_handler_1.EnergyManagerError(`Failed to publish message: ${err.message}`, error_handler_1.ErrorType.COMMAND_FAILED, err)); } else { pubLogger.debug('Message successfully published', { topic, qos: publishOptions.qos, retain: publishOptions.retain }); resolve(); } }); }); } /** * Subscribes to an MQTT topic * * @param topic - MQTT topic to subscribe to * @param callback - Optional callback function for this specific topic * @returns Promise that resolves when subscribed or rejects on error * @throws {EnergyManagerError} If not connected or subscribe fails */ subscribe(topic, callback) { const subscriptionId = `sub_${Date.now()}`; const subLogger = this.logger.withCorrelationId(subscriptionId); return new Promise((resolve, reject) => { if (!this.client || !this.isConnected) { subLogger.error('Subscribe failed - not connected to MQTT broker', { topic }); reject(new error_handler_1.EnergyManagerError('Not connected to MQTT broker', error_handler_1.ErrorType.CONNECTION)); return; } subLogger.debug('Subscribing to MQTT topic', { topic, hasCallback: !!callback }); // Register callback if provided if (callback) { this.topicSubscriptions.set(topic, callback); subLogger.trace('Registered callback for topic', { topic }); } this.client.subscribe(topic, { qos: 1 }, (err) => { if (err) { subLogger.error('Failed to subscribe to topic', { topic, errorMessage: err.message, errorStack: err.stack }); reject(new error_handler_1.EnergyManagerError(`Failed to subscribe to topic: ${err.message}`, error_handler_1.ErrorType.COMMAND_FAILED, err)); } else { subLogger.info('Successfully subscribed to topic', { topic }); resolve(); } }); }); } /** * Unsubscribes from an MQTT topic * * @param topic - MQTT topic to unsubscribe from * @returns Promise that resolves when unsubscribed or rejects on error * @throws {EnergyManagerError} If not connected or unsubscribe fails */ unsubscribe(topic) { const unsubscribeId = `unsub_${Date.now()}`; const unsubLogger = this.logger.withCorrelationId(unsubscribeId); return new Promise((resolve, reject) => { if (!this.client || !this.isConnected) { unsubLogger.error('Unsubscribe failed - not connected to MQTT broker', { topic }); reject(new error_handler_1.EnergyManagerError('Not connected to MQTT broker', error_handler_1.ErrorType.CONNECTION)); return; } unsubLogger.debug('Unsubscribing from MQTT topic', { topic }); // Check if we have a callback registered const hadCallback = this.topicSubscriptions.has(topic); // Remove registered callback this.topicSubscriptions.delete(topic); this.client.unsubscribe(topic, (err) => { if (err) { unsubLogger.error('Failed to unsubscribe from topic', { topic, errorMessage: err.message, errorStack: err.stack }); reject(new error_handler_1.EnergyManagerError(`Failed to unsubscribe from topic: ${err.message}`, error_handler_1.ErrorType.COMMAND_FAILED, err)); } else { unsubLogger.info('Successfully unsubscribed from topic', { topic, hadCallback }); resolve(); } }); }); } /** * Checks if connected to the MQTT broker * * @returns True if connected to the broker */ isClientConnected() { return this.isConnected && !!this.client; } /** * Handles incoming MQTT messages * @private */ handleMessage(topic, payload) { // Create a unique message ID for tracing const messageId = `msg_${Date.now()}_${Math.random().toString(36).substring(2, 5)}`; const msgLogger = this.logger.withCorrelationId(messageId); msgLogger.debug('Message received', { topic, payloadSize: payload.length }); // Emit event for all listeners this.emit('message', topic, payload); // Call specific callback if registered const callback = this.topicSubscriptions.get(topic); if (callback) { try { msgLogger.trace('Executing registered callback for topic', { topic }); callback(topic, payload); } catch (err) { msgLogger.error('Error in message callback handler', { topic, errorMessage: err.message, errorStack: err.stack }); } } } } exports.MqttHandler = MqttHandler;