matterbridge-roborock-vacuum-plugin
Version:
Matterbridge Roborock Vacuum Plugin
361 lines (316 loc) • 15.7 kB
text/typescript
import { RvcRunMode, PowerSource, ServiceArea, RvcOperationalState, RvcCleanMode } from 'matterbridge/matter/clusters';
import { getVacuumProperty } from './helper.js';
import { getRunningMode } from './initialData/getSupportedRunModes.js';
import { CloudMessageModel } from './model/CloudMessageModel.js';
import { RoborockMatterbridgePlatform } from './platform.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 { CloudMessageResult } from './roborockCommunication/Zmodel/messageResult.js';
import { Protocol } from './roborockCommunication/broadcast/model/protocol.js';
import { DpsPayload } from './roborockCommunication/broadcast/model/dps.js';
import { RoborockVacuumCleaner } from './rvc.js';
import { hasDockingStationError, parseDockingStationStatus } from './model/DockingStationStatus.js';
import { BatteryMessage, Device, DeviceErrorMessage, DeviceStatusNotify, Home } from './roborockCommunication/index.js';
import { OperationStatusCode } from './roborockCommunication/Zenum/operationStatusCode.js';
import { getCurrentCleanModeFunc } from './share/runtimeHelper.js';
import { debugStringify } from 'matterbridge/logger';
export class PlatformRunner {
platform: RoborockMatterbridgePlatform;
constructor(platform: RoborockMatterbridgePlatform) {
this.platform = platform;
}
public async updateRobot(messageSource: NotifyMessageTypes, homeData: unknown): Promise<void> {
if (messageSource === NotifyMessageTypes.HomeData) {
this.updateFromHomeData(homeData as Home);
} else {
await this.updateFromMQTTMessage(messageSource, homeData);
}
}
public async requestHomeData(): Promise<void> {
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);
}
public async getRoomMapFromDevice(device: Device): Promise<RoomMap> {
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(): Promise<RoomMap | undefined> {
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;
}
private async updateFromMQTTMessage(messageSource: NotifyMessageTypes, messageData: unknown, tracked = false): Promise<void> {
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 as DeviceStatusNotify)}`);
}
const duid = (messageData as DeviceStatusNotify).duid;
if (platform.robot === undefined) return;
if (!platform.robot.serialNumber) {
platform.log.error('Robot serial number is undefined');
return;
}
// duid is set as device serial number
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 as DeviceErrorMessage;
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 as BatteryMessage;
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 as CloudMessageResult;
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 as number;
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 as CloudMessageModel;
if (!data) return;
this.handlerCloudMessage(data, duid);
break;
}
default:
break;
}
}
private handlerCloudMessage(data: CloudMessageModel, duid: string): void {
const platform = this.platform;
const messageTypes = Object.keys(data.dps).map(Number);
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
// Known: 122, 121, 102,
// Unknown: 128, 139
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] as DpsPayload;
// ignore network info
if (!self.isStatusUpdate(response.result)) {
platform.log.debug('Ignore message:', debugStringify(data));
return;
}
let roboStatus: CloudMessageResult | undefined;
if (Array.isArray(response.result) && response.result.length > 0) {
roboStatus = response.result[0] as CloudMessageResult;
}
if (roboStatus) {
const message = { duid: duid, ...roboStatus } as DeviceStatusNotify;
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; // Do nothing, handled in local message
}
case Protocol.additional_props:
case Protocol.back_type: {
// TODO: check if this is needed
break;
}
default: {
platform.log.notice(`Unknown message type: ${Protocol[messageType] ?? messageType} ,`, debugStringify(data));
break;
}
}
});
}
private async processAdditionalProps(robot: RoborockVacuumCleaner, message: CloudMessageResult): Promise<void> {
// dss -> DockingStationStatus
const dssStatus = this.getDssStatus(message);
if (dssStatus) {
this.triggerDssError();
}
}
private getDssStatus(message: CloudMessageResult): RvcOperationalState.OperationalState | undefined {
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;
}
private isStatusUpdate(result: unknown): boolean {
return (
Array.isArray(result) &&
result.length > 0 &&
typeof result[0] === 'object' &&
result[0] !== null &&
'msg_ver' in result[0] &&
(result[0] as CloudMessageResult).msg_ver !== undefined &&
(result[0] as CloudMessageResult).msg_ver !== null
);
}
private updateFromHomeData(homeData: Home): void {
const platform = this.platform;
if (platform.robot === undefined) return;
const device = homeData.devices.find((d: Device) => 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);
}
}
private triggerDssError(): boolean {
const platform = this.platform;
const currentOperationState = platform?.robot?.getAttribute(RvcOperationalState.Cluster.id, 'operationalState') as RvcOperationalState.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;
}
}