matterbridge-xiaomi-roborock
Version:
Matterbridge Xiaomi Roborock Plugin
150 lines (149 loc) • 8.48 kB
JavaScript
import { RoboticVacuumCleaner } from 'matterbridge/devices';
import { firstValueFrom, mergeMap } from 'rxjs';
import { PowerSource, RvcRunMode, RvcCleanMode, RvcOperationalState } from 'matterbridge/matter/clusters';
import { applyConfigDefaults } from './services/config_service.js';
import { DeviceManager } from './services/device_manager.js';
import { getLogger } from './utils/logger.js';
import { findSpeedModes } from './utils/find_speed_modes.js';
const SUPPORTED_MODES = [
{ label: 'Idle', mode: 0, modeTags: [{ value: RvcRunMode.ModeTag.Idle }] },
{ label: 'Cleaning', mode: 1, modeTags: [{ value: RvcRunMode.ModeTag.Cleaning }] },
];
const SUPPORTED_CLEAN_MODES = [
{ label: 'Vacuum', mode: 0, modeTags: [{ value: RvcCleanMode.ModeTag.Vacuum }] },
{ label: 'Mop', mode: 1, modeTags: [{ value: RvcCleanMode.ModeTag.Mop }] },
];
const SUPPORTED_OPERATIONAL_STATES = [
{ operationalStateId: RvcOperationalState.OperationalState.Docked },
{ operationalStateId: RvcOperationalState.OperationalState.SeekingCharger },
{ operationalStateId: RvcOperationalState.OperationalState.Charging },
{ operationalStateId: RvcOperationalState.OperationalState.Running },
{ operationalStateId: RvcOperationalState.OperationalState.Stopped },
{ operationalStateId: RvcOperationalState.OperationalState.Paused },
{ operationalStateId: RvcOperationalState.OperationalState.Error },
];
export class VacuumDeviceAccessory {
config;
log;
deviceManager;
endpoint;
constructor(config, logger) {
this.config = applyConfigDefaults(config);
this.log = getLogger(logger, this.config);
this.deviceManager = new DeviceManager(this.log, this.config);
}
async initializeMatterbridgeEndpoint() {
this.log.info(`Waiting for the connection to the vacuum to be established...`);
await firstValueFrom(this.deviceManager.deviceConnected$);
this.log.info(`Connected to device!`);
const serial = await this.deviceManager.device.call('get_serial_number');
const serialNumber = serial[0].serial_number;
const firmware = await this.deviceManager.device.call('miIO.info');
this.log.info(`Serial number: ${serialNumber}`);
this.log.info(`Firmware: ${firmware.fw_ver}`);
const modelSpeeds = findSpeedModes(this.deviceManager.model, firmware.fw_ver);
const supportedCleanModes = modelSpeeds.waterspeed ? SUPPORTED_CLEAN_MODES : [SUPPORTED_CLEAN_MODES[0]];
this.endpoint = new RoboticVacuumCleaner(this.config.name, serialNumber, 'server', SUPPORTED_MODES[0].mode, SUPPORTED_MODES, supportedCleanModes[0].mode, supportedCleanModes, undefined, undefined, RvcOperationalState.OperationalState.Docked, SUPPORTED_OPERATIONAL_STATES);
this.endpoint.vendorName = 'Xiaomi';
this.endpoint.productName = this.deviceManager.model;
this.endpoint.softwareVersionString = firmware.fw_ver;
this.endpoint.productUrl = 'https://github.com/afharo/matterbridge-xiaomi-roborock';
this.endpoint.hardwareVersionString = this.deviceManager.model;
this.endpoint.lifecycle.destroying.on(() => {
this.deviceManager.stop();
});
this.endpoint?.addCommandHandler('changeToMode', async (data) => {
this.log.debug(`Start command received: ${JSON.stringify(data)}`);
switch (data.request.newMode) {
case 0:
await this.deviceManager.device.pause();
break;
case 1:
await this.deviceManager.device.activateCleaning();
break;
default:
this.log.warn(`Unknown mode ${data.request.newMode}`);
break;
}
});
this.endpoint?.addCommandHandler('stop', async () => {
await this.deviceManager.device.deactivateCleaning();
});
this.endpoint?.addCommandHandler('pause', async () => {
await this.deviceManager.device.pause();
});
this.endpoint?.addCommandHandler('resume', async () => {
await this.deviceManager.device.activateCleaning();
});
this.endpoint?.addCommandHandler('goHome', async () => {
await this.deviceManager.device.activateCharging();
});
this.endpoint?.addCommandHandler('identify', async () => {
await this.deviceManager.device.find();
});
return this.endpoint;
}
postRegister() {
this.deviceManager.stateChanged$
.pipe(mergeMap(async ({ key, value }) => {
this.log.debug(`Device state changed: ${key} = ${value}`);
if (key in this.stateChangedHandlers) {
await this.stateChangedHandlers[key](value);
}
}))
.subscribe();
}
stateChangedHandlers = {
batteryLevel: async (level) => {
this.log.debug(`Battery level: ${level}`);
await this.endpoint?.updateAttribute(PowerSource.Cluster.id, 'batPercentRemaining', level * 2);
await this.endpoint?.updateAttribute(PowerSource.Cluster.id, 'batChargeLevel', getBatteryChargeLevel(level));
},
charging: async (charging) => {
await this.endpoint?.updateAttribute(PowerSource.Cluster.id, 'batChargeState', charging === true ? PowerSource.BatChargeState.IsCharging : PowerSource.BatChargeState.IsNotCharging);
},
state: async (state) => {
await this.stateChangedHandlers.charging(state === 'charging');
switch (state) {
case 'cleaning':
case 'spot-cleaning':
case 'zone-cleaning':
await this.endpoint?.updateAttribute(RvcRunMode.Cluster.id, 'currentMode', SUPPORTED_MODES[1].mode);
await this.endpoint?.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', RvcOperationalState.OperationalState.Running);
break;
case 'returning':
case 'docking':
await this.endpoint?.updateAttribute(RvcRunMode.Cluster.id, 'currentMode', SUPPORTED_MODES[1].mode);
await this.endpoint?.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', RvcOperationalState.OperationalState.SeekingCharger);
break;
case 'error':
await this.endpoint?.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', RvcOperationalState.OperationalState.Error);
break;
case 'full':
await this.endpoint?.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', RvcOperationalState.OperationalState.Error);
await this.endpoint?.updateAttribute(RvcOperationalState.Cluster.id, 'operationalError', RvcOperationalState.ErrorState.DustBinFull);
break;
case 'charging-error':
case 'charger-offline':
await this.endpoint?.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', RvcOperationalState.OperationalState.Error);
await this.endpoint?.updateAttribute(RvcOperationalState.Cluster.id, 'operationalError', RvcOperationalState.ErrorState.FailedToFindChargingDock);
break;
case 'initializing':
case 'waiting':
await this.endpoint?.updateAttribute(RvcRunMode.Cluster.id, 'currentMode', SUPPORTED_MODES[0].mode);
await this.endpoint?.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', RvcOperationalState.OperationalState.Stopped);
break;
case 'charging':
await this.endpoint?.updateAttribute(RvcRunMode.Cluster.id, 'currentMode', SUPPORTED_MODES[0].mode);
await this.endpoint?.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', RvcOperationalState.OperationalState.Charging);
break;
default:
this.log.warn(`Unknown state: ${state}`);
break;
}
},
};
}
function getBatteryChargeLevel(batteryLevel) {
return batteryLevel < 10 ? PowerSource.BatChargeLevel.Critical : batteryLevel < 20 ? PowerSource.BatChargeLevel.Warning : PowerSource.BatChargeLevel.Ok;
}