UNPKG

matterbridge-roborock-vacuum-plugin

Version:
361 lines (316 loc) 15.7 kB
import { RvcRunMode, PowerSource, ServiceArea, RvcOperationalState, RvcCleanMode } from 'matterbridge/matter/clusters'; import { getVacuumProperty } from './helper.js'; import { getRunningMode } from './initialData/getSupportedRunModes.js'; import { CloudMessageModel } from './model/CloudMessageModel.js'; import { RoborockMatterbridgePlatform } from './platform.js'; import { state_to_matter_operational_status, state_to_matter_state } from './share/function.js'; import RoomMap from './model/RoomMap.js'; import { getBatteryState, getBatteryStatus, getOperationalErrorState } from './initialData/index.js'; import { NotifyMessageTypes } from './notifyMessageTypes.js'; import { CloudMessageResult } from './roborockCommunication/Zmodel/messageResult.js'; import { Protocol } from './roborockCommunication/broadcast/model/protocol.js'; import { DpsPayload } from './roborockCommunication/broadcast/model/dps.js'; import { RoborockVacuumCleaner } from './rvc.js'; import { hasDockingStationError, parseDockingStationStatus } from './model/DockingStationStatus.js'; import { BatteryMessage, Device, DeviceErrorMessage, DeviceStatusNotify, Home } from './roborockCommunication/index.js'; import { OperationStatusCode } from './roborockCommunication/Zenum/operationStatusCode.js'; import { getCurrentCleanModeFunc } from './share/runtimeHelper.js'; import { debugStringify } from 'matterbridge/logger'; export class PlatformRunner { platform: RoborockMatterbridgePlatform; constructor(platform: RoborockMatterbridgePlatform) { this.platform = platform; } public async updateRobot(messageSource: NotifyMessageTypes, homeData: unknown): Promise<void> { if (messageSource === NotifyMessageTypes.HomeData) { this.updateFromHomeData(homeData as Home); } else { await this.updateFromMQTTMessage(messageSource, homeData); } } public async requestHomeData(): Promise<void> { const platform = this.platform; if (!platform.robot || !platform.robot.rrHomeId) return; if (platform.roborockService === undefined) return; const homeData = await platform.roborockService.getHomeDataForUpdating(platform.robot.rrHomeId); await this.updateRobot(NotifyMessageTypes.HomeData, homeData); } public async getRoomMapFromDevice(device: Device): Promise<RoomMap> { const platform = this.platform; const rooms = device?.rooms ?? []; if (device && platform.roborockService) { const roomData = await platform.roborockService.getRoomMappings(device.duid); return new RoomMap(roomData ?? [], rooms); } return new RoomMap([], rooms); } async getRoomMap(): Promise<RoomMap | undefined> { const platform = this.platform; const rooms = platform.robot?.device.rooms ?? []; if (platform.robot?.device === undefined || platform.roborockService === undefined) return undefined; if (platform.robot.roomInfo === undefined) { const roomData = await platform.roborockService.getRoomMappings(platform.robot.device.duid); platform.robot.roomInfo = new RoomMap(roomData ?? [], rooms); return platform.robot.roomInfo; } return platform.robot.roomInfo; } private async updateFromMQTTMessage(messageSource: NotifyMessageTypes, messageData: unknown, tracked = false): Promise<void> { const platform = this.platform; const deviceData = platform.robot?.device.data; if (deviceData === undefined) { platform.log.error('Device data is undefined'); return; } if (!tracked) { platform.log.debug(`${messageSource} updateFromMQTTMessage: ${debugStringify(messageData as DeviceStatusNotify)}`); } const duid = (messageData as DeviceStatusNotify).duid; if (platform.robot === undefined) return; if (!platform.robot.serialNumber) { platform.log.error('Robot serial number is undefined'); return; } // duid is set as device serial number if (platform.robot.serialNumber !== duid) { platform.log.notice(`DUID mismatch: ${duid}, device serial number: ${platform.robot.serialNumber}`); return; } switch (messageSource) { case NotifyMessageTypes.ErrorOccurred: { const message = messageData as DeviceErrorMessage; const operationalStateId = getOperationalErrorState(message.errorCode); if (operationalStateId) { platform.log.error(`Error occurred: ${message.errorCode}`); platform.robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', operationalStateId, platform.log); } break; } case NotifyMessageTypes.BatteryUpdate: { const message = messageData as BatteryMessage; const batteryLevel = message.percentage; if (batteryLevel) { platform.robot.updateAttribute(PowerSource.Cluster.id, 'batPercentRemaining', batteryLevel * 2, platform.log); platform.robot.updateAttribute(PowerSource.Cluster.id, 'batChargeLevel', getBatteryStatus(batteryLevel), platform.log); } break; } case NotifyMessageTypes.LocalMessage: { const data = messageData as CloudMessageResult; if (data) { const state = state_to_matter_state(data.state); if (state) { platform.robot.updateAttribute(RvcRunMode.Cluster.id, 'currentMode', getRunningMode(state), platform.log); } const currentRoom = data.cleaning_info?.segment_id ?? -1; const currentMappedAreas = this.platform.roborockService?.getSupportedAreas(duid); const isMappedArea = currentMappedAreas?.some((x) => x.areaId == currentRoom); if (currentRoom !== -1 && isMappedArea) { const roomMap = await this.getRoomMap(); this.platform.log.debug(`RoomMap: ${roomMap ? debugStringify(roomMap) : 'undefined'}`); this.platform.log.debug('CurrentRoom:', currentRoom); platform.robot.updateAttribute(ServiceArea.Cluster.id, 'currentArea', currentRoom, platform.log); } if (data.battery) { const batteryLevel = data.battery as number; platform.robot.updateAttribute(PowerSource.Cluster.id, 'batPercentRemaining', batteryLevel * 2, platform.log); platform.robot.updateAttribute(PowerSource.Cluster.id, 'batChargeState', getBatteryState(data.state, data.battery), platform.log); platform.robot.updateAttribute(PowerSource.Cluster.id, 'batChargeLevel', getBatteryStatus(batteryLevel), platform.log); } const currentCleanModeSetting = { suctionPower: data.cleaning_info?.fan_power ?? data.fan_power, waterFlow: data.cleaning_info?.water_box_status ?? data.water_box_mode, distance_off: data.distance_off, mopRoute: data.cleaning_info?.mop_mode ?? data.mop_mode, }; this.platform.log.debug(`data: ${debugStringify(data)}`); this.platform.log.debug(`currentCleanModeSetting: ${debugStringify(currentCleanModeSetting)}`); if (currentCleanModeSetting.mopRoute && currentCleanModeSetting.suctionPower && currentCleanModeSetting.waterFlow) { const currentCleanMode = getCurrentCleanModeFunc( deviceData.model, this.platform.enableExperimentalFeature?.advancedFeature?.forceRunAtDefault ?? false, )(currentCleanModeSetting); this.platform.log.debug(`Current clean mode: ${currentCleanMode}`); if (currentCleanMode) { platform.robot.updateAttribute(RvcCleanMode.Cluster.id, 'currentMode', currentCleanMode, platform.log); } } this.processAdditionalProps(platform.robot, data); } break; } case NotifyMessageTypes.CloudMessage: { const data = messageData as CloudMessageModel; if (!data) return; this.handlerCloudMessage(data, duid); break; } default: break; } } private handlerCloudMessage(data: CloudMessageModel, duid: string): void { const platform = this.platform; const messageTypes = Object.keys(data.dps).map(Number); // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; // Known: 122, 121, 102, // Unknown: 128, 139 messageTypes.forEach(async (messageType) => { switch (messageType) { case Protocol.status_update: { const status = Number(data.dps[messageType]); const matterState = state_to_matter_state(status); if (matterState) { platform.robot?.updateAttribute(RvcRunMode.Cluster.id, 'currentMode', getRunningMode(matterState), platform.log); } const operationalStateId = state_to_matter_operational_status(status); if (operationalStateId) { const dssHasError = hasDockingStationError(platform.robot?.dockStationStatus); if (!(dssHasError && self.triggerDssError())) { platform.robot?.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', operationalStateId, platform.log); } } break; } case Protocol.rpc_response: { const response = data.dps[messageType] as DpsPayload; // ignore network info if (!self.isStatusUpdate(response.result)) { platform.log.debug('Ignore message:', debugStringify(data)); return; } let roboStatus: CloudMessageResult | undefined; if (Array.isArray(response.result) && response.result.length > 0) { roboStatus = response.result[0] as CloudMessageResult; } if (roboStatus) { const message = { duid: duid, ...roboStatus } as DeviceStatusNotify; platform.log.debug('rpc_response:', debugStringify(message)); await self.updateFromMQTTMessage(NotifyMessageTypes.LocalMessage, message, true); } break; } case Protocol.suction_power: case Protocol.water_box_mode: { await platform.roborockService?.getCleanModeData(duid).then((cleanModeData) => { if (cleanModeData && platform.robot) { const currentCleanMode = getCurrentCleanModeFunc( platform.robot.device.data.model, platform.enableExperimentalFeature?.advancedFeature?.forceRunAtDefault ?? false, )({ suctionPower: cleanModeData.suctionPower, waterFlow: cleanModeData.waterFlow, distance_off: cleanModeData.distance_off, mopRoute: cleanModeData.mopRoute, }); platform.log.debug(`Clean mode data: ${debugStringify(cleanModeData)}`); platform.log.debug(`Current clean mode: ${currentCleanMode}`); if (currentCleanMode) { platform.robot?.updateAttribute(RvcCleanMode.Cluster.id, 'currentMode', currentCleanMode, platform.log); } } }); break; // Do nothing, handled in local message } case Protocol.additional_props: case Protocol.back_type: { // TODO: check if this is needed break; } default: { platform.log.notice(`Unknown message type: ${Protocol[messageType] ?? messageType} ,`, debugStringify(data)); break; } } }); } private async processAdditionalProps(robot: RoborockVacuumCleaner, message: CloudMessageResult): Promise<void> { // dss -> DockingStationStatus const dssStatus = this.getDssStatus(message); if (dssStatus) { this.triggerDssError(); } } private getDssStatus(message: CloudMessageResult): RvcOperationalState.OperationalState | undefined { const platform = this.platform; const robot = platform.robot; if ( platform.enableExperimentalFeature && platform.enableExperimentalFeature.enableExperimentalFeature && platform.enableExperimentalFeature.advancedFeature.includeDockStationStatus && message.dss !== undefined ) { const dss = parseDockingStationStatus(message.dss); if (dss && robot) { robot.dockStationStatus = dss; } if (dss && hasDockingStationError(dss)) { return RvcOperationalState.OperationalState.Error; } } return undefined; } private isStatusUpdate(result: unknown): boolean { return ( Array.isArray(result) && result.length > 0 && typeof result[0] === 'object' && result[0] !== null && 'msg_ver' in result[0] && (result[0] as CloudMessageResult).msg_ver !== undefined && (result[0] as CloudMessageResult).msg_ver !== null ); } private updateFromHomeData(homeData: Home): void { const platform = this.platform; if (platform.robot === undefined) return; const device = homeData.devices.find((d: Device) => d.duid === platform.robot?.serialNumber); const deviceData = platform.robot?.device.data; if (!device || deviceData === undefined) { platform.log.error('Device not found in home data'); return; } device.schema = homeData.products.find((prd) => prd.id == device.productId || prd.model == device.data.model)?.schema ?? []; this.platform.log.debug('updateFromHomeData-homeData:', debugStringify(homeData)); this.platform.log.debug('updateFromHomeData-device:', debugStringify(device)); const batteryLevel = getVacuumProperty(device, 'battery'); if (batteryLevel) { platform.robot.updateAttribute(PowerSource.Cluster.id, 'batPercentRemaining', batteryLevel ? batteryLevel * 2 : 200, platform.log); platform.robot.updateAttribute(PowerSource.Cluster.id, 'batChargeLevel', getBatteryStatus(batteryLevel), platform.log); } const state = getVacuumProperty(device, 'state'); const matterState = state_to_matter_state(state); if (!state || !matterState) { return; } this.platform.log.debug(`updateFromHomeData-RvcRunMode code: ${state} name: ${OperationStatusCode[state]}, matterState: ${RvcRunMode.ModeTag[matterState]}`); if (matterState) { platform.robot.updateAttribute(RvcRunMode.Cluster.id, 'currentMode', getRunningMode(matterState), platform.log); } const operationalStateId = state_to_matter_operational_status(state); if (operationalStateId) { const dssHasError = hasDockingStationError(platform.robot?.dockStationStatus); this.platform.log.debug(`dssHasError: ${dssHasError}, dockStationStatus: ${debugStringify(platform.robot?.dockStationStatus ?? {})}`); if (!(dssHasError && this.triggerDssError())) { this.platform.log.debug(`updateFromHomeData-OperationalState: ${RvcOperationalState.OperationalState[operationalStateId]}`); platform.robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', operationalStateId, platform.log); } } if (batteryLevel) { platform.robot.updateAttribute(PowerSource.Cluster.id, 'batChargeState', getBatteryState(state, batteryLevel), platform.log); } } private triggerDssError(): boolean { const platform = this.platform; const currentOperationState = platform?.robot?.getAttribute(RvcOperationalState.Cluster.id, 'operationalState') as RvcOperationalState.OperationalState; if (currentOperationState === RvcOperationalState.OperationalState.Error) { return true; } if (currentOperationState === RvcOperationalState.OperationalState.Docked) { platform.robot?.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', RvcOperationalState.OperationalState.Error, platform.log); return true; } return false; } }