UNPKG

matterbridge-roborock-vacuum-plugin

Version:
270 lines 11.4 kB
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