UNPKG

matterbridge-roborock-vacuum-plugin

Version:
215 lines 9.79 kB
import { debugStringify } from 'matterbridge/logger'; import mqtt from 'mqtt'; import { KEEPALIVE_INTERVAL_MS } from '../../constants/timeouts.js'; import * as CryptoUtils from '../helper/cryptoHelper.js'; import { AbstractClient } from '../routing/abstractClient.js'; export class MQTTClient extends AbstractClient { clientName = 'MQTTClient'; rriot; mqttUsername; mqttPassword; mqttClient = undefined; keepConnectionAliveInterval = undefined; connected = false; isForceReconnecting = false; consecutiveAuthErrors = 0; authErrorBackoffTimeout = undefined; constructor(logger, context, userdata, responseBroadcaster, responseTracker) { super(logger, context, responseBroadcaster, responseTracker); this.rriot = userdata.rriot; this.mqttUsername = CryptoUtils.md5hex(`${userdata.rriot.u}:${userdata.rriot.k}`).substring(2, 10); this.mqttPassword = CryptoUtils.md5hex(`${userdata.rriot.s}:${userdata.rriot.k}`).substring(16); this.initializeConnectionStateListener(this); } isConnected() { return this.connected; } isReady() { return this.connected && this.mqttClient !== undefined; } connect() { if (this.mqttClient) { return; // Already connected } super.connect(); this.mqttClient = mqtt.connect(this.rriot.r.m, { clientId: this.mqttUsername, username: this.mqttUsername, password: this.mqttPassword, keepalive: 30, log: () => { // ...args: unknown[] this.logger.debug(`MQTTClient args: ${debugStringify(args)}`); }, }); this.mqttClient.on('connect', this.onConnect.bind(this)); this.mqttClient.on('error', this.onError.bind(this)); this.mqttClient.on('reconnect', this.onReconnect.bind(this)); this.mqttClient.on('close', this.onClose.bind(this)); this.mqttClient.on('disconnect', this.onDisconnect.bind(this)); this.mqttClient.on('offline', this.onOffline.bind(this)); this.mqttClient.on('message', this.onMessage.bind(this)); this.keepConnectionAlive(); } async disconnect() { await super.disconnect(); if (!this.mqttClient || !this.connected) { return Promise.resolve(); } try { if (this.keepConnectionAliveInterval) { clearInterval(this.keepConnectionAliveInterval); this.keepConnectionAliveInterval = undefined; } if (this.authErrorBackoffTimeout) { clearTimeout(this.authErrorBackoffTimeout); this.authErrorBackoffTimeout = undefined; } this.mqttClient.end(); this.mqttClient = undefined; this.connected = false; } catch (error) { this.logger.error(`[MQTTClient] client failed to disconnect with error: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`); } } async sendInternal(duid, request) { if (!this.mqttClient || !this.connected) { this.logger.error(`${duid}: mqtt is not available, ${debugStringify(request)}`); return; } const mqttRequest = request.toMqttRequest(); mqttRequest.version = mqttRequest.version ?? this.context.getMQTTProtocolVersion(duid); const message = this.serializer.serialize(duid, mqttRequest); const topic = `rr/m/i/${this.rriot.u}/${this.mqttUsername}/${duid}`; this.logger.debug(`[MQTTClient] sending message to ${topic}: ${debugStringify(mqttRequest)}`); this.mqttClient.publish(topic, message.buffer, { qos: 1 }); this.logger.debug(`[MQTTClient] sent message to ${duid}`); } keepConnectionAlive() { if (this.keepConnectionAliveInterval) { clearInterval(this.keepConnectionAliveInterval); this.keepConnectionAliveInterval.unref(); } // Always do a reconnect because the mqtt sometimes does not response any more this.keepConnectionAliveInterval = setInterval(() => { if (this.mqttClient) { this.logger.debug('[MQTTClient] Force reconnecting to ensure fresh connection'); this.isForceReconnecting = true; this.mqttClient.end(); this.mqttClient.reconnect(); } else { this.logger.info('[MQTTClient] Force reconnecting to ensure fresh connection (new connection)'); this.connect(); } }, KEEPALIVE_INTERVAL_MS); } async onConnect(result) { if (!result) { this.logger.error('[MQTTClient] onConnect called with no result'); return; } const wasForceReconnecting = this.isForceReconnecting; this.isForceReconnecting = false; this.connected = true; this.consecutiveAuthErrors = 0; this.logger.info(`[MQTTClient] connected to MQTT broker with result: ${debugStringify(result)}`); this.subscribeToQueue(); if (!wasForceReconnecting) { await this.connectionBroadcaster.onConnected(`mqtt-${this.mqttUsername}`); } } subscribeToQueue() { if (!this.mqttClient || !this.connected) { this.logger.error('[MQTTClient] cannot subscribe, client not connected'); return; } this.mqttClient.subscribe(`rr/m/o/${this.rriot.u}/${this.mqttUsername}/#`, this.onSubscribe.bind(this)); } async onSubscribe(err, subscription) { const hasError = err !== null && err !== undefined; if (hasError) { this.logger.error(`[MQTTClient] Failed to subscribe: ${String(err)}`); this.connected = false; await this.connectionBroadcaster.onDisconnected(`mqtt-${this.mqttUsername}`, `Failed to subscribe to the queue: ${String(err)}`); return; } this.logger.info(`[MQTTClient] Connection subscribed: ${subscription ? debugStringify(subscription) : 'unknown'}`); } async onDisconnect() { this.connected = false; await this.connectionBroadcaster.onDisconnected(`mqtt-${this.mqttUsername}`, 'Disconnected from MQTT broker'); } async onError(result) { // MQTT error code 5 = Connection Refused: Not Authorized (authentication failure) const isAuthError = 'code' in result && result.code === 5; const errorMessage = isAuthError ? 'Connection refused: Not authorized' : debugStringify(result); this.logger.error(`MQTT connection error: ${errorMessage}`); await this.connectionBroadcaster.onError(`mqtt-${this.mqttUsername}`, `MQTT connection error: ${errorMessage}`); if (isAuthError) { this.consecutiveAuthErrors++; this.logger.warn(`[MQTTClient] Auth error count: ${this.consecutiveAuthErrors}/5`); if (this.consecutiveAuthErrors >= 5) { this.logger.error('[MQTTClient] Auth error threshold reached, entering 60-minute backoff'); this.terminateConnection(); // Wait 60 minutes then reconnect this.authErrorBackoffTimeout = setTimeout(() => { this.authErrorBackoffTimeout = undefined; this.consecutiveAuthErrors = 0; this.logger.info('[MQTTClient] Auth error backoff period ended, attempting reconnection'); this.connect(); }, KEEPALIVE_INTERVAL_MS); this.authErrorBackoffTimeout.unref(); } } } terminateConnection() { if (this.keepConnectionAliveInterval) { clearInterval(this.keepConnectionAliveInterval); this.keepConnectionAliveInterval = undefined; } if (this.authErrorBackoffTimeout) { clearTimeout(this.authErrorBackoffTimeout); this.authErrorBackoffTimeout = undefined; } if (this.mqttClient) { this.mqttClient.end(true); this.mqttClient = undefined; } this.connected = false; } async onClose() { if (this.connected && !this.isForceReconnecting) { await this.connectionBroadcaster.onClose(`mqtt-${this.mqttUsername}`); } this.connected = false; } async onOffline() { this.connected = false; await this.connectionBroadcaster.onOffline(`mqtt-${this.mqttUsername}`); } onReconnect() { // Note: 'reconnect' event fires when MQTT library *starts* a reconnection attempt, // NOT when it successfully reconnects. The 'connect' event fires on successful reconnection. // Do NOT call subscribeToQueue() here - it will be called by onConnect() when successful. this.connectionBroadcaster.onReconnect('mqtt-' + this.mqttUsername, 'Attempting to reconnect to MQTT broker'); } async onMessage(topic, message) { if (!message) { // Ignore empty messages this.logger.notice(`[MQTTClient] received empty message from topic: ${topic}`); return; } try { const duid = topic.split('/').slice(-1)[0]; const response = this.deserializer.deserialize(duid, message, 'MQTTClient'); this.responseBroadcaster.tryResolve(response); await this.responseBroadcaster.onMessage(response); } catch (error) { const errMsg = error instanceof Error ? (error.stack ?? error.message) : String(error); this.logger.error(`[MQTTClient]: unable to process message ${topic}: ${errMsg}`); } } } //# sourceMappingURL=mqttClient.js.map