matterbridge-roborock-vacuum-plugin
Version:
Matterbridge Roborock Vacuum Plugin
270 lines • 11.4 kB
JavaScript
import { Socket } from 'node:net';
import { clearInterval } from 'node:timers';
import { debugStringify } from 'matterbridge/logger';
import { ProtocolVersion } from '../enums/index.js';
import { ChunkBuffer } from '../helper/chunkBuffer.js';
import { Sequence } from '../helper/sequence.js';
import { Protocol, RequestMessage } from '../models/index.js';
import { AbstractClient } from '../routing/abstractClient.js';
import { HelloResponseListener } from '../routing/listeners/implementation/helloResponseListener.js';
import { LocalPingResponseListener } from './localPingResponseListener.js';
export class LocalNetworkClient extends AbstractClient {
duid;
ip;
clientName = 'LocalNetworkClient';
socket = undefined;
buffer = new ChunkBuffer();
messageIdSeq;
checkConnectionInterval;
helloResponseListener;
pingResponseListener;
connected = false;
intentionalDisconnect = false;
constructor(logger, context, duid, ip, responseBroadcaster, responseTracker) {
super(logger, context, responseBroadcaster, responseTracker);
this.duid = duid;
this.ip = ip;
this.messageIdSeq = new Sequence(100000, 999999);
this.helloResponseListener = new HelloResponseListener(this.duid, logger);
this.pingResponseListener = new LocalPingResponseListener(this.duid, logger);
this.responseBroadcaster.register(this.helloResponseListener);
this.responseBroadcaster.register(this.pingResponseListener);
}
isReady() {
return this.connected;
}
isReconnecting() {
return this.intentionalDisconnect;
}
isConnected() {
return !!this.socket && this.socket.readyState === 'open' && !this.socket.destroyed;
}
connect() {
if (this.socket) {
return; // Already connected
}
this.intentionalDisconnect = false;
this.logger.notice(`[LocalNetworkClient] [${this.duid}] Trying to connect to ip: ${this.ip}`);
super.connect();
this.buffer.reset();
this.socket = new Socket();
const socket = this.socket;
// Socket event listeners
socket.on('close', this.safeHandler(async (hadError) => {
if (this.socket !== socket)
return;
await this.onDisconnect(hadError);
}));
socket.on('end', this.safeHandler(async () => {
if (this.socket !== socket)
return;
await this.onEnd();
}));
socket.on('error', this.safeHandler(async (error) => {
if (this.socket !== socket)
return;
await this.onError(error);
}));
socket.on('connect', this.safeHandler(this.onConnect));
socket.on('timeout', this.safeHandler(this.onTimeout));
// Data event listener
socket.on('data', this.safeHandler(this.onMessage));
socket.setTimeout(15000);
socket.connect(58867, this.ip);
}
async disconnect() {
await super.disconnect();
if (!this.socket) {
return Promise.resolve();
}
if (this.checkConnectionInterval) {
clearInterval(this.checkConnectionInterval);
this.checkConnectionInterval = undefined;
}
this.intentionalDisconnect = true;
this.socket.destroy();
this.socket = undefined;
this.connected = false;
}
async sendInternal(duid, request) {
if (!this.socket || !this.isConnected()) {
this.logger.error(`[LocalNetworkClient] [${this.duid}]: socket is not online, request: ${debugStringify(request)}`);
return;
}
if (!request.isForProtocol(Protocol.hello_request) && !this.connected) {
this.logger.error(`[LocalNetworkClient] [${this.duid}]: socket is not connected, cannot send request, request: ${debugStringify(request)}`);
return;
}
const protocolVersion = request.version ?? this.context.getLocalProtocolVersion(duid);
const localRequest = request.toLocalRequest(protocolVersion);
const message = this.serializer.serialize(duid, localRequest);
this.logger.debug(`[LocalNetworkClient] sending message ${message.messageId}, protocol version: ${localRequest.version}, protocol:${Protocol[localRequest.protocol]}, method:${localRequest.method}, secure:${request.secure} to ${duid}`);
this.socket.write(this.wrapWithLengthData(message.buffer));
this.logger.debug(`[LocalNetworkClient] sent message ${message.messageId} to ${duid}`);
}
async onConnect() {
this.logger.debug(`[LocalNetworkClient]: ${this.duid} connected to ${this.ip}`);
this.logger.debug(`[LocalNetworkClient]: ${this.duid} socket writable: ${this.socket?.writable}, readable: ${this.socket?.readable}`);
await this.trySendHelloRequest();
}
async onDisconnect(hadError) {
if (this.intentionalDisconnect) {
return;
}
this.logger.warn(`[LocalNetworkClient] [${this.duid}]: socket disconnected. ${hadError ? 'Is having error' : 'No error detected'}`);
if (this.socket) {
this.socket.destroy();
this.socket = undefined;
}
this.connected = false;
await this.connectionBroadcaster.onDisconnected(this.duid, `Socket disconnected. Had error: ${hadError}`);
}
async onError(error) {
if (this.intentionalDisconnect) {
return;
}
this.logger.error(`[LocalNetworkClient]: Socket error for ${this.duid}: ${error.message}`);
if (this.socket) {
this.socket.destroy();
this.socket = undefined;
}
this.connected = false;
await this.connectionBroadcaster.onDisconnected(this.duid, `Socket error: ${error.message}`);
}
async onTimeout() {
this.logger.error(`[LocalNetworkClient]: Socket for ${this.duid} timed out.`);
}
async onEnd() {
this.logger.notice(`[LocalNetworkClient]: ${this.duid} socket ended.`);
}
async onMessage(message) {
if (!this.socket) {
return;
}
if (!message || message.length == 0) {
this.logger.debug('[LocalNetworkClient] received empty message from socket.');
return;
}
try {
this.buffer.append(message);
const receivedBuffer = this.buffer.get();
if (!this.isMessageComplete(receivedBuffer)) {
return;
}
this.buffer.reset();
let offset = 0;
while (offset + 4 <= receivedBuffer.length) {
const segmentLength = receivedBuffer.readUInt32BE(offset);
try {
const currentBuffer = receivedBuffer.subarray(offset + 4, offset + segmentLength + 4);
const response = this.deserializer.deserialize(this.duid, currentBuffer, 'LocalNetworkClient');
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(`[LocalNetworkClient]: unable to process message with error: ${errMsg}`);
}
offset += 4 + segmentLength;
}
}
catch (error) {
this.logger.error(`[LocalNetworkClient]: read socket buffer error: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
}
}
isMessageComplete(buffer) {
let totalLength = 0;
let offset = 0;
while (offset + 4 <= buffer.length) {
const segmentLength = buffer.readUInt32BE(offset);
totalLength += 4 + segmentLength;
offset += 4 + segmentLength;
if (offset > buffer.length) {
return false;
}
}
return totalLength <= buffer.length;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
safeHandler(fn) {
return (...args) => {
fn.apply(this, args).catch((error) => {
this.logger.error(`[LocalNetworkClient]: unhandled error in ${fn.name}: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
});
};
}
wrapWithLengthData(buffer) {
const lengthBuffer = Buffer.alloc(4);
lengthBuffer.writeUInt32BE(buffer.length, 0);
return Buffer.concat([lengthBuffer, buffer]);
}
async trySendHelloRequest() {
const isV1 = await this.sendHelloMessage(ProtocolVersion.V1);
if (!isV1) {
await this.sendHelloMessage(ProtocolVersion.L01);
}
}
async sendHelloMessage(version) {
const request = new RequestMessage({
version: version,
protocol: Protocol.hello_request,
messageId: this.messageIdSeq.next(),
nonce: this.context.nonce,
});
try {
const responsePromise = this.helloResponseListener.waitFor(version);
await this.send(this.duid, request);
const response = await responsePromise;
await this.processHelloResponse(response);
return true;
}
catch (error) {
this.logger.error(`[LocalNetworkClient]: ${this.duid} failed to receive hello response: ${error}`);
return false;
}
}
async processHelloResponse(response) {
this.logger.info(`[LocalNetworkClient]: ${this.duid} received hello response: ${debugStringify(response)}`);
if (response.header === undefined) {
this.logger.error(`[LocalNetworkClient]: ${this.duid} hello response missing header.`);
return;
}
this.context.updateNonce(this.duid, response.header.nonce);
this.context.updateLocalProtocolVersion(this.duid, response.header.version);
this.connected = true;
this.pingResponseListener.resetLastPingResponse();
if (this.checkConnectionInterval) {
clearInterval(this.checkConnectionInterval);
}
this.checkConnectionInterval = setInterval(this.checkConnection.bind(this), 5000);
}
checkingConnection = false;
async checkConnection() {
if (this.checkingConnection) {
return;
}
this.checkingConnection = true;
try {
const now = Date.now();
if (now - this.pingResponseListener.lastPingResponse > 15000) {
this.logger.warn(`[LocalNetworkClient]: There is no local ping response for device ${this.duid} for 15s, try reconnect now`);
await this.disconnect();
this.connect();
return;
}
await this.sendPingRequest();
}
finally {
this.checkingConnection = false;
}
}
async sendPingRequest() {
const request = new RequestMessage({
version: this.context.getLocalProtocolVersion(this.duid),
protocol: Protocol.ping_request,
messageId: this.messageIdSeq.next(),
});
await this.send(this.duid, request);
}
}
//# sourceMappingURL=localClient.js.map