homebridge-valetudo-xiaomi-vacuum
Version:
A Homebridge plugin for Xiaomi Vacuum Cleaner 1st gen (Mi Robot) and 2nd gen (Roborock S50), with Valetudo installed.
436 lines (369 loc) • 11.7 kB
JavaScript
const { sendJSONRequest } = require('./request');
const types = require('./types');
class VacuumRe {
constructor(log, config, statusCallback) {
this.ip = config.ip;
this.log = log;
this.authentication = config.authentication || null;
const powerControl = config['power-control'];
if (powerControl) {
const defaultSpeedValue = VacuumRe.getSpeedValue(powerControl['default-speed'] || 'balanced');
const highSpeedValue = VacuumRe.getSpeedValue(powerControl['high-speed'] || 'max');
this.powerControl = {
defaultSpeed: defaultSpeedValue,
highSpeed: highSpeedValue,
mop: powerControl['mop-enabled'] === true,
};
log.debug(`Setting power control: default speed - ${this.powerControl.defaultSpeed}, high speed - ${this.powerControl.highSpeed}, mop enabled - ${this.powerControl.mop}`);
}
this.current_status = null;
this.status_callbacks = [];
this.current_status_time = null;
this.status_timer = null;
this.idle_update_interval = 120000;
this.busy_update_interval = 10000;
this.status_callback = statusCallback;
if (!this.ip) {
throw new Error('You must provide an ip address of the vacuum cleaner.');
}
}
static statusUrl(ip) {
return `http://${ip}/api/current_status`;
}
static parseStatus(response) {
return response;
}
isHighSpeedMode(callback) {
this.getStatus(false, (error, status) => {
if (error) {
callback(error, false);
return;
}
callback(null, status.fan_power === this.powerControl.highSpeed);
});
}
isMopMode(callback) {
this.getStatus(false, (error, status) => {
if (error) {
callback(error, false);
return;
}
callback(null, status.fan_power === VacuumRe.SPEEDS.mop);
});
}
setHighSpeedMode(on, callback) {
this.isHighSpeedMode((error, isOn) => {
if (error) {
callback(error);
return;
}
if (on === isOn) {
callback(null);
return;
}
if (on) {
this.setFanSpeed(this.powerControl.highSpeed, callback);
} else {
this.setFanSpeed(this.powerControl.defaultSpeed, callback);
}
});
}
setMopMode(on, callback) {
this.isMopMode((error, isOn) => {
if (error) {
callback(error);
}
if (on && isOn) {
callback(null);
}
if (!on && !isOn) {
callback(null);
}
if (on) {
this.setFanSpeed(VacuumRe.SPEEDS.mop, callback);
} else {
this.setFanSpeed(this.powerControl.defaultSpeed, callback);
}
});
}
async setFanSpeed(value, callback) {
this.log.debug(`Setting fan power to ${value}`);
try {
await sendJSONRequest({
url: `http://${this.ip}/api/fanspeed`, method: 'PUT', content: { speed: value }, raw_response: true, authentication: this.authentication,
});
this.updateStatus(true);
callback(null);
} catch (e) {
this.log.error(`Failed to change fan power: ${e}`);
callback(e);
}
}
getBatteryLevel(callback) {
this.getStatus(false, (error, status) => {
if (error) {
callback(error, null);
} else {
callback(null, status.battery);
}
});
}
getChargingState(callback) {
this.getStatus(false, (error, status) => {
if (error) {
callback(error);
} else if (status.state === VacuumRe.STATES.CHARGING) {
callback(null, types.CHARGING_STATE.CHARGING);
} else if (
this.current_status.state === VacuumRe.STATES.CHARGER_DISCONNECTED
|| this.current_status.state === VacuumRe.STATES.CHARGING_PROBLEM
) {
callback(null, types.CHARGING_STATE.DISCHARGING);
} else {
callback(null, types.CHARGING_STATE.CHARGED);
}
});
}
async version(callback) {
try {
const response = await sendJSONRequest({ url: `http://${this.ip}/api/get_fw_version`, authentication: this.authentication });
if (response != null) {
callback(null, response.version);
} else {
throw Error('Cannot get current version');
}
} catch (e) {
this.log.error(`Error parsing firmware info: ${e}`);
callback(e);
}
}
/* getBatteryLow(callback) {
this.log.debug('getting the battery level');
this.getStatus(false, (error) => {
if (error) {
callback(error);
} else if (this.current_status.battery < 10) {
callback(null, Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW);
} else {
callback(null, Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL);
}
});
} */
async doFind(callback) {
const { log } = this;
try {
await sendJSONRequest({
url: `http://${this.ip}/api/find_robot`, method: 'PUT', content: { action: 'locate' }, raw_response: true, authentication: this.authentication,
});
callback();
} catch (e) {
log.error(`Failed to identify robot: ${e}`);
callback(e);
}
}
async goHome(callback) {
const { log } = this;
log.debug('Executing go home');
try {
await sendJSONRequest({ url: `http://${this.ip}/api/drive_home`, method: 'PUT', raw_response: true, authentication: this.authentication });
callback();
} catch (e) {
log.error(`Failed to execute go home: ${e}`);
callback(e);
} finally {
setTimeout(() => { this.updateStatus(true); }, 2000);
}
}
isGoingHome(callback) {
this.getStatus(false, (error, status) => {
if (error) {
callback(new Error(`Error retrieving going home status: ${error}`));
return;
}
callback(null, status.state === VacuumRe.STATES.RETURNING_HOME);
});
}
async startCleaning(callback) {
this.log.debug('Executing cleaning');
try {
await sendJSONRequest({ url: `http://${this.ip}/api/start_cleaning`, method: 'PUT', raw_response: true, authentication: this.authentication });
callback();
} catch (e) {
this.log.error(`Failed to start cleaning: ${e}`);
callback(e);
} finally {
setTimeout(() => { this.updateStatus(true); }, 2000);
}
}
async stopCleaning(callback) {
this.log.debug('Executing stop cleaning');
this.getStatus(true, async (err, status) => {
if (err) {
callback(err);
return;
}
if (
status.state === VacuumRe.STATES.IDLE
|| this.current_status.state === VacuumRe.STATES.RETURNING_HOME
|| this.current_status.state === VacuumRe.STATES.CHARGING
|| this.current_status.state === VacuumRe.STATES.PAUSED
|| this.current_status.state === VacuumRe.STATES.SPOT_CLEANING
|| this.current_status.state === VacuumRe.STATES.DOCKING
|| this.current_status.state === VacuumRe.STATES.GOING_TO_TARGET
) {
callback(new Error('Cannot stop cleaning in current state'));
}
try {
await sendJSONRequest({ url: `http://${this.ip}/api/stop_cleaning`, method: 'PUT', raw_response: true, authentication: this.authentication });
callback();
} catch (e) {
this.log.error(`Failed to stop cleaning: ${e}`);
callback(e);
} finally {
setTimeout(() => { this.updateStatus(true); }, 2000);
}
});
}
isCleaning(callback) {
this.getStatus(false, (error, status) => {
this.log.debug(`Is cleaning? error: ${error}, state: ${status !== null ? status.state : null}`);
if (error) {
return callback(error);
}
return callback(null, status.state === VacuumRe.STATES.CLEANING);
});
}
async startSpotCleaning(callback) {
this.log.debug('Executing spot cleaning');
try {
await sendJSONRequest({ url: `http://${this.ip}/api/spot_clean`, method: 'PUT', raw_response: true, authentication: this.authentication });
callback();
} catch (e) {
this.log.error(`Failed to start spot cleaning: ${e}`);
callback(e);
} finally {
setTimeout(() => { this.updateStatus(true); }, 2000);
}
}
isSpotCleaning(callback) {
this.getStatus(false, (error, status) => {
if (error) {
callback(error);
return;
}
callback(null, status.state === VacuumRe.STATES.SPOT_CLEANING);
});
}
updateStatus(forced = false) {
this.log.debug('Updating vacuum status');
this.getStatus(forced, (err, status) => {
if (err) {
return;
}
try {
this.status_callback(status);
} catch (e) {
this.log.error('status callback function errored out');
}
});
}
updateInterval() {
if (this.current_status !== null) {
switch (this.current_status.state) {
case VacuumRe.STATES.CHARGING:
case VacuumRe.STATES.IDLE:
return this.idle_update_interval; // slow update interval for idle states
default:
break;
}
}
return this.busy_update_interval; // fast update interval for non-idle states
}
clearUpdateTimer() {
clearTimeout(this.status_timer);
}
setupUpdateTimer() {
this.status_timer = setTimeout(() => { this.updateStatus(true); }, this.updateInterval());
}
async getStatus(forced, callback) {
if (this.status_callbacks.length > 0) {
this.log.debug('Pushing status callback to queue - updating');
this.status_callbacks.push(callback);
return;
}
const now = Date.now();
if (!forced && this.current_status !== null
&& this.current_status_time !== null
&& (now - this.current_status_time < this.busy_update_interval)) {
this.log.debug('Returning cached status');
callback(null, this.current_status);
return;
}
this.clearUpdateTimer();
this.log.debug(`Executing update, forced: ${forced}`);
this.status_callbacks.push(callback);
try {
const response = await sendJSONRequest({
url: VacuumRe.statusUrl(this.ip),
authentication: this.authentication,
});
this.log.debug('Done executing update');
const status = VacuumRe.parseStatus(response);
this.current_status = status;
this.current_status_time = Date.now();
const callbacks = this.status_callbacks;
this.status_callbacks = [];
this.log.debug(`Calling ${callbacks.length} queued callbacks`);
callbacks.forEach((element) => {
element(null, status);
});
this.setupUpdateTimer();
} catch (e) {
this.log.error(`Error parsing current status info: ${e}`);
const callbacks = this.status_callbacks;
this.status_callbacks = [];
callbacks.forEach((element) => {
element(e, null);
});
this.setupUpdateTimer();
}
}
static getSpeedValue(preset) {
switch (preset) {
case 'quiet': return VacuumRe.SPEEDS.quiet;
case 'balanced': return VacuumRe.SPEEDS.balanced;
case 'turbo': return VacuumRe.SPEEDS.turbo;
case 'max': return VacuumRe.SPEEDS.max;
case 'mop': return VacuumRe.SPEEDS.mop;
default: throw Error(`Invalid power preset given: ${preset}`);
}
}
}
VacuumRe.SPEEDS = {
mop: 105,
quiet: 38,
balanced: 60,
turbo: 75,
max: 100,
};
VacuumRe.STATES = {
STARTING: 1,
CHARGER_DISCONNECTED: 2,
IDLE: 3,
REMOTE_ACTIVE: 4,
CLEANING: 5,
RETURNING_HOME: 6,
MANUAL_MODE: 7,
CHARGING: 8,
CHARGING_PROBLEM: 9,
PAUSED: 10,
SPOT_CLEANING: 11,
ERROR: 12,
SHUTTING_DOWN: 13,
UPDATING: 14,
DOCKING: 15,
GOING_TO_TARGET: 16,
ZONE_CLEANING: 17,
ROOMS_CLEANING: 18,
};
module.exports = { VacuumRe };