UNPKG

matterbridge-roborock-vacuum-plugin

Version:
236 lines 12.3 kB
import { MatterbridgeRvcCleanModeServer, RoboticVacuumCleaner } from 'matterbridge/devices'; import { debugStringify } from 'matterbridge/logger'; import { CommonAreaNamespaceTag } from 'matterbridge/matter'; import { RvcCleanMode, RvcOperationalState, ServiceArea } from 'matterbridge/matter/clusters'; import { CommandNames } from '../behaviors/BehaviorDeviceGeneric.js'; import { baseRunModeConfigs, getRunModeOptions } from '../behaviors/roborock.vacuum/core/runModeConfig.js'; import { RoborockServiceAreaServer } from '../behaviors/roborockServiceAreaServer.js'; import { ROUTINE_MAP_ID } from '../constants/ids.js'; import { getOperationalStates, getSupportedCleanModes, getSupportedRoutines } from '../initialData/index.js'; import { getNextPendingArea, markAreaSkipped } from '../runtimes/handlers/serviceAreaHandler.js'; export class RoborockVacuumCleaner extends RoboticVacuumCleaner { device; homeInFo; roborockService; dockStationStatus; cleanModeSetting; lastUpdateAt = null; operationSessionStartMs = null; operationPausedSinceMs = null; operationPausedAccumMs = 0; skipAreaHandler; /** * Create a new Roborock Vacuum Cleaner device. * Initializes the device with supported cleaning modes, run modes, areas, and routines. */ constructor(device, homeInFo, configManager, roborockService, log, resolvedAreas = [], resolvedMaps = []) { const deviceConfig = RoborockVacuumCleaner.initializeDeviceConfiguration(device, homeInFo, configManager, roborockService, log, resolvedAreas, resolvedMaps); super(deviceConfig.deviceName, device.sn ?? device.duid, deviceConfig.bridgeMode, deviceConfig.runModeConfigs[0].mode, deviceConfig.runModeConfigs, deviceConfig.cleanModes[0].mode, deviceConfig.cleanModes, undefined, undefined, RvcOperationalState.OperationalState.Docked, deviceConfig.operationalState, deviceConfig.supportedAreaAndRoutines, undefined, null, deviceConfig.supportedMaps); this.device = device; this.homeInFo = homeInFo; this.roborockService = roborockService; log.debug(`Creating RoborockVacuumCleaner for device: ${deviceConfig.deviceName}, model: ${device.specs.model}, forceRunAtDefault: ${configManager.forceRunAtDefault} bridgeMode: ${deviceConfig.bridgeMode}, Supported Clean Modes: ${debugStringify(deviceConfig.cleanModes)}, Supported Areas: ${debugStringify(deviceConfig.supportedAreas)}, Supported Maps: ${debugStringify(deviceConfig.supportedMaps)} Supported Areas and Routines: ${debugStringify(deviceConfig.supportedAreaAndRoutines)}, Supported Operational States: ${debugStringify(deviceConfig.operationalState)}`); } /** * Override to enable ServiceArea.Feature.ProgressReporting for per-room cleaning status. * Adds progress attribute to track area cleaning completion (Pending/Operating/Completed/Skipped). */ createDefaultServiceAreaClusterServer(supportedAreas, selectedAreas, currentArea, supportedMaps) { this.behaviors.require(RoborockServiceAreaServer.with(ServiceArea.Feature.Maps, ServiceArea.Feature.ProgressReporting), { supportedAreas: supportedAreas ?? [ { areaId: 1, mapId: null, areaInfo: { locationInfo: { locationName: 'Living', floorNumber: 0, areaType: CommonAreaNamespaceTag.LivingRoom.tag }, landmarkInfo: null, }, }, { areaId: 2, mapId: null, areaInfo: { locationInfo: { locationName: 'Kitchen', floorNumber: 0, areaType: CommonAreaNamespaceTag.Kitchen.tag }, landmarkInfo: null, }, }, { areaId: 3, mapId: null, areaInfo: { locationInfo: { locationName: 'Bedroom', floorNumber: 1, areaType: CommonAreaNamespaceTag.Bedroom.tag }, landmarkInfo: null, }, }, { areaId: 4, mapId: null, areaInfo: { locationInfo: { locationName: 'Bathroom', floorNumber: 1, areaType: CommonAreaNamespaceTag.Bathroom.tag }, landmarkInfo: null, }, }, ], selectedAreas: selectedAreas ?? [], currentArea: currentArea !== undefined ? currentArea : 1, supportedMaps: supportedMaps ?? [], estimatedEndTime: null, progress: [], }); return this; } /** * Override to enable RvcCleanMode.Feature.DirectModeChange for mode changes during active cleaning. * Allows controllers to change clean mode (e.g., suction power/water flow) without requiring idle state. */ createDefaultRvcCleanModeClusterServer(currentMode, supportedModes) { this.behaviors.require(MatterbridgeRvcCleanModeServer.with(RvcCleanMode.Feature.DirectModeChange), { supportedModes: supportedModes ?? [ { label: 'Vacuum', mode: 1, modeTags: [{ value: RvcCleanMode.ModeTag.Vacuum }] }, { label: 'Mop', mode: 2, modeTags: [{ value: RvcCleanMode.ModeTag.Mop }] }, { label: 'Clean', mode: 3, modeTags: [{ value: RvcCleanMode.ModeTag.DeepClean }] }, ], currentMode: currentMode ?? 1, }); return this; } /** * Configure command handlers for the vacuum device. * Sets up handlers for identify, area selection, mode changes, and cleaning operations. */ configureHandler(behaviorHandler) { this.addCommandHandlerWithErrorHandling(CommandNames.IDENTIFY, async ({ request, cluster, attributes, endpoint }) => { this.log.info(`Identify command received for endpoint ${endpoint}, cluster ${cluster}, attributes ${debugStringify(attributes)}, request: ${JSON.stringify(request)}`); behaviorHandler.executeCommand(CommandNames.IDENTIFY, request.identifyTime ?? 5); }); this.addCommandHandlerWithErrorHandling(CommandNames.SELECT_AREAS, async ({ request }) => { const { newAreas } = request; const requestedAreas = newAreas ?? []; this.log.info(`Selecting areas: ${requestedAreas.join(', ')}`); behaviorHandler.executeCommand(CommandNames.SELECT_AREAS, requestedAreas); }); this.skipAreaHandler = async (skippedArea) => { await behaviorHandler.executeCommand(CommandNames.SKIP_AREA, skippedArea); }; this.addCommandHandlerWithErrorHandling(CommandNames.CHANGE_TO_MODE, async ({ request }) => { const { newMode } = request; this.log.info(`Changing to mode: ${newMode}`); behaviorHandler.executeCommand(CommandNames.CHANGE_TO_MODE, newMode); }); this.addCommandHandlerWithErrorHandling(CommandNames.PAUSE, async () => { this.log.info('Pause command received'); behaviorHandler.executeCommand(CommandNames.PAUSE); }); this.addCommandHandlerWithErrorHandling(CommandNames.RESUME, async () => { this.log.info('Resume command received'); behaviorHandler.executeCommand(CommandNames.RESUME); }); this.addCommandHandlerWithErrorHandling(CommandNames.GO_HOME, async () => { this.log.info('GoHome command received'); behaviorHandler.executeCommand(CommandNames.GO_HOME); }); this.addCommandHandlerWithErrorHandling(CommandNames.STOP, async () => { this.log.info('Stop command received'); behaviorHandler.executeCommand(CommandNames.STOP); }); } /** * Initialize device configuration including modes, areas, and maps. */ static initializeDeviceConfiguration(device, homeInFo, configManager, roborockService, log, resolvedAreas = [], resolvedMaps = []) { const cleanModes = getSupportedCleanModes(device.specs.model, configManager, device.featureSet, device.newFeatureSet); const operationalState = getOperationalStates(); const runModeConfigs = getRunModeOptions(baseRunModeConfigs); const bridgeMode = configManager.isServerModeEnabled ? 'server' : 'matter'; const supportedMaps = [...resolvedMaps]; let routineAsRooms = []; if (configManager.showRoutinesAsRoom) { routineAsRooms = getSupportedRoutines(device.scenes ?? [], log); roborockService.setSupportedRoutines(device.duid, routineAsRooms); } if (routineAsRooms.length > 0) { const mapForRoutine = { mapId: ROUTINE_MAP_ID, name: 'Routine' }; supportedMaps.push(mapForRoutine); routineAsRooms.forEach((rt) => { rt.mapId = ROUTINE_MAP_ID; }); } const supportedAreaAndRoutines = [...resolvedAreas, ...routineAsRooms]; const deviceName = device.name; return { deviceName, bridgeMode, cleanModes, runModeConfigs, supportedAreas: resolvedAreas, supportedMaps, supportedAreaAndRoutines, operationalState, }; } async finalizeSkipArea(skippedArea) { const selectedAreas = this.getAttribute(ServiceArea.id, 'selectedAreas', this.log) ?? []; const existingProgress = this.roborockService.getProgress(this.device.duid); const nextAreaId = getNextPendingArea(selectedAreas, existingProgress, skippedArea); const updatedProgress = markAreaSkipped(existingProgress, selectedAreas, skippedArea, nextAreaId); this.roborockService.setProgress(this.device.duid, updatedProgress); await this.updateAttribute(ServiceArea.id, 'progress', updatedProgress, this.log); await this.updateAttribute(ServiceArea.id, 'currentArea', nextAreaId, this.log); return { updatedProgress, nextAreaId }; } async trySwitchMap(selectedAreaIds) { const duid = this.device.duid; const supportedAreas = this.roborockService.getSupportedAreas(duid); const targetMapId = supportedAreas.find((a) => a.areaId === selectedAreaIds[0])?.mapId; if (targetMapId === undefined || targetMapId === null) return; if (targetMapId === this.homeInFo.activeMapId) return; this.log.info(`[${duid}] Switching map from ${this.homeInFo.activeMapId} to ${targetMapId}`); try { await this.roborockService.switchMap(duid, targetMapId); } catch (err) { this.log.error(`[${duid}] Failed to switch map: ${String(err)}`); } } resolveAllRoomsForActiveMap() { const duid = this.device.duid; const supportedAreas = this.roborockService.getSupportedAreas(duid); if (supportedAreas.length === 0) return []; const currentSelectedAreas = this.getAttribute(ServiceArea.id, 'selectedAreas', this.log) ?? []; let activeMapId = supportedAreas.find((a) => currentSelectedAreas.includes(a.areaId))?.mapId; if (activeMapId === undefined && this.homeInFo.activeMapId !== -1) { activeMapId = this.homeInFo.activeMapId; } if (activeMapId === undefined || !supportedAreas.some((a) => a.mapId === activeMapId)) { activeMapId = supportedAreas[0].mapId; } return supportedAreas.filter((a) => a.mapId === activeMapId).map((a) => a.areaId); } /** * Helper method to add command handler with error handling. * Wraps handler logic in try-catch to avoid code duplication. */ addCommandHandlerWithErrorHandling(commandName, handler) { this.addCommandHandler(commandName, async (context) => { try { await handler(context); } catch (error) { this.log.error(`Error executing ${commandName} command: ${error}`); throw error; } }); } } //# sourceMappingURL=roborockVacuumCleaner.js.map