UNPKG

matterbridge-roborock-vacuum-plugin

Version:
250 lines 11.8 kB
import { CONNECTION_RETRY_DELAY_MS, MAX_CONNECTION_ATTEMPTS } from '../constants/index.js'; import { DeviceConnectionError, DeviceError, DeviceInitializationError } from '../errors/index.js'; import { ProtocolVersion } from '../roborockCommunication/enums/index.js'; import { LocalNetworkUDPClient } from '../roborockCommunication/local/udpClient.js'; import { Protocol, RPC_Request_Segments, } from '../roborockCommunication/models/index.js'; import { MessageDispatcherFactory } from '../roborockCommunication/protocol/dispatcher/dispatcherFactory.js'; import { SimpleMessageHandler } from '../roborockCommunication/routing/handlers/implementation/simpleMessageHandler.js'; import { DeviceStatusListener } from '../roborockCommunication/routing/listeners/implementation/deviceStatusListener.js'; import { DisconnectNotificationListener } from '../roborockCommunication/routing/listeners/implementation/disconnectNotificationListener.js'; import { MapResponseListener } from '../roborockCommunication/routing/listeners/implementation/mapResponseListener.js'; import { SimpleMessageListener } from '../roborockCommunication/routing/listeners/implementation/simpleMessageListener.js'; import { EmailNotificationService } from './emailNotificationService.js'; /** Manages device connections (MQTT and local network). */ export class ConnectionService { clientManager; logger; messageRoutingService; configManager; clientRouter; ipMap = new Map(); localClientMap = new Map(); deviceNotify; emailService; constructor(clientManager, logger, messageRoutingService, configManager) { this.clientManager = clientManager; this.logger = logger; this.messageRoutingService = messageRoutingService; this.configManager = configManager; } async sendTestEmailNotification() { const emailService = this.getEmailService(); if (emailService) { await emailService.sendTestEmail(); } } getEmailService() { if (!this.configManager?.isEmailNotificationEnabled) return undefined; const settings = this.configManager.emailNotificationSettings; if (!settings) return undefined; this.emailService ??= new EmailNotificationService(settings, this.logger); return this.emailService; } /** Set callback for device notifications. */ setDeviceNotify(callback) { this.deviceNotify = callback; } /** Wait for connection with retry logic. Returns attempt count. */ async waitForConnection(checkConnection, maxAttempts = MAX_CONNECTION_ATTEMPTS, delayMs = CONNECTION_RETRY_DELAY_MS) { let attempts = 0; while (!checkConnection() && attempts < maxAttempts) { await this.sleep(delayMs); attempts++; } if (!checkConnection()) { throw new Error(`Connection timeout after ${attempts} attempts`); } return attempts; } /** * Initialize the message client for cloud/MQTT communication. * Registers device, sets up message listeners, and waits for connection. */ async initializeMessageClient(device, userdata) { if (!this.clientManager) { throw new DeviceInitializationError(device.duid, 'ClientManager not initialized'); } try { this.clientRouter = this.clientManager.get(userdata); if (!this.clientRouter) { throw new DeviceInitializationError(device.duid, 'Failed to get ClientRouter from ClientManager'); } this.logger.debug('Initializing message client for device:', device.duid); this.clientRouter.registerDevice(device.duid, device.localKey, device.pv, undefined); this.clientRouter.connect(); // Register email notification listener if enabled (covers both MQTT and local via shared broadcaster) const emailService = this.getEmailService(); if (emailService) { this.clientRouter.registerConnectionListener(new DisconnectNotificationListener(emailService, this.logger, 'MQTT')); } // Wait for connection try { await this.waitForConnection(() => (this.clientRouter?.isReady() && this.clientRouter?.isConnected()) ?? false); this.logger.debug(`clientRouter.isReady: ${this.clientRouter?.isReady()}`); this.logger.debug(`clientRouter.isConnected: ${this.clientRouter?.isConnected()}`); device.specs.hasRealTimeConnection = true; } catch { throw new DeviceConnectionError(device.duid, 'MQTT connection timeout'); } this.logger.debug('clientRouter connected for device:', device.duid); } catch (error) { this.logger.error('Failed to initialize message client:', error); if (error instanceof DeviceError) { throw error; } throw new DeviceInitializationError(device.duid, error instanceof Error ? error.message : String(error)); } } /** * Initialize local network client for direct device communication. * Creates message processor, retrieves device IP, and establishes local connection. * Devices with protocol version B01 will skip local connection and use MQTT only. */ async initializeMessageClientForLocal(device) { this.logger.debug('Initializing local network client for device:', device.duid); if (!this.clientRouter) { this.logger.error('clientRouter not initialized'); return false; } if (!this.deviceNotify) { this.logger.error('deviceNotify callback not set'); return false; } this.clientRouter.registerMessageListener(new MapResponseListener(device.duid, this.logger)); const simpleMessageListener = new SimpleMessageListener(device.duid, this.logger); simpleMessageListener.registerHandler(new SimpleMessageHandler(device.duid, this.logger, this.deviceNotify)); const deviceStatusListener = new DeviceStatusListener(device.duid, this.logger); this.clientRouter.registerMessageListener(deviceStatusListener); this.clientRouter.registerMessageListener(simpleMessageListener); const deviceSpecs = device.specs; const messageDispatcher = new MessageDispatcherFactory(this.clientRouter, this.logger).getMessageDispatcher(deviceSpecs.protocol, deviceSpecs.model); this.logger.debug(`[ConnectionService] Resolve ${messageDispatcher.dispatcherName} for device: ${device.duid}, protocol: ${deviceSpecs.protocol}, model: ${deviceSpecs.model}`); // Register message listeners this.messageRoutingService.registerMessageDispatcher(device.duid, messageDispatcher); // B01 devices use MQTT-only communication if (device.pv === ProtocolVersion.B01) { this.logger.debug(`Device: ${device.duid} uses B01 protocol, switch to use UDPClient`); const localNetworkUDPClient = new LocalNetworkUDPClient(this.logger); const networkInfo = this.getNetworkInfoFromDeviceStatus(device); if (networkInfo?.ipAddress) { this.logger.debug(`Device ${device.duid} has network info IP: ${networkInfo.ipAddress}, setting up UDP listener`); const success = await this.setupLocalClient(device, networkInfo.ipAddress); if (success) { return true; } this.logger .error(`Failed to set up local client for device ${device.duid} at IP ${networkInfo.ipAddress} via B01 setup. Continuing to listen for broadcasts.`); } localNetworkUDPClient.registerListener({ onMessage: async (duid, ip) => { this.logger.debug(`Received UDP broadcast from device ${duid} at IP ${ip}`); await this.setupLocalClient(device, ip); }, }); localNetworkUDPClient.connect(); return true; } // Get device IP address from network info let localIp = this.ipMap.get(device.duid); if (!localIp) { this.logger.debug(`Device ${device.duid} IP not cached, fetching from device`); const networkInfo = await messageDispatcher.getNetworkInfo(device.duid); if (!networkInfo?.ip) { this.logger.warn('Failed to get network info, using MQTT only for device:', device.duid); return false; } this.logger.debug(`Device ${device.duid} is on local network, attempting local connection at IP ${networkInfo.ip}`); localIp = networkInfo.ip; } if (localIp) { return await this.setupLocalClient(device, localIp); } return false; } /** * Get the current message client instance. * @returns The active ClientRouter or undefined if not initialized */ getMessageClient() { return this.clientRouter; } /** * Shutdown all connections and cleanup resources. */ async shutdown() { // Disconnect main message client if (this.clientRouter) { try { this.clientRouter.disconnect(); } catch (error) { this.logger.error('Error disconnecting message client:', error); } this.clientRouter = undefined; } // Disconnect all local clients for (const [duid, client] of this.localClientMap) { try { this.logger.debug('Disconnecting local client:', duid); client.disconnect(); } catch (error) { this.logger.error(`Error disconnecting local client ${duid}:`, error); } } // Clear all state this.localClientMap.clear(); this.ipMap.clear(); this.clientRouter = undefined; this.deviceNotify = undefined; } /** * Sleep for specified milliseconds. * @param ms - Milliseconds to sleep */ async sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Helper: Set up a local client for the given device and IP. */ async setupLocalClient(device, ip) { if (!this.clientRouter) { this.logger.error('clientRouter not initialized'); return false; } try { const localClient = this.clientRouter.registerClient(device.duid, ip); if (!localClient) { this.logger.error(`Failed to create local client for device ${device.duid} at IP ${ip}`); return false; } localClient.connect(); await this.waitForConnection(() => localClient.isReady()); device.specs.hasRealTimeConnection = true; this.ipMap.set(device.duid, ip); this.localClientMap.set(device.duid, localClient); this.logger.debug(`Local connection established for device ${device.duid} at ${ip}`); return true; } catch (error) { this.logger.error(`Error setting up local client for device ${device.duid} at IP ${ip}:`, error); return false; } } /** * Extract network info from device status. */ getNetworkInfoFromDeviceStatus(device) { const rpcRequest = device.deviceStatus?.[Protocol.rpc_request]; if (!rpcRequest) return undefined; return rpcRequest[RPC_Request_Segments.network_info]; } } //# sourceMappingURL=connectionService.js.map