UNPKG

matterbridge-roborock-vacuum-plugin

Version:
175 lines (144 loc) 6.11 kB
import mqtt, { ErrorWithReasonCode, IConnackPacket, ISubscriptionGrant, MqttClient as MqttLibClient } from 'mqtt'; import * as CryptoUtils from '../../helper/cryptoHelper.js'; import { RequestMessage } from '../model/requestMessage.js'; import { AbstractClient } from '../abstractClient.js'; import { MessageContext } from '../model/messageContext.js'; import { Rriot, UserData } from '../../Zmodel/userData.js'; import { AnsiLogger, debugStringify } from 'matterbridge/logger'; export class MQTTClient extends AbstractClient { protected override clientName = 'MQTTClient'; protected override shouldReconnect = false; private readonly rriot: Rriot; private readonly mqttUsername: string; private readonly mqttPassword: string; private mqttClient: MqttLibClient | undefined = undefined; private keepConnectionAliveInterval: NodeJS.Timeout | undefined = undefined; public constructor(logger: AnsiLogger, context: MessageContext, userdata: UserData) { super(logger, context); 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(); } public connect(): void { if (this.mqttClient) { return; // Already connected } 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(); } public async disconnect(): Promise<void> { if (!this.mqttClient || !this.connected) { return; } try { this.isInDisconnectingStep = true; this.mqttClient.end(); } catch (error) { this.logger.error('MQTT client failed to disconnect with error: ' + error); } } public async send(duid: string, request: RequestMessage): Promise<void> { if (!this.mqttClient || !this.connected) { this.logger.error(`${duid}: mqtt is not available, ${debugStringify(request)}`); return; } const mqttRequest = request.toMqttRequest(); const message = this.serializer.serialize(duid, mqttRequest); this.logger.debug(`MQTTClient sending message to ${duid}: ${debugStringify(mqttRequest)}`); this.mqttClient.publish(`rr/m/i/${this.rriot.u}/${this.mqttUsername}/${duid}`, message.buffer, { qos: 1 }); this.logger.debug(`MQTTClient published message to topic: rr/m/i/${this.rriot.u}/${this.mqttUsername}/${duid}`); } private keepConnectionAlive(): void { if (this.keepConnectionAliveInterval) { clearTimeout(this.keepConnectionAliveInterval); this.keepConnectionAliveInterval.unref(); } this.keepConnectionAliveInterval = setInterval( () => { if (this.mqttClient) { this.mqttClient.end(); this.mqttClient.reconnect(); } else { this.connect(); } }, 30 * 60 * 1000, ); } private async onConnect(result: IConnackPacket): Promise<void> { if (!result) { return; } this.connected = true; await this.connectionListeners.onConnected('mqtt-' + this.mqttUsername); this.subscribeToQueue(); } private subscribeToQueue(): void { if (!this.mqttClient || !this.connected) { return; } this.mqttClient.subscribe('rr/m/o/' + this.rriot.u + '/' + this.mqttUsername + '/#', this.onSubscribe.bind(this)); } private async onSubscribe(err: Error | null, granted: ISubscriptionGrant[] | undefined): Promise<void> { if (!err) { this.logger.info('onSubscribe: ' + JSON.stringify(granted)); return; } this.logger.error('failed to subscribe: ' + err); this.connected = false; await this.connectionListeners.onDisconnected('mqtt-' + this.mqttUsername, 'Failed to subscribe to the queue: ' + err.toString()); } private async onDisconnect(): Promise<void> { this.connected = false; await this.connectionListeners.onDisconnected('mqtt-' + this.mqttUsername, 'Disconnected from MQTT broker'); } private async onError(result: Error | ErrorWithReasonCode): Promise<void> { this.logger.error('MQTT connection error: ' + result); this.connected = false; await this.connectionListeners.onError('mqtt-' + this.mqttUsername, result.toString()); } private async onClose(): Promise<void> { if (this.connected) { await this.connectionListeners.onDisconnected('mqtt-' + this.mqttUsername, 'MQTT connection closed'); } this.connected = false; } private async onOffline(): Promise<void> { this.connected = false; await this.connectionListeners.onDisconnected('mqtt-' + this.mqttUsername, 'MQTT connection offline'); } private onReconnect(): void { this.subscribeToQueue(); this.connectionListeners.onReconnect('mqtt-' + this.mqttUsername, 'Reconnected to MQTT broker'); } private async onMessage(topic: string, message: Buffer<ArrayBufferLike>): Promise<void> { 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); await this.messageListeners.onMessage(response); } catch (error) { this.logger.error('MQTTClient: unable to process message with error: ' + topic + ': ' + error); } } }