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