matterbridge-roborock-vacuum-plugin
Version:
Matterbridge Roborock Vacuum Plugin
131 lines (107 loc) • 4.14 kB
text/typescript
import mqtt, { ErrorWithReasonCode, IConnackPacket, 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 {
private readonly rriot: Rriot;
private readonly mqttUsername: string;
private readonly mqttPassword: string;
private client: MqttLibClient | 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);
}
public connect(): void {
if (this.client) {
return;
}
this.client = 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:: ' + args[0]);
},
});
this.client.on('connect', this.onConnect.bind(this));
this.client.on('error', this.onError.bind(this));
this.client.on('reconnect', this.onReconnect.bind(this));
this.client.on('close', this.onDisconnect.bind(this));
this.client.on('disconnect', this.onDisconnect.bind(this));
this.client.on('offline', this.onDisconnect.bind(this));
// message events
this.client.on('message', this.onMessage.bind(this));
}
public async disconnect(): Promise<void> {
if (!this.client || !this.connected) {
return;
}
try {
this.client.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.client || !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.client.publish('rr/m/i/' + this.rriot.u + '/' + this.mqttUsername + '/' + duid, message.buffer, { qos: 1 });
}
private async onConnect(result: IConnackPacket) {
if (!result) {
return;
}
this.connected = true;
await this.connectionListeners.onConnected();
this.subscribeToQueue();
}
private subscribeToQueue() {
if (!this.client) {
return;
}
this.client.subscribe('rr/m/o/' + this.rriot.u + '/' + this.mqttUsername + '/#', this.onSubscribe.bind(this));
}
private async onSubscribe(err: Error | null) {
if (!err) {
return;
}
this.logger.error('failed to subscribe to the queue: ' + err);
this.connected = false;
await this.connectionListeners.onDisconnected();
}
private async onDisconnect() {
await this.connectionListeners.onDisconnected();
}
private async onError(result: Error | ErrorWithReasonCode) {
this.logger.error('MQTT connection error: ' + result);
this.connected = false;
await this.connectionListeners.onError(result.toString());
}
private onReconnect() {
this.subscribeToQueue();
}
private async onMessage(topic: string, message: Buffer<ArrayBufferLike>) {
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);
}
}
}