UNPKG

node-miio

Version:

Control Mi Home devices, such as Mi Robot Vacuums, Mi Air Purifiers, Mi Smart Home Gateway (Aqara) and more

519 lines (474 loc) 12.5 kB
'use strict'; const { ChargingState, AutonomousCharging } = require('abstract-things'); const { Vacuum, AdjustableFanSpeed, AutonomousCleaning, SpotCleaning, } = require('abstract-things/climate'); const MiioApi = require('../../device'); const BatteryLevel = require('../capabilities/battery-level'); const checkResult = require('../../checkResult'); /** * Implementation of the interface used by the Mi Robot Vacuum. This device * doesn't use properties via get_prop but instead has a get_status. */ module.exports = class extends ( Vacuum.with( MiioApi, BatteryLevel, AutonomousCharging, AutonomousCleaning, SpotCleaning, AdjustableFanSpeed, ChargingState ) ) { static get type() { return 'miio:vacuum'; } constructor(options) { super(options); this.defineProperty('error_code', { name: 'error', mapper: (e) => { let message; switch (e) { // https://github.com/marcelrv/XiaomiRobotVacuumProtocol/blob/master/status.md#error-codes case 0: return null; case 1: message = 'Laser sensor fault'; break; case 2: message = 'Collision sensor fault'; break; case 3: message = 'Wheel floating'; break; case 4: message = 'Cliff sensor fault'; break; case 5: message = 'Main brush blocked'; break; case 6: message = 'Side brush blocked'; break; case 7: message = 'Wheel blocked'; break; case 8: message = 'Device stuck'; break; case 9: message = 'Dust bin missing'; break; case 10: message = 'Filter blocked'; break; case 11: message = 'Magnetic field detected'; break; case 12: message = 'Low battery'; break; case 13: message = 'Charging problem'; break; case 14: message = 'Battery failure'; break; case 15: message = 'Wall sensor fault'; break; case 16: message = 'Uneven surface'; break; case 17: message = 'Side brush failure'; break; case 18: message = 'Suction fan failure'; break; case 19: message = 'Unpowered charging station'; break; // case 20: // message = 'Unknown Error 20'; // break; case 21: message = 'Laser pressure sensor problem'; break; case 22: message = 'Charge sensor problem'; break; case 23: message = 'Dock problem'; break; case 24: message = 'No-go zone or invisible wall detected'; break; case 254: message = 'Bin full'; break; case 255: message = 'Internal error'; break; // case -1: // message = 'Unknown Error -1'; // break; default: message = 'Unknown error ' + e; } return { code: e, message, }; // TODO: Find a list of error codes and map them correctly }, }); this.defineProperty('state', (s) => { // https://github.com/marcelrv/XiaomiRobotVacuumProtocol/blob/master/status.md#status-codes switch (s) { case 1: return 'initiating'; case 2: return 'sleeping'; case 3: return 'idle'; case 4: return 'remote-control'; case 5: return 'cleaning'; case 6: return 'returning'; case 7: return 'manual-mode'; case 8: return 'charging'; case 9: return 'charging-error'; case 10: return 'paused'; case 11: return 'spot-cleaning'; case 12: return 'error'; case 13: return 'shutting-down'; case 14: return 'updating'; case 15: return 'docking'; case 16: return 'going-to-location'; case 17: return 'zone-cleaning'; case 18: return 'room-cleaning'; case 22: return 'dust-collection'; case 100: return 'fully-charged'; } return 'unknown-' + s; }); // Define the batteryLevel property for monitoring battery this.defineProperty('battery', { name: 'batteryLevel', }); this.defineProperty('clean_time', { name: 'cleanTime', }); this.defineProperty('clean_area', { name: 'cleanArea', mapper: (v) => v / 1000000, }); this.defineProperty('fan_power', { name: 'fanSpeed', }); this.defineProperty('in_cleaning', { name: 'cleaningMode', mapper: (v) => { switch (v) { case 0: return 'idle'; case 1: return 'cleaning'; case 2: return 'zone-cleaning'; case 3: return 'room-cleaning'; } return 'unknown-' + v; }, }); this.defineProperty('in_returning'); // Consumable status - times for brushes and filters this.defineProperty('main_brush_work_time', { name: 'mainBrushWorkTime', }); this.defineProperty('side_brush_work_time', { name: 'sideBrushWorkTime', }); this.defineProperty('filter_work_time', { name: 'filterWorkTime', }); this.defineProperty('sensor_dirty_time', { name: 'sensorDirtyTime', }); this.defineProperty('water_box_mode', { name: 'waterBoxMode', }); this.defineProperty('auto_dust_collection', { name: 'autoDustCollection', }); this.defineProperty('dust_collection_status', { name: 'dustCollectionStatus', }); this._monitorInterval = 60000; } propertyUpdated(key, value, oldValue) { if (key === 'state') { // Update charging state this.updateCharging(value === 'charging'); switch (value) { case 'cleaning': case 'spot-cleaning': case 'zone-cleaning': case 'room-cleaning': // The vacuum is cleaning this.updateCleaning(true); break; case 'paused': // Cleaning has been paused, do nothing special break; case 'error': // An error has occurred, rely on error mapping this.updateError(this.property('error')); break; case 'charging-error': // Charging error, trigger an error this.updateError({ code: 'charging-error', message: 'Error during charging', }); break; case 'charger-offline': // Charger is offline, trigger an error this.updateError({ code: 'charger-offline', message: 'Charger is offline', }); break; default: // The vacuum is not cleaning this.updateCleaning(false); break; } } else if (key === 'fanSpeed') { this.updateFanSpeed(value); } super.propertyUpdated(key, value, oldValue); } getDeviceInfo() { return this.call('miIO.info'); } async getSerialNumber() { const serial = await this.call('get_serial_number'); return serial[0].serial_number; } getRoomMap() { return this.call('get_room_mapping'); } cleanRooms(listOfRooms) { return this.call('app_segment_clean', listOfRooms, { refresh: ['state'], refreshDelay: 1000, }).then(checkResult); } resumeCleanRooms(listOfRooms) { return this.call('resume_segment_clean', listOfRooms, { refresh: ['state'], refreshDelay: 1000, }).then(checkResult); } cleanZones(listOfZones) { return this.call('app_zoned_clean', listOfZones, { refresh: ['state'], refreshDelay: 1000, }).then(checkResult); } getTimer() { return this.call('get_timer'); } /** * Start a cleaning session. */ activateCleaning() { return this.call('app_start', [], { refresh: ['state'], refreshDelay: 1000, }).then(checkResult); } /** * Pause the current cleaning session. */ pause() { return this.call('app_pause', [], { refresh: ['state'], refreshDelay: 1000, // https://github.com/homebridge-xiaomi-roborock-vacuum/homebridge-xiaomi-roborock-vacuum/issues/236 }).then(checkResult); } /** * Stop the current cleaning session. */ deactivateCleaning() { return this.call('app_stop', [], { refresh: ['state'], refreshDelay: 1000, }).then(checkResult); } /** * Stop the current cleaning session and return to charge. */ activateCharging() { return ( this.pause() .catch(() => this.deactivateCleaning()) // Wait 1 second .then(() => new Promise((resolve) => setTimeout(resolve, 1000))) .then(() => this.call('app_charge', [], { refresh: ['state'], refreshDelay: 1000, }) ) .then(checkResult) ); } /** * Start cleaning the current spot. */ activateSpotClean() { return this.call('app_spot', [], { refresh: ['state'], }).then(checkResult); } /** * Start dustCollection. */ startDustCollection() { return this.call('app_start_collect_dust', [], { refresh: ['state'], refreshDelay: 1000, }).then(checkResult); } /** * Stop dustCollection. */ stopDustCollection() { return this.call('app_stop_collect_dust', [], { refresh: ['state'], refreshDelay: 1000, }).then(checkResult); } /** * Set the power of the fan. Usually 38, 60 or 77. */ changeFanSpeed(speed) { return this.call('set_custom_mode', [speed], { refresh: ['fanSpeed'], }).then(checkResult); } /** * Get WaterBoxMode (only working for the model S6) * @returns {Promise<*>} */ async getWaterBoxMode() { // From https://github.com/marcelrv/XiaomiRobotVacuumProtocol/blob/master/water_box_custom_mode.md const response = await this.call('get_water_box_custom_mode', [], { refresh: ['waterBoxMode'], }); return response[0]; } setWaterBoxMode(mode) { // From https://github.com/marcelrv/XiaomiRobotVacuumProtocol/blob/master/water_box_custom_mode.md return this.call('set_water_box_custom_mode', [mode], { refresh: ['waterBoxMode'], }).then(checkResult); } /** * Activate the find function, will make the device give off a sound. */ find() { return this.call('find_me', ['']).then(() => null); } /** * Send the vacuum to a specific location. */ sendToLocation(x, y) { return this.call('app_goto_target', [x, y], { refresh: ['state'], refreshDelay: 1000, }).then(checkResult); } /** * Get information about the cleaning history of the device. Contains * information about the number of times it has been started and * the days it has been run. */ getHistory() { return this.call('get_clean_summary').then((result) => { return { count: result[2], days: result[3].map((ts) => new Date(ts * 1000)), }; }); } /** * Get history for the specified day. The day should be fetched from * `getHistory`. */ getHistoryForDay(day) { let record = day; if (record instanceof Date) { record = Math.floor(record.getTime() / 1000); } return this.call('get_clean_record', [record]).then((result) => ({ day: day, history: result.map((data) => ({ // Start and end times start: new Date(data[0] * 1000), end: new Date(data[1] * 1000), // How long it took in seconds duration: data[2], // Area in m2 area: data[3] / 1000000, // If it was a complete run complete: data[5] === 1, })), })); } loadProperties(props) { // We override loadProperties to use get_status and get_consumables props = props.map((key) => this._reversePropertyDefinitions[key] || key); return Promise.all([ this.call('get_status'), this.call('get_consumable'), ]).then((result) => { const status = result[0][0]; const consumables = result[1][0]; const mapped = {}; props.forEach((prop) => { let value = status[prop]; if (typeof value === 'undefined') { value = consumables[prop]; } this._pushProperty(mapped, prop, value); }); return mapped; }); } };