UNPKG

matterbridge-roborock-vacuum-plugin

Version:
267 lines 14.2 kB
import { debugStringify } from 'matterbridge/logger'; import { RvcOperationalState, ServiceArea } from 'matterbridge/matter/clusters'; import { INVALID_SEGMENT_ID } from '../../constants/index.js'; import { OperationStatusCode, ProtocolVersion } from '../../roborockCommunication/enums/index.js'; import { computeAreaEstimatedTime, computeEstimatedEndTimeFromCleanProgress } from '../../share/estimatedEndTime.js'; const 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, ]); function isActivelyCleaningOperationalState(operationalState) { return (operationalState !== undefined && operationalState !== RvcOperationalState.OperationalState.Docked && operationalState !== RvcOperationalState.OperationalState.Stopped && operationalState !== RvcOperationalState.OperationalState.Error); } export function buildProgressUpdate(existing, selectedAreas, activeAreaId, estimatedTimeForActiveArea) { const progressMap = new Map(existing.map((p) => [p.areaId, p])); for (const areaId of selectedAreas) { if (!progressMap.has(areaId)) { const newEntry = { areaId, status: ServiceArea.OperationalStatus.Pending, }; if (areaId === activeAreaId && estimatedTimeForActiveArea !== undefined) { newEntry.estimatedTime = estimatedTimeForActiveArea ?? null; } progressMap.set(areaId, newEntry); } } for (const areaId of progressMap.keys()) { if (!selectedAreas.includes(areaId)) { progressMap.delete(areaId); } } if (activeAreaId !== null) { for (const [areaId, progress] of progressMap.entries()) { if (areaId === activeAreaId) { progress.status = ServiceArea.OperationalStatus.Operating; } else if (progress.status === ServiceArea.OperationalStatus.Operating) { progress.status = ServiceArea.OperationalStatus.Completed; } } } return Array.from(progressMap.values()); } export function getNextPendingArea(selectedAreas, progress, afterSkippedId) { const skippedIndex = selectedAreas.indexOf(afterSkippedId); if (skippedIndex === -1) { return null; } const progressMap = new Map(progress.map((entry) => [entry.areaId, entry.status])); for (let index = skippedIndex + 1; index < selectedAreas.length; index++) { const areaId = selectedAreas[index]; const status = progressMap.get(areaId); if (status === undefined || status === ServiceArea.OperationalStatus.Pending) { return areaId; } } return null; } export function markAreaSkipped(progress, selectedAreas, skippedAreaId, nextAreaId) { const progressMap = new Map(progress.map((entry) => [entry.areaId, { ...entry }])); for (const areaId of selectedAreas) { if (!progressMap.has(areaId)) { progressMap.set(areaId, { areaId, status: ServiceArea.OperationalStatus.Pending, }); } } const skippedEntry = progressMap.get(skippedAreaId); if (skippedEntry) { skippedEntry.status = ServiceArea.OperationalStatus.Skipped; } if (nextAreaId !== null) { const nextEntry = progressMap.get(nextAreaId); if (nextEntry) { nextEntry.status = ServiceArea.OperationalStatus.Operating; } } return Array.from(progressMap.values()); } async function updateCurrentAreaAndEstimate(robot, currentArea, message, platform) { const logger = platform.log; await robot.updateAttribute(ServiceArea.id, 'currentArea', currentArea, logger); const estimatedEndTime = currentArea === null ? null : shouldPublishEstimatedEndTime(platform, message.state) ? (computeEstimatedEndTimeFromCleanProgress(message.cleaningProcess.clean_time, message.cleaningProcess.clean_percent, currentArea) ?? null) : null; await robot.updateAttribute(ServiceArea.id, 'estimatedEndTime', estimatedEndTime, logger); } function shouldPublishEstimatedEndTime(platform, state) { return platform.configManager.isEstimatedEndTimeEnabled && CLEANING_STATES.has(state); } export async function handleServiceAreaUpdate(robot, message, platform) { const logger = platform.log; logger.debug(`Handling service area update: ${debugStringify(message)}`); // Detect transition from not-actively-cleaning to actively-cleaning and reset progress. const operationalState = robot.getAttribute(RvcOperationalState.id, 'operationalState', logger); const isActivelyCleaningNow = isActivelyCleaningOperationalState(operationalState); const wasActivelyCleaning = platform.roborockService?.getLastActivelyCleaningState(robot.device.duid) ?? false; platform.roborockService?.setLastActivelyCleaningState(robot.device.duid, isActivelyCleaningNow); if (!wasActivelyCleaning && isActivelyCleaningNow) { logger.debug(`[${robot.device.duid}] Detected transition to actively cleaning (operationalState=${operationalState}), resetting progress`); platform.roborockService?.setProgress(robot.device.duid, []); await robot.updateAttribute(ServiceArea.id, 'progress', [], logger); } if (message.state === OperationStatusCode.Idle) { logger.debug('Robot is idle, updating selectedAreas from Roborock service'); const selectedAreas = platform.roborockService?.getSelectedAreas(robot.device.duid) ?? []; await robot.updateAttribute(ServiceArea.id, 'selectedAreas', selectedAreas, logger); platform.roborockService?.setProgress(robot.device.duid, []); await robot.updateAttribute(ServiceArea.id, 'progress', [], logger); await robot.updateAttribute(ServiceArea.id, 'currentArea', null, logger); await robot.updateAttribute(ServiceArea.id, 'estimatedEndTime', null, logger); return; } if (!message.cleaningInfo && CLEANING_STATES.has(message.state)) { await handleCleaningWithoutInfo(robot, message, platform); return; } if (!message.cleaningInfo) { logger.debug('No cleaning_info available, skipping service area update'); return; } await resolveAreaFromCleaningInfo(robot, message, platform); } function getSelectedAreas(robot, message, platform) { return (robot.getAttribute(ServiceArea.id, 'selectedAreas', platform.log) ?? platform.roborockService?.getSelectedAreas(message.duid) ?? []); } async function handleCleaningWithoutInfo(robot, message, platform) { const logger = platform.log; logger.notice('Vacuum is cleaning with no cleaning_info'); const selectedAreas = getSelectedAreas(robot, message, platform); if (message.cleaningProcess.clean_area === 0 || message.cleaningProcess.clean_time === 0) { await robot.updateAttribute(ServiceArea.id, 'selectedAreas', selectedAreas, logger); await updateCurrentAreaAndEstimate(robot, null, message, platform); return; } if (selectedAreas.length === 1 || (selectedAreas.length > 1 && message.cleaningProcess.clean_time > 0)) { await robot.updateAttribute(ServiceArea.id, 'selectedAreas', selectedAreas, logger); await updateCurrentAreaAndEstimate(robot, selectedAreas[0], message, platform); const estimatedTimeForActiveArea = shouldPublishEstimatedEndTime(platform, message.state) ? computeAreaEstimatedTime(message.cleaningProcess.clean_time, message.cleaningProcess.clean_percent) : null; const existingProgress = platform.roborockService?.getProgress(robot.device.duid) ?? []; const updatedProgress = buildProgressUpdate(existingProgress, selectedAreas, selectedAreas[0], estimatedTimeForActiveArea); platform.roborockService?.setProgress(robot.device.duid, updatedProgress); await robot.updateAttribute(ServiceArea.id, 'progress', updatedProgress, logger); } else { await robot.updateAttribute(ServiceArea.id, 'selectedAreas', [], logger); let currentAreaId = null; if (robot.device.pv === ProtocolVersion.V1 && platform.roborockService) { const roomIndexMap = platform.roborockService.getSupportedAreasIndexMap(robot.device.duid); if (roomIndexMap) { const cachedSegmentId = platform.roborockService.getV1ResolvedSegment(robot.device.duid); if (cachedSegmentId !== undefined) { currentAreaId = roomIndexMap.getAreaId(cachedSegmentId, robot.homeInFo.activeMapId) ?? roomIndexMap.getAreaIdV2(cachedSegmentId) ?? null; } else { void platform.roborockService.requestV1MapRefresh(robot.device.duid); } } } await updateCurrentAreaAndEstimate(robot, currentAreaId, message, platform); } } export async function handleActiveMapChanged(robot, mapId, platform) { const logger = platform.log; const operationalState = robot.getAttribute(RvcOperationalState.id, 'operationalState', logger); if (isActivelyCleaningOperationalState(operationalState)) { logger.debug(`[${robot.device.duid}] ActiveMapChanged: ignoring map change to ${mapId} while actively cleaning (operationalState=${operationalState})`); return; } let supportedAreas = platform.roborockService?.getSupportedAreas(robot.device.duid) ?? []; let areasOnMap = supportedAreas.filter((area) => area.mapId === mapId); if (areasOnMap.length === 0) { const loaded = await platform.roborockService?.ensureAreasForMap(robot.device.duid, mapId); if (loaded) { supportedAreas = platform.roborockService?.getSupportedAreas(robot.device.duid) ?? []; areasOnMap = supportedAreas.filter((area) => area.mapId === mapId); } } if (areasOnMap.length === 0) { logger.debug(`[${robot.device.duid}] ActiveMapChanged: no areas found for mapId ${mapId}`); return; } const allAreaIds = areasOnMap.map((area) => area.areaId); const matterAreas = robot.getAttribute(ServiceArea.id, 'supportedAreas', logger) ?? []; const validAreaIds = allAreaIds.filter((id) => matterAreas.some((a) => a.areaId === id)); if (validAreaIds.length === 0) { logger.debug(`[${robot.device.duid}] ActiveMapChanged: no Matter-supported areas match mapId ${mapId} — skipping selectedAreas update`); return; } logger.debug(`[${robot.device.duid}] ActiveMapChanged: setting selectedAreas to [${validAreaIds.join(', ')}] and currentArea to null (mapId ${mapId})`); await robot.updateAttribute(ServiceArea.id, 'selectedAreas', validAreaIds, logger); await robot.updateAttribute(ServiceArea.id, 'currentArea', null, logger); await robot.updateAttribute(ServiceArea.id, 'estimatedEndTime', null, logger); platform.roborockService?.setProgress(robot.device.duid, []); await robot.updateAttribute(ServiceArea.id, 'progress', [], logger); } async function resolveAreaFromCleaningInfo(robot, message, platform) { const logger = platform.log; const cleaningInfo = message.cleaningInfo; if (!cleaningInfo) { return; } const roomIndexMap = platform.roborockService?.getSupportedAreasIndexMap(robot.device.duid); if (!roomIndexMap || !platform.roborockService) { logger.debug('Room map not yet available, skipping room area resolution'); return; } const sourceSegmentId = cleaningInfo.segment_id ?? INVALID_SEGMENT_ID; const sourceTargetSegmentId = cleaningInfo.target_segment_id ?? INVALID_SEGMENT_ID; const segmentId = sourceSegmentId !== INVALID_SEGMENT_ID ? sourceSegmentId : sourceTargetSegmentId; if (segmentId === INVALID_SEGMENT_ID) { logger.debug('No active segment, skipping currentArea update'); return; } const mappedArea = roomIndexMap.getAreaId(segmentId, robot.homeInFo.activeMapId) ?? roomIndexMap.getAreaIdV2(segmentId); if (!mappedArea) { logger.debug(`No mapped area found, skipping area mapping. sourceSegmentId: ${sourceSegmentId}, sourceTargetSegmentId: ${sourceTargetSegmentId}, segmentId: ${segmentId}, currentMappedAreas: ${debugStringify(roomIndexMap)}`); await updateCurrentAreaAndEstimate(robot, null, message, platform); return; } const supportedAreas = platform.roborockService.getSupportedAreas(robot.device.duid); logger.debug(`Mapped area found: sourceSegmentId: ${sourceSegmentId}, sourceTargetSegmentId: ${sourceTargetSegmentId}, segmentId: ${segmentId}, currentMappedAreas: ${debugStringify(roomIndexMap)}, activeArea: ${debugStringify(supportedAreas.find((x) => x.areaId === mappedArea))}`); await updateCurrentAreaAndEstimate(robot, mappedArea, message, platform); const selectedAreas = robot.getAttribute(ServiceArea.id, 'selectedAreas', logger) ?? []; const estimatedTimeForActiveArea = shouldPublishEstimatedEndTime(platform, message.state) ? computeAreaEstimatedTime(message.cleaningProcess.clean_time, message.cleaningProcess.clean_percent) : null; const existingProgress = platform.roborockService.getProgress(robot.device.duid); const updatedProgress = buildProgressUpdate(existingProgress, selectedAreas, mappedArea, estimatedTimeForActiveArea); platform.roborockService.setProgress(robot.device.duid, updatedProgress); await robot.updateAttribute(ServiceArea.id, 'progress', updatedProgress, logger); } //# sourceMappingURL=serviceAreaHandler.js.map