matterbridge-roborock-vacuum-plugin
Version:
Matterbridge Roborock Vacuum Plugin
250 lines • 11.8 kB
JavaScript
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