matterbridge-roborock-vacuum-plugin
Version:
Matterbridge Roborock Vacuum Plugin
301 lines (300 loc) • 16 kB
JavaScript
import { RvcRunMode, PowerSource, ServiceArea, RvcOperationalState, RvcCleanMode } from 'matterbridge/matter/clusters';
import { getVacuumProperty } from './helper.js';
import { getRunningMode } from './initialData/getSupportedRunModes.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 { Protocol } from './roborockCommunication/broadcast/model/protocol.js';
import { hasDockingStationError, parseDockingStationStatus } from './model/DockingStationStatus.js';
import { OperationStatusCode } from './roborockCommunication/Zenum/operationStatusCode.js';
import { getCurrentCleanModeFunc } from './share/runtimeHelper.js';
import { debugStringify } from 'matterbridge/logger';
export class PlatformRunner {
platform;
constructor(platform) {
this.platform = platform;
}
async updateRobot(messageSource, homeData) {
if (messageSource === NotifyMessageTypes.HomeData) {
this.updateFromHomeData(homeData);
}
else {
await this.updateFromMQTTMessage(messageSource, homeData);
}
}
async requestHomeData() {
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);
}
async getRoomMapFromDevice(device) {
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() {
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;
}
async updateFromMQTTMessage(messageSource, messageData, tracked = false) {
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)}`);
}
const duid = messageData.duid;
if (platform.robot === undefined)
return;
if (!platform.robot.serialNumber) {
platform.log.error('Robot serial number is undefined');
return;
}
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;
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;
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;
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;
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;
if (!data)
return;
this.handlerCloudMessage(data, duid);
break;
}
default:
break;
}
}
handlerCloudMessage(data, duid) {
const platform = this.platform;
const messageTypes = Object.keys(data.dps).map(Number);
const self = this;
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];
if (!self.isStatusUpdate(response.result)) {
platform.log.debug('Ignore message:', debugStringify(data));
return;
}
let roboStatus;
if (Array.isArray(response.result) && response.result.length > 0) {
roboStatus = response.result[0];
}
if (roboStatus) {
const message = { duid: duid, ...roboStatus };
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;
}
case Protocol.additional_props:
case Protocol.back_type: {
break;
}
default: {
platform.log.notice(`Unknown message type: ${Protocol[messageType] ?? messageType} ,`, debugStringify(data));
break;
}
}
});
}
async processAdditionalProps(robot, message) {
const dssStatus = this.getDssStatus(message);
if (dssStatus) {
this.triggerDssError();
}
}
getDssStatus(message) {
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;
}
isStatusUpdate(result) {
return (Array.isArray(result) &&
result.length > 0 &&
typeof result[0] === 'object' &&
result[0] !== null &&
'msg_ver' in result[0] &&
result[0].msg_ver !== undefined &&
result[0].msg_ver !== null);
}
updateFromHomeData(homeData) {
const platform = this.platform;
if (platform.robot === undefined)
return;
const device = homeData.devices.find((d) => 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);
}
}
triggerDssError() {
const platform = this.platform;
const currentOperationState = platform?.robot?.getAttribute(RvcOperationalState.Cluster.id, '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;
}
}