matterbridge-roborock-vacuum-plugin
Version:
Matterbridge Roborock Vacuum Plugin
175 lines (144 loc) • 6.11 kB
text/typescript
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);
}
}
}