UNPKG

matterbridge-roborock-vacuum-plugin

Version:
378 lines 22.2 kB
import { debugStringify } from 'matterbridge/logger'; import { PowerSource, RvcCleanMode, RvcOperationalState, RvcRunMode, ServiceArea } from 'matterbridge/matter/clusters'; import { CleanModeSetting } from './behaviors/roborock.vacuum/core/CleanModeSetting.js'; import { CleanSequenceType } from './behaviors/roborock.vacuum/enums/CleanSequenceType.js'; import { INVALID_SEGMENT_ID } from './constants/index.js'; import { getRunningMode } from './initialData/getSupportedRunModes.js'; import { getBatteryState, getBatteryStatus } from './initialData/index.js'; import { DockStationStatus } from './model/DockStationStatus.js'; import { VacuumStatus } from './model/VacuumStatus.js'; import { BurstPollingManager } from './platform/burstPollingManager.js'; import { DockErrorCode, OperationStatusCode } from './roborockCommunication/enums/index.js'; import { updateFromHomeData } from './runtimes/handleHomeDataMessage.js'; import { triggerDssError } from './runtimes/handleLocalMessage.js'; import { state_to_matter_operational_status, state_to_matter_state } from './share/function.js'; import { getCleanModeName, getOperationalErrorName, getOperationalStateName, getRunModeName, getRunModeNameV2, } from './share/matterStateNames.js'; import { getCleanModeResolver } from './share/runtimeHelper.js'; import { resolveDeviceState } from './share/stateResolver.js'; import { NotifyMessageTypes } from './types/notifyMessageTypes.js'; export class PlatformRunner { platform; activateHandlers = false; static CLEANING_STATES = new Set([ OperationStatusCode.RoomClean, OperationStatusCode.ZoneClean, OperationStatusCode.SpotCleaning, OperationStatusCode.RoomMopping, OperationStatusCode.ZoneMopping, OperationStatusCode.RoomCleanMopCleaning, OperationStatusCode.RoomCleanMopMopping, OperationStatusCode.ZoneCleanMopCleaning, OperationStatusCode.ZoneCleanMopMopping, OperationStatusCode.Paused, OperationStatusCode.Cleaning, OperationStatusCode.Mapping, OperationStatusCode.CleanMopCleaning, OperationStatusCode.CleanMopMopping, ]); burstPolling; constructor(platform) { this.platform = platform; this.burstPolling = new BurstPollingManager(platform); } activateHandlerFunctions() { this.activateHandlers = true; } /** * Request and process home data update from Roborock service. * Fetches latest home data including device states and triggers robot state updates. * Returns early if no robots configured, no home ID set, or service unavailable. */ async requestHomeData() { const platform = this.platform; if (platform.registry.robotsMap.size === 0 || !platform.rrHomeId) return; if (platform.roborockService === undefined) return; const robots = [...platform.registry.robotsMap.values()]; const allDevicesHaveRealTimeConnection = robots.every((x) => x.device.specs.hasRealTimeConnection); if (allDevicesHaveRealTimeConnection) return; const homeData = await platform.roborockService.getHomeDataForUpdating(platform.rrHomeId); if (homeData === undefined) return; await this.updateRobotWithPayload({ type: NotifyMessageTypes.HomeData, data: homeData }); } /** * Update robot state based on message payload. * Routes to appropriate handler using type-safe discriminated unions. */ async updateRobotWithPayload(payload) { if (!this.activateHandlers) return; const { type } = payload; switch (type) { case NotifyMessageTypes.ErrorOccurred: await this.executeWithRobot(payload.data.duid, payload.data, this.handleErrorOccurred.bind(this)); break; case NotifyMessageTypes.BatteryUpdate: await this.executeWithRobot(payload.data.duid, payload.data, this.handleBatteryUpdate.bind(this)); break; case NotifyMessageTypes.DeviceStatus: await this.executeWithRobot(payload.data.duid, payload.data, this.handleDeviceStatusUpdate.bind(this)); break; case NotifyMessageTypes.DeviceStatusSimple: await this.executeWithRobot(payload.data.duid, payload.data, this.handleDeviceStatusSimpleUpdate.bind(this)); break; case NotifyMessageTypes.CleanModeUpdate: await this.executeWithRobot(payload.data.duid, payload.data, this.handleCleanModeUpdate.bind(this)); break; case NotifyMessageTypes.ServiceAreaUpdate: await this.executeWithRobot(payload.data.duid, payload.data, this.handleServiceAreaUpdate.bind(this)); break; case NotifyMessageTypes.HomeData: await updateFromHomeData(payload.data, this.platform); break; default: this.platform.log.warn(`No handler registered for message type: ${type}`); } } /** * Template method: Execute handler with robot instance. * Handles robot lookup, error logging, and passes data to handler. */ async executeWithRobot(duid, data, handler) { const robot = this.getRobotOrLogError(duid); if (!robot) return; await handler(robot, data); } /** * Get robot by DUID or log error if not found. */ getRobotOrLogError(duid) { const robot = this.platform.registry.getRobot(duid); if (!robot) { this.platform.log.error(`Robot with DUID ${duid} not found`); } return robot; } /** * Handle error occurred messages and update robot operational state. * Processes vacuum and docking station errors, updating Matter attributes accordingly. * Prioritizes vacuum errors over docking station errors. */ async handleErrorOccurred(robot, message) { this.platform.log.debug(`Handling error occurred: ${debugStringify(message)}`); if (!this.platform.configManager.includeVacuumErrorStatus) { this.platform.log.debug(`Skipping error handling: includeVacuumErrorStatus is disabled, message: ${debugStringify(message)}`); return; } this.platform.log.debug(`Handling error occurred: ${debugStringify(message)}`); const currentOperationState = robot.getAttribute(RvcOperationalState.Cluster.id, 'operationalState', this.platform.log); // Process vacuum errors (highest priority) const vacuumStatus = new VacuumStatus(message.vacuumErrorCode ?? 0); if (vacuumStatus.hasError()) { const errorDetail = vacuumStatus.getErrorState(); this.platform.log.warn(`Vacuum error detected: ${getOperationalErrorName(errorDetail)}`); await robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', RvcOperationalState.OperationalState.Error, this.platform.log); await robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalError', { errorStateId: errorDetail }, this.platform.log); return; } // If vacuum is running with no errors, clear any previous errors and skip dock processing if (currentOperationState === RvcOperationalState.OperationalState.Running) { this.platform.log.debug('Vacuum running without errors, clearing error state.'); await robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalError', { errorStateId: RvcOperationalState.ErrorState.NoError }, this.platform.log); return; } if (!this.platform.configManager.includeDockStationStatus) { return; } if (message.dockStationStatus !== undefined && message.dockStationStatus !== null) { // Process dock station errors (only when vacuum not running) const dockStatus = DockStationStatus.parseDockStationStatus(message.dockStationStatus); robot.dockStationStatus = dockStatus; if (dockStatus.hasError()) { await robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', RvcOperationalState.OperationalState.Error, this.platform.log); const errorDetail = dockStatus.getMatterOperationalError(); this.platform.log.warn(`Docking station error detected: ${getOperationalErrorName(errorDetail)}`); await robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalError', { errorStateId: errorDetail }, this.platform.log); } else { this.platform.log.debug('No docking station errors detected.'); await robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalError', { errorStateId: RvcOperationalState.ErrorState.NoError }, this.platform.log); } return; } if (message.dockErrorCode !== DockErrorCode.None) { const dockStatus = DockStationStatus.parseDockErrorCode(message.dockErrorCode); if (dockStatus !== RvcOperationalState.ErrorState.NoError) { this.platform.log.warn(`Docking station error detected: ${getOperationalErrorName(dockStatus)}`); await robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalError', { errorStateId: dockStatus }, this.platform.log); } else { this.platform.log.debug('No docking station errors detected.'); await robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalError', { errorStateId: RvcOperationalState.ErrorState.NoError }, this.platform.log); } return; } // No errors detected and no dock station processing this.platform.log.debug('No errors detected, clearing operational error state.'); await robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalError', { errorStateId: RvcOperationalState.ErrorState.NoError }, this.platform.log); } /** * Handle battery update messages and update robot power attributes. */ async handleBatteryUpdate(robot, message) { this.platform.log.debug(`Handling battery update: ${debugStringify(message)}`); const batteryLevel = message.percentage; const deviceStatus = message.deviceStatus; const batteryChargeLevel = getBatteryStatus(batteryLevel); if (batteryLevel) { await robot.updateAttribute(PowerSource.Cluster.id, 'batPercentRemaining', batteryLevel * 2, this.platform.log); await robot.updateAttribute(PowerSource.Cluster.id, 'batChargeLevel', batteryChargeLevel, this.platform.log); } if (batteryLevel && deviceStatus) { const batteryChargeState = getBatteryState(deviceStatus, batteryLevel); await robot.updateAttribute(PowerSource.Cluster.id, 'batChargeState', batteryChargeState, this.platform.log); const currentOperationState = robot.getAttribute(RvcOperationalState.Cluster.id, 'operationalState', this.platform.log); if (batteryChargeState === PowerSource.BatChargeState.IsCharging && currentOperationState === RvcOperationalState.OperationalState.Docked) { await robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', RvcOperationalState.OperationalState.Charging, this.platform.log); } if (batteryChargeState === PowerSource.BatChargeState.IsAtFullCharge && currentOperationState === RvcOperationalState.OperationalState.Charging) { await robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', RvcOperationalState.OperationalState.Docked, this.platform.log); } } } /** * Handle device status notify messages and update robot run mode and operational state. * Uses state resolution matrix to determine final state based on status code and modifiers. */ async handleDeviceStatusUpdate(robot, message) { this.platform.log.debug(`Handling device status update: ${debugStringify(message)}`); // Check docking station errors before state resolution const includeDockStationStatus = this.platform.configManager.includeDockStationStatus; const dssHasError = includeDockStationStatus && (robot.dockStationStatus?.hasError() ?? false); if (dssHasError) { await triggerDssError(robot, this.platform); return; } const currentRunMode = robot.getAttribute(RvcRunMode.Cluster.id, 'currentMode'); const currentOperationState = robot.getAttribute(RvcOperationalState.Cluster.id, 'operationalState'); // Resolve state using state resolution matrix const resolvedState = resolveDeviceState(message); this.platform.log.notice(`[${robot.device.duid}] [${robot.device.name}] Resolved state: currentRunMode=${getRunModeNameV2(currentRunMode)}, code=${currentRunMode} currentOperationState=${getOperationalStateName(currentOperationState)}, code=${currentOperationState} newRunMode=${getRunModeName(resolvedState.runMode)}, code=${getRunningMode(resolvedState.runMode)} newOperationalState=${getOperationalStateName(resolvedState.operationalState)}, code=${resolvedState.operationalState}`); if (currentOperationState === RvcOperationalState.OperationalState.Charging && resolvedState.runMode === RvcRunMode.ModeTag.Idle && resolvedState.operationalState === RvcOperationalState.OperationalState.Docked) { // Device is still charging; skip Docked update and let handleBatteryUpdate transition away from Charging when battery is full. this.platform.log.debug(`Device is still charging, skipping Docked state update`); return; } // Update Matter attributes await robot.updateAttribute(RvcRunMode.Cluster.id, 'currentMode', getRunningMode(resolvedState.runMode), this.platform.log); await robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', resolvedState.operationalState, this.platform.log); // Auto-start burst polling when the device enters an active state const isActive = resolvedState.runMode === RvcRunMode.ModeTag.Cleaning || resolvedState.runMode === RvcRunMode.ModeTag.Mapping; if (isActive && !this.burstPolling.has(robot.device.duid)) { this.burstPolling.startBurstPolling(robot.device.duid); } } /** * Handle simple device status updates from home data API. * For devices without real-time connection, uses only status code without modifier flags. * Converts to StatusChangeMessage with undefined modifiers for state resolution. */ async handleDeviceStatusSimpleUpdate(robot, message) { this.platform.log.debug(`Handling simple device status update: ${debugStringify(message)}`); const state = state_to_matter_state(message.status); this.platform.log.debug(`Resolved state from simple update: ${state !== undefined ? getRunModeName(state) : 'undefined'}`); if (state !== undefined) { await robot.updateAttribute(RvcRunMode.Cluster.id, 'currentMode', getRunningMode(state), this.platform.log); } const includeDockStationStatus = this.platform.configManager.includeDockStationStatus; const operationalStateId = state_to_matter_operational_status(state); const dssHasError = includeDockStationStatus && (robot.dockStationStatus?.hasError() ?? false); if (dssHasError) { await triggerDssError(robot, this.platform); return; } if (operationalStateId !== undefined) { this.platform.log.debug(`Updating operational state to: ${getOperationalStateName(operationalStateId)}`); await robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', operationalStateId, this.platform.log); } } /** * Handle clean mode update messages and update robot clean mode. * Processes clean mode settings (suction power, water flow, mop route). */ async handleCleanModeUpdate(robot, message) { this.platform.log.debug(`Handling clean mode update: ${debugStringify(message)}`); const deviceData = robot.device.specs; // Update RvcCleanMode based on clean mode settings const currentCleanModeSetting = new CleanModeSetting(message.suctionPower, message.waterFlow, message.distance_off, message.mopRoute, message.seq_type ?? CleanSequenceType.Persist); if (currentCleanModeSetting.hasFullSettings) { robot.cleanModeSetting = currentCleanModeSetting; const forceRunAtDefault = this.platform.configManager.forceRunAtDefault; const currentCleanModeResolver = getCleanModeResolver(deviceData.model, forceRunAtDefault); const currentCleanMode = currentCleanModeResolver.resolve(currentCleanModeSetting); if (currentCleanMode) { this.platform.log.notice(`Calculated current clean mode: ${getCleanModeName(currentCleanMode)}`); await robot.updateAttribute(RvcCleanMode.Cluster.id, 'currentMode', currentCleanMode, this.platform.log); } } } /** * Handle service area update messages and update robot service area attributes. * Processes service area changes (supported areas, maps, selected areas, current area). */ async handleServiceAreaUpdate(robot, message) { const logger = this.platform.log; logger.debug(`Handling service area update: ${debugStringify(message)}`); if (message.state === OperationStatusCode.Idle) { logger.debug('Robot is idle, updating selectedAreas from Roborock service'); const selectedAreas = this.platform.roborockService?.getSelectedAreas(robot.device.duid) ?? []; await robot.updateAttribute(ServiceArea.Cluster.id, 'selectedAreas', selectedAreas, logger); return; } if (!message.cleaningInfo && PlatformRunner.CLEANING_STATES.has(message.state)) { await this.handleCleaningWithoutInfo(robot, message); return; } if (!message.cleaningInfo) { logger.debug('No cleaning_info available, skipping service area update'); return; } await this.resolveAreaFromCleaningInfo(robot, message); } getSelectedAreas(robot, message) { return (robot.getAttribute(ServiceArea.Cluster.id, 'selectedAreas', this.platform.log) ?? this.platform.roborockService?.getSelectedAreas(message.duid) ?? []); } async handleCleaningWithoutInfo(robot, message) { const logger = this.platform.log; logger.notice('Vacuum is cleaning with no cleaning_info'); const selectedAreas = this.getSelectedAreas(robot, message); if (message.cleaningProcess.clean_area === 0 || message.cleaningProcess.clean_time === 0) { // Robot not started cleaning yet → "Traveling to room" await robot.updateAttribute(ServiceArea.Cluster.id, 'selectedAreas', selectedAreas, logger); await robot.updateAttribute(ServiceArea.Cluster.id, 'currentArea', null, logger); return; } if (selectedAreas.length === 1) { // Single room → "Cleaning (Room)" await robot.updateAttribute(ServiceArea.Cluster.id, 'selectedAreas', selectedAreas, logger); await robot.updateAttribute(ServiceArea.Cluster.id, 'currentArea', selectedAreas[0], logger); } else { // Multiple rooms, no cleaningInfo → "Preparing" (workaround) await robot.updateAttribute(ServiceArea.Cluster.id, 'selectedAreas', [], logger); await robot.updateAttribute(ServiceArea.Cluster.id, 'currentArea', null, logger); } } async resolveAreaFromCleaningInfo(robot, message) { const logger = this.platform.log; const { cleaningInfo } = message; if (!cleaningInfo) return; const roomIndexMap = this.platform.roborockService?.getSupportedAreasIndexMap(robot.device.duid); if (!roomIndexMap || !this.platform.roborockService) { logger.error('No room mapping found.'); return; } if (this.platform.configManager.isMultipleMapEnabled) { const roomData = await this.platform.roborockService.getRoomMap(robot.device.duid, robot.homeInFo.activeMapId); robot.homeInFo.activeMapId = robot.homeInFo.mapInfo.getActiveMapId(roomData); } const source_segment_id = cleaningInfo.segment_id ?? INVALID_SEGMENT_ID; const source_target_segment_id = cleaningInfo.target_segment_id ?? INVALID_SEGMENT_ID; const segment_id = source_segment_id !== INVALID_SEGMENT_ID ? source_segment_id : source_target_segment_id; const mappedArea = roomIndexMap.getAreaId(segment_id, robot.homeInFo.activeMapId); if (!mappedArea) { logger.debug(`No mapped area found, skipping area mapping. source_segment_id: ${source_segment_id}, source_target_segment_id: ${source_target_segment_id}, segment_id: ${segment_id}, currentMappedAreas: ${debugStringify(roomIndexMap)}`); await robot.updateAttribute(ServiceArea.Cluster.id, 'currentArea', null, logger); return; } const supportedAreas = this.platform.roborockService.getSupportedAreas(robot.device.duid); const activeArea = supportedAreas.find((x) => x.areaId === mappedArea); logger.debug(`Mapped area found: source_segment_id: ${source_segment_id}, source_target_segment_id: ${source_target_segment_id}, segment_id: ${segment_id}, currentMappedAreas: ${debugStringify(roomIndexMap)}, activeArea: ${debugStringify(activeArea)}`); await robot.updateAttribute(ServiceArea.Cluster.id, 'currentArea', mappedArea, logger); } } //# sourceMappingURL=platformRunner.js.map