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