UNPKG

matterbridge-roborock-vacuum-plugin

Version:
360 lines (359 loc) 15.4 kB
import assert from 'node:assert'; import { debugStringify } from 'matterbridge/logger'; import { NotifyMessageTypes } from './notifyMessageTypes.js'; import { clearInterval } from 'node:timers'; import { RoborockAuthenticateApi, RoborockIoTApi, MessageProcessor, Protocol, RequestMessage, ResponseMessage, } from './roborockCommunication/index.js'; export default class RoborockService { loginApi; logger; iotApiFactory; iotApi; userdata; deviceNotify; messageClient; remoteDevices = new Set(); messageProcessor; ip; localClient; clientManager; refreshInterval; requestDeviceStatusInterval; supportedAreas = new Map(); supportedRoutines = new Map(); selectedAreas = new Map(); constructor(authenticateApiSupplier = (logger) => new RoborockAuthenticateApi(logger), iotApiSupplier = (logger, ud) => new RoborockIoTApi(ud, logger), refreshInterval, clientManager, logger) { this.logger = logger; this.loginApi = authenticateApiSupplier(logger); this.iotApiFactory = iotApiSupplier; this.refreshInterval = refreshInterval; this.clientManager = clientManager; } async loginWithPassword(username, password) { const userdata = await this.loginApi.loginWithPassword(username, password); return this.auth(userdata); } getMessageProcessor() { if (!this.messageProcessor) { this.logger.error('MessageApi is not initialized.'); } return this.messageProcessor; } setSelectedAreas(duid, selectedAreas) { this.logger.debug('RoborockService - setSelectedAreas', selectedAreas); this.selectedAreas.set(duid, selectedAreas); } setSupportedAreas(duid, supportedAreas) { this.supportedAreas.set(duid, supportedAreas); } setSupportedScenes(duid, routineAsRooms) { this.supportedRoutines.set(duid, routineAsRooms); } getSupportedAreas(duid) { return this.supportedAreas.get(duid); } async getCleanModeData(duid) { this.logger.notice('RoborockService - getCleanModeData'); const data = await this.messageProcessor?.getCleanModeData(duid); if (!data) { throw new Error('Failed to retrieve clean mode data'); } return data; } async changeCleanMode(duid, { suctionPower, waterFlow, distance_off, mopRoute }) { this.logger.notice('RoborockService - changeCleanMode'); return this.messageProcessor?.changeCleanMode(duid, suctionPower, waterFlow, mopRoute, distance_off); } async startClean(duid) { const supportedRooms = this.supportedAreas.get(duid) ?? []; const supportedRoutines = this.supportedRoutines.get(duid) ?? []; const selected = this.selectedAreas.get(duid) ?? []; this.logger.debug('RoborockService - begin cleaning', debugStringify({ duid, supportedRooms, supportedRoutines, selected })); if (supportedRoutines.length === 0) { if (selected.length == supportedRooms.length || selected.length === 0 || supportedRooms.length === 0) { this.logger.debug('RoborockService - startGlobalClean'); this.getMessageProcessor()?.startClean(duid); } else { this.logger.debug('RoborockService - startRoomClean', debugStringify({ duid, selected })); return this.messageProcessor?.startRoomClean(duid, selected, 1); } } else { const rooms = selected.filter((slt) => supportedRooms.some((a) => a.areaId == slt)); const rt = selected.filter((slt) => supportedRoutines.some((a) => a.areaId == slt)); if (rt.length > 1) { this.logger.warn('RoborockService - Multiple routines selected, which is not supported.', debugStringify({ duid, rt })); } else if (rt.length === 1) { this.logger.debug('RoborockService - startScene', debugStringify({ duid, rooms })); await this.iotApi?.startScene(rt[0]); return; } else if (rooms.length == supportedRooms.length || rooms.length === 0 || supportedRooms.length === 0) { this.logger.debug('RoborockService - startGlobalClean'); this.getMessageProcessor()?.startClean(duid); } else if (rooms.length > 0) { this.logger.debug('RoborockService - startRoomClean', debugStringify({ duid, rooms })); return this.messageProcessor?.startRoomClean(duid, rooms, 1); } else { this.logger.warn('RoborockService - something goes wrong.', debugStringify({ duid, rooms, rt, selected, supportedRooms, supportedRoutines })); return; } } } async pauseClean(duid) { this.logger.debug('RoborockService - pauseClean'); await this.getMessageProcessor()?.pauseClean(duid); } async stopAndGoHome(duid) { this.logger.debug('RoborockService - stopAndGoHome'); await this.getMessageProcessor()?.gotoDock(duid); } async resumeClean(duid) { this.logger.debug('RoborockService - resumeClean'); await this.getMessageProcessor()?.resumeClean(duid); } async playSoundToLocate(duid) { this.logger.debug('RoborockService - findMe'); await this.getMessageProcessor()?.findMyRobot(duid); } async customGet(duid, method) { this.logger.debug('RoborockService - customSend-message', method); return this.getMessageProcessor()?.getCustomMessage(duid, new RequestMessage({ method })); } async customGetInSecure(duid, method) { this.logger.debug('RoborockService - customGetInSecure-message', method); return this.getMessageProcessor()?.getCustomMessage(duid, new RequestMessage({ method, secure: true })); } async customSend(duid, request) { return this.getMessageProcessor()?.sendCustomMessage(duid, request); } stopService() { if (this.messageClient) { this.messageClient.disconnect(); this.messageClient = undefined; } if (this.localClient) { this.localClient.disconnect(); this.localClient = undefined; } if (this.messageProcessor) { this.messageProcessor = undefined; } if (this.requestDeviceStatusInterval) { clearInterval(this.requestDeviceStatusInterval); this.requestDeviceStatusInterval = undefined; } } setDeviceNotify(callback) { this.deviceNotify = callback; } async activateDeviceNotify(device) { const self = this; this.logger.debug('Requesting device info for device', device.duid); this.requestDeviceStatusInterval = setInterval(async () => { if (this.messageProcessor) { await this.messageProcessor.getDeviceStatus(device.duid).then((response) => { if (self.deviceNotify) { const message = { duid: device.duid, ...response.errorStatus, ...response.message }; self.logger.debug('Device status update', debugStringify(message)); self.deviceNotify(NotifyMessageTypes.LocalMessage, message); } }); } else { self.logger.error('Local client not initialized'); } }, this.refreshInterval * 1000); } async listDevices(username) { assert(this.iotApi !== undefined); assert(this.userdata !== undefined); const homeDetails = await this.loginApi.getHomeDetails(); if (!homeDetails) { throw new Error('Failed to retrieve the home details'); } const homeData = (await this.iotApi.getHome(homeDetails.rrHomeId)); if (!homeData) { return []; } const scenes = (await this.iotApi.getScenes(homeDetails.rrHomeId)) ?? []; const products = new Map(); homeData.products.forEach((p) => products.set(p.id, p.model)); const devices = [...homeData.devices, ...homeData.receivedDevices]; const result = devices.map((device) => { return { ...device, rrHomeId: homeDetails.rrHomeId, rooms: homeData.rooms, localKey: device.localKey, pv: device.pv, serialNumber: device.sn, scenes: scenes.filter((sc) => sc.param && JSON.parse(sc.param).action.items.some((x) => x.entityId == device.duid)), data: { id: device.duid, firmwareVersion: device.fv, serialNumber: device.sn, model: homeData.products.find((p) => p.id === device.productId)?.model, category: homeData.products.find((p) => p.id === device.productId)?.category, batteryLevel: device.deviceStatus?.[Protocol.battery] ?? 100, }, store: { username: username, userData: this.userdata, localKey: device.localKey, pv: device.pv, model: products.get(device.productId), }, }; }); return result; } async getHomeDataForUpdating(homeid) { assert(this.iotApi !== undefined); assert(this.userdata !== undefined); const homeData = await this.iotApi.getHomev2(homeid); if (!homeData) { throw new Error('Failed to retrieve the home data'); } const products = new Map(); homeData.products.forEach((p) => products.set(p.id, p.model)); const devices = homeData.devices.length > 0 ? homeData.devices : homeData.receivedDevices; const dvs = devices.map((device) => { return { ...device, rrHomeId: homeid, rooms: homeData.rooms, serialNumber: device.sn, data: { id: device.duid, firmwareVersion: device.fv, serialNumber: device.sn, model: homeData.products.find((p) => p.id === device.productId)?.model, category: homeData.products.find((p) => p.id === device.productId)?.category, batteryLevel: device.deviceStatus?.[Protocol.battery] ?? 100, }, store: { userData: this.userdata, localKey: device.localKey, pv: device.pv, model: products.get(device.productId), }, }; }); return { ...homeData, devices: dvs, }; } async getScenes(homeId) { assert(this.iotApi !== undefined); return this.iotApi.getScenes(homeId); } async startScene(sceneId) { assert(this.iotApi !== undefined); return this.iotApi.startScene(sceneId); } getRoomMappings(duid) { if (!this.messageClient) { this.logger.warn('messageClient not initialized. Waititing for next execution'); return undefined; } return this.messageClient.get(duid, new RequestMessage({ method: 'get_room_mapping' })); } async initializeMessageClient(username, device, userdata) { if (this.clientManager === undefined) { this.logger.error('ClientManager not initialized'); return; } const self = this; this.messageClient = this.clientManager.get(username, userdata); this.messageClient.registerDevice(device.duid, device.localKey, device.pv); this.messageClient.registerConnectionListener({ onConnected: () => { self.logger.notice('Connected to MQTT broker'); }, onDisconnected: () => { self.logger.notice('Disconnected from MQTT broker'); }, onError: (message) => { self.logger.error('Error from MQTT broker', message); }, }); this.messageClient.registerMessageListener({ onMessage: (message) => { if (message instanceof ResponseMessage) { const duid = message.duid; if (message.contain(Protocol.battery)) return; if (duid && self.deviceNotify) { self.deviceNotify(NotifyMessageTypes.CloudMessage, message); } } }, }); this.messageClient.connect(); while (!this.messageClient.isConnected()) { await this.sleep(500); } this.logger.debug('MessageClient connected'); } async initializeMessageClientForLocal(device) { this.logger.debug('Begin get local ip'); if (this.messageClient === undefined) { this.logger.error('messageClient not initialized'); return; } const self = this; this.messageProcessor = new MessageProcessor(this.messageClient); this.messageProcessor.injectLogger(this.logger); this.messageProcessor.registerListener({ onError: (message) => { if (self.deviceNotify) { self.deviceNotify(NotifyMessageTypes.ErrorOccurred, { duid: device.duid, errorCode: message }); } }, onBatteryUpdate: (percentage) => { if (self.deviceNotify) { self.deviceNotify(NotifyMessageTypes.BatteryUpdate, { duid: device.duid, percentage }); } }, }); this.logger.debug('Local device', device.duid); try { if (!this.ip) { this.logger.debug('Requesting network info for device', device.duid); const networkInfo = await this.messageProcessor.getNetworkInfo(device.duid); this.ip = networkInfo.ip; } if (this.ip) { this.logger.debug('initializing the local connection for this client towards ' + this.ip); this.localClient = this.messageClient.registerClient(device.duid, this.ip); this.localClient.connect(); let count = 0; while (!this.localClient.isConnected() && count < 20) { this.logger.debug('Keep waiting for local client to connect'); count++; await this.sleep(500); } if (!this.localClient.isConnected()) { throw new Error('Local client did not connect after 10 attempts, something is wrong'); } this.logger.debug('LocalClient connected'); } } catch (error) { this.logger.error('Error requesting network info', error); } } sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } auth(userdata) { this.userdata = userdata; this.iotApi = this.iotApiFactory(this.logger, userdata); return userdata; } }